huckiyang commited on
Commit
0c55212
·
verified ·
1 Parent(s): f33234e

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. README.md +109 -1
  3. THIRD_PARTY_NOTICES.md +7 -0
  4. chat_template.jinja +129 -0
  5. config.json +119 -0
  6. inkling_mlx/LICENSE +13 -0
  7. inkling_mlx/__init__.py +14 -0
  8. inkling_mlx/attention.py +131 -0
  9. inkling_mlx/audio.py +34 -0
  10. inkling_mlx/cache.py +66 -0
  11. inkling_mlx/common.py +63 -0
  12. inkling_mlx/config.py +207 -0
  13. inkling_mlx/convert.py +254 -0
  14. inkling_mlx/convert_cli.py +47 -0
  15. inkling_mlx/generate.py +90 -0
  16. inkling_mlx/layers.py +47 -0
  17. inkling_mlx/load.py +69 -0
  18. inkling_mlx/model.py +79 -0
  19. inkling_mlx/moe.py +117 -0
  20. inkling_mlx/processing.py +188 -0
  21. inkling_mlx/text.py +50 -0
  22. inkling_mlx/vision.py +126 -0
  23. model-00001-of-00023.safetensors +3 -0
  24. model-00002-of-00023.safetensors +3 -0
  25. model-00003-of-00023.safetensors +3 -0
  26. model-00004-of-00023.safetensors +3 -0
  27. model-00005-of-00023.safetensors +3 -0
  28. model-00006-of-00023.safetensors +3 -0
  29. model-00007-of-00023.safetensors +3 -0
  30. model-00008-of-00023.safetensors +3 -0
  31. model-00009-of-00023.safetensors +3 -0
  32. model-00010-of-00023.safetensors +3 -0
  33. model-00011-of-00023.safetensors +3 -0
  34. model-00012-of-00023.safetensors +3 -0
  35. model-00013-of-00023.safetensors +3 -0
  36. model-00014-of-00023.safetensors +3 -0
  37. model-00015-of-00023.safetensors +3 -0
  38. model-00016-of-00023.safetensors +3 -0
  39. model-00017-of-00023.safetensors +3 -0
  40. model-00018-of-00023.safetensors +3 -0
  41. model-00019-of-00023.safetensors +3 -0
  42. model-00020-of-00023.safetensors +3 -0
  43. model-00021-of-00023.safetensors +3 -0
  44. model-00022-of-00023.safetensors +3 -0
  45. model-00023-of-00023.safetensors +3 -0
  46. model.safetensors.index.json +0 -0
  47. processor_config.json +46 -0
  48. special_tokens_map.json +22 -0
  49. tiktoken/tokenizer.model +3 -0
  50. tokenizer.json +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,3 +1,111 @@
1
  ---
2
- license: mit
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ license: apache-2.0
3
+ library_name: mlx
4
+ tags: [mlx, inkling, moe, multimodal, text-generation, image-text-to-text, apple-silicon]
5
+ base_model: thinkingmachines/Inkling-Small
6
+ pipeline_tag: image-text-to-text
7
  ---
8
+
9
+ # Inkling-Small-mlx-3bit
10
+
11
+ An **MLX 3bit** build of [`thinkingmachines/Inkling-Small`](https://huggingface.co/thinkingmachines/Inkling-Small)
12
+ — 264 B total parameters, **~12 B active**; 42 layers, 256 routed experts (top-6) + 2 shared;
13
+ natively multimodal (image + audio in, text out).
14
+
15
+ Sized to run **fully resident on a 128 GB Mac** — no expert-offload, no expert pruning, no
16
+ layer streaming. The whole model sits in unified memory and decodes at conversational speed.
17
+
18
+ > Why this model is the interesting one for Apple Silicon: speed follows the **active**
19
+ > parameter count, not how hard you compress. Inkling-Small activates ~12 B params per token,
20
+ > so this tier reads roughly 9.8 GB per token. The 975 B Inkling, by contrast, needs SSD
21
+ > expert-offload even at 2-bit and manages ~0.2–0.4 tok/s.
22
+
23
+ ## Tiers
24
+
25
+ Pick the tier that fits your machine. Sizes are on-disk and **measured from the built
26
+ artifacts**; peak/speed columns are filled in only where a benchmark has actually run.
27
+
28
+ | tier | target Mac | recipe | on-disk | peak | load | prefill tok/s |
29
+ |---|---|---|---|---|---|---|
30
+ | [4bit](https://huggingface.co/mlx-community/Inkling-Small-mlx-4bit) | 192 GB | experts 4-bit, non-expert 8-bit | 153.5 GB | — | — | — |
31
+ | **3bit** ← **this repo** | 128 GB | experts 3-bit, non-expert 8-bit | 120.9 GB | — | — | — |
32
+ | [2bit](https://huggingface.co/mlx-community/Inkling-Small-mlx-2bit) | 96 GB | experts 2-bit, non-expert 8-bit | 88.4 GB | — | — | — |
33
+
34
+ > **Speeds are not yet measured.** On-disk sizes above are real (taken from the built artifacts), but load/peak/tok-s columns stay empty until `serving/bench_mlx.py` has run on the target hardware — we would rather ship a blank column than a guess. Run it yourself with the command below and please open a discussion with your numbers.
35
+
36
+ ## Why MLX on Apple Silicon
37
+
38
+ MLX is Apple's array framework for Apple Silicon. For a big sparse MoE like this one, the
39
+ practical differences from a CPU/GPU-split runtime are:
40
+
41
+ | | what it means here |
42
+ |---|---|
43
+ | **Unified memory** | The GPU reads the same DRAM as the CPU, so a 120 GB build needs 120 GB of *system* memory — not VRAM plus a host copy. There is no PCIe transfer per layer, which is what makes a >100 GB model practical on a desktop at all. |
44
+ | **Lazy, mmap'd loading** | `mx.load` memory-maps safetensors, so weights page in on demand instead of being read and copied up front. |
45
+ | **Native quantized matmul** | Quantized weights are multiplied in their packed form (`quantized_matmul`, and `gather_qmm` for MoE expert gathers) rather than dequantized to fp16 first — so low-bit tiers save bandwidth at *runtime*, not just on disk. |
46
+ | **Per-module precision** | `nn.quantize(..., class_predicate=...)` lets one checkpoint mix bit-widths per tensor class, which is exactly how these tiers keep attention and the router high-precision while the experts go low. |
47
+ | **Small dependency surface** | `pip install mlx mlx-lm` and a single Python model file. No compile step, no separate server binary. |
48
+
49
+ Honest limits: MLX's ecosystem is younger than llama.cpp's, it has no importance-matrix
50
+ ("i-quant") style calibrated quantization yet, and it runs on Apple Silicon only. If you want a
51
+ GGUF build instead, one exists at
52
+ [`unsloth/Inkling-Small-GGUF`](https://huggingface.co/unsloth/Inkling-Small-GGUF).
53
+
54
+ ## Memory notes
55
+
56
+ `peak` (once measured) is peak unified-memory use, not file size — leave headroom for the KV
57
+ cache and the OS. Two things matter on a machine near its limit:
58
+
59
+ * **Raise the Metal wired limit.** It defaults to ~75% of RAM, which is below this tier's
60
+ footprint on a 128 GB machine:
61
+ ```bash
62
+ sudo sysctl iogpu.wired_limit_mb=180000 # ~180 GB on a 192 GB Mac; scale to your RAM
63
+ ```
64
+ * **macOS will kill a process that stays too large for too long** (jetsam), even while memory
65
+ pressure looks fine. If long generations die silently, move down a tier.
66
+
67
+ ## Quantization recipe
68
+
69
+ `experts_only`: the routed experts (and the vision/audio matmuls) carry the tier's bit-width,
70
+ while **attention, token/output embeddings, RMSNorms, the router gate, the per-layer
71
+ short-convolutions and the relative-position bias stay high-precision (8-bit)**.
72
+
73
+ That asymmetry is the point, and it is nearly free: non-expert weights are only **5.9 B of the
74
+ 264 B params (~6 GB at 8-bit)**, yet they are read **in full on every token**, while just 6 of
75
+ 256 experts are. Bits spent there cost ~2% of the file and protect the paths every token
76
+ depends on. Expert precision degrades *gracefully*; attention and router precision degrade
77
+ *globally*.
78
+
79
+ ## Usage
80
+
81
+ ```python
82
+ # pip install mlx mlx-lm transformers (+ scipy for audio, pillow for images)
83
+ # this repo bundles the inkling_mlx/ loader, so there is nothing else to install
84
+ from inkling_mlx.load import load
85
+ from inkling_mlx.generate import greedy_generate
86
+ from transformers import AutoTokenizer
87
+
88
+ path = "./Inkling-Small-mlx-3bit"
89
+ model, config = load(path)
90
+ tok = AutoTokenizer.from_pretrained(path, trust_remote_code=True)
91
+ ids = tok("The capital of France is")["input_ids"]
92
+ print(tok.decode(greedy_generate(model, config, ids, max_new_tokens=64)))
93
+ ```
94
+
95
+ Images are cut into 40 px patches (one soft-token each) and audio is d-mel encoded at 20
96
+ tokens/s; both go through `InklingProcessor`. See the source repo for a multimodal runner.
97
+
98
+ ### Benchmark it yourself
99
+
100
+ ```bash
101
+ python serving/bench_mlx.py --model ./Inkling-Small-mlx-3bit --tier 3bit
102
+ ```
103
+
104
+ Reports load time, prefill tok/s, decode tok/s at several context lengths, and peak memory.
105
+
106
+ ## Attribution
107
+
108
+ - Base model: [`thinkingmachines/Inkling-Small`](https://huggingface.co/thinkingmachines/Inkling-Small), Apache-2.0.
109
+ - The bundled `inkling_mlx/` loader is vendored from
110
+ [PipeNetwork/inkling-mlx](https://github.com/PipeNetwork/inkling-mlx), Apache-2.0.
111
+ - Quantized with `mlx`; see the recipe above for exactly which tensors were touched.
THIRD_PARTY_NOTICES.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Third-party notices
2
+
3
+ This project vendors the `inkling_mlx/` model package from
4
+ **PipeNetwork/inkling-mlx** (https://github.com/PipeNetwork/inkling-mlx), licensed under **Apache-2.0**
5
+ (Copyright 2026 PipeNetwork). Vendored commit: `b6dc0ac9219b33e4e667bf08cafcdc2651fd9884`.
6
+ The package's LICENSE is preserved at `inkling_mlx/LICENSE`. Our conversion
7
+ tooling (convert/, models/, serving/, publish/) is separate.
chat_template.jinja ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set effort_map = {"none": 0.0, "minimal": 0.1, "low": 0.2, "medium": 0.7, "high": 0.9, "max": 0.99} -%}
2
+ {%- set role_token = {"user": "<|message_user|>", "assistant": "<|message_model|>", "system": "<|message_system|>", "tool": "<|message_tool|>"} -%}
3
+
4
+ {%- macro emit_thinking_effort() -%}
5
+ {%- set eff = reasoning_effort if reasoning_effort is defined and reasoning_effort is not none else 0.9 -%}
6
+ {%- if eff is string -%}
7
+ {%- set key = eff | trim -%}
8
+ {%- if key not in effort_map -%}
9
+ {{- raise_exception("Unknown reasoning_effort: " ~ eff) -}}
10
+ {%- endif -%}
11
+ {%- set num = effort_map[key] -%}
12
+ {%- else -%}
13
+ {%- set num = eff | float -%}
14
+ {%- endif -%}
15
+ {%- if num < 0.0 or num > 0.99 -%}
16
+ {{- raise_exception("reasoning_effort must be in [0.0, 0.99]") -}}
17
+ {%- endif -%}
18
+ {{- "<|message_system|><|content_text|>Thinking effort level: " -}}
19
+ {%- if num == 0.0 -%}0{%- else -%}{{ num }}{%- endif -%}
20
+ {{- "<|end_message|>" -}}
21
+ {%- endmacro -%}
22
+
23
+ {%- if tools -%}
24
+ {%- set tool_state = namespace(specs=[]) -%}
25
+ {%- for tool in tools -%}
26
+ {%- set fn = tool.function if tool.function is defined else tool -%}
27
+ {%- set spec = {
28
+ "description": (fn.description if fn.description is defined and fn.description else ""),
29
+ "name": fn.name,
30
+ "parameters": (fn.parameters if fn.parameters is defined and fn.parameters else {}),
31
+ "type": (tool.type if tool.type is defined and tool.type else "function"),
32
+ } -%}
33
+ {%- set tool_state.specs = tool_state.specs + [spec] -%}
34
+ {%- endfor -%}
35
+ {{- "<|message_system|>tool_declare<|content_xml|>" -}}
36
+ {{- tool_state.specs | tojson(sort_keys=true, separators=(",", ":")) -}}
37
+ {{- "<|end_message|>" -}}
38
+ {%- endif -%}
39
+
40
+ {%- set state = namespace(effort_emitted=false) -%}
41
+ {%- for message in messages -%}
42
+ {%- if message.role not in role_token -%}
43
+ {{- raise_exception("Unknown message role: " ~ message.role) -}}
44
+ {%- endif -%}
45
+ {%- if not state.effort_emitted and message.role != "system" -%}
46
+ {{- emit_thinking_effort() -}}
47
+ {%- set state.effort_emitted = true -%}
48
+ {%- endif -%}
49
+
50
+ {%- set rtok = role_token[message.role] -%}
51
+
52
+ {%- if message.role == "tool" -%}
53
+ {%- set tool_name_state = namespace(name="") -%}
54
+ {%- if message.name is defined and message.name -%}
55
+ {%- set tool_name_state.name = message.name -%}
56
+ {%- elif message.tool_call_id is defined and message.tool_call_id -%}
57
+ {%- for prev in messages -%}
58
+ {%- if prev.role == "assistant" and prev.tool_calls -%}
59
+ {%- for tc in prev.tool_calls -%}
60
+ {%- if tc.id is defined and tc.id == message.tool_call_id and tc.function.name is defined -%}
61
+ {%- set tool_name_state.name = tc.function.name -%}
62
+ {%- endif -%}
63
+ {%- endfor -%}
64
+ {%- endif -%}
65
+ {%- endfor -%}
66
+ {%- endif -%}
67
+ {{- rtok -}}
68
+ {%- if tool_name_state.name -%}{{- tool_name_state.name -}}{%- endif -%}
69
+ {{- "<|content_text|>" -}}
70
+ {%- if message.content is string -%}{{- message.content -}}{%- endif -%}
71
+ {{- "<|end_message|>" -}}
72
+
73
+ {%- else -%}
74
+ {%- if message.role == "assistant" and message.reasoning_content is defined and message.reasoning_content -%}
75
+ {{- "<|message_model|><|content_thinking|>" ~ message.reasoning_content ~ "<|end_message|>" -}}
76
+ {%- endif -%}
77
+
78
+ {%- if message.content is string -%}
79
+ {{- rtok ~ "<|content_text|>" ~ message.content ~ "<|end_message|>" -}}
80
+ {%- elif message.content -%}
81
+ {%- for part in message.content -%}
82
+ {%- if part is string -%}
83
+ {{- rtok ~ "<|content_text|>" ~ part ~ "<|end_message|>" -}}
84
+ {%- elif part.type is not defined or part.type in ("text", "input_text") -%}
85
+ {%- set text_part = (part.text if part.text is defined and part.text is string else "") -%}
86
+ {{- rtok ~ "<|content_text|>" ~ text_part ~ "<|end_message|>" -}}
87
+ {%- elif part.type in ("image", "input_image", "image_url") -%}
88
+ {{- rtok ~ "<|content_image|><|unused_200054|><|end_message|>" -}}
89
+ {%- elif part.type in ("audio", "input_audio", "audio_url") -%}
90
+ {{- rtok ~ "<|content_audio_input|><|unused_200053|><|audio_end|><|end_message|>" -}}
91
+ {%- else -%}
92
+ {{- raise_exception("Unsupported content part type: " ~ part.type) -}}
93
+ {%- endif -%}
94
+ {%- endfor -%}
95
+ {%- endif -%}
96
+
97
+ {%- if message.role == "assistant" and message.tool_calls -%}
98
+ {%- for tc in message.tool_calls -%}
99
+ {%- set fn = tc.function -%}
100
+ {%- if fn.name is not defined or fn.name is not string -%}
101
+ {{- raise_exception("tool call function name must be a string") -}}
102
+ {%- endif -%}
103
+ {%- set args = fn.arguments if fn.arguments is defined and fn.arguments else {} -%}
104
+ {%- if args is string -%}
105
+ {{- raise_exception("tool call arguments must be a parsed object, not a JSON string; canonicalize upstream") -}}
106
+ {%- endif -%}
107
+ {%- if args is not mapping -%}
108
+ {{- raise_exception("tool call arguments must be an object") -}}
109
+ {%- endif -%}
110
+ {{- "<|message_model|>" ~ fn.name ~ "<|content_invoke_tool_json|>" -}}
111
+ {{- '{"name":' ~ (fn.name | tojson(sort_keys=true, separators=(",", ":"))) ~ ',"args":' -}}
112
+ {{- (args | tojson(sort_keys=true, separators=(",", ":"))) -}}
113
+ {{- "}<|end_message|>" -}}
114
+ {%- endfor -%}
115
+ {%- endif -%}
116
+
117
+ {%- if message.role == "assistant" -%}
118
+ {{- "<|content_model_end_sampling|>" -}}
119
+ {%- endif -%}
120
+ {%- endif -%}
121
+ {%- endfor -%}
122
+
123
+ {%- if not state.effort_emitted -%}
124
+ {{- emit_thinking_effort() -}}
125
+ {%- endif -%}
126
+
127
+ {%- if add_generation_prompt -%}
128
+ {{- "<|message_model|>" -}}
129
+ {%- endif -%}
config.json ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "InklingForConditionalGeneration"
4
+ ],
5
+ "model_type": "inkling_mm_model",
6
+ "eos_token_id": 200006,
7
+ "text_config": {
8
+ "model_max_length": 1048576,
9
+ "torch_dtype": "bfloat16",
10
+ "hidden_size": 4096,
11
+ "num_hidden_layers": 42,
12
+ "vocab_size": 201024,
13
+ "num_attention_heads": 32,
14
+ "num_key_value_heads": 8,
15
+ "head_dim": 128,
16
+ "d_rel": 16,
17
+ "rel_extent": 1024,
18
+ "q_bias": false,
19
+ "o_bias": false,
20
+ "log_scaling_n_floor": 128000,
21
+ "log_scaling_alpha": 0.1,
22
+ "rms_norm_eps": 1e-06,
23
+ "use_embed_norm": true,
24
+ "local_layer_ids": [
25
+ 0,
26
+ 1,
27
+ 2,
28
+ 3,
29
+ 4,
30
+ 6,
31
+ 7,
32
+ 8,
33
+ 9,
34
+ 10,
35
+ 12,
36
+ 13,
37
+ 14,
38
+ 15,
39
+ 16,
40
+ 18,
41
+ 19,
42
+ 20,
43
+ 21,
44
+ 22,
45
+ 24,
46
+ 25,
47
+ 26,
48
+ 27,
49
+ 28,
50
+ 30,
51
+ 31,
52
+ 32,
53
+ 33,
54
+ 34,
55
+ 36,
56
+ 37,
57
+ 38,
58
+ 39,
59
+ 40
60
+ ],
61
+ "dense_mlp_idx": 2,
62
+ "use_sconv": true,
63
+ "sconv_kernel_size": 4,
64
+ "unpadded_vocab_size": 200058,
65
+ "logits_mup_width_multiplier": 16.0,
66
+ "final_logit_softcapping": null,
67
+ "swa_head_dim": 128,
68
+ "swa_num_attention_heads": 32,
69
+ "swa_num_key_value_heads": 8,
70
+ "sliding_window_size": 512,
71
+ "n_routed_experts": 256,
72
+ "num_experts_per_tok": 6,
73
+ "n_shared_experts": 2,
74
+ "shared_expert_sink": true,
75
+ "dense_intermediate_size": 16384,
76
+ "intermediate_size": 2048,
77
+ "route_scale": 8.0,
78
+ "use_gate_bias": true,
79
+ "gate_activation": "sigmoid",
80
+ "norm_after_topk": true,
81
+ "use_global_scale": true
82
+ },
83
+ "audio_config": {
84
+ "decoder_dmodel": 4096,
85
+ "n_mel_bins": 80,
86
+ "mel_vocab_size": 16,
87
+ "bias": false,
88
+ "dmel_min_value": -7.0,
89
+ "dmel_max_value": 2.0,
90
+ "use_audio_norm": true,
91
+ "audio_mode": "dmel"
92
+ },
93
+ "vision_config": {
94
+ "vision_encoder_type": "hmlp",
95
+ "decoder_dmodel": 4096,
96
+ "patch_size": 40,
97
+ "temporal_patch_size": 2,
98
+ "n_channels": 3,
99
+ "n_layers": 4,
100
+ "use_vision_norm": true
101
+ },
102
+ "mtp_config": {
103
+ "num_nextn_predict_layers": 8,
104
+ "chain_hidden_post_norm": false,
105
+ "local_layer_ids": [
106
+ 0,
107
+ 2,
108
+ 4,
109
+ 5,
110
+ 6,
111
+ 7
112
+ ]
113
+ },
114
+ "quantization": {
115
+ "group_size": 64,
116
+ "bits": 3,
117
+ "recipe": "experts_only"
118
+ }
119
+ }
inkling_mlx/LICENSE ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2026 PipeNetwork
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
inkling_mlx/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MLX port of thinkingmachines/Inkling (975B MoE, natively multimodal)."""
2
+
3
+ from .config import AudioConfig, InklingConfig, TextConfig, VisionConfig
4
+ from .model import InklingForConditionalGeneration
5
+ from .text import TextModel
6
+
7
+ __all__ = [
8
+ "InklingConfig",
9
+ "TextConfig",
10
+ "VisionConfig",
11
+ "AudioConfig",
12
+ "InklingForConditionalGeneration",
13
+ "TextModel",
14
+ ]
inkling_mlx/attention.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inkling attention: hybrid local/global, per-head q/k RMSNorm, relative-position
2
+ logits bias, optional log-scaling, and short-convolution on k/v.
3
+
4
+ Mirrors ``InklingAttention`` + ``InklingRelativeLogits`` from transformers PR #47347.
5
+ This implementation is prefill-oriented (full-sequence, no KV cache); an incremental
6
+ cache (including the 4 per-layer conv states) can be layered on top later.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+
13
+ import mlx.core as mx
14
+ import mlx.nn as nn
15
+
16
+ from .common import RMSNorm, ShortConvolution
17
+ from .config import TextConfig
18
+
19
+ NEG_INF = -1e30
20
+
21
+
22
+ class RelativeLogits(nn.Module):
23
+ """Hidden-state-conditioned relative position bias.
24
+
25
+ ``proj`` is a bank of bias-vs-distance profiles ``[d_rel, rel_extent]``. Each
26
+ query's ``d_rel`` relative-state vector mixes them into one bias value per
27
+ backward distance; the bias is zero outside ``0 <= distance < rel_extent``.
28
+ """
29
+
30
+ def __init__(self, d_rel: int, rel_extent: int):
31
+ super().__init__()
32
+ self.rel_extent = rel_extent
33
+ self.proj = mx.zeros((d_rel, rel_extent))
34
+
35
+ def __call__(self, relative_states, q_pos, kv_pos):
36
+ # relative_states: [B, Lq, heads, d_rel]
37
+ # rel_logits: [B, Lq, heads, rel_extent] -> [B, heads, Lq, rel_extent]
38
+ rel_logits = mx.swapaxes(relative_states @ self.proj, 1, 2)
39
+ B, H, Lq, _ = rel_logits.shape
40
+ distance = q_pos[:, None] - kv_pos[None, :] # [Lq, Lkv]
41
+ gather = mx.clip(distance, 0, self.rel_extent - 1) # [Lq, Lkv]
42
+ gather = mx.broadcast_to(gather[None, None], (B, H, Lq, gather.shape[-1]))
43
+ bias = mx.take_along_axis(rel_logits, gather, axis=-1) # [B, H, Lq, Lkv]
44
+ valid = (distance >= 0) & (distance < self.rel_extent) # [Lq, Lkv]
45
+ return mx.where(valid[None, None], bias, 0.0)
46
+
47
+
48
+ class Attention(nn.Module):
49
+ def __init__(self, config: TextConfig, layer_idx: int):
50
+ super().__init__()
51
+ self.config = config
52
+ self.layer_idx = layer_idx
53
+ self.is_sliding = config.layer_types[layer_idx] == "hybrid_sliding"
54
+
55
+ self.head_dim = config.swa_head_dim if self.is_sliding else config.head_dim
56
+ self.num_heads = config.swa_num_attention_heads if self.is_sliding else config.num_attention_heads
57
+ self.num_kv_heads = config.swa_num_key_value_heads if self.is_sliding else config.num_key_value_heads
58
+ self.n_rep = self.num_heads // self.num_kv_heads
59
+ self.sliding_window = config.sliding_window_size if self.is_sliding else None
60
+ self.rel_extent = config.sliding_window_size if self.is_sliding else config.rel_extent
61
+ self.d_rel = config.d_rel
62
+ # q/k are per-head RMS-normalized, hence 1/d rather than 1/sqrt(d)
63
+ self.scaling = 1.0 / self.head_dim
64
+
65
+ h = config.hidden_size
66
+ self.wq_du = nn.Linear(h, self.num_heads * self.head_dim, bias=False)
67
+ self.wk_dv = nn.Linear(h, self.num_kv_heads * self.head_dim, bias=False)
68
+ self.wv_dv = nn.Linear(h, self.num_kv_heads * self.head_dim, bias=False)
69
+ self.wr_du = nn.Linear(h, self.num_heads * self.d_rel, bias=False)
70
+ self.wo_ud = nn.Linear(self.num_heads * self.head_dim, h, bias=False)
71
+
72
+ self.k_sconv = ShortConvolution(self.num_kv_heads * self.head_dim, config.sconv_kernel_size)
73
+ self.v_sconv = ShortConvolution(self.num_kv_heads * self.head_dim, config.sconv_kernel_size)
74
+ self.q_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
75
+ self.k_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
76
+ self.rel_logits_proj = RelativeLogits(self.d_rel, self.rel_extent)
77
+
78
+ def __call__(self, hidden_states, start_pos=0, kv_cache=None,
79
+ k_conv=None, v_conv=None, conv_mask=None):
80
+ B, L, _ = hidden_states.shape
81
+
82
+ q = self.wq_du(hidden_states)
83
+ k = self.k_sconv(self.wk_dv(hidden_states), mask=conv_mask, cache=k_conv)
84
+ v = self.v_sconv(self.wv_dv(hidden_states), mask=conv_mask, cache=v_conv)
85
+ rel = self.wr_du(hidden_states)
86
+
87
+ q = self.q_norm(q.reshape(B, L, self.num_heads, self.head_dim))
88
+ k = self.k_norm(k.reshape(B, L, self.num_kv_heads, self.head_dim))
89
+ v = v.reshape(B, L, self.num_kv_heads, self.head_dim)
90
+
91
+ # -> [B, heads, L, head_dim]
92
+ q = q.transpose(0, 2, 1, 3)
93
+ k = k.transpose(0, 2, 1, 3)
94
+ v = v.transpose(0, 2, 1, 3)
95
+
96
+ q_pos = mx.arange(L) + start_pos
97
+ if kv_cache is not None:
98
+ k, v = kv_cache.update(k, v) # full history
99
+ kv_pos = mx.arange(k.shape[2])
100
+
101
+ rel = rel.reshape(B, L, self.num_heads, self.d_rel)
102
+ position_bias = self.rel_logits_proj(rel, q_pos, kv_pos) # [B, heads, Lq, Lkv]
103
+
104
+ # log-scaling (global layers only; no-op for context <= n_floor)
105
+ if not self.is_sliding and self.config.log_scaling_n_floor is not None:
106
+ n_floor = self.config.log_scaling_n_floor
107
+ eff_n = (q_pos + 1).astype(mx.float32)
108
+ tau = 1.0 + self.config.log_scaling_alpha * mx.log(
109
+ mx.maximum(eff_n / n_floor, 1.0)
110
+ )
111
+ tau_q = tau.reshape(1, 1, -1, 1)
112
+ q = (q.astype(mx.float32) * tau_q).astype(q.dtype)
113
+ position_bias = (position_bias.astype(mx.float32) * tau_q).astype(position_bias.dtype)
114
+
115
+ # Fused SDPA: O = softmax(scale·QKᵀ + mask)·V, softmax in fp32, native GQA
116
+ # (k/v passed un-tiled). mask = relative-position bias + causal/sliding, additive.
117
+ mask = position_bias + self._causal_mask(q_pos, kv_pos) # [B, heads, Lq, Lkv]
118
+ out = mx.fast.scaled_dot_product_attention(
119
+ q, k, v, scale=self.scaling, mask=mask.astype(q.dtype)
120
+ ) # [B, heads, Lq, head_dim]
121
+
122
+ out = out.transpose(0, 2, 1, 3).reshape(B, L, self.num_heads * self.head_dim)
123
+ return self.wo_ud(out)
124
+
125
+ def _causal_mask(self, q_pos, kv_pos):
126
+ distance = q_pos[:, None] - kv_pos[None, :] # [Lq, Lkv]
127
+ allowed = distance >= 0
128
+ if self.sliding_window is not None:
129
+ allowed = allowed & (distance < self.sliding_window)
130
+ mask = mx.where(allowed, 0.0, NEG_INF)
131
+ return mask[None, None].astype(mx.float32)
inkling_mlx/audio.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inkling audio tower: discrete dMel-token embedding + norm.
2
+
3
+ Each audio frame is ``n_mel_bins`` discretized bins (values in ``[0, mel_vocab_size)``);
4
+ each bin is embedded from its own slice of a shared table (offset ``bin * mel_vocab_size``)
5
+ and the per-bin embeddings are summed. Mirrors ``InklingAudioModel`` /
6
+ ``InklingAudioModelEmbeddings``. Checkpoint keys: ``audio.encoder.weight`` (the
7
+ ``[n_mel_bins*mel_vocab_size, hidden]`` table) and ``audio.final_norm.weight``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import mlx.core as mx
13
+ import mlx.nn as nn
14
+
15
+ from .common import RMSNorm
16
+ from .config import AudioConfig
17
+
18
+
19
+ class AudioModel(nn.Module):
20
+ def __init__(self, config: AudioConfig):
21
+ super().__init__()
22
+ self.config = config
23
+ self.encoder = nn.Embedding(
24
+ config.n_mel_bins * config.mel_vocab_size, config.text_hidden_size
25
+ )
26
+ self.final_norm = RMSNorm(config.text_hidden_size, eps=config.rms_norm_eps)
27
+ # non-persistent: arange(n_mel_bins) * mel_vocab_size
28
+ self._offsets = mx.arange(config.n_mel_bins) * config.mel_vocab_size
29
+
30
+ def __call__(self, audio_input_ids: mx.array) -> mx.array:
31
+ # audio_input_ids: [..., n_mel_bins] with values in [0, mel_vocab_size)
32
+ embeds = self.encoder(audio_input_ids + self._offsets) # [..., n_mel_bins, hidden]
33
+ embeds = embeds.sum(axis=-2) # [..., hidden]
34
+ return self.final_norm(embeds)
inkling_mlx/cache.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Incremental caches for Inkling generation.
2
+
3
+ Two kinds of per-layer state must persist across decode steps:
4
+
5
+ * ``KVCache`` — the appended key/value tensors for attention.
6
+ * ``ConvCache`` — the last ``kernel-1`` inputs of each depthwise short-convolution
7
+ (there are 4 per layer: k, v, post-attn, post-mlp).
8
+
9
+ A ``LayerCache`` bundles one KVCache + the 4 ConvCaches; ``make_cache`` builds one
10
+ per decoder layer. Absolute key positions are always ``arange(kv_len)`` because the
11
+ cache holds every key from position 0 (KVCache keeps the full history — correct for
12
+ both global and sliding layers, since the sliding-window constraint is enforced by
13
+ the attention mask).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import mlx.core as mx
19
+
20
+
21
+ class ConvCache:
22
+ """Holds the last ``kernel-1`` inputs of a short convolution."""
23
+
24
+ __slots__ = ("state",)
25
+
26
+ def __init__(self):
27
+ self.state = None # [B, kernel-1, C] or None
28
+
29
+
30
+ class KVCache:
31
+ """Appends keys/values along the sequence axis (full history)."""
32
+
33
+ __slots__ = ("keys", "values")
34
+
35
+ def __init__(self):
36
+ self.keys = None # [B, heads, T, d]
37
+ self.values = None
38
+
39
+ @property
40
+ def offset(self) -> int:
41
+ return 0 if self.keys is None else self.keys.shape[2]
42
+
43
+ def update(self, k: mx.array, v: mx.array):
44
+ if self.keys is None:
45
+ self.keys, self.values = k, v
46
+ else:
47
+ self.keys = mx.concatenate([self.keys, k], axis=2)
48
+ self.values = mx.concatenate([self.values, v], axis=2)
49
+ return self.keys, self.values
50
+
51
+
52
+ class LayerCache:
53
+ __slots__ = ("kv", "k_conv", "v_conv", "attn_conv", "mlp_conv")
54
+
55
+ def __init__(self):
56
+ self.kv = KVCache()
57
+ self.k_conv = ConvCache()
58
+ self.v_conv = ConvCache()
59
+ self.attn_conv = ConvCache()
60
+ self.mlp_conv = ConvCache()
61
+
62
+
63
+ def make_cache(model) -> list[LayerCache]:
64
+ """One LayerCache per text decoder layer."""
65
+ n = len(model.model.llm.layers) if hasattr(model, "model") else len(model.layers)
66
+ return [LayerCache() for _ in range(n)]
inkling_mlx/common.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared low-level modules for the Inkling MLX port."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import mlx.core as mx
6
+ import mlx.nn as nn
7
+
8
+
9
+ class RMSNorm(nn.Module):
10
+ """Llama-style RMSNorm (compute in fp32, weight is a gain).
11
+
12
+ Matches ``LlamaRMSNorm``: ``x_fp32 * rsqrt(mean(x^2) + eps) * weight``.
13
+ """
14
+
15
+ def __init__(self, dims: int, eps: float = 1e-6):
16
+ super().__init__()
17
+ self.weight = mx.ones((dims,))
18
+ self.eps = eps
19
+
20
+ def __call__(self, x: mx.array) -> mx.array:
21
+ return mx.fast.rms_norm(x, self.weight, self.eps)
22
+
23
+
24
+ class ShortConvolution(nn.Module):
25
+ """Depthwise causal 1-D convolution with a residual add, computed in fp32.
26
+
27
+ Mirrors ``InklingShortConvolution``: a per-channel (groups == channels) causal
28
+ conv1d of ``kernel_size`` taps, no bias, no activation, then ``out + input``.
29
+ The reference keeps this module in fp32 regardless of the model dtype
30
+ (``_keep_in_fp32_modules_strict``), so we upcast here too.
31
+
32
+ Weight layout (MLX ``conv1d``): ``[channels, kernel_size, 1]``.
33
+ """
34
+
35
+ def __init__(self, channels: int, kernel_size: int):
36
+ super().__init__()
37
+ self.channels = channels
38
+ self.kernel_size = kernel_size
39
+ # [C_out, K, C_in // groups] with groups == channels -> [C, K, 1]
40
+ self.weight = mx.zeros((channels, kernel_size, 1))
41
+
42
+ def __call__(self, x: mx.array, mask: mx.array | None = None, cache=None) -> mx.array:
43
+ # x: [batch, seq, channels]
44
+ in_dtype = x.dtype
45
+ xf = x.astype(mx.float32)
46
+ residual = xf
47
+ if mask is not None:
48
+ xf = xf * mask.astype(mx.float32)
49
+ k = self.kernel_size
50
+ B, seq, C = xf.shape
51
+ w = self.weight.astype(mx.float32)
52
+ if cache is not None:
53
+ # left-context = cached last (k-1) inputs (zeros on the first call);
54
+ # a "valid" conv over [left, xf] yields exactly `seq` causal outputs.
55
+ left = cache.state if cache.state is not None else mx.zeros((B, k - 1, C), dtype=mx.float32)
56
+ x_in = mx.concatenate([left, xf], axis=1)
57
+ out = mx.conv1d(x_in, w, padding=0, groups=self.channels)
58
+ cache.state = x_in[:, -(k - 1):, :]
59
+ else:
60
+ # causal: left-pad by (k-1), keep first `seq` outputs (== zero left-context)
61
+ out = mx.conv1d(xf, w, padding=k - 1, groups=self.channels)[:, :seq, :]
62
+ out = out + residual
63
+ return out.astype(in_dtype)
inkling_mlx/config.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration for the Inkling multimodal model (MLX port).
2
+
3
+ Mirrors ``thinkingmachines/Inkling`` ``config.json`` and the transformers PR #47347
4
+ reference (``InklingConfig`` / ``InklingTextConfig`` / ``InklingVisionConfig`` /
5
+ ``InklingAudioConfig``). We parse the *checkpoint* config layout (top-level
6
+ ``text_config`` / ``vision_config`` / ``audio_config`` / ``mtp_config``), not the
7
+ flattened transformers layout.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Any
14
+
15
+
16
+ def _get(d: dict, *names, default=None):
17
+ for n in names:
18
+ if n in d and d[n] is not None:
19
+ return d[n]
20
+ return default
21
+
22
+
23
+ @dataclass
24
+ class TextConfig:
25
+ hidden_size: int = 6144
26
+ num_hidden_layers: int = 66
27
+ vocab_size: int = 201024
28
+ unpadded_vocab_size: int | None = 200058
29
+
30
+ # global (full) attention
31
+ num_attention_heads: int = 64
32
+ num_key_value_heads: int = 8
33
+ head_dim: int = 128
34
+ # sliding-window attention
35
+ swa_num_attention_heads: int = 64
36
+ swa_num_key_value_heads: int = 16
37
+ swa_head_dim: int = 128
38
+ sliding_window_size: int = 512
39
+
40
+ # relative-position logits
41
+ d_rel: int = 16
42
+ rel_extent: int = 1024
43
+ log_scaling_n_floor: int | None = 128000
44
+ log_scaling_alpha: float = 0.1
45
+
46
+ rms_norm_eps: float = 1e-6
47
+ use_embed_norm: bool = True
48
+
49
+ # short convolution
50
+ sconv_kernel_size: int = 4
51
+
52
+ # dense vs MoE MLP
53
+ dense_mlp_idx: int = 2
54
+ dense_intermediate_size: int = 24576 # dense MLP intermediate
55
+ moe_intermediate_size: int = 3072 # per-expert intermediate
56
+
57
+ # MoE routing
58
+ n_routed_experts: int = 256
59
+ num_experts_per_tok: int = 6
60
+ n_shared_experts: int = 2
61
+ shared_expert_sink: bool = True
62
+ route_scale: float = 8.0
63
+ use_gate_bias: bool = True
64
+ norm_after_topk: bool = True
65
+ use_global_scale: bool = True
66
+
67
+ logits_mup_width_multiplier: float = 24.0
68
+ hidden_act: str = "silu"
69
+
70
+ max_position_embeddings: int = 1048576
71
+
72
+ # which layer indices use sliding-window ("local") attention
73
+ local_layer_ids: list[int] = field(default_factory=list)
74
+
75
+ # MTP head (dropped for inference)
76
+ num_mtp_layers: int | None = None
77
+
78
+ @property
79
+ def layer_types(self) -> list[str]:
80
+ local = set(self.local_layer_ids)
81
+ return [
82
+ "hybrid_sliding" if i in local else "hybrid"
83
+ for i in range(self.num_hidden_layers)
84
+ ]
85
+
86
+ @property
87
+ def mlp_layer_types(self) -> list[str]:
88
+ return [
89
+ "dense" if i < self.dense_mlp_idx else "sparse"
90
+ for i in range(self.num_hidden_layers)
91
+ ]
92
+
93
+ @classmethod
94
+ def from_dict(cls, tc: dict) -> "TextConfig":
95
+ return cls(
96
+ hidden_size=_get(tc, "hidden_size", default=6144),
97
+ num_hidden_layers=_get(tc, "num_hidden_layers", default=66),
98
+ vocab_size=_get(tc, "vocab_size", default=201024),
99
+ unpadded_vocab_size=_get(tc, "unpadded_vocab_size"),
100
+ num_attention_heads=_get(tc, "num_attention_heads", default=64),
101
+ num_key_value_heads=_get(tc, "num_key_value_heads", default=8),
102
+ head_dim=_get(tc, "head_dim", default=128),
103
+ swa_num_attention_heads=_get(tc, "swa_num_attention_heads", default=64),
104
+ swa_num_key_value_heads=_get(tc, "swa_num_key_value_heads", default=16),
105
+ swa_head_dim=_get(tc, "swa_head_dim", default=128),
106
+ sliding_window_size=_get(tc, "sliding_window_size", default=512),
107
+ d_rel=_get(tc, "d_rel", default=16),
108
+ rel_extent=_get(tc, "rel_extent", default=1024),
109
+ log_scaling_n_floor=_get(tc, "log_scaling_n_floor"),
110
+ log_scaling_alpha=_get(tc, "log_scaling_alpha", default=0.1),
111
+ rms_norm_eps=_get(tc, "rms_norm_eps", default=1e-6),
112
+ use_embed_norm=_get(tc, "use_embed_norm", default=True),
113
+ sconv_kernel_size=_get(tc, "sconv_kernel_size", default=4),
114
+ dense_mlp_idx=_get(tc, "dense_mlp_idx", default=2),
115
+ dense_intermediate_size=_get(tc, "dense_intermediate_size", default=24576),
116
+ # checkpoint labels the *MoE* intermediate as `intermediate_size`
117
+ moe_intermediate_size=_get(tc, "intermediate_size", default=3072),
118
+ n_routed_experts=_get(tc, "n_routed_experts", default=256),
119
+ num_experts_per_tok=_get(tc, "num_experts_per_tok", default=6),
120
+ n_shared_experts=_get(tc, "n_shared_experts", default=2),
121
+ shared_expert_sink=_get(tc, "shared_expert_sink", default=True),
122
+ route_scale=_get(tc, "route_scale", default=8.0),
123
+ use_gate_bias=_get(tc, "use_gate_bias", default=True),
124
+ norm_after_topk=_get(tc, "norm_after_topk", default=True),
125
+ use_global_scale=_get(tc, "use_global_scale", default=True),
126
+ logits_mup_width_multiplier=_get(tc, "logits_mup_width_multiplier", default=24.0),
127
+ max_position_embeddings=_get(tc, "model_max_length", "max_position_embeddings", default=1048576),
128
+ local_layer_ids=list(_get(tc, "local_layer_ids", default=[]) or []),
129
+ )
130
+
131
+
132
+ @dataclass
133
+ class VisionConfig:
134
+ text_hidden_size: int = 6144
135
+ patch_size: int = 40
136
+ temporal_patch_size: int = 2
137
+ num_channels: int = 3
138
+ n_layers: int = 4
139
+ rms_norm_eps: float = 1e-6
140
+ use_vision_norm: bool = True
141
+
142
+ @classmethod
143
+ def from_dict(cls, vc: dict, text_hidden: int) -> "VisionConfig":
144
+ return cls(
145
+ text_hidden_size=text_hidden,
146
+ patch_size=_get(vc, "patch_size", default=40),
147
+ temporal_patch_size=_get(vc, "temporal_patch_size", default=2),
148
+ num_channels=_get(vc, "n_channels", "num_channels", default=3),
149
+ n_layers=_get(vc, "n_layers", "num_hidden_layers", default=4),
150
+ rms_norm_eps=_get(vc, "rms_norm_eps", default=1e-6),
151
+ use_vision_norm=_get(vc, "use_vision_norm", default=True),
152
+ )
153
+
154
+
155
+ @dataclass
156
+ class AudioConfig:
157
+ text_hidden_size: int = 6144
158
+ n_mel_bins: int = 80
159
+ mel_vocab_size: int = 16
160
+ rms_norm_eps: float = 1e-6
161
+
162
+ @classmethod
163
+ def from_dict(cls, ac: dict, text_hidden: int) -> "AudioConfig":
164
+ return cls(
165
+ text_hidden_size=text_hidden,
166
+ n_mel_bins=_get(ac, "n_mel_bins", default=80),
167
+ mel_vocab_size=_get(ac, "mel_vocab_size", default=16),
168
+ rms_norm_eps=_get(ac, "rms_norm_eps", default=1e-6),
169
+ )
170
+
171
+
172
+ @dataclass
173
+ class InklingConfig:
174
+ text: TextConfig
175
+ vision: VisionConfig
176
+ audio: AudioConfig
177
+ image_token_id: int = 200054
178
+ audio_token_id: int = 200053
179
+ image_bos_token_id: int = 200005
180
+ audio_bos_token_id: int = 200020
181
+ eos_token_id: int = 200006
182
+ model_type: str = "inkling_mm_model"
183
+
184
+ @classmethod
185
+ def from_dict(cls, cfg: dict) -> "InklingConfig":
186
+ tc = dict(cfg.get("text_config", {}))
187
+ mtp = cfg.get("mtp_config") or {}
188
+ if mtp.get("num_nextn_predict_layers") is not None:
189
+ tc.setdefault("num_mtp_layers", mtp.get("num_nextn_predict_layers"))
190
+ text = TextConfig.from_dict(tc)
191
+ vision = VisionConfig.from_dict(cfg.get("vision_config", {}) or {}, text.hidden_size)
192
+ audio = AudioConfig.from_dict(cfg.get("audio_config", {}) or {}, text.hidden_size)
193
+ return cls(
194
+ text=text,
195
+ vision=vision,
196
+ audio=audio,
197
+ image_token_id=_get(cfg, "image_token_id", default=200054),
198
+ audio_token_id=_get(cfg, "audio_token_id", default=200053),
199
+ image_bos_token_id=_get(cfg, "image_bos_token_id", default=200005),
200
+ audio_bos_token_id=_get(cfg, "audio_bos_token_id", default=200020),
201
+ eos_token_id=_get(cfg, "eos_token_id", default=200006),
202
+ model_type=_get(cfg, "model_type", default="inkling_mm_model"),
203
+ )
204
+
205
+ @property
206
+ def raw(self) -> dict[str, Any]:
207
+ return {"model_type": self.model_type}
inkling_mlx/convert.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streaming HF -> MLX conversion + quantization for Inkling.
2
+
3
+ The model is far too large (~1.9 TB bf16) to instantiate in RAM, so we convert
4
+ tensor-by-tensor: read each source shard (mmap), remap the name, apply the layout
5
+ transform, optionally affine-quantize the weight, and write output shards. Affine
6
+ quantization has no cross-tensor dependency, so per-tensor streaming is exactly
7
+ equivalent to ``nn.quantize(model)``.
8
+
9
+ Name/layout transforms vs. the checkpoint:
10
+ * ``*_sconv.weight`` [C,1,K] -> [C,K,1] (MLX conv1d layout)
11
+ * ``mlp.w13_dn`` [2I,H] -> gate_proj/up_proj (split dense fused gate+up)
12
+ * ``experts.w13_weight`` [E,2I,H] -> gate_proj/up_proj (split)
13
+ * ``experts.w2_weight`` [E,H,I] -> down_proj (identity)
14
+ * ``shared_experts.shared_w13`` [2,2I,H] -> gate_proj/up_proj (split)
15
+ * ``model.mtp.*`` dropped (inference-irrelevant)
16
+ * everything else: identity
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import glob
22
+ import json
23
+ import os
24
+ import re
25
+ import shutil
26
+
27
+ import mlx.core as mx
28
+
29
+ _LAYER_RE = re.compile(r"model\.llm\.layers\.(\d+)\.")
30
+ _N_SHARED = 2 # Inkling: 2 shared experts (router rows after the routed ones)
31
+
32
+
33
+ def prune_moe_tensor(name: str, w: mx.array, keep, dense_mlp_idx: int) -> mx.array:
34
+ """Subset a MoE tensor to the kept experts (REAP). ``keep`` maps sparse-layer
35
+ index -> array of kept routed-expert indices. Applied BEFORE map_name/transform.
36
+ Routed experts + the router (gate) are subset; shared experts are untouched."""
37
+ m = _LAYER_RE.search(name)
38
+ if m is None:
39
+ return w
40
+ L = int(m.group(1))
41
+ if L < dense_mlp_idx: # dense layer — no routed experts
42
+ return w
43
+ kidx = mx.array(keep[L - dense_mlp_idx]) # [K]
44
+ if name.endswith(("experts.w13_weight", "experts.w2_weight")) and "shared" not in name:
45
+ return w[kidx] # [E, ...] -> [K, ...]
46
+ if name.endswith("mlp.gate.weight"): # [n_routed + n_shared, hidden]
47
+ n_routed = w.shape[0] - _N_SHARED # rows: [routed... , shared...]
48
+ return mx.concatenate([w[kidx], w[n_routed:]], axis=0) # [K + n_shared, hidden]
49
+ if name.endswith("mlp.gate.bias"): # [n_routed] correction bias
50
+ return w[kidx]
51
+ return w # shared_experts.*, gate.global_scale, etc.
52
+
53
+
54
+ def map_name(name: str):
55
+ """HF checkpoint tensor name -> list of (out_name, kind) for the MLX model."""
56
+ if name.startswith("model.mtp."):
57
+ return [] # drop MTP head
58
+
59
+ if name.endswith(("k_sconv.weight", "v_sconv.weight", "attn_sconv.weight", "mlp_sconv.weight")):
60
+ return [(name, "sconv")]
61
+
62
+ # dense MLP fused gate+up / down
63
+ if name.endswith("mlp.w13_dn.weight"):
64
+ base = name[: -len("w13_dn.weight")]
65
+ return [(base + "gate_proj.weight", "w13_gate"), (base + "up_proj.weight", "w13_up")]
66
+ if name.endswith("mlp.w2_md.weight"):
67
+ return [(name[: -len("w2_md.weight")] + "down_proj.weight", "identity")]
68
+
69
+ # routed experts fused
70
+ if name.endswith("experts.w13_weight"):
71
+ base = name[: -len("w13_weight")]
72
+ return [(base + "gate_proj.weight", "w13_gate"), (base + "up_proj.weight", "w13_up")]
73
+ if name.endswith("experts.w2_weight"):
74
+ return [(name[: -len("w2_weight")] + "down_proj.weight", "identity")]
75
+
76
+ # shared experts fused
77
+ if name.endswith("shared_experts.shared_w13_weight"):
78
+ base = name[: -len("shared_w13_weight")]
79
+ return [(base + "gate_proj.weight", "w13_gate"), (base + "up_proj.weight", "w13_up")]
80
+ if name.endswith("shared_experts.shared_w2_weight"):
81
+ return [(name[: -len("shared_w2_weight")] + "down_proj.weight", "identity")]
82
+
83
+ return [(name, "identity")]
84
+
85
+
86
+ def transform(w: mx.array, kind: str) -> mx.array:
87
+ if kind == "identity":
88
+ return w
89
+ if kind == "sconv":
90
+ # [C, 1, K] -> [C, K, 1]
91
+ return mx.swapaxes(w, 1, 2)
92
+ if kind in ("w13_gate", "w13_up"):
93
+ # The checkpoint stores gate/up INTERLEAVED row-wise: [g0, u0, g1, u1, ...]
94
+ # (SGLang `deinterleave_w13`). De-interleave: gate = rows 0::2, up = rows 1::2.
95
+ # A contiguous [:half]/[half:] split scrambles gate<->up in every MLP.
96
+ n = w.shape[-2] // 2
97
+ g = w.reshape(*w.shape[:-2], n, 2, w.shape[-1])
98
+ return g[..., 0, :] if kind == "w13_gate" else g[..., 1, :]
99
+ raise ValueError(kind)
100
+
101
+
102
+ # ---- quantization target predicate (must be identical in convert and load) ----
103
+
104
+ # Quant "recipes" — which module leaves get affine-quantized.
105
+ # uniform : everything (attention, MLP/experts, embed/unembed, audio, vision)
106
+ # experts_only : ONLY the MLP/expert matmuls (+ audio/vision); attention and
107
+ # embed/unembed stay bf16. Inkling attention dominates 4-bit error
108
+ # (~58% per layer vs ~15% for experts), so this keeps a 4-bit-sized
109
+ # build coherent while the ~927 B experts still fit in 512 GB.
110
+ _RECIPES = {
111
+ "uniform": {"wq_du", "wk_dv", "wv_dv", "wr_du", "wo_ud",
112
+ "gate_proj", "up_proj", "down_proj", "embed", "unembed", "encoder"},
113
+ "experts_only": {"gate_proj", "up_proj", "down_proj", "encoder"},
114
+ }
115
+
116
+
117
+ def is_quant_target(out_name: str, quant_axis_size: int, group_size: int, recipe: str = "uniform") -> bool:
118
+ """Whether ``out_name`` (a converted param path) should be affine-quantized."""
119
+ if not out_name.endswith(".weight"):
120
+ return False
121
+ leaf = out_name[: -len(".weight")].rsplit(".", 1)[-1]
122
+ leaves = _RECIPES[recipe]
123
+ # vision projection layers (linear_0 .. linear_3) — quantized in both recipes
124
+ is_vision_linear = leaf.startswith("linear_") and ".visual." in out_name
125
+ if leaf not in leaves and not is_vision_linear:
126
+ return False
127
+ # router gate stays fp (leaf == "gate", excluded above); norms/sconv excluded by leaf
128
+ # can only group-quantize when the input dim is a multiple of group_size
129
+ return quant_axis_size % group_size == 0
130
+
131
+
132
+ # ------------------------------ streaming driver ------------------------------
133
+
134
+ _SHARD_CAP_BYTES = 5_000_000_000 # ~5 GB per output shard
135
+
136
+
137
+ def _process_tensor(name, w, bits, group_size, out_dtype, recipe="uniform", keep=None, dmi=2):
138
+ """Yield (out_name, array) pairs for one source tensor."""
139
+ if keep is not None:
140
+ w = prune_moe_tensor(name, w, keep, dmi) # REAP: subset to kept experts
141
+ for out_name, kind in map_name(name):
142
+ wt = transform(w, kind)
143
+ quantize = bits is not None and is_quant_target(out_name, wt.shape[-1], group_size, recipe)
144
+ if quantize:
145
+ qw, scales, biases = mx.quantize(wt, group_size=group_size, bits=bits)
146
+ base = out_name[: -len(".weight")]
147
+ yield out_name, qw
148
+ yield base + ".scales", scales
149
+ yield base + ".biases", biases
150
+ else:
151
+ # keep norms/router/sconv/rel-proj in fp32-safe dtype; matmul weights in out_dtype
152
+ keep_hi = wt.dtype == mx.float32 and (".global_scale" in out_name or ".bias" in out_name
153
+ or out_name.endswith(("_norm.weight", "norm.weight")))
154
+ yield out_name, wt.astype(mx.float32 if keep_hi else out_dtype)
155
+
156
+
157
+ def convert_model(src: str, dst: str, bits=None, group_size: int = 64, out_dtype=mx.bfloat16,
158
+ recipe: str = "uniform", keep_path=None):
159
+ """Stream-convert an Inkling checkpoint from ``src`` to ``dst``.
160
+
161
+ ``bits=None`` -> plain dtype cast (bf16). ``bits in {4,6,8}`` -> affine quant.
162
+ ``recipe`` selects which modules are quantized (see ``_RECIPES``).
163
+ ``keep_path`` (REAP): npz with ``keep`` [n_sparse_layers, K] + ``dense_mlp_idx`` ->
164
+ prune each MoE layer to its K kept experts and set ``n_routed_experts=K`` in config.
165
+ Processes one source shard at a time; never holds the whole model in RAM.
166
+ """
167
+ os.makedirs(dst, exist_ok=True)
168
+ keep = dmi = new_ne = None
169
+ if keep_path is not None:
170
+ import numpy as np
171
+ kd = np.load(keep_path)
172
+ keep = kd["keep"]; dmi = int(kd["dense_mlp_idx"]); new_ne = int(kd["K"])
173
+ index = json.load(open(os.path.join(src, "model.safetensors.index.json")))
174
+ weight_map = index["weight_map"]
175
+
176
+ shard_to_names: dict[str, list[str]] = {}
177
+ for n, s in weight_map.items():
178
+ shard_to_names.setdefault(s, []).append(n)
179
+
180
+ out_index: dict[str, str] = {}
181
+ buffer: dict[str, mx.array] = {}
182
+ buffer_bytes = 0
183
+ out_shard_id = 0
184
+ total_out_shards_placeholder = "{:05d}"
185
+
186
+ def flush(final=False):
187
+ nonlocal buffer, buffer_bytes, out_shard_id
188
+ if not buffer:
189
+ return
190
+ out_shard_id += 1
191
+ fname = f"model-{total_out_shards_placeholder.format(out_shard_id)}.safetensors"
192
+ mx.save_safetensors(os.path.join(dst, fname), buffer, metadata={"format": "mlx"})
193
+ for k in buffer:
194
+ out_index[k] = fname
195
+ buffer = {}
196
+ buffer_bytes = 0
197
+
198
+ for shard in sorted(shard_to_names):
199
+ path = os.path.join(src, shard)
200
+ tensors = mx.load(path) # mmap
201
+ for name in shard_to_names[shard]:
202
+ w = tensors[name]
203
+ for out_name, arr in _process_tensor(name, w, bits, group_size, out_dtype, recipe, keep, dmi):
204
+ mx.eval(arr)
205
+ buffer[out_name] = arr
206
+ buffer_bytes += arr.nbytes
207
+ if buffer_bytes >= _SHARD_CAP_BYTES:
208
+ flush()
209
+ del tensors
210
+ flush(final=True)
211
+
212
+ # rename shards with correct total, build index.json
213
+ _finalize_index(dst, out_index, out_shard_id)
214
+ _write_config(src, dst, bits, group_size, recipe, new_ne)
215
+ _copy_aux(src, dst)
216
+ return dst
217
+
218
+
219
+ def _finalize_index(dst, out_index, n_shards):
220
+ # rewrite shard filenames to model-XXXXX-of-YYYYY.safetensors
221
+ remap = {}
222
+ for i in range(1, n_shards + 1):
223
+ old = f"model-{i:05d}.safetensors"
224
+ new = f"model-{i:05d}-of-{n_shards:05d}.safetensors"
225
+ if old != new and os.path.exists(os.path.join(dst, old)):
226
+ os.rename(os.path.join(dst, old), os.path.join(dst, new))
227
+ remap[old] = new
228
+ weight_map = {k: remap[v] for k, v in out_index.items()}
229
+ total = sum(os.path.getsize(os.path.join(dst, f)) for f in set(weight_map.values()))
230
+ with open(os.path.join(dst, "model.safetensors.index.json"), "w") as f:
231
+ json.dump({"metadata": {"total_size": total}, "weight_map": weight_map}, f, indent=2)
232
+
233
+
234
+ def _write_config(src, dst, bits, group_size, recipe="uniform", new_ne=None):
235
+ cfg = json.load(open(os.path.join(src, "config.json")))
236
+ if bits is not None:
237
+ cfg["quantization"] = {"group_size": group_size, "bits": bits, "recipe": recipe}
238
+ if new_ne is not None: # REAP: fewer routed experts
239
+ cfg["text_config"]["n_routed_experts"] = new_ne
240
+ cfg.setdefault("reap", {})["kept_experts"] = new_ne
241
+ with open(os.path.join(dst, "config.json"), "w") as f:
242
+ json.dump(cfg, f, indent=2)
243
+
244
+
245
+ def _copy_aux(src, dst):
246
+ for pat in ("tokenizer*", "special_tokens_map.json", "*.tiktoken", "tiktoken",
247
+ "chat_template.jinja", "processor_config.json"):
248
+ for p in glob.glob(os.path.join(src, pat)):
249
+ base = os.path.basename(p)
250
+ target = os.path.join(dst, base)
251
+ if os.path.isdir(p):
252
+ shutil.copytree(p, target, dirs_exist_ok=True)
253
+ else:
254
+ shutil.copy2(p, target)
inkling_mlx/convert_cli.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CLI: convert/quantize an Inkling checkpoint to MLX.
2
+
3
+ Examples:
4
+ python -m inkling_mlx.convert_cli --src /path/Inkling-src --dst out-bf16
5
+ python -m inkling_mlx.convert_cli --src /path/Inkling-src --dst out-4bit --bits 4
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import time
12
+
13
+ import mlx.core as mx
14
+
15
+ from .convert import convert_model
16
+
17
+
18
+ def main():
19
+ ap = argparse.ArgumentParser()
20
+ ap.add_argument("--src", required=True, help="Inkling bf16 source dir (HF layout)")
21
+ ap.add_argument("--dst", required=True, help="output dir")
22
+ ap.add_argument("--bits", type=int, default=None, choices=[2, 3, 4, 5, 6, 8],
23
+ help="quantization bits; omit for bf16 passthrough")
24
+ ap.add_argument("--group-size", type=int, default=64)
25
+ ap.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "float16"])
26
+ ap.add_argument("--device", default="gpu", choices=["gpu", "cpu"],
27
+ help="cpu avoids the Metal GPU-timeout watchdog on huge tensors (slower, robust)")
28
+ ap.add_argument("--recipe", default="uniform", choices=["uniform", "experts_only"],
29
+ help="experts_only keeps attention + embed/unembed at bf16 (coherent 4-bit-sized build)")
30
+ ap.add_argument("--prune", default=None,
31
+ help="REAP keep-indices npz (from prune_experts.py) -> prune experts during convert")
32
+ args = ap.parse_args()
33
+
34
+ if args.device == "cpu":
35
+ mx.set_default_device(mx.cpu)
36
+ print("[convert] using CPU device (avoids Metal command-buffer timeout)")
37
+
38
+ dtype = {"bfloat16": mx.bfloat16, "float16": mx.float16}[args.dtype]
39
+ t0 = time.time()
40
+ print(f"[convert] {args.src} -> {args.dst} bits={args.bits} group_size={args.group_size} dtype={args.dtype} recipe={args.recipe} prune={args.prune}")
41
+ convert_model(args.src, args.dst, bits=args.bits, group_size=args.group_size, out_dtype=dtype,
42
+ recipe=args.recipe, keep_path=args.prune)
43
+ print(f"[convert] done in {time.time()-t0:.0f}s -> {args.dst}")
44
+
45
+
46
+ if __name__ == "__main__":
47
+ main()
inkling_mlx/generate.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Greedy generation for an Inkling MLX model, using an incremental KV + conv-state
2
+ cache: the prompt is prefilled once, then each new token is a single-position step.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import time
9
+
10
+ import mlx.core as mx
11
+
12
+ from .cache import make_cache
13
+ from .load import load
14
+
15
+
16
+ def load_tokenizer(path: str):
17
+ try:
18
+ from transformers import AutoTokenizer
19
+ return AutoTokenizer.from_pretrained(path, trust_remote_code=True)
20
+ except Exception:
21
+ from transformers import PreTrainedTokenizerFast
22
+ import os
23
+ return PreTrainedTokenizerFast(tokenizer_file=os.path.join(path, "tokenizer.json"))
24
+
25
+
26
+ def greedy_generate(model, config, input_ids, max_new_tokens=32, eos_id=None,
27
+ pixel_values=None, audio_input_ids=None):
28
+ """Greedy decode. For multimodal, pass ``pixel_values`` / ``audio_input_ids``
29
+ (from ``InklingProcessor``); they are consumed only by the prompt prefill."""
30
+ eos_id = eos_id if eos_id is not None else config.eos_token_id
31
+ caches = make_cache(model)
32
+ prompt = list(input_ids)
33
+
34
+ # prefill the whole prompt (with any media) in one pass
35
+ logits = model(mx.array([prompt]), caches=caches, start_pos=0, last_logit_only=True,
36
+ pixel_values=pixel_values, audio_input_ids=audio_input_ids)
37
+ next_id = int(mx.argmax(logits[0, -1]).item())
38
+ out = [next_id]
39
+ pos = len(prompt)
40
+
41
+ for _ in range(max_new_tokens - 1):
42
+ if next_id == eos_id:
43
+ break
44
+ logits = model(mx.array([[next_id]]), caches=caches, start_pos=pos, last_logit_only=True)
45
+ next_id = int(mx.argmax(logits[0, -1]).item())
46
+ out.append(next_id)
47
+ pos += 1
48
+
49
+ return prompt + out
50
+
51
+
52
+ def main():
53
+ ap = argparse.ArgumentParser()
54
+ ap.add_argument("--model", required=True, help="converted MLX model dir")
55
+ ap.add_argument("--prompt", default="The capital of France is")
56
+ ap.add_argument("--max-new-tokens", type=int, default=32)
57
+ ap.add_argument("--wired-limit-gb", type=float, default=500.0,
58
+ help="mx wired-memory limit; needs `sudo sysctl iogpu.wired_limit_mb` set too")
59
+ ap.add_argument("--lazy", action="store_true",
60
+ help="mmap weights instead of eager-loading (lower peak RAM, but forwards "
61
+ "re-read from disk and thrash near the memory ceiling)")
62
+ args = ap.parse_args()
63
+
64
+ # eager load pins the weights wired-resident so prefill/decode don't re-read the
65
+ # mmap (the big win for near-capacity models); pass --lazy to opt out.
66
+ try:
67
+ mx.set_wired_limit(int(args.wired_limit_gb * 1e9))
68
+ except Exception as e:
69
+ print(f"[warn] set_wired_limit: {e}")
70
+
71
+ print(f"[load] {args.model} ({'lazy mmap' if args.lazy else 'eager, wired-resident'})")
72
+ t0 = time.time()
73
+ model, config = load(args.model, lazy=args.lazy)
74
+ print(f"[load] ready in {time.time()-t0:.0f}s")
75
+
76
+ tok = load_tokenizer(args.model)
77
+ input_ids = tok(args.prompt)["input_ids"]
78
+ print(f"[prompt] {args.prompt!r} -> {len(input_ids)} tokens")
79
+
80
+ t0 = time.time()
81
+ out_ids = greedy_generate(model, config, input_ids, args.max_new_tokens)
82
+ dt = time.time() - t0
83
+ text = tok.decode(out_ids)
84
+ n_new = len(out_ids) - len(input_ids)
85
+ print(f"\n{text}\n")
86
+ print(f"[gen] {n_new} tokens in {dt:.1f}s ({n_new/dt:.2f} tok/s)")
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()
inkling_mlx/layers.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inkling decoder layer: attention + MLP, each wrapped by a pre-norm and a
2
+ trailing short-convolution, with residual adds. Mirrors ``InklingDecoderLayer``.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import mlx.core as mx
8
+ import mlx.nn as nn
9
+
10
+ from .attention import Attention
11
+ from .common import RMSNorm, ShortConvolution
12
+ from .config import TextConfig
13
+ from .moe import DenseMLP, MoE
14
+
15
+
16
+ class DecoderLayer(nn.Module):
17
+ def __init__(self, config: TextConfig, layer_idx: int):
18
+ super().__init__()
19
+ self.attn = Attention(config, layer_idx)
20
+ self.attn_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
21
+ self.mlp_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
22
+ if config.mlp_layer_types[layer_idx] == "sparse":
23
+ self.mlp = MoE(config)
24
+ else:
25
+ self.mlp = DenseMLP(config)
26
+ self.attn_sconv = ShortConvolution(config.hidden_size, config.sconv_kernel_size)
27
+ self.mlp_sconv = ShortConvolution(config.hidden_size, config.sconv_kernel_size)
28
+
29
+ def __call__(self, x, start_pos=0, cache=None, conv_mask=None):
30
+ kv = cache.kv if cache is not None else None
31
+ residual = x
32
+ h = self.attn_norm(x)
33
+ h = self.attn(
34
+ h, start_pos=start_pos, kv_cache=kv,
35
+ k_conv=cache.k_conv if cache is not None else None,
36
+ v_conv=cache.v_conv if cache is not None else None,
37
+ conv_mask=conv_mask,
38
+ )
39
+ h = self.attn_sconv(h, mask=conv_mask, cache=cache.attn_conv if cache is not None else None)
40
+ x = residual + h
41
+
42
+ residual = x
43
+ h = self.mlp_norm(x)
44
+ h = self.mlp(h)
45
+ h = self.mlp_sconv(h, mask=conv_mask, cache=cache.mlp_conv if cache is not None else None)
46
+ x = residual + h
47
+ return x
inkling_mlx/load.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load a (possibly quantized) Inkling MLX model produced by ``convert_model``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import glob
6
+ import json
7
+ import os
8
+
9
+ import mlx.core as mx
10
+ import mlx.nn as nn
11
+ from mlx.utils import tree_flatten
12
+
13
+ from .config import InklingConfig
14
+ from .model import InklingForConditionalGeneration
15
+
16
+
17
+ def quant_predicate(group_size: int, recipe: str = "uniform"):
18
+ """Quantize exactly the modules the converter did, by delegating to
19
+ ``convert.is_quant_target`` with the same ``recipe``. Guarantees the loaded
20
+ module set matches the checkpoint (e.g. under ``experts_only``, attention and
21
+ embed/unembed stay bf16 and must NOT be re-quantized here)."""
22
+ from .convert import is_quant_target
23
+
24
+ def pred(path, module):
25
+ if not hasattr(module, "to_quantized"):
26
+ return False
27
+ w = getattr(module, "weight", None)
28
+ if w is None:
29
+ return False
30
+ return is_quant_target(path + ".weight", w.shape[-1], group_size, recipe)
31
+
32
+ return pred
33
+
34
+
35
+ def load(path: str, lazy: bool = False):
36
+ cfg_dict = json.load(open(os.path.join(path, "config.json")))
37
+ config = InklingConfig.from_dict(cfg_dict)
38
+ model = InklingForConditionalGeneration(config)
39
+
40
+ q = cfg_dict.get("quantization")
41
+ if q:
42
+ nn.quantize(model, group_size=q["group_size"], bits=q["bits"],
43
+ class_predicate=quant_predicate(q["group_size"], q.get("recipe", "uniform")))
44
+
45
+ # Stream shards: assign each, then release its handle. We do NOT eagerly
46
+ # mx.eval() the whole parameter tree — for a ~500 GB model that builds one
47
+ # enormous eval graph and trips a Metal resource limit. Weights stay lazy
48
+ # (mmap-backed) and materialize on demand during the forward pass, exactly
49
+ # like mlx-lm loads large models.
50
+ loaded = set()
51
+ shards = sorted(glob.glob(os.path.join(path, "*.safetensors")))
52
+ for shard in shards:
53
+ w = mx.load(shard)
54
+ model.load_weights(list(w.items()), strict=False)
55
+ if not lazy:
56
+ # materialize THIS shard's tensors now (bounded graph) and keep them
57
+ # resident. Avoids one enormous eval over all ~500 GB of params, which
58
+ # trips a Metal resource limit; also prevents per-token disk paging.
59
+ mx.eval(list(w.values()))
60
+ loaded.update(w.keys())
61
+ del w
62
+
63
+ expected = {k for k, _ in tree_flatten(model.parameters())}
64
+ missing = expected - loaded
65
+ if missing:
66
+ raise ValueError(f"{len(missing)} params not found in checkpoint, e.g. {sorted(missing)[:3]}")
67
+
68
+ model.eval()
69
+ return model, config
inkling_mlx/model.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Top-level Inkling multimodal model.
2
+
3
+ Checkpoint layout: ``model.llm.*`` (text backbone + untied unembed), ``model.visual.*``
4
+ (HMLP vision tower), ``model.audio.*`` (dMel audio tower). Image/audio features are
5
+ scattered into the token-embedding stream at their placeholder-token positions, then
6
+ the text backbone runs and the untied unembed head produces (muP-scaled) logits.
7
+ The MTP head (``model.mtp.*``) is intentionally not loaded (inference-irrelevant).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import mlx.core as mx
13
+ import mlx.nn as nn
14
+ import numpy as np
15
+
16
+ from .audio import AudioModel
17
+ from .config import InklingConfig
18
+ from .text import TextModel
19
+ from .vision import VisionModel
20
+
21
+
22
+ def _scatter_features(embeds, input_ids, token_id, features):
23
+ """Replace ``embeds`` rows where ``input_ids == token_id`` with ``features``
24
+ (in sequence order). ``input_ids`` is host-known so we resolve positions on CPU."""
25
+ B, L, H = embeds.shape
26
+ ids = np.array(input_ids).reshape(-1)
27
+ pos = np.nonzero(ids == token_id)[0]
28
+ if pos.size == 0:
29
+ return embeds
30
+ flat = embeds.reshape(B * L, H)
31
+ flat[mx.array(pos)] = features.astype(flat.dtype)
32
+ return flat.reshape(B, L, H)
33
+
34
+
35
+ class InnerModel(nn.Module):
36
+ """The ``model.`` level holding the three towers."""
37
+
38
+ def __init__(self, config: InklingConfig):
39
+ super().__init__()
40
+ self.llm = TextModel(config.text)
41
+ self.visual = VisionModel(config.vision)
42
+ self.audio = AudioModel(config.audio)
43
+
44
+
45
+ class InklingForConditionalGeneration(nn.Module):
46
+ def __init__(self, config: InklingConfig):
47
+ super().__init__()
48
+ self.config = config
49
+ self.model = InnerModel(config)
50
+
51
+ # --- convenience accessors ---
52
+ @property
53
+ def llm(self) -> TextModel:
54
+ return self.model.llm
55
+
56
+ def __call__(
57
+ self,
58
+ input_ids: mx.array,
59
+ pixel_values: mx.array | None = None,
60
+ audio_input_ids: mx.array | None = None,
61
+ conv_mask=None,
62
+ caches=None,
63
+ start_pos: int = 0,
64
+ last_logit_only: bool = False,
65
+ ) -> mx.array:
66
+ embeds = self.model.llm.embed_tokens(input_ids)
67
+
68
+ if pixel_values is not None:
69
+ img = self.model.visual(pixel_values)
70
+ embeds = _scatter_features(embeds, input_ids, self.config.image_token_id, img)
71
+
72
+ if audio_input_ids is not None:
73
+ aud = self.model.audio(audio_input_ids)
74
+ embeds = _scatter_features(embeds, input_ids, self.config.audio_token_id, aud)
75
+
76
+ hidden = self.model.llm.backbone(embeds, conv_mask=conv_mask, caches=caches, start_pos=start_pos)
77
+ if last_logit_only:
78
+ hidden = hidden[:, -1:, :]
79
+ return self.model.llm.logits(hidden)
inkling_mlx/moe.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inkling MLP variants: dense SwiGLU (with a learned output scale) and the
2
+ sparse MoE (sigmoid router with correction bias, softmax-over-selected weights,
3
+ route/global scaling, and 2 always-on shared experts forming a routing "sink").
4
+
5
+ Mirrors ``InklingMLP`` / ``InklingTopkRouter`` / ``InklingExperts`` /
6
+ ``InklingSharedExperts`` / ``InklingMoE`` from transformers PR #47347.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import mlx.core as mx
12
+ import mlx.nn as nn
13
+
14
+ from mlx_lm.models.switch_layers import SwitchGLU
15
+
16
+ from .config import TextConfig
17
+
18
+
19
+ class DenseMLP(nn.Module):
20
+ """SwiGLU MLP with a learned scalar output gain (``global_scale``).
21
+
22
+ The checkpoint fuses gate+up into ``w13_dn``; the converter splits it into
23
+ ``gate_proj``/``up_proj`` so the standard MLX quantizer sees plain ``nn.Linear``s.
24
+ """
25
+
26
+ def __init__(self, config: TextConfig):
27
+ super().__init__()
28
+ h = config.hidden_size
29
+ inter = config.dense_intermediate_size
30
+ self.gate_proj = nn.Linear(h, inter, bias=False)
31
+ self.up_proj = nn.Linear(h, inter, bias=False)
32
+ self.down_proj = nn.Linear(inter, h, bias=False)
33
+ self.global_scale = mx.ones((1,))
34
+
35
+ def __call__(self, x: mx.array) -> mx.array:
36
+ y = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
37
+ return y * self.global_scale
38
+
39
+
40
+ class Router(nn.Module):
41
+ """Sigmoid top-k router with a correction bias and a shared-expert sink.
42
+
43
+ Kept in full precision (tiny). Returns per-token routed weights/indices plus
44
+ the two shared-expert gammas produced by the same softmax (the "sink").
45
+ """
46
+
47
+ def __init__(self, config: TextConfig):
48
+ super().__init__()
49
+ self.num_experts = config.n_routed_experts
50
+ self.n_shared = config.n_shared_experts
51
+ self.n_total = self.num_experts + self.n_shared
52
+ self.top_k = config.num_experts_per_tok
53
+ self.route_scale = config.route_scale
54
+ self.hidden = config.hidden_size
55
+ self.weight = mx.zeros((self.n_total, config.hidden_size))
56
+ self.bias = mx.zeros((self.num_experts,)) # e_score_correction_bias
57
+ self.global_scale = mx.ones((1,))
58
+
59
+ def __call__(self, x: mx.array):
60
+ # Routing (esp. the top-k selection) is precision-sensitive: in bf16 the
61
+ # rounding of near-tied expert scores flips which experts fire, and a wrong
62
+ # choice compounds over 64 MoE layers into incoherent output. Compute the
63
+ # whole router in fp32.
64
+ flat = x.reshape(-1, self.hidden).astype(mx.float32)
65
+ router_logits = flat @ self.weight.T.astype(mx.float32) # [T, n_total]
66
+ scores = mx.sigmoid(router_logits)
67
+ routed_scores = scores[:, : self.num_experts]
68
+ scores_for_choice = routed_scores + self.bias
69
+
70
+ # top-k experts (order within the top-k is irrelevant downstream)
71
+ topk_idx = mx.argpartition(-scores_for_choice, kth=self.top_k - 1, axis=-1)[:, : self.top_k]
72
+
73
+ routed_logits = router_logits[:, : self.num_experts]
74
+ shared_logits = router_logits[:, self.num_experts :] # [T, n_shared]
75
+ gathered = mx.take_along_axis(routed_logits, topk_idx, axis=-1) # [T, top_k]
76
+ topk_logits = mx.concatenate([gathered, shared_logits], axis=-1) # [T, top_k+n_shared]
77
+
78
+ # softmax over the selected (+shared) logits, computed in the log domain
79
+ log_probs = -mx.logaddexp(mx.zeros_like(topk_logits), -topk_logits) # logsigmoid
80
+ weights = mx.softmax(log_probs, axis=-1)
81
+ weights = weights * self.route_scale * self.global_scale
82
+
83
+ shared_gammas = weights[:, self.top_k :].astype(x.dtype) # [T, n_shared]
84
+ topk_weights = weights[:, : self.top_k].astype(x.dtype) # [T, top_k]
85
+ return topk_weights, topk_idx, shared_gammas
86
+
87
+
88
+ class MoE(nn.Module):
89
+ def __init__(self, config: TextConfig):
90
+ super().__init__()
91
+ self.config = config
92
+ self.n_shared = config.n_shared_experts
93
+ self.gate = Router(config)
94
+ self.experts = SwitchGLU(
95
+ config.hidden_size, config.moe_intermediate_size, config.n_routed_experts, bias=False
96
+ )
97
+ self.shared_experts = SwitchGLU(
98
+ config.hidden_size, config.moe_intermediate_size, config.n_shared_experts, bias=False
99
+ )
100
+
101
+ def __call__(self, x: mx.array) -> mx.array:
102
+ B, L, H = x.shape
103
+ topk_weights, topk_idx, shared_gammas = self.gate(x)
104
+ xf = x.reshape(-1, H) # [T, H]
105
+ T = xf.shape[0]
106
+
107
+ routed = self.experts(xf, topk_idx) # [T, top_k, H]
108
+ routed = (routed * topk_weights[..., None]).sum(axis=1)
109
+
110
+ shared_idx = mx.broadcast_to(
111
+ mx.arange(self.n_shared)[None], (T, self.n_shared)
112
+ )
113
+ shared = self.shared_experts(xf, shared_idx) # [T, n_shared, H]
114
+ shared = (shared.astype(mx.float32) * shared_gammas[..., None].astype(mx.float32)).sum(axis=1)
115
+ shared = shared.astype(routed.dtype)
116
+
117
+ return (routed + shared).reshape(B, L, H)
inkling_mlx/processing.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image + audio preprocessing for Inkling (MLX), ported from the reference
2
+ `InklingImageProcessor` / `InklingFeatureExtractor` / `InklingProcessor`.
3
+
4
+ * image -> pixel_values [num_patches, T=2, 40, 40, 3] (feeds `VisionModel`)
5
+ * audio -> audio_input_ids [num_frames, 80] dMel bins (feeds `AudioModel`)
6
+
7
+ `InklingProcessor.apply` builds the full multimodal input (input_ids + features)
8
+ from a chat message list, inserting the right number of placeholder soft-tokens.
9
+ Uses numpy/PIL + transformers' mel filterbank; no torch needed at inference.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import math
15
+
16
+ import numpy as np
17
+
18
+ # CLIP normalization (OPENAI_CLIP_MEAN / STD), per processor_config.json
19
+ CLIP_MEAN = np.array([0.48145466, 0.4578275, 0.40821073], dtype=np.float32)
20
+ CLIP_STD = np.array([0.26862954, 0.26130258, 0.27577711], dtype=np.float32)
21
+
22
+ PATCH = 40 # image patch size (== vision patch_size)
23
+ TEMPORAL = 2 # temporal_patch_size (images duplicated across 2 frames)
24
+
25
+ # audio (processor_config.json / feature_extraction_inkling.py)
26
+ SR = 16000
27
+ HOP = 800 # audio_token_duration_s (0.05) * SR
28
+ WIN = 1600 # * window_size_multiplier (2.0)
29
+ N_FFT = 1600
30
+ N_MEL = 80
31
+ DMEL_BINS = 16
32
+ DMEL_MIN, DMEL_MAX = -7.0, 2.0
33
+
34
+ # special tokens
35
+ IMAGE_TOKEN_ID = 200054 # <|unused_200054|> (soft-token slot)
36
+ AUDIO_TOKEN_ID = 200053 # <|unused_200053|>
37
+ IMAGE_BOS = "<|content_image|>"
38
+ AUDIO_BOS = "<|content_audio_input|>"
39
+
40
+
41
+ # ------------------------------- image -------------------------------
42
+
43
+ def preprocess_image(image, max_long_edge: int | None = None) -> tuple[np.ndarray, int]:
44
+ """PIL.Image or HxWx3 uint8 array -> (pixel_values [N,2,40,40,3] float32, N).
45
+
46
+ ``max_long_edge`` (optional) downscales the image (LANCZOS, aspect preserved) so its
47
+ long edge is at most this many pixels *before* patchify. Each 40px patch is one
48
+ vision soft-token, so this directly cuts the prompt length / prefill cost for big
49
+ images (e.g. cap 512 -> ~130 patches vs ~450 at full 960px). ``None`` keeps native
50
+ resolution (the reference default)."""
51
+ from PIL import Image
52
+ if not hasattr(image, "convert"):
53
+ image = Image.fromarray(np.asarray(image).astype(np.uint8))
54
+ image = image.convert("RGB")
55
+ if max_long_edge and max(image.size) > max_long_edge:
56
+ r = max_long_edge / max(image.size)
57
+ image = image.resize((max(1, round(image.width * r)), max(1, round(image.height * r))), Image.LANCZOS)
58
+ image = np.asarray(image)
59
+ if image.ndim == 2:
60
+ image = np.stack([image] * 3, axis=-1)
61
+ img = image[..., :3].astype(np.float32).transpose(2, 0, 1) # -> [C, H, W]
62
+ C, H, W = img.shape
63
+
64
+ num_rows = (H + PATCH - 1) // PATCH
65
+ num_cols = W // PATCH + 1 # reference: W//P + 1
66
+ patches = []
67
+ for i in range(num_rows):
68
+ for j in range(num_cols):
69
+ p = img[:, i * PATCH:(i + 1) * PATCH, j * PATCH:(j + 1) * PATCH] # may be < 40
70
+ padded = np.full((C, PATCH, PATCH), -1.0, dtype=np.float32) # pad value -1.0
71
+ padded[:, : p.shape[1], : p.shape[2]] = p
72
+ patches.append(padded)
73
+ patches = np.stack(patches, axis=0) # [N, C, 40, 40]
74
+
75
+ # rescale (1/255) + CLIP normalize per channel
76
+ patches = patches / 255.0
77
+ patches = (patches - CLIP_MEAN[None, :, None, None]) / CLIP_STD[None, :, None, None]
78
+
79
+ # add temporal dim, duplicate x2, then -> [N, T, H, W, C]
80
+ patches = np.repeat(patches[..., None], TEMPORAL, axis=-1) # [N, C, 40, 40, 2]
81
+ pixel_values = patches.transpose(0, 4, 2, 3, 1) # [N, 2, 40, 40, C]
82
+ return pixel_values.astype(np.float32), pixel_values.shape[0]
83
+
84
+
85
+ # ------------------------------- audio -------------------------------
86
+
87
+ _mel_fb = None
88
+ def _mel_filters() -> np.ndarray:
89
+ global _mel_fb
90
+ if _mel_fb is None:
91
+ from transformers.audio_utils import mel_filter_bank
92
+ fb = mel_filter_bank(num_frequency_bins=N_FFT // 2 + 1, num_mel_filters=N_MEL,
93
+ min_frequency=0.0, max_frequency=SR / 2.0, sampling_rate=SR,
94
+ norm="slaney", mel_scale="slaney") # [801, 80]
95
+ _mel_fb = np.ascontiguousarray(fb.T, dtype=np.float32) # [80, 801]
96
+ return _mel_fb
97
+
98
+
99
+ def _log_mel(waveform: np.ndarray) -> np.ndarray:
100
+ """raw mono waveform -> log10-mel spectrogram [num_frames, 80]."""
101
+ wav = np.asarray(waveform, dtype=np.float32).reshape(-1)
102
+ right = math.ceil(wav.shape[0] / HOP) * HOP - wav.shape[0]
103
+ left = max(N_FFT - HOP, 0)
104
+ wav = np.pad(wav, (left, right))
105
+ window = np.hanning(WIN + 1)[:-1].astype(np.float32) # periodic Hann
106
+ n_frames = 1 + (wav.shape[0] - N_FFT) // HOP # center=False
107
+ frames = np.stack([wav[i * HOP: i * HOP + N_FFT] * window for i in range(n_frames)]) # [T, N_FFT]
108
+ mag = np.abs(np.fft.rfft(frames, n=N_FFT, axis=-1)) # [T, 801]
109
+ mag = np.maximum(mag, 1e-10)
110
+ mel = _mel_filters() @ mag.T # [80, T]
111
+ mel = np.log10(np.maximum(mel, 1e-10))
112
+ return mel.T # [T, 80]
113
+
114
+
115
+ def preprocess_audio(waveform: np.ndarray, sampling_rate: int = SR) -> np.ndarray:
116
+ """raw 16 kHz mono waveform -> dMel bin ids [num_frames, 80] (int32, 0..15)."""
117
+ if sampling_rate != SR:
118
+ raise ValueError(f"Inkling audio expects {SR} Hz, got {sampling_rate}")
119
+ mel = _log_mel(waveform) # [T, 80] log10
120
+ n_valid = math.ceil(len(np.asarray(waveform).reshape(-1)) / HOP)
121
+ mel = mel[:n_valid] # drop trailing pad frames
122
+ centers = np.linspace(DMEL_MIN, DMEL_MAX, DMEL_BINS) # 16 bin centers
123
+ clamped = np.clip(mel.astype(np.float64), DMEL_MIN, DMEL_MAX)
124
+ bins = np.abs(clamped[..., None] - centers).argmin(-1) # nearest center
125
+ return bins.astype(np.int32) # [T, 80]
126
+
127
+
128
+ # --------------------------- prompt assembly ---------------------------
129
+
130
+ class InklingProcessor:
131
+ """Assembles multimodal model inputs from chat messages with image/audio parts.
132
+
133
+ Content parts: {"type":"text","text":...}, {"type":"image","image":PIL/array},
134
+ {"type":"audio","audio":waveform, "sampling_rate":16000}.
135
+ """
136
+
137
+ def __init__(self, tokenizer, chat_template: str):
138
+ self.tok = tokenizer
139
+ self.chat_template = chat_template
140
+ self.image_bos_id = tokenizer.encode(IMAGE_BOS, add_special_tokens=False)[0]
141
+ self.audio_bos_id = tokenizer.encode(AUDIO_BOS, add_special_tokens=False)[0]
142
+
143
+ def apply(self, messages, reasoning_effort: str = "none", max_long_edge: int | None = None):
144
+ import mlx.core as mx
145
+ pixel_values, audio_ids = [], []
146
+ # Render text via the chat template with placeholders stripped to a sentinel,
147
+ # then splice media spans in. We build ids directly for robustness.
148
+ ids: list[int] = []
149
+
150
+ def emit_text(s):
151
+ ids.extend(self.tok.encode(s, add_special_tokens=False))
152
+
153
+ # header: thinking-effort system message (matches chat_template)
154
+ eff = {"none": 0.0, "minimal": 0.1, "low": 0.2, "medium": 0.7, "high": 0.9, "max": 0.99}[reasoning_effort]
155
+ emit_text(f"<|message_system|><|content_text|>Thinking effort level: {0 if eff == 0 else eff}<|end_message|>")
156
+
157
+ for msg in messages:
158
+ role = {"user": "<|message_user|>", "assistant": "<|message_model|>",
159
+ "system": "<|message_system|>"}[msg["role"]]
160
+ content = msg["content"]
161
+ if isinstance(content, str):
162
+ content = [{"type": "text", "text": content}]
163
+ for part in content:
164
+ t = part.get("type", "text")
165
+ if t == "text":
166
+ emit_text(role + "<|content_text|>" + part["text"] + "<|end_message|>")
167
+ elif t == "image":
168
+ pv, n = preprocess_image(part["image"], max_long_edge=max_long_edge)
169
+ pixel_values.append(pv)
170
+ ids.append(self.tok.encode(role, add_special_tokens=False)[0])
171
+ ids.append(self.image_bos_id)
172
+ ids.extend([IMAGE_TOKEN_ID] * n)
173
+ ids.extend(self.tok.encode("<|end_message|>", add_special_tokens=False))
174
+ elif t == "audio":
175
+ aid = preprocess_audio(part["audio"], part.get("sampling_rate", SR))
176
+ audio_ids.append(aid)
177
+ ids.append(self.tok.encode(role, add_special_tokens=False)[0])
178
+ ids.append(self.audio_bos_id)
179
+ ids.extend([AUDIO_TOKEN_ID] * aid.shape[0])
180
+ ids.extend(self.tok.encode("<|end_message|>", add_special_tokens=False))
181
+ emit_text("<|message_model|>") # generation prompt
182
+
183
+ out = {"input_ids": ids}
184
+ if pixel_values:
185
+ out["pixel_values"] = mx.array(np.concatenate(pixel_values, axis=0))
186
+ if audio_ids:
187
+ out["audio_input_ids"] = mx.array(np.concatenate(audio_ids, axis=0))
188
+ return out
inkling_mlx/text.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inkling text backbone (``model.llm.*``): token embedding + embed-norm,
2
+ 66 decoder layers, final norm, and the (untied) unembed head.
3
+
4
+ Mirrors ``InklingTextModel`` + the unembed / muP-logit scaling from
5
+ ``InklingForConditionalGeneration``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import mlx.core as mx
11
+ import mlx.nn as nn
12
+
13
+ from .common import RMSNorm
14
+ from .config import TextConfig
15
+ from .layers import DecoderLayer
16
+
17
+
18
+ class TextModel(nn.Module):
19
+ def __init__(self, config: TextConfig):
20
+ super().__init__()
21
+ self.config = config
22
+ self.embed = nn.Embedding(config.vocab_size, config.hidden_size)
23
+ self.embed_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
24
+ self.layers = [DecoderLayer(config, i) for i in range(config.num_hidden_layers)]
25
+ self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
26
+ self.unembed = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
27
+
28
+ def embed_tokens(self, input_ids: mx.array) -> mx.array:
29
+ return self.embed_norm(self.embed(input_ids))
30
+
31
+ def backbone(self, inputs_embeds: mx.array, conv_mask=None, caches=None, start_pos=0) -> mx.array:
32
+ h = inputs_embeds
33
+ for i, layer in enumerate(self.layers):
34
+ h = layer(h, start_pos=start_pos,
35
+ cache=caches[i] if caches is not None else None,
36
+ conv_mask=conv_mask)
37
+ return self.norm(h)
38
+
39
+ def logits(self, hidden: mx.array) -> mx.array:
40
+ hidden = hidden / self.config.logits_mup_width_multiplier
41
+ logits = self.unembed(hidden)
42
+ uv = self.config.unpadded_vocab_size
43
+ if uv is not None and uv < logits.shape[-1]:
44
+ logits = logits[..., :uv]
45
+ return logits
46
+
47
+ def __call__(self, input_ids: mx.array, conv_mask=None) -> mx.array:
48
+ h = self.embed_tokens(input_ids)
49
+ h = self.backbone(h, conv_mask=conv_mask)
50
+ return self.logits(h)
inkling_mlx/vision.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inkling vision tower: an HMLP (hierarchical MLP) patch encoder.
2
+
3
+ No attention — each layer folds space/time into the channel dim then projects
4
+ (Linear -> RMSNorm -> GELU), progressively growing channels up to the text hidden
5
+ size. Mirrors ``InklingVisionModel`` / ``InklingVisionEncoderLayer`` /
6
+ ``plan_out_scales``. Checkpoint keys are flat: ``visual.layers.linear_{i}`` and
7
+ ``visual.layers.norm_{i}`` plus ``visual.final_norm``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+
14
+ import mlx.core as mx
15
+ import mlx.nn as nn
16
+
17
+ from .common import RMSNorm
18
+ from .config import VisionConfig
19
+
20
+
21
+ def _prime_factors(n: int) -> list[int]:
22
+ factors = []
23
+ while n % 2 == 0:
24
+ factors.append(2)
25
+ n //= 2
26
+ p = 3
27
+ while p * p <= n:
28
+ while n % p == 0:
29
+ factors.append(p)
30
+ n //= p
31
+ p += 2
32
+ if n > 1:
33
+ factors.append(n)
34
+ return factors
35
+
36
+
37
+ def plan_out_scales(temporal_patch_size: int, patch_size: int, n_layers: int, n_channels: int):
38
+ """Port of the reference ``plan_out_scales`` (returns an ``(n_layers+1, 4)``
39
+ array of (t, h, w, c) grid sizes). Uses numpy + scipy for the assignment."""
40
+ import numpy as np
41
+ from scipy.optimize import linear_sum_assignment
42
+
43
+ h = np.cumprod(np.array(_prime_factors(patch_size)[::-1]))
44
+ t = np.cumprod(np.array(_prime_factors(temporal_patch_size)[::-1]))
45
+
46
+ h_ch = np.ceil(h**2 * n_channels / 64).astype(np.int64) * 64
47
+ t_ch = (np.ceil(h[-1] ** 2 * n_channels * t)).astype(np.int64) * 64
48
+
49
+ base = np.array([[1, 1, 1, n_channels]], dtype=np.int64)
50
+ spatial = np.stack([np.ones_like(h), h, h, h_ch], axis=1)
51
+ temporal = np.stack([t, np.full_like(t, h[-1]), np.full_like(t, h[-1]), t_ch], axis=1)
52
+ scales = np.concatenate([base, spatial, temporal], axis=0).astype(np.int64)
53
+
54
+ size_reduction = np.prod(scales[:, :-1], axis=1).astype(np.float64)
55
+ total_elements = patch_size * patch_size * temporal_patch_size * n_channels
56
+ log_ideal = np.linspace(0.0, math.log(total_elements), n_layers + 1)
57
+ cost = np.abs(log_ideal[:, None] - np.log(size_reduction)[None, :])
58
+
59
+ if n_layers >= scales.shape[0]:
60
+ idxs = np.argmin(cost, axis=1)
61
+ else:
62
+ _, idxs = linear_sum_assignment(cost)
63
+ idxs = np.array(idxs)
64
+ idxs[0] = 0
65
+ idxs[-1] = scales.shape[0] - 1
66
+ return scales[idxs]
67
+
68
+
69
+ def _fold_timespace_to_depth(x, t_fold, hw_fold):
70
+ # x: [B, T, H, W, C] -> [B, T//t, H//hw, W//hw, C*t*hw*hw]
71
+ B, T, H, W, C = x.shape
72
+ t_new, h_new, w_new = T // t_fold, H // hw_fold, W // hw_fold
73
+ x = x.reshape(B, t_new, t_fold, h_new, hw_fold, w_new, hw_fold, C)
74
+ x = x.transpose(0, 1, 3, 5, 2, 4, 6, 7)
75
+ x = x.reshape(B, t_new, h_new, w_new, t_fold * hw_fold * hw_fold * C)
76
+ return x
77
+
78
+
79
+ class _VisionLayers(nn.Module):
80
+ """Holds ``linear_{i}`` / ``norm_{i}`` to match checkpoint keys."""
81
+
82
+ def __init__(self, config: VisionConfig):
83
+ super().__init__()
84
+ scales = plan_out_scales(
85
+ config.temporal_patch_size, config.patch_size, config.n_layers, config.num_channels
86
+ )
87
+ self.n_layers = config.n_layers
88
+ self.folds = [] # (t_fold, hw_fold, add_norm)
89
+ for i in range(config.n_layers):
90
+ start, end = scales[i], scales[i + 1]
91
+ shuffle = (
92
+ (end[0] // start[0]) * (end[1] // start[1]) * (end[2] // start[2])
93
+ )
94
+ hw_fold = int(end[1] // start[1])
95
+ t_fold = int(end[0] // start[0])
96
+ in_dim = int(start[3]) * int(shuffle)
97
+ add_norm = i != config.n_layers - 1
98
+ out_dim = config.text_hidden_size if i == config.n_layers - 1 else int(end[3])
99
+ setattr(self, f"linear_{i}", nn.Linear(in_dim, out_dim, bias=False))
100
+ if add_norm:
101
+ setattr(self, f"norm_{i}", RMSNorm(out_dim, eps=config.rms_norm_eps))
102
+ self.folds.append((t_fold, hw_fold, add_norm))
103
+
104
+ def __call__(self, x):
105
+ for i, (t_fold, hw_fold, add_norm) in enumerate(self.folds):
106
+ if hw_fold > 1 or t_fold > 1:
107
+ x = _fold_timespace_to_depth(x, t_fold, hw_fold)
108
+ x = getattr(self, f"linear_{i}")(x)
109
+ if add_norm:
110
+ x = getattr(self, f"norm_{i}")(x)
111
+ x = nn.gelu(x)
112
+ return x
113
+
114
+
115
+ class VisionModel(nn.Module):
116
+ def __init__(self, config: VisionConfig):
117
+ super().__init__()
118
+ self.config = config
119
+ self.layers = _VisionLayers(config)
120
+ self.final_norm = RMSNorm(config.text_hidden_size, eps=config.rms_norm_eps)
121
+
122
+ def __call__(self, pixel_values: mx.array) -> mx.array:
123
+ num_patches = pixel_values.shape[0]
124
+ h = self.layers(pixel_values)
125
+ h = self.final_norm(h)
126
+ return h.reshape(num_patches, -1)
model-00001-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9e42734850bdcdbaeedb40b04ec4d5602368fddd11099b5e42398995d986bf21
3
+ size 5594332943
model-00002-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ddfaeba71d13110153129d30e626b31a68c73f39360761d9653670969a3f663f
3
+ size 5000887318
model-00003-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f5c6478694d5c2dea4165f0c618257d28881261a309264e2263a50eb8a16d2c
3
+ size 5580625118
model-00004-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bc55b82655c224201a5b54faf3a85d4b9ad36d5ebf9bf5634724b89d332d71c5
3
+ size 5035610204
model-00005-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b521d9ee213d4e60cb7bfd213fc626cd0b0ea442b7fabe8fe80aa3c36fd80e65
3
+ size 5057678969
model-00006-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4759bad1e4af29df75916ca01bea2368204cbe48227edb7486e21663dedc4164
3
+ size 5033513658
model-00007-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a4c8e4016a34b788ff9f72367c299dd5a7c06ce269199685bce9532b49b0b52d
3
+ size 5011868077
model-00008-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a21f18fad8207cf2c5f7d9acd2888a15b9247ab12d8c29df1f812fc1e048bea9
3
+ size 5019857728
model-00009-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:900765f2db646433da17ffbe77e1153ae7b699a178d9ef41f2d6855cb4f7d4cf
3
+ size 5001781520
model-00010-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ee46ca2cdda187a133021be9ada09039acb309328708d608575610634cc4ee46
3
+ size 5661531108
model-00011-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d5fb7c4b974ecb52ced7cbffb816f50c892012e04b53584393ee73cce913497b
3
+ size 5795660286
model-00012-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c843327ff1a2abcf69d38324ae46b370f806780560981f2f68e84a717e9d3aa0
3
+ size 5039732129
model-00013-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1c1f97a0adf24967d92988f60a4c2cf110a6d22340380c7fce056f5ed7dd70d7
3
+ size 5617638322
model-00014-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:81fa417091e2a5472c93d94d51c4dfab6b46d50542a769cab034946cae19181d
3
+ size 5628885415
model-00015-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:69bc0de3d94e5bd99b18154c4eb1c1228bb3dd51f55270e141cdf17bc2c670e9
3
+ size 5267393483
model-00016-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:52712f0e018f906c2a2e0406a39f8d4d3e0e380145739551731370385698019c
3
+ size 5009388976
model-00017-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fe1c46b6d45fa427ebe4c8852e511f961a42aa1e5c320127d685d9f051bd7373
3
+ size 5650057900
model-00018-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e2f96c8ac308f6027252bfe0b3369841948c8048473f3dac6bf0398e60562b99
3
+ size 5060808586
model-00019-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4040ecd3725b5ce786fbd708e1686295d27cce7921868c134a613843f38b8f0a
3
+ size 5797784251
model-00020-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:52639817b466372de4e030cfd76c17ff574d643611e0e4dbf97b871da37148f7
3
+ size 5031172621
model-00021-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b6a1d3b7894320c5c07388c9bccbf106f21d452e055c5bd0194d0f2e891935e
3
+ size 5281288361
model-00022-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4c03d985bd8761e409f8f40ad5cc76c048a4a7f7c767484bd7ae8d24a9319d61
3
+ size 5683582825
model-00023-of-00023.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:45dbe282191d5afeec100f4e6826fc556d8738e8b077555a089cb99ac0021633
3
+ size 4051827493
model.safetensors.index.json ADDED
The diff for this file is too large to render. See raw diff
 
processor_config.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_token": "<|unused_200053|>",
3
+ "audio_bos_token": "<|content_audio_input|>",
4
+ "dmel_max_value": 2.0,
5
+ "dmel_min_value": -7.0,
6
+ "feature_extractor": {
7
+ "audio_token_duration_s": 0.05,
8
+ "feature_extractor_type": "InklingFeatureExtractor",
9
+ "feature_size": 80,
10
+ "hop_length": 800,
11
+ "n_fft": 1600,
12
+ "padding_side": "right",
13
+ "padding_value": 0.0,
14
+ "return_attention_mask": true,
15
+ "sampling_rate": 16000,
16
+ "window_size": 1600,
17
+ "window_size_multiplier": 2.0
18
+ },
19
+ "image_processor": {
20
+ "do_convert_rgb": true,
21
+ "do_normalize": true,
22
+ "do_rescale": true,
23
+ "do_resize": true,
24
+ "image_mean": [
25
+ 0.48145466,
26
+ 0.4578275,
27
+ 0.40821073
28
+ ],
29
+ "image_processor_type": "InklingImageProcessor",
30
+ "image_std": [
31
+ 0.26862954,
32
+ 0.26130258,
33
+ 0.27577711
34
+ ],
35
+ "resample": 3,
36
+ "rescale_factor": 0.00392156862745098,
37
+ "size": {
38
+ "height": 40,
39
+ "width": 40
40
+ }
41
+ },
42
+ "image_token": "<|unused_200054|>",
43
+ "image_bos_token": "<|content_image|>",
44
+ "num_dmel_bins": 16,
45
+ "processor_class": "InklingProcessor"
46
+ }
special_tokens_map.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ "<|endoftext|>",
4
+ "<|message_user|>",
5
+ "<|message_model|>",
6
+ "<|message_system|>",
7
+ "<|message_tool|>",
8
+ "<|content_text|>",
9
+ "<|content_image|>",
10
+ "<|content_model_end_sampling|>",
11
+ "<|content_thinking|>",
12
+ "<|end_message|>",
13
+ "<|content_audio_input|>",
14
+ "<|content_tool_error|>",
15
+ "<|audio|>",
16
+ "<|content_xml|>",
17
+ "<|begin_of_text|>",
18
+ "<|audio_end|>",
19
+ "<|content_invoke_tool_json|>",
20
+ "<|content_invoke_tool_text|>"
21
+ ]
22
+ }
tiktoken/tokenizer.model ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bc253fd2b702f7a6da7105eaa8f3463b2f1247e83614f23e5323b921088bed2a
3
+ size 3615874
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9fb6333a7db8fe5da90728e741e4a3ee4ac2ae12c5dd4958cc6f31688787d3c2
3
+ size 27875797