mlx-bun 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/THIRD_PARTY_LICENSES.md +41 -0
  4. package/bin/mlx-bun.mjs +32 -0
  5. package/docs/curve-designer.html +303 -0
  6. package/docs/design/adapters-end-to-end.md +201 -0
  7. package/docs/design/compat-cli-surface-design.md +227 -0
  8. package/docs/design/docs-reorg-plan.md +152 -0
  9. package/docs/design/hlg-sampling.md +340 -0
  10. package/docs/design/memory-system.md +339 -0
  11. package/docs/design/optimization_plan.md +344 -0
  12. package/docs/design/parallel-slots.md +515 -0
  13. package/docs/design/runtime-isolation.md +247 -0
  14. package/docs/design/segmented-backward-training.md +508 -0
  15. package/docs/investigations/chunk-finetune-experiment.md +84 -0
  16. package/docs/investigations/curve-bisector-routing.md +123 -0
  17. package/docs/investigations/curve-runs/bisector-route.json +98 -0
  18. package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
  19. package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
  20. package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
  21. package/docs/investigations/curve-terrain.html +105 -0
  22. package/docs/investigations/expert-offload-single-user-moe.md +292 -0
  23. package/docs/investigations/hlg-ab.html +34 -0
  24. package/docs/investigations/hlg-explorer.html +238 -0
  25. package/docs/investigations/hlg-figs/cliff.svg +1 -0
  26. package/docs/investigations/hlg-figs/cross.svg +1 -0
  27. package/docs/investigations/hlg-figs/edges.svg +1 -0
  28. package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
  29. package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
  30. package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
  31. package/docs/investigations/hlg-figs/terrain.svg +1 -0
  32. package/docs/investigations/hlg-report.html +262 -0
  33. package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
  34. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
  35. package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
  36. package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
  37. package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
  38. package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
  39. package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
  40. package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
  41. package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
  42. package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
  43. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
  44. package/docs/investigations/hlg-runs/hlg-map.json +383 -0
  45. package/docs/investigations/hlg-runs/run0-original.log +159 -0
  46. package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
  47. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
  48. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
  49. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
  50. package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
  51. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
  52. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
  53. package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
  54. package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
  55. package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
  56. package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
  57. package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
  58. package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
  59. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
  60. package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
  61. package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
  62. package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
  63. package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
  64. package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
  65. package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
  66. package/docs/investigations/hlg-runs/runN-verify.log +116 -0
  67. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
  68. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
  69. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
  70. package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
  71. package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
  72. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
  73. package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
  74. package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
  75. package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
  76. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
  77. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
  78. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
  79. package/docs/investigations/hlg-sampling-investigation.md +970 -0
  80. package/docs/investigations/lab-build-journal.md +157 -0
  81. package/docs/investigations/pi-builtin-investigation.md +308 -0
  82. package/docs/investigations/starter-model-port-handoff.md +323 -0
  83. package/docs/planning/IDEAS.md +26 -0
  84. package/docs/planning/PRODUCT_ROADMAP.md +426 -0
  85. package/docs/planning/ResearchTopics.md +119 -0
  86. package/docs/planning/chunk-task-roadmap.md +77 -0
  87. package/docs/planning/journal.md +155 -0
  88. package/docs/reference/distribution.md +121 -0
  89. package/docs/reference/embedding.md +168 -0
  90. package/docs/reference/library-api.md +153 -0
  91. package/docs/reference/server-api.md +224 -0
  92. package/docs/reference/server-config.md +243 -0
  93. package/docs/segmented-backward-handoff.md +148 -0
  94. package/package.json +68 -0
  95. package/src/anthropic.ts +491 -0
  96. package/src/chat-template.ts +186 -0
  97. package/src/cli.ts +1075 -0
  98. package/src/config.ts +229 -0
  99. package/src/curve-sampler.ts +122 -0
  100. package/src/dataset/generators.ts +1087 -0
  101. package/src/dataset/index.ts +12 -0
  102. package/src/dataset/job.ts +81 -0
  103. package/src/dataset/llm.ts +71 -0
  104. package/src/dataset/registry.ts +528 -0
  105. package/src/download.ts +283 -0
  106. package/src/eval/README.md +86 -0
  107. package/src/eval/kl-prompts.ts +29 -0
  108. package/src/eval/kl.ts +294 -0
  109. package/src/eval/quality-db.ts +92 -0
  110. package/src/eval/runner.ts +102 -0
  111. package/src/eval/score.ts +49 -0
  112. package/src/eval/tasks/bfcl.ts +500 -0
  113. package/src/eval/tasks/gsm8k.ts +85 -0
  114. package/src/eval/tasks/hashhop.ts +109 -0
  115. package/src/eval/tasks/humaneval.ts +252 -0
  116. package/src/eval/tasks/ifeval.ts +354 -0
  117. package/src/eval/tasks/mmlu.ts +145 -0
  118. package/src/evaldb.ts +199 -0
  119. package/src/expert-offload-build.ts +77 -0
  120. package/src/expert-offload.ts +61 -0
  121. package/src/expert-trace.ts +67 -0
  122. package/src/fit.ts +254 -0
  123. package/src/generate.ts +451 -0
  124. package/src/harness-pi.ts +177 -0
  125. package/src/hf-push.ts +628 -0
  126. package/src/index.ts +22 -0
  127. package/src/jobs/db.ts +139 -0
  128. package/src/jobs/index.ts +29 -0
  129. package/src/jobs/job-entry.ts +106 -0
  130. package/src/jobs/runner.ts +304 -0
  131. package/src/jobs/sse.ts +114 -0
  132. package/src/jobs/types.ts +58 -0
  133. package/src/kv-store.ts +185 -0
  134. package/src/lora.ts +314 -0
  135. package/src/md-modules.d.ts +7 -0
  136. package/src/mlx/array.ts +237 -0
  137. package/src/mlx/autograd.ts +267 -0
  138. package/src/mlx/checkpoint.ts +116 -0
  139. package/src/mlx/compile.ts +158 -0
  140. package/src/mlx/custom-vjp.ts +140 -0
  141. package/src/mlx/ffi.ts +433 -0
  142. package/src/mlx/metal-kernel.ts +130 -0
  143. package/src/mlx/ops.ts +707 -0
  144. package/src/mmap.ts +90 -0
  145. package/src/model/batched-mask.ts +204 -0
  146. package/src/model/batched-rotating.ts +300 -0
  147. package/src/model/compiled-decode.ts +681 -0
  148. package/src/model/factory.ts +27 -0
  149. package/src/model/fingerprint.ts +33 -0
  150. package/src/model/flash-attention.ts +674 -0
  151. package/src/model/fused-decode-kernel.ts +305 -0
  152. package/src/model/fused-geglu-kernel.ts +87 -0
  153. package/src/model/gemma4-base.ts +1616 -0
  154. package/src/model/gemma4.ts +1012 -0
  155. package/src/model/generated/gemma4-12b.ts +526 -0
  156. package/src/model/generated/gemma4-26b.ts +500 -0
  157. package/src/model/generated/gemma4-e4b.ts +757 -0
  158. package/src/model/generated/index.ts +18 -0
  159. package/src/model/minicpm5.ts +278 -0
  160. package/src/model/qwen3-delta.ts +224 -0
  161. package/src/model/qwen3_5.ts +429 -0
  162. package/src/model/support.ts +46 -0
  163. package/src/native-pack.ts +152 -0
  164. package/src/pi-provider.ts +95 -0
  165. package/src/pi-terminal.ts +264 -0
  166. package/src/pi-web.ts +924 -0
  167. package/src/preflight.ts +123 -0
  168. package/src/prompt-cache.ts +113 -0
  169. package/src/quantize/allocator.ts +627 -0
  170. package/src/quantize/calibration.ts +180 -0
  171. package/src/quantize/config-writer.ts +132 -0
  172. package/src/quantize/index.ts +57 -0
  173. package/src/quantize/job.ts +136 -0
  174. package/src/quantize/quantizer.ts +462 -0
  175. package/src/quantize/safetensors-writer.ts +171 -0
  176. package/src/quantize/sensitivity.ts +234 -0
  177. package/src/registry.ts +336 -0
  178. package/src/responses.ts +774 -0
  179. package/src/safetensors.ts +158 -0
  180. package/src/sampler.ts +517 -0
  181. package/src/serve/batch-scheduler.ts +362 -0
  182. package/src/serve/generation-gateway.ts +160 -0
  183. package/src/server.ts +1891 -0
  184. package/src/spec/drafter.ts +261 -0
  185. package/src/spec/generate.ts +221 -0
  186. package/src/status-page.html +497 -0
  187. package/src/tokenizer.ts +38 -0
  188. package/src/tool-call.ts +242 -0
  189. package/src/train/dataset.ts +461 -0
  190. package/src/train/export.ts +32 -0
  191. package/src/train/forward.ts +229 -0
  192. package/src/train/index.ts +43 -0
  193. package/src/train/job.ts +148 -0
  194. package/src/train/lora-params.ts +194 -0
  195. package/src/train/loss.ts +342 -0
  196. package/src/train/merge.ts +195 -0
  197. package/src/train/optimizer.ts +176 -0
  198. package/src/train/rank.ts +101 -0
  199. package/src/train/segmented.ts +520 -0
  200. package/src/train/trainer.ts +600 -0
  201. package/src/tui.ts +161 -0
  202. package/src/vision/embedder.ts +168 -0
  203. package/src/vision/preprocess.ts +247 -0
  204. package/src/vision/prompt.ts +158 -0
  205. package/src/vision/siglip.ts +599 -0
  206. package/src/web/app.html +2471 -0
  207. package/src/web/skills/web-research/SKILL.md +44 -0
  208. package/src/web/skills.ts +59 -0
  209. package/src/web-tools.ts +426 -0
  210. package/src/weights.ts +91 -0
@@ -0,0 +1,18 @@
1
+ // Registry of generated per-architecture specializations (hand-kept;
2
+ // the files themselves come from scripts/gen-model.ts). Dispatch is by
3
+ // config fingerprint; anything unregistered runs the monolith.
4
+
5
+ import type { Weights } from "../../weights";
6
+ import type { ModelConfig } from "../../config";
7
+ import { Gemma4Model } from "../gemma4";
8
+ import * as g12b from "./gemma4-12b";
9
+ import * as ge4b from "./gemma4-e4b";
10
+ import * as g26b from "./gemma4-26b";
11
+
12
+ type ModelCtor = new (weights: Weights, config: ModelConfig) => Gemma4Model;
13
+
14
+ export const GENERATED = new Map<string, ModelCtor>([
15
+ [g12b.FINGERPRINT, g12b.GeneratedGemma4],
16
+ [ge4b.FINGERPRINT, ge4b.GeneratedGemma4],
17
+ [g26b.FINGERPRINT, g26b.GeneratedGemma4],
18
+ ]);
@@ -0,0 +1,278 @@
1
+ // Concrete model graph for MiniCPM5-1B-OptiQ-4bit.
2
+ // Port target: mlx_lm.models.llama from the oracle venv.
3
+
4
+ import type { ModelConfig } from "../config";
5
+ import type { Weights } from "../weights";
6
+ import { MlxArray } from "../mlx/array";
7
+ import * as ops from "../mlx/ops";
8
+ import {
9
+ argmaxLastPosition,
10
+ disposeTriple,
11
+ disposing,
12
+ KVCache,
13
+ LoraState,
14
+ QuantizedKVCache,
15
+ QuantizedEmbedding,
16
+ QuantizedLinear,
17
+ quantizedSdpa,
18
+ RMSNorm,
19
+ type Cache,
20
+ type Mask,
21
+ } from "./gemma4-base";
22
+ import { flashAttention, getTrainingAttn, flashSupported } from "./flash-attention";
23
+
24
+ class LlamaAttention {
25
+ readonly qProj: QuantizedLinear;
26
+ readonly kProj: QuantizedLinear;
27
+ readonly vProj: QuantizedLinear;
28
+ readonly oProj: QuantizedLinear;
29
+ readonly nHeads: number;
30
+ readonly nKvHeads: number;
31
+ readonly headDim: number;
32
+ readonly scale: number;
33
+ readonly ropeBase: number;
34
+
35
+ constructor(weights: Weights, config: ModelConfig, prefix: string) {
36
+ const t = config.text;
37
+ this.nHeads = t.numAttentionHeads;
38
+ this.nKvHeads = t.numKeyValueHeads;
39
+ this.headDim = t.headDim;
40
+ this.scale = Math.pow(this.headDim, -0.5);
41
+ this.ropeBase = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
42
+ this.qProj = QuantizedLinear.load(weights, `${prefix}.q_proj`, config);
43
+ this.kProj = QuantizedLinear.load(weights, `${prefix}.k_proj`, config);
44
+ this.vProj = QuantizedLinear.load(weights, `${prefix}.v_proj`, config);
45
+ this.oProj = QuantizedLinear.load(weights, `${prefix}.o_proj`, config);
46
+ }
47
+
48
+ forward(x: MlxArray, mask: Mask, cache: Cache): MlxArray {
49
+ const [B, L] = x.shape as [number, number, number];
50
+ let q = this.qProj.forward(x);
51
+ let k = this.kProj.forward(x);
52
+ let v = this.vProj.forward(x);
53
+
54
+ q = disposing(q, ops.reshape(q, [B, L, this.nHeads, this.headDim]));
55
+ k = disposing(k, ops.reshape(k, [B, L, this.nKvHeads, this.headDim]));
56
+ v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
57
+
58
+ q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
59
+ k = disposing(k, ops.transposeAxes(k, [0, 2, 1, 3]));
60
+ v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
61
+
62
+ // RoPE: a number offset takes the static fast::rope; an array offset
63
+ // (per-row positions for left-padded batched decode) takes the dynamic
64
+ // variant — same kernel, offset read from the array. Captured once: rope
65
+ // runs before updateAndFetch, so K and Q share the pre-write offset.
66
+ const offsetArr = cache.ropeOffsetArr;
67
+ const ropeStep = (x: MlxArray): MlxArray =>
68
+ offsetArr
69
+ ? ops.ropeDynamic(x, this.headDim, this.ropeBase, offsetArr, null)
70
+ : ops.rope(x, this.headDim, this.ropeBase, cache.offset, null);
71
+ q = disposing(q, ropeStep(q));
72
+ k = disposing(k, ropeStep(k));
73
+ let attn: MlxArray;
74
+ if (cache instanceof QuantizedKVCache) {
75
+ const [keys, values] = cache.updateAndFetchQuantized(k, v);
76
+ k.dispose();
77
+ v.dispose();
78
+ attn = quantizedSdpa(q, keys, values, this.scale, mask, cache.groupSize, cache.bits);
79
+ disposeTriple(keys);
80
+ disposeTriple(values);
81
+ } else {
82
+ const [keys, values] = cache.updateAndFetch(k, v);
83
+ k.dispose();
84
+ v.dispose();
85
+ // Training: ops.sdpa's dK vjp is wrong, so route the backward through
86
+ // the validated flash kernel. MiniCPM5 is full-attention → window 0.
87
+ const ta = getTrainingAttn();
88
+ if (ta === "flash" && flashSupported(q) && (mask.mode === "causal" || mask.mode === "array")) {
89
+ attn = flashAttention(q, keys, values, this.scale, true, 0);
90
+ } else {
91
+ attn = ops.sdpa(q, keys, values, this.scale, mask.mode, mask.arr);
92
+ }
93
+ keys.dispose();
94
+ values.dispose();
95
+ }
96
+ q.dispose();
97
+ const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
98
+ attn.dispose();
99
+ const merged = ops.reshape(attnT, [B, L, -1]);
100
+ attnT.dispose();
101
+ const out = this.oProj.forward(merged);
102
+ merged.dispose();
103
+ return out;
104
+ }
105
+ }
106
+
107
+ class LlamaMLP {
108
+ readonly gate: QuantizedLinear;
109
+ readonly up: QuantizedLinear;
110
+ readonly down: QuantizedLinear;
111
+
112
+ constructor(weights: Weights, config: ModelConfig, prefix: string) {
113
+ this.gate = QuantizedLinear.load(weights, `${prefix}.gate_proj`, config);
114
+ this.up = QuantizedLinear.load(weights, `${prefix}.up_proj`, config);
115
+ this.down = QuantizedLinear.load(weights, `${prefix}.down_proj`, config);
116
+ }
117
+
118
+ forward(x: MlxArray): MlxArray {
119
+ const gate = this.gate.forward(x);
120
+ const up = this.up.forward(x);
121
+ const sig = ops.sigmoid(gate);
122
+ const silu = ops.mul(gate, sig);
123
+ gate.dispose();
124
+ sig.dispose();
125
+ const hidden = ops.mul(silu, up);
126
+ silu.dispose();
127
+ up.dispose();
128
+ const out = this.down.forward(hidden);
129
+ hidden.dispose();
130
+ return out;
131
+ }
132
+ }
133
+
134
+ class LlamaLayer {
135
+ readonly attn: LlamaAttention;
136
+ readonly mlp: LlamaMLP;
137
+ readonly inputNorm: RMSNorm;
138
+ readonly postAttnNorm: RMSNorm;
139
+
140
+ constructor(weights: Weights, config: ModelConfig, prefix: string) {
141
+ this.attn = new LlamaAttention(weights, config, `${prefix}.self_attn`);
142
+ this.mlp = new LlamaMLP(weights, config, `${prefix}.mlp`);
143
+ this.inputNorm = new RMSNorm(weights.tensor(`${prefix}.input_layernorm.weight`), config.text.rmsNormEps);
144
+ this.postAttnNorm = new RMSNorm(weights.tensor(`${prefix}.post_attention_layernorm.weight`), config.text.rmsNormEps);
145
+ }
146
+
147
+ forward(x: MlxArray, mask: Mask, cache: Cache): MlxArray {
148
+ const xn = this.inputNorm.forward(x);
149
+ const a = this.attn.forward(xn, mask, cache);
150
+ xn.dispose();
151
+ const h = ops.add(x, a);
152
+ a.dispose();
153
+ const hn = this.postAttnNorm.forward(h);
154
+ const m = this.mlp.forward(hn);
155
+ hn.dispose();
156
+ const out = ops.add(h, m);
157
+ h.dispose();
158
+ m.dispose();
159
+ return out;
160
+ }
161
+ }
162
+
163
+ export class MiniCPM5Model {
164
+ readonly config: ModelConfig;
165
+ readonly weightsBytes: number;
166
+ readonly prefixBase = "model";
167
+ readonly loraState = new LoraState();
168
+ readonly embed: QuantizedEmbedding;
169
+ readonly layers: LlamaLayer[];
170
+ readonly finalNorm: RMSNorm;
171
+ readonly lmHead: QuantizedLinear;
172
+
173
+ constructor(weights: Weights, config: ModelConfig) {
174
+ if (config.text.tieWordEmbeddings)
175
+ throw new Error("llama tied embeddings are not supported yet");
176
+ this.config = config;
177
+ this.weightsBytes = [...weights.shards.files.values()]
178
+ .reduce((a, f) => a + f.mmap.size, 0);
179
+ this.embed = QuantizedEmbedding.load(weights, "model.embed_tokens", config);
180
+ this.layers = Array.from(
181
+ { length: config.text.numHiddenLayers },
182
+ (_, i) => new LlamaLayer(weights, config, `model.layers.${i}`),
183
+ );
184
+ this.finalNorm = new RMSNorm(weights.tensor("model.norm.weight"), config.text.rmsNormEps);
185
+ this.lmHead = QuantizedLinear.load(weights, "lm_head", config);
186
+ }
187
+
188
+ loraTargets(): Map<string, QuantizedLinear> {
189
+ const out = new Map<string, QuantizedLinear>();
190
+ for (let i = 0; i < this.layers.length; i++) {
191
+ const l = this.layers[i]!;
192
+ const p = `model.layers.${i}`;
193
+ out.set(`${p}.self_attn.q_proj`, l.attn.qProj);
194
+ out.set(`${p}.self_attn.k_proj`, l.attn.kProj);
195
+ out.set(`${p}.self_attn.v_proj`, l.attn.vProj);
196
+ out.set(`${p}.self_attn.o_proj`, l.attn.oProj);
197
+ out.set(`${p}.mlp.gate_proj`, l.mlp.gate);
198
+ out.set(`${p}.mlp.up_proj`, l.mlp.up);
199
+ out.set(`${p}.mlp.down_proj`, l.mlp.down);
200
+ }
201
+ return out;
202
+ }
203
+
204
+ makeCache(): Cache[] {
205
+ return this.layers.map(() => new KVCache());
206
+ }
207
+
208
+ forwardHidden(ids: MlxArray, cache: Cache[]): MlxArray {
209
+ const h = this.embed.encode(ids);
210
+ return this.forwardLayers(h, cache);
211
+ }
212
+
213
+ forwardEmbeddings(_embeds: MlxArray, _cache: Cache[], _bidir: MlxArray | null): MlxArray {
214
+ throw new Error("llama vision/input-embedding path is not supported");
215
+ }
216
+
217
+ protected forwardLayers(h0: MlxArray, cache: Cache[]): MlxArray {
218
+ const h = this.runLayerRange(h0, 0, this.layers.length, cache);
219
+ h0.dispose(); // runLayerRange keeps its input alive; we own h0 here
220
+ return disposing(h, this.finalNorm.forward(h));
221
+ }
222
+
223
+ /** Run decoder layers `[aIdx, bIdx)` on hidden `h`, returning the residual
224
+ * stream after the last layer in the range (NO finalNorm). The input `h` is
225
+ * NEVER disposed — the caller owns it (for segmented backward `h` is a leaf
226
+ * whose lifetime is managed by the autograd closure); intra-range
227
+ * intermediates ARE disposed. Building block for `forwardLayers` and the
228
+ * segmented-backward training path (docs/design/segmented-backward-training.md).
229
+ * MiniCPM5 is plain full-attention with no KV-sharing, so each layer reads
230
+ * its own stateless `cache[i]` and a single causal mask. */
231
+ runLayerRange(h: MlxArray, aIdx: number, bIdx: number, cache: Cache[]): MlxArray {
232
+ if (aIdx >= bIdx) return h;
233
+ const L = h.shape[1]!;
234
+ const mask = cache[aIdx]!.makeMask(L, null);
235
+ let cur = h;
236
+ for (let i = aIdx; i < bIdx; i++) {
237
+ const next = this.layers[i]!.forward(cur, mask, cache[i]!);
238
+ if (i > aIdx) cur.dispose(); // keep the input boundary leaf; drop interiors
239
+ cur = next;
240
+ }
241
+ mask.arr?.dispose();
242
+ return cur;
243
+ }
244
+
245
+ logitsFromHidden(h: MlxArray): MlxArray {
246
+ return this.lmHead.forward(h);
247
+ }
248
+
249
+ forward(tokens: number[] | MlxArray, cache: Cache[]): MlxArray {
250
+ const ids = Array.isArray(tokens)
251
+ ? ops.fromInt32(tokens, [1, tokens.length])
252
+ : tokens;
253
+ const h = this.forwardHidden(ids, cache);
254
+ if (Array.isArray(tokens)) ids.dispose();
255
+ const logits = this.logitsFromHidden(h);
256
+ h.dispose();
257
+ return logits;
258
+ }
259
+
260
+ generate(promptTokens: number[], maxTokens: number, eosIds: number[] = []): number[] {
261
+ const cache = this.makeCache();
262
+ const out: number[] = [];
263
+ try {
264
+ let tokens = promptTokens;
265
+ for (let step = 0; step < maxTokens; step++) {
266
+ const logits = this.forward(tokens, cache);
267
+ const next = argmaxLastPosition(logits);
268
+ logits.dispose();
269
+ if (eosIds.includes(next)) break;
270
+ out.push(next);
271
+ tokens = [next];
272
+ }
273
+ } finally {
274
+ for (const c of cache) c.dispose();
275
+ }
276
+ return out;
277
+ }
278
+ }
@@ -0,0 +1,224 @@
1
+ // Gated DeltaNet recurrence for Qwen3.5 linear-attention layers.
2
+ //
3
+ // Port target: mlx_lm.models.gated_delta (compute_g + the `gated_delta_step`
4
+ // Metal kernel) and the recurrent-state cache. mlx-lm runs the GPU kernel by
5
+ // default (use_kernel = not training), and its float accumulation / simd_sum
6
+ // reduction order differ from the pure-ops fallback — so BIT-EXACT parity with
7
+ // mlx-lm requires the SAME kernel, dispatched with the SAME grid/threadgroup.
8
+ // We port the non-vectorized, non-masked variant (g.ndim == 3, mask is None),
9
+ // which is the B=1 single-stream path (ssm_mask is None at batch 1).
10
+ //
11
+ // Numerics (must match the reference dtypes exactly — mlx infers the kernel's
12
+ // pointer element types from the input arrays):
13
+ // q, k bf16 [B, T, Hk, Dk] (after inv_scale * rms_norm(., None))
14
+ // v bf16 [B, T, Hv, Dv]
15
+ // g f32 [B, T, Hv] (= exp(-exp(A_log_f32) * softplus(a+dt_bias)))
16
+ // beta bf16 [B, T, Hv] (= sigmoid(b))
17
+ // state_in f32 [B, Hv, Dv, Dk]
18
+ // y (out) bf16 [B, T, Hv, Dv] (InT)
19
+ // state_out f32 [B, Hv, Dv, Dk] (StT)
20
+ // GQA is handled inside the kernel (hk_idx = hv_idx / (Hv/Hk)); q/k stay at Hk.
21
+
22
+ import { MlxArray } from "../mlx/array";
23
+ import { Dtype } from "../mlx/ffi";
24
+ import { MetalKernel } from "../mlx/metal-kernel";
25
+ import * as ops from "../mlx/ops";
26
+ import type { Cache, Mask } from "./gemma4-base";
27
+
28
+ // Verbatim body of mlx-lm's gated_delta_step (has_mask=False, vectorized=False),
29
+ // with the only change being that the time count `T` arrives as a 1-element
30
+ // int32 input (`Tin`) and is read once — numerically identical to the
31
+ // reference (T only drives loop bounds and integer offsets), but avoids a
32
+ // kernel recompile per distinct sequence length.
33
+ const SOURCE = String.raw`
34
+ const int T = Tin[0];
35
+ auto n = thread_position_in_grid.z;
36
+ auto b_idx = n / Hv;
37
+ auto hv_idx = n % Hv;
38
+ auto hk_idx = hv_idx / (Hv / Hk);
39
+ constexpr int n_per_t = Dk / 32;
40
+
41
+ // q, k: [B, T, Hk, Dk]
42
+ auto q_ = q + b_idx * T * Hk * Dk + hk_idx * Dk;
43
+ auto k_ = k + b_idx * T * Hk * Dk + hk_idx * Dk;
44
+
45
+ // v, y: [B, T, Hv, Dv]
46
+ auto v_ = v + b_idx * T * Hv * Dv + hv_idx * Dv;
47
+ y += b_idx * T * Hv * Dv + hv_idx * Dv;
48
+
49
+ auto dk_idx = thread_position_in_threadgroup.x;
50
+ auto dv_idx = thread_position_in_grid.y;
51
+
52
+ // state_in, state_out: [B, Hv, Dv, Dk]
53
+ auto i_state = state_in + (n * Dv + dv_idx) * Dk;
54
+ auto o_state = state_out + (n * Dv + dv_idx) * Dk;
55
+
56
+ float state[n_per_t];
57
+ for (int i = 0; i < n_per_t; ++i) {
58
+ auto s_idx = n_per_t * dk_idx + i;
59
+ state[i] = static_cast<float>(i_state[s_idx]);
60
+ }
61
+
62
+ // g: [B, T, Hv]
63
+ auto g_ = g + b_idx * T * Hv;
64
+ auto beta_ = beta + b_idx * T * Hv;
65
+
66
+ for (int t = 0; t < T; ++t) {
67
+ if (true) {
68
+ float kv_mem = 0.0f;
69
+ for (int i = 0; i < n_per_t; ++i) {
70
+ auto s_idx = n_per_t * dk_idx + i;
71
+ state[i] = state[i] * g_[hv_idx];
72
+ kv_mem += state[i] * k_[s_idx];
73
+ }
74
+ kv_mem = simd_sum(kv_mem);
75
+
76
+ auto delta = (v_[dv_idx] - kv_mem) * beta_[hv_idx];
77
+
78
+ float out = 0.0f;
79
+ for (int i = 0; i < n_per_t; ++i) {
80
+ auto s_idx = n_per_t * dk_idx + i;
81
+ state[i] = state[i] + k_[s_idx] * delta;
82
+ out += state[i] * q_[s_idx];
83
+ }
84
+ out = simd_sum(out);
85
+ if (thread_index_in_simdgroup == 0) {
86
+ y[dv_idx] = static_cast<InT>(out);
87
+ }
88
+ } else {
89
+ y[dv_idx] = static_cast<InT>(0);
90
+ }
91
+ // Increment data pointers to next time step
92
+ q_ += Hk * Dk;
93
+ k_ += Hk * Dk;
94
+ v_ += Hv * Dv;
95
+ y += Hv * Dv;
96
+ g_ += Hv;
97
+ beta_ += Hv;
98
+ }
99
+ for (int i = 0; i < n_per_t; ++i) {
100
+ auto s_idx = n_per_t * dk_idx + i;
101
+ o_state[s_idx] = static_cast<StT>(state[i]);
102
+ }
103
+ `;
104
+
105
+ let kernel: MetalKernel | null = null;
106
+ function getKernel(): MetalKernel {
107
+ if (!kernel)
108
+ kernel = new MetalKernel({
109
+ name: "gated_delta_step",
110
+ inputNames: ["q", "k", "v", "g", "beta", "state_in", "Tin"],
111
+ outputNames: ["y", "state_out"],
112
+ source: SOURCE,
113
+ ensureRowContiguous: true,
114
+ });
115
+ return kernel;
116
+ }
117
+
118
+ /** compute_g: exp(-exp(A_log_f32) * softplus(a + dt_bias)). Output is f32
119
+ * (the f32 exp(A_log) promotes the bf16 softplus term) — the kernel reads g
120
+ * as float, so this dtype is load-bearing. */
121
+ export function computeG(aLog: MlxArray, a: MlxArray, dtBias: MlxArray): MlxArray {
122
+ const aLogF = aLog.astype(Dtype.float32);
123
+ const expA = ops.exp(aLogF); // f32 [Hv]
124
+ aLogF.dispose();
125
+ const negExpA = ops.neg(expA); // f32 [Hv]
126
+ expA.dispose();
127
+ const adt = ops.add(a, dtBias); // bf16 [B,S,Hv]
128
+ const sp = ops.softplus(adt); // bf16 [B,S,Hv]
129
+ adt.dispose();
130
+ const prod = ops.mul(negExpA, sp); // f32 [B,S,Hv]
131
+ negExpA.dispose();
132
+ sp.dispose();
133
+ const g = ops.exp(prod); // f32 [B,S,Hv]
134
+ prod.dispose();
135
+ return g;
136
+ }
137
+
138
+ /** gated_delta_update (use_kernel path): returns [y, newState].
139
+ * q, k: [B, S, Hk, Dk] bf16 v: [B, S, Hv, Dv] bf16
140
+ * a, b: [B, S, Hv] bf16 aLog, dtBias: [Hv]
141
+ * state: [B, Hv, Dv, Dk] f32 (or null → zeros) */
142
+ export function gatedDeltaUpdate(
143
+ q: MlxArray, k: MlxArray, v: MlxArray, a: MlxArray, b: MlxArray,
144
+ aLog: MlxArray, dtBias: MlxArray, state: MlxArray | null,
145
+ ): [MlxArray, MlxArray] {
146
+ const [B, , Hk, Dk] = q.shape as [number, number, number, number];
147
+ const [, , Hv, Dv] = v.shape as [number, number, number, number];
148
+
149
+ const beta = ops.sigmoid(b); // bf16
150
+ const g = computeG(aLog, a, dtBias); // f32
151
+
152
+ let stateIn = state;
153
+ let ownState = false;
154
+ if (!stateIn) {
155
+ stateIn = ops.zeros([B, Hv, Dv, Dk], Dtype.float32);
156
+ ownState = true;
157
+ }
158
+
159
+ const T = q.shape[1]!;
160
+ const tArr = MlxArray.fromInt32(new Int32Array([T]), [1]);
161
+ const [y, stateOut] = getKernel().apply([q, k, v, g, beta, stateIn, tArr], {
162
+ outputs: [
163
+ { shape: [B, T, Hv, Dv], dtype: q.dtype },
164
+ { shape: [B, Hv, Dv, Dk], dtype: Dtype.float32 },
165
+ ],
166
+ grid: [32, Dv, B * Hv],
167
+ threadGroup: [32, 4, 1],
168
+ templateInts: { Dk, Dv, Hk, Hv },
169
+ templateDtypes: { InT: q.dtype, StT: Dtype.float32 },
170
+ });
171
+ beta.dispose();
172
+ g.dispose();
173
+ tArr.dispose();
174
+ if (ownState) stateIn.dispose();
175
+ return [y!, stateOut!];
176
+ }
177
+
178
+ /** Recurrent cache for a gated-DeltaNet layer — port of mlx-lm
179
+ * cache.ArraysCache(size=2): slot 0 = causal-conv state [B, K-1, conv_dim],
180
+ * slot 1 = recurrent state [B, Hv, Dv, Dk] f32. The linear-attn layer reads
181
+ * and writes these slots directly; `advance(N)` only tracks token count for
182
+ * B=1 single-stream (lengths / left_padding are batched-decode concerns).
183
+ * Not a KVCache, so maybeQuantizeKv skips it. */
184
+ export class SSMCache implements Cache {
185
+ conv: MlxArray | null = null;
186
+ recurrent: MlxArray | null = null;
187
+ offset = 0;
188
+
189
+ /** Linear-attn layers don't go through the KV update path. */
190
+ updateAndFetch(): [MlxArray, MlxArray] {
191
+ throw new Error("SSMCache has no KV updateAndFetch (gated-DeltaNet layer)");
192
+ }
193
+
194
+ makeMask(_N: number, _windowSize: number | null): Mask {
195
+ // B=1 single-stream: ssm_mask is None (no left-pad / variable lengths).
196
+ return { mode: "", arr: null };
197
+ }
198
+
199
+ advance(n: number): void {
200
+ this.offset += n;
201
+ }
202
+
203
+ state(): MlxArray[] {
204
+ const out: MlxArray[] = [];
205
+ if (this.conv) out.push(this.conv);
206
+ if (this.recurrent) out.push(this.recurrent);
207
+ return out;
208
+ }
209
+
210
+ isTrimmable(): boolean {
211
+ return false; // recurrent state can't be rolled back per token
212
+ }
213
+
214
+ trim(_n: number): void {
215
+ throw new Error("SSMCache is not trimmable");
216
+ }
217
+
218
+ dispose(): void {
219
+ this.conv?.dispose();
220
+ this.recurrent?.dispose();
221
+ this.conv = null;
222
+ this.recurrent = null;
223
+ }
224
+ }