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,261 @@
1
+ // Gemma-4 "-assistant" drafter — port of optiq's
2
+ // runtime/spec/drafters/gemma_assistant.py (itself a port of Ollama's
3
+ // x/models/gemma4/assistant.go, MIT).
4
+ //
5
+ // Q-only 4-layer transformer: K/V come from the TARGET's last sliding-
6
+ // and full-attention donor caches at call time; pre/post projections
7
+ // bridge target hidden space (2560) <-> drafter space (256); output via
8
+ // the centroid-clustered head (2048 centroids, top-32 × 128 tokens).
9
+ //
10
+ // Implementation deviation (argmax-equivalent): the reference scatters
11
+ // candidate scores into full-vocab logits initialised to -1e30 and
12
+ // argmaxes; we argmax over the 4096 candidate scores and map through
13
+ // candidate ids directly — same winner, no 262k materialisation. The
14
+ // drafter's numeric details only influence acceptance RATE, never
15
+ // output correctness (the target's verify decides every token).
16
+
17
+ import type { ModelConfig } from "../config";
18
+ import { Weights } from "../weights";
19
+ import { MlxArray } from "../mlx/array";
20
+ import { Dtype } from "../mlx/ffi";
21
+ import * as ops from "../mlx/ops";
22
+ import type { SharedKv } from "../model/gemma4";
23
+
24
+ export interface DrafterStep {
25
+ /** Drafted token id. */
26
+ token: number;
27
+ /** Post-projected hidden (1,1,backbone) — feeds the next draft step. */
28
+ nextHidden: MlxArray;
29
+ }
30
+
31
+ interface Block {
32
+ layerType: string;
33
+ headDim: number;
34
+ ropeBase: number | null;
35
+ ropeFreqs: MlxArray | null;
36
+ qProj: MlxArray;
37
+ oProj: MlxArray;
38
+ qNorm: MlxArray;
39
+ inputNorm: MlxArray;
40
+ postAttnNorm: MlxArray;
41
+ preFfNorm: MlxArray;
42
+ postFfNorm: MlxArray;
43
+ gateProj: MlxArray;
44
+ upProj: MlxArray;
45
+ downProj: MlxArray;
46
+ layerScalar: MlxArray;
47
+ }
48
+
49
+ export class GemmaAssistantDrafter {
50
+ readonly hidden: number;
51
+ readonly nHeads: number;
52
+ readonly slidingWindow: number;
53
+ readonly eps: number;
54
+ readonly numCentroids: number;
55
+ readonly topK: number;
56
+ readonly vocabPerCentroid: number;
57
+ #w: Weights;
58
+ #blocks: Block[] = [];
59
+ #prePT: MlxArray; // pre_projection transposed [in, out]
60
+ #postPT: MlxArray;
61
+ #centroidsT: MlxArray; // [hidden, numCentroids]
62
+ #ordering: MlxArray; // [numCentroids, vocabPerCentroid] int32
63
+ #embed: MlxArray; // drafter token embeddings [vocab, hidden]
64
+ #norm: MlxArray;
65
+
66
+ private constructor(w: Weights, cfg: Record<string, any>) {
67
+ const t = cfg.text_config ?? cfg;
68
+ this.hidden = t.hidden_size;
69
+ this.nHeads = t.num_attention_heads;
70
+ this.slidingWindow = t.sliding_window;
71
+ this.eps = t.rms_norm_eps;
72
+ this.numCentroids = cfg.num_centroids;
73
+ this.topK = cfg.centroid_intermediate_top_k;
74
+ this.vocabPerCentroid = Math.floor(t.vocab_size / this.numCentroids);
75
+ this.#w = w;
76
+
77
+ const T = (name: string) => w.tensor(name);
78
+ const transposed = (name: string): MlxArray => {
79
+ const a = ops.transposeAxes(T(name), [1, 0]);
80
+ const c = ops.contiguous(a);
81
+ a.dispose();
82
+ c.eval();
83
+ return c;
84
+ };
85
+ this.#prePT = transposed("pre_projection.weight");
86
+ this.#postPT = transposed("post_projection.weight");
87
+ this.#centroidsT = transposed("masked_embedding.centroids.weight");
88
+ this.#embed = T("model.embed_tokens.weight");
89
+ this.#norm = T("model.norm.weight");
90
+ const ord64 = T("masked_embedding.token_ordering");
91
+ const ord32 = ord64.astype(Dtype.int32);
92
+ this.#ordering = ops.reshape(ord32, [this.numCentroids, this.vocabPerCentroid]);
93
+ this.#ordering.eval();
94
+ ord32.dispose();
95
+
96
+ const ropeP = t.rope_parameters ?? {};
97
+ for (let i = 0; i < t.num_hidden_layers; i++) {
98
+ const layerType = t.layer_types[i];
99
+ const isFull = layerType === "full_attention";
100
+ const headDim = isFull ? t.global_head_dim : t.head_dim;
101
+ const rp = ropeP[layerType] ?? {};
102
+ let ropeBase: number | null = rp.rope_theta ?? (isFull ? 1e6 : 1e4);
103
+ let ropeFreqs: MlxArray | null = null;
104
+ const partial = rp.partial_rotary_factor ?? 1.0;
105
+ if (isFull && partial < 1.0) {
106
+ // ProportionalRoPE freqs, matching the target's donor-K rope
107
+ const rotated = Math.floor(headDim * partial);
108
+ const n = headDim / 2;
109
+ const freqs = new Float32Array(n).fill(Infinity);
110
+ for (let k = 0; k < rotated / 2; k++)
111
+ freqs[k] = Math.pow(ropeBase!, (2 * k) / headDim);
112
+ ropeFreqs = MlxArray.fromFloat32(freqs, [n]);
113
+ ropeBase = null;
114
+ }
115
+ const p = `model.layers.${i}`;
116
+ this.#blocks.push({
117
+ layerType, headDim, ropeBase, ropeFreqs,
118
+ qProj: transposed(`${p}.self_attn.q_proj.weight`),
119
+ oProj: transposed(`${p}.self_attn.o_proj.weight`),
120
+ qNorm: T(`${p}.self_attn.q_norm.weight`),
121
+ inputNorm: T(`${p}.input_layernorm.weight`),
122
+ postAttnNorm: T(`${p}.post_attention_layernorm.weight`),
123
+ preFfNorm: T(`${p}.pre_feedforward_layernorm.weight`),
124
+ postFfNorm: T(`${p}.post_feedforward_layernorm.weight`),
125
+ gateProj: transposed(`${p}.mlp.gate_proj.weight`),
126
+ upProj: transposed(`${p}.mlp.up_proj.weight`),
127
+ downProj: transposed(`${p}.mlp.down_proj.weight`),
128
+ layerScalar: T(`${p}.layer_scalar`),
129
+ });
130
+ }
131
+ }
132
+
133
+ static async load(modelDir: string): Promise<GemmaAssistantDrafter> {
134
+ const cfg = (await Bun.file(`${modelDir}/config.json`).json()) as Record<string, any>;
135
+ return new GemmaAssistantDrafter(await Weights.open(modelDir), cfg);
136
+ }
137
+
138
+ /** f32-internal RMSNorm (reference _RMSNorm casts through float32). */
139
+ #rms(x: MlxArray, weight: MlxArray): MlxArray {
140
+ const f = x.astype(Dtype.float32);
141
+ const n = ops.rmsNorm(f, weight, this.eps);
142
+ f.dispose();
143
+ const back = n.astype(x.dtype);
144
+ n.dispose();
145
+ return back;
146
+ }
147
+
148
+ /** One draft step. All inputs (1,1,·); sharedKv holds the target's
149
+ * donor K/V in chronological order. position = target's last position. */
150
+ forward(
151
+ lastTokenEmb: MlxArray, targetHidden: MlxArray,
152
+ sharedKv: { sliding: [MlxArray, MlxArray]; full: [MlxArray, MlxArray] },
153
+ position: number,
154
+ ): DrafterStep {
155
+ let h = ops.concatAxis([lastTokenEmb, targetHidden], 2); // (1,1,2*backbone)
156
+ h = disposing(h, ops.matmul(h, this.#prePT)); // (1,1,256)
157
+
158
+ for (const blk of this.#blocks) {
159
+ const [k, v] = blk.layerType === "sliding_attention" ? sharedKv.sliding : sharedKv.full;
160
+ const kLen = k.shape[2]!;
161
+
162
+ // sliding-window additive mask over cached K positions
163
+ let mask: MlxArray | null = null;
164
+ if (blk.layerType === "sliding_attention") {
165
+ const firstCached = position - kLen + 1;
166
+ const windowStart = Math.max(0, position - this.slidingWindow + 1);
167
+ const allowedFrom = Math.max(0, windowStart - firstCached);
168
+ if (allowedFrom > 0) {
169
+ const m = new Float32Array(kLen);
170
+ for (let i = 0; i < allowedFrom; i++) m[i] = -1e9;
171
+ const mf = MlxArray.fromFloat32(m, [1, 1, 1, kLen]);
172
+ mask = mf.astype(k.dtype);
173
+ mf.dispose();
174
+ }
175
+ }
176
+
177
+ const residual = h;
178
+ let x = this.#rms(h, blk.inputNorm);
179
+ // Q-only attention
180
+ let q = ops.matmul(x, blk.qProj);
181
+ x.dispose();
182
+ q = disposing(q, ops.reshape(q, [1, 1, this.nHeads, blk.headDim]));
183
+ q = disposing(q, this.#rms(q, blk.qNorm));
184
+ q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
185
+ q = disposing(q, ops.rope(q, blk.headDim, blk.ropeBase, position, blk.ropeFreqs));
186
+ let attn = ops.sdpa(q, k, v, Math.pow(blk.headDim, -0.5), mask ? "array" : "", mask);
187
+ q.dispose();
188
+ mask?.dispose();
189
+ attn = disposing(attn, ops.transposeAxes(attn, [0, 2, 1, 3]));
190
+ attn = disposing(attn, ops.reshape(attn, [1, 1, this.nHeads * blk.headDim]));
191
+ attn = disposing(attn, ops.matmul(attn, blk.oProj));
192
+ attn = disposing(attn, this.#rms(attn, blk.postAttnNorm));
193
+ h = ops.add(residual, attn);
194
+ residual.dispose();
195
+ attn.dispose();
196
+
197
+ const res2 = h;
198
+ let f = this.#rms(h, blk.preFfNorm);
199
+ const g = ops.matmul(f, blk.gateProj);
200
+ const u = ops.matmul(f, blk.upProj);
201
+ f.dispose();
202
+ const act = ops.geluApprox(g);
203
+ g.dispose();
204
+ let mlp = ops.mul(act, u);
205
+ act.dispose();
206
+ u.dispose();
207
+ mlp = disposing(mlp, ops.matmul(mlp, blk.downProj));
208
+ mlp = disposing(mlp, this.#rms(mlp, blk.postFfNorm));
209
+ h = ops.add(res2, mlp);
210
+ res2.dispose();
211
+ mlp.dispose();
212
+
213
+ h = disposing(h, ops.mul(h, blk.layerScalar));
214
+ }
215
+
216
+ h = disposing(h, this.#rms(h, this.#norm));
217
+
218
+ // centroid decode → drafted token (argmax-equivalent shortcut)
219
+ const cScores = ops.matmul(h, this.#centroidsT); // (1,1,2048)
220
+ const flat = ops.reshape(cScores, [1, this.numCentroids]);
221
+ cScores.dispose();
222
+ const neg = ops.neg(flat);
223
+ flat.dispose();
224
+ const part = ops.argpartitionAxis(neg, this.topK - 1, -1);
225
+ neg.dispose();
226
+ const topIdx = part.slice([0, 0], [1, this.topK]); // (1, topK)
227
+ part.dispose();
228
+ const topFlat = ops.reshape(topIdx, [this.topK]);
229
+ topIdx.dispose();
230
+ const candIds = ops.takeAxis(this.#ordering, topFlat, 0); // (topK, vpc) int32
231
+ topFlat.dispose();
232
+ const candFlat = ops.reshape(candIds, [this.topK * this.vocabPerCentroid]);
233
+ candIds.dispose();
234
+ const candEmb = ops.takeAxis(this.#embed, candFlat, 0); // (topK*vpc, hidden)
235
+ const hVec = ops.reshape(h, [this.hidden, 1]);
236
+ const scores = ops.matmul(candEmb, hVec); // (topK*vpc, 1)
237
+ candEmb.dispose();
238
+ hVec.dispose();
239
+ const scoresFlat = ops.reshape(scores, [1, this.topK * this.vocabPerCentroid]);
240
+ scores.dispose();
241
+ const am = ops.argmaxAxis(scoresFlat, -1);
242
+ scoresFlat.dispose();
243
+ const winner = ops.itemUint32(am);
244
+ am.dispose();
245
+ const tokArr = candFlat.slice([winner], [winner + 1]);
246
+ candFlat.dispose();
247
+ const tokU32 = tokArr.astype(Dtype.uint32);
248
+ tokArr.dispose();
249
+ const token = ops.itemUint32(tokU32);
250
+ tokU32.dispose();
251
+
252
+ const nextHidden = ops.matmul(h, this.#postPT); // (1,1,2560)
253
+ h.dispose();
254
+ return { token, nextHidden };
255
+ }
256
+ }
257
+
258
+ function disposing(old: MlxArray, next: MlxArray): MlxArray {
259
+ old.dispose();
260
+ return next;
261
+ }
@@ -0,0 +1,221 @@
1
+ // Speculative decoding loop — port of optiq runtime/spec/runtime.py
2
+ // (γ-draft / verify-in-one-pass / accept-longest-prefix / rollback).
3
+ //
4
+ // Correctness contract: greedy spec output is token-for-token identical
5
+ // to greedy non-spec decode — the verify step re-derives every emitted
6
+ // token with the SAME pick function as the stock path (logprobs argmax
7
+ // over [1, V]); drafts only ever skip target forwards, never change
8
+ // what the target would emit.
9
+ //
10
+ // Limitation (same as the reference): partial-accept rollback requires
11
+ // trimmable caches; RotatingKVCache loses trimability once its ring
12
+ // wraps (offset ≥ sliding_window). We throw, as the reference does.
13
+
14
+ import { MlxArray } from "../mlx/array";
15
+ import * as ops from "../mlx/ops";
16
+ import { Gemma4Model, type Cache } from "../model/gemma4";
17
+ import { toLogprobs } from "../sampler";
18
+ import type { GemmaAssistantDrafter } from "./drafter";
19
+
20
+ export interface SpecOptions {
21
+ gamma?: number;
22
+ maxTokens?: number;
23
+ eosTokenIds?: number[];
24
+ }
25
+
26
+ export interface SpecStats {
27
+ emitted: number;
28
+ drafted: number;
29
+ accepted: number;
30
+ targetCalls: number;
31
+ decodeMs: number;
32
+ prefillMs: number;
33
+ acceptanceRate: number;
34
+ decodeTps: number;
35
+ }
36
+
37
+ export interface SpecResult {
38
+ tokens: number[];
39
+ stats: SpecStats;
40
+ }
41
+
42
+ /** The stock path's greedy pick, bit-identical shapes: slice ONE hidden
43
+ * position [1,1,H], lm-head on it (exactly what generate() does for a
44
+ * decode step), then logprobs argmax over [1, V]. Computing the lm-head
45
+ * per position (instead of batched over the verify window) keeps every
46
+ * pick in the same kernel shapes as stock decode — batched lm-head
47
+ * rounds differently and flips bf16 knife-edges. */
48
+ function pickFromHidden(model: Gemma4Model, hidden: MlxArray, pos: number): number {
49
+ const H = hidden.shape[2]!;
50
+ const hSl = hidden.slice([0, pos, 0], [1, pos + 1, H]);
51
+ const logits = model.logitsFromHidden(hSl);
52
+ hSl.dispose();
53
+ const V = logits.shape[2]!;
54
+ const flat = ops.reshape(logits, [1, V]);
55
+ logits.dispose();
56
+ const lp = toLogprobs(flat);
57
+ flat.dispose();
58
+ const am = ops.argmaxAxis(lp, -1);
59
+ lp.dispose();
60
+ const t = ops.itemUint32(am);
61
+ am.dispose();
62
+ return t;
63
+ }
64
+
65
+ /** Donor caches: the target's LAST sliding and LAST full cache owners
66
+ * (port of optiq kv_view.find_donor_layers). */
67
+ function donorIndices(model: Gemma4Model): { sliding: number; full: number } {
68
+ let sliding = -1;
69
+ let full = -1;
70
+ for (let i = 0; i < model.numDonors; i++) {
71
+ if (model.layers[i]!.layerType === "sliding_attention") sliding = i;
72
+ else full = i;
73
+ }
74
+ if (sliding < 0 || full < 0) throw new Error("missing donor layer type");
75
+ return { sliding, full };
76
+ }
77
+
78
+ export function specGenerate(
79
+ model: Gemma4Model,
80
+ drafter: GemmaAssistantDrafter,
81
+ promptTokens: number[],
82
+ options: SpecOptions = {},
83
+ ): SpecResult {
84
+ const { gamma = 2, maxTokens = 256, eosTokenIds = model.config.eosTokenIds } = options;
85
+ if (gamma < 1) throw new Error("gamma must be >= 1");
86
+
87
+ const caches = model.makeCache();
88
+ const donors = donorIndices(model);
89
+ const stats: SpecStats = {
90
+ emitted: 0, drafted: 0, accepted: 0, targetCalls: 0,
91
+ decodeMs: 0, prefillMs: 0, acceptanceRate: 0, decodeTps: 0,
92
+ };
93
+ const out: number[] = [];
94
+
95
+ const embedScaled = (token: number): MlxArray => {
96
+ const ids = ops.fromInt32([token], [1, 1]);
97
+ const e = model.embed.encode(ids);
98
+ ids.dispose();
99
+ const s = ops.mulScalar(e, model.embedScale);
100
+ e.dispose();
101
+ return s;
102
+ };
103
+
104
+ const readDonors = () => ({
105
+ sliding: (caches[donors.sliding] as never as { temporalView(): [MlxArray, MlxArray] }).temporalView(),
106
+ full: (caches[donors.full] as never as { temporalView(): [MlxArray, MlxArray] }).temporalView(),
107
+ });
108
+
109
+ try {
110
+ // 1. prefill
111
+ const t0 = performance.now();
112
+ const ids = ops.fromInt32(promptTokens, [1, promptTokens.length]);
113
+ const hidden = model.forwardHidden(ids, caches);
114
+ ids.dispose();
115
+ stats.targetCalls++;
116
+ const H = hidden.shape[2]!;
117
+ const Lp = hidden.shape[1]!;
118
+ let next = pickFromHidden(model, hidden, Lp - 1);
119
+ let lastHidden = hidden.slice([0, Lp - 1, 0], [1, Lp, H]);
120
+ hidden.dispose();
121
+ stats.prefillMs = performance.now() - t0;
122
+
123
+ // EOS convention: an EOS id stops generation and counts toward
124
+ // stats.emitted (reference-faithful: optiq runtime.py counts it) but
125
+ // is NOT part of the returned content — matching our generate(),
126
+ // which never yields EOS. optiq's spec yields it only as a stream
127
+ // EVENT (clients see the stop); its token array role here is content.
128
+ const nextIsEos = eosTokenIds.includes(next);
129
+ if (!nextIsEos) out.push(next);
130
+ stats.emitted++;
131
+ if (nextIsEos) return { tokens: out, stats };
132
+
133
+ // 2. outer loop
134
+ const tDecode = performance.now();
135
+ outer: while (stats.emitted < maxTokens) {
136
+ const position = caches[0]!.offset - 1;
137
+ const shared = readDonors();
138
+
139
+ // 2a. draft γ tokens against the frozen donor views
140
+ const drafts: number[] = [];
141
+ let dTok = next;
142
+ let dHid = lastHidden; // borrowed for k=0
143
+ const ownedHiddens: MlxArray[] = [];
144
+ for (let k = 0; k < gamma; k++) {
145
+ const emb = embedScaled(dTok);
146
+ const step = drafter.forward(emb, dHid, shared, position + k);
147
+ emb.dispose();
148
+ drafts.push(step.token);
149
+ stats.drafted++;
150
+ ownedHiddens.push(step.nextHidden);
151
+ dTok = step.token;
152
+ dHid = step.nextHidden;
153
+ }
154
+ for (const a of ownedHiddens) a.dispose();
155
+ for (const [k, v] of [shared.sliding, shared.full]) {
156
+ k.dispose();
157
+ v.dispose();
158
+ }
159
+
160
+ // 2b. verify all γ drafts (+ the pending token) in one forward
161
+ const vIds = ops.fromInt32([next, ...drafts], [1, gamma + 1]);
162
+ const vHidden = model.forwardHidden(vIds, caches);
163
+ vIds.dispose();
164
+ stats.targetCalls++;
165
+
166
+ // 2c. accept the longest matching prefix (per-position lm-head —
167
+ // see pickFromHidden)
168
+ const gt: number[] = [];
169
+ for (let k = 0; k <= gamma; k++) gt.push(pickFromHidden(model, vHidden, k));
170
+ let kAccept = 0;
171
+ while (kAccept < gamma && drafts[kAccept] === gt[kAccept]) kAccept++;
172
+ stats.accepted += kAccept;
173
+
174
+ // 2d. emit accepted drafts
175
+ for (let k = 0; k < kAccept; k++) {
176
+ const isEos = eosTokenIds.includes(drafts[k]!);
177
+ if (!isEos) out.push(drafts[k]!);
178
+ stats.emitted++;
179
+ if (isEos || stats.emitted >= maxTokens) {
180
+ vHidden.dispose();
181
+ break outer;
182
+ }
183
+ }
184
+
185
+ // 2e. emit the correction (or bonus when all accepted)
186
+ const emit = gt[kAccept]!;
187
+ const emitIsEos = eosTokenIds.includes(emit);
188
+ if (!emitIsEos) out.push(emit);
189
+ stats.emitted++;
190
+
191
+ // 2f. roll back rejected entries
192
+ if (kAccept < gamma) {
193
+ const n = gamma - kAccept;
194
+ for (const c of caches) {
195
+ if (!c.isTrimmable())
196
+ throw new Error(`${c.constructor.name} not trimmable — context exceeded the sliding window during spec decode`);
197
+ c.trim(n);
198
+ }
199
+ }
200
+
201
+ if (emitIsEos || stats.emitted >= maxTokens) {
202
+ vHidden.dispose();
203
+ break;
204
+ }
205
+
206
+ // 2g. chain state
207
+ next = emit;
208
+ lastHidden.dispose();
209
+ lastHidden = vHidden.slice([0, kAccept, 0], [1, kAccept + 1, H]);
210
+ vHidden.dispose();
211
+ }
212
+ stats.decodeMs = performance.now() - tDecode;
213
+ lastHidden.dispose();
214
+ } finally {
215
+ for (const c of caches) c.dispose();
216
+ }
217
+
218
+ stats.acceptanceRate = stats.drafted ? stats.accepted / stats.drafted : 0;
219
+ stats.decodeTps = stats.decodeMs ? ((stats.emitted - 1) / stats.decodeMs) * 1000 : 0;
220
+ return { tokens: out, stats };
221
+ }