mlx-bun 0.3.0 → 0.5.0

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 (231) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +58 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  5. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  6. package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
  7. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  8. package/docs/design/batching.md +1535 -50
  9. package/docs/design/decode-speed-program.md +394 -1171
  10. package/docs/design/generic-model-support.md +195 -3
  11. package/docs/design/kv-cache.md +538 -225
  12. package/docs/design/speculative-decoding.md +552 -50
  13. package/docs/design/turboquant.md +140 -17
  14. package/docs/design/unified-engine-frontier-plan.md +259 -150
  15. package/docs/planning/PRODUCT_ROADMAP.md +21 -6
  16. package/docs/planning/public-readiness-handoff.md +244 -0
  17. package/docs/planning/public-readiness.md +149 -0
  18. package/docs/reference/benchmarks.md +3537 -1112
  19. package/docs/reference/cli.md +102 -2
  20. package/docs/reference/distribution.md +45 -13
  21. package/docs/reference/environment.md +159 -13
  22. package/docs/reference/library-api.md +18 -4
  23. package/docs/reference/memory.md +1 -1
  24. package/docs/reference/models.md +28 -7
  25. package/docs/reference/server-api.md +257 -33
  26. package/docs/reference/server-config.md +372 -151
  27. package/docs/reference/troubleshooting.md +13 -21
  28. package/package.json +2 -2
  29. package/src/audio/audiotoolbox.ts +112 -0
  30. package/src/audio/conformer.ts +8 -0
  31. package/src/audio/mic-capture.ts +98 -0
  32. package/src/audio/silero-vad.ts +379 -0
  33. package/src/audio/whisper-decode.ts +774 -0
  34. package/src/audio/whisper-fast.ts +732 -0
  35. package/src/audio/whisper-format.ts +73 -0
  36. package/src/audio/whisper-mel.ts +185 -0
  37. package/src/audio/whisper-timing.ts +274 -0
  38. package/src/audio/whisper-tokenizer.ts +239 -0
  39. package/src/audio/whisper-transcribe.ts +410 -0
  40. package/src/backends/mlx/assistant-target.ts +46 -0
  41. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  42. package/src/backends/mlx/autoregressive.ts +22 -1
  43. package/src/backends/mlx/batch-group.ts +464 -236
  44. package/src/backends/mlx/cache-attachment.ts +67 -0
  45. package/src/backends/mlx/cache-layout.ts +31 -0
  46. package/src/backends/mlx/checkpoint-state.ts +53 -0
  47. package/src/backends/mlx/continuation-persistence.ts +107 -0
  48. package/src/backends/mlx/continuation-request.ts +43 -0
  49. package/src/backends/mlx/continuation.ts +77 -0
  50. package/src/backends/mlx/draft-target.ts +7 -22
  51. package/src/backends/mlx/encoder-cache.ts +25 -0
  52. package/src/backends/mlx/fill-append.ts +21 -8
  53. package/src/backends/mlx/fill-group.ts +497 -0
  54. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  55. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  56. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  57. package/src/backends/mlx/gateway-binding.ts +122 -19
  58. package/src/backends/mlx/kv-codec.ts +9 -0
  59. package/src/backends/mlx/kv-maintenance.ts +161 -0
  60. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  61. package/src/backends/mlx/mixed-iteration.ts +62 -0
  62. package/src/backends/mlx/model-prompt.ts +46 -32
  63. package/src/backends/mlx/model-serving.ts +5 -4
  64. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  65. package/src/backends/mlx/prefill-cohort.ts +100 -0
  66. package/src/backends/mlx/prefill-policy.ts +26 -0
  67. package/src/backends/mlx/prefill-rows.ts +244 -0
  68. package/src/backends/mlx/prefill.ts +1 -1
  69. package/src/backends/mlx/projected-context-rows.ts +72 -0
  70. package/src/backends/mlx/prompt-input.ts +29 -0
  71. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  72. package/src/backends/mlx/request-state-policy.ts +54 -0
  73. package/src/backends/mlx/rollback.ts +45 -4
  74. package/src/backends/mlx/row-sampling.ts +49 -0
  75. package/src/backends/mlx/serial-executor.ts +51 -73
  76. package/src/backends/mlx/speculative-group.ts +438 -0
  77. package/src/backends/mlx/speculative-round.ts +161 -0
  78. package/src/backends/mlx/speculative.ts +24 -7
  79. package/src/backends/mlx/state-rows.ts +104 -0
  80. package/src/backends/mlx/state-views.ts +21 -1
  81. package/src/backends/mlx/target-layout-capability.ts +35 -0
  82. package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
  83. package/src/cli.ts +333 -36
  84. package/src/contracts/execution.ts +21 -0
  85. package/src/contracts/object-cache.ts +8 -0
  86. package/src/contracts/pi-provider.ts +1 -1
  87. package/src/contracts/pi.ts +3 -1
  88. package/src/contracts/prefix-cache.ts +33 -0
  89. package/src/contracts/resources.ts +7 -0
  90. package/src/contracts/sampling.ts +23 -0
  91. package/src/contracts/scheduling.ts +27 -0
  92. package/src/engine/execution-plan.ts +38 -25
  93. package/src/engine/execution-tasks.ts +44 -0
  94. package/src/engine/namespaced-cache.ts +13 -0
  95. package/src/engine/resources.ts +12 -1
  96. package/src/engine/scheduler.ts +32 -7
  97. package/src/fill/fill-session.ts +14 -3
  98. package/src/fit.ts +29 -15
  99. package/src/generate.ts +105 -195
  100. package/src/grammar.ts +13 -0
  101. package/src/inference/draft-acceptance.ts +41 -0
  102. package/src/inference/draft-output.ts +49 -0
  103. package/src/inference/prefill.ts +6 -0
  104. package/src/inference/rollback.ts +2 -2
  105. package/src/kv-scheme.ts +92 -12
  106. package/src/kv-store.ts +183 -93
  107. package/src/lab/README.md +5 -1
  108. package/src/lab/paged-kv/paged-attention.ts +117 -0
  109. package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
  110. package/src/lab/paged-kv/paged-kv.ts +126 -46
  111. package/src/md-modules.d.ts +5 -0
  112. package/src/mlx/array.ts +39 -1
  113. package/src/mlx/ffi.ts +19 -0
  114. package/src/mlx/normalized-argmax.ts +116 -0
  115. package/src/mlx/ops.ts +97 -10
  116. package/src/model/artifact-identity.ts +73 -0
  117. package/src/model/batched-kv.ts +235 -0
  118. package/src/model/batched-mask.ts +12 -4
  119. package/src/model/batched-quant.ts +8 -2
  120. package/src/model/batched-quantized-kv.ts +144 -0
  121. package/src/model/batched-rotating-quant.ts +76 -15
  122. package/src/model/batched-rotating-state.ts +76 -0
  123. package/src/model/batched-rotating.ts +64 -22
  124. package/src/model/batched-row-storage.ts +52 -0
  125. package/src/model/batched-ssm.ts +52 -0
  126. package/src/model/batched-turboquant-kv.ts +94 -0
  127. package/src/model/compiled-decode.ts +7 -1
  128. package/src/model/decoded-kv-donor.ts +12 -0
  129. package/src/model/delayed-quantized-kv.ts +72 -0
  130. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  131. package/src/model/delayed-turboquant-kv.ts +57 -0
  132. package/src/model/factory.ts +26 -0
  133. package/src/model/fingerprint.ts +12 -0
  134. package/src/model/full-prefill-padding.ts +56 -0
  135. package/src/model/full-prefill-row.ts +105 -0
  136. package/src/model/gemma4-base.ts +183 -82
  137. package/src/model/gemma4.ts +116 -28
  138. package/src/model/generated/gemma4-12b.ts +4 -1
  139. package/src/model/generated/gemma4-26b.ts +4 -1
  140. package/src/model/generated/gemma4-e4b.ts +4 -1
  141. package/src/model/glm52-cache.ts +80 -4
  142. package/src/model/glm52-mla.ts +12 -6
  143. package/src/model/glm52.ts +19 -40
  144. package/src/model/kv-attention-view.ts +70 -0
  145. package/src/model/minicpm5.ts +11 -7
  146. package/src/model/profile.ts +29 -4
  147. package/src/model/quantized-append-attention.ts +31 -0
  148. package/src/model/qwen3-delta-state.ts +74 -0
  149. package/src/model/qwen3-delta.ts +59 -36
  150. package/src/model/qwen3-mrope.ts +7 -9
  151. package/src/model/qwen3_5.ts +177 -75
  152. package/src/model/rotating-kv-donor.ts +35 -0
  153. package/src/model/rotating-kv-layout.ts +262 -0
  154. package/src/model/rotating-row-transaction.ts +35 -0
  155. package/src/model/speculative-rotating-kv.ts +101 -0
  156. package/src/model/ssm-prefill-padding.ts +39 -0
  157. package/src/model/support.ts +8 -0
  158. package/src/model/token-groups.ts +66 -0
  159. package/src/model/trellis-linear.ts +4 -4
  160. package/src/model/turboquant-codec.ts +89 -0
  161. package/src/model/universal/dense.ts +14 -13
  162. package/src/model/whisper.ts +435 -0
  163. package/src/native/mic_capture.swift +92 -0
  164. package/src/native-pack.ts +6 -5
  165. package/src/pi-web.ts +9 -1
  166. package/src/preflight.ts +31 -15
  167. package/src/prompt-cache.ts +306 -86
  168. package/src/runtime-config.ts +1 -5
  169. package/src/safetensors.ts +9 -2
  170. package/src/sampler.ts +37 -25
  171. package/src/serve/audio-routes.ts +323 -0
  172. package/src/serve/chat-request.ts +2 -0
  173. package/src/serve/chat-stage.ts +19 -14
  174. package/src/serve/completion-executor.ts +9 -1
  175. package/src/serve/discovery-routes.ts +17 -1
  176. package/src/serve/generated-token-history.ts +57 -0
  177. package/src/serve/generation-gateway.ts +88 -38
  178. package/src/serve/model-binding.ts +7 -1
  179. package/src/serve/model-host.ts +3 -4
  180. package/src/serve/preparation.ts +3 -3
  181. package/src/serve/prompt-response-trace.ts +14 -0
  182. package/src/serve/request-plan.ts +6 -3
  183. package/src/serve/request-prep.ts +40 -25
  184. package/src/serve/text-completion-stage.ts +2 -2
  185. package/src/serve/transcription-server.ts +64 -0
  186. package/src/serve/transcription-service.ts +502 -0
  187. package/src/server.ts +185 -261
  188. package/src/spec/assistant-rows.ts +93 -0
  189. package/src/spec/assistant-source.ts +28 -18
  190. package/src/spec/cache-identity.ts +22 -0
  191. package/src/spec/deepspec-rows.ts +29 -0
  192. package/src/spec/deepspec-source.ts +12 -10
  193. package/src/spec/dflash-source.ts +28 -4
  194. package/src/spec/drafter.ts +153 -142
  195. package/src/spec/dspark/deepspec-module.ts +95 -120
  196. package/src/spec/dspark/module-dflash.ts +126 -170
  197. package/src/spec/glm52-mtp-graph.ts +29 -0
  198. package/src/spec/glm52-mtp-rows.ts +132 -0
  199. package/src/spec/glm52-mtp-source.ts +54 -136
  200. package/src/spec/glm52-mtp-state.ts +33 -0
  201. package/src/spec/ngram-source.ts +115 -21
  202. package/src/spec/projected-draft-rows.ts +73 -0
  203. package/src/spec/qwen-mtp-module.ts +171 -0
  204. package/src/spec/qwen-mtp-rows.ts +230 -0
  205. package/src/spec/qwen-mtp-source.ts +84 -226
  206. package/src/spec/qwen-mtp-state.ts +30 -0
  207. package/src/spec/serve-loop.ts +73 -69
  208. package/src/spec/source.ts +93 -15
  209. package/src/spec/two-model-rows.ts +62 -0
  210. package/src/spec/two-model.ts +45 -103
  211. package/src/ssd-cache.ts +107 -36
  212. package/src/ssd-durability.ts +29 -27
  213. package/src/storage/host-buffer.ts +24 -0
  214. package/src/storage/kv-writer.ts +83 -0
  215. package/src/storage/kv-writer.worker.js +264 -0
  216. package/src/storage/retention-policy.ts +37 -0
  217. package/src/tiered-prompt-cache.ts +45 -0
  218. package/src/vision/embedder.ts +8 -0
  219. package/src/vision/prompt.ts +101 -65
  220. package/src/vision/qwen3vl-prompt.ts +101 -64
  221. package/src/vision/qwen3vl-tower.ts +11 -0
  222. package/src/vision/siglip.ts +8 -0
  223. package/src/web/app.html +12 -0
  224. package/src/web/app.js +217 -57
  225. package/src/web/src/chat.ts +4 -1
  226. package/src/web/src/composer.ts +3 -0
  227. package/src/web/src/voice.ts +141 -0
  228. package/src/web/sw.js +1 -1
  229. package/src/backends/mlx/memory-guard.ts +0 -40
  230. package/src/spec/prefix-state.ts +0 -45
  231. /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
@@ -28,8 +28,32 @@ import { Weights } from "../weights";
28
28
  import { MlxArray } from "../mlx/array";
29
29
  import { Dtype } from "../mlx/ffi";
30
30
  import * as ops from "../mlx/ops";
31
+ import { disposeResources } from "../engine/resources";
31
32
  import type { SharedKv } from "../model/gemma4";
32
33
 
34
+ /** The graph borrows donor attention; storage owns validity and encoding. */
35
+ export interface AssistantAttention {
36
+ attend(query: MlxArray, scale: number, window: number | null): MlxArray;
37
+ }
38
+ export interface AssistantDonors { sliding: AssistantAttention; full: AssistantAttention; }
39
+
40
+ /** Legacy plain donor adapter. The common graph never reads cache geometry. */
41
+ export function plainAssistantDonors(shared: { sliding: [MlxArray, MlxArray]; full: [MlxArray, MlxArray] },
42
+ position: number): AssistantDonors {
43
+ const bind = ([keys, values]: [MlxArray, MlxArray]): AssistantAttention => ({
44
+ attend(query, scale, window) {
45
+ const length = keys.shape[2]!;
46
+ const from = window === null ? 0 : Math.max(0, Math.max(0, position - window + 1) - (position - length + 1));
47
+ using floats = from ? MlxArray.fromFloat32(Float32Array.from({ length }, (_, i) => i < from ? -1e9 : 0), [1,1,1,length]) : null;
48
+ using mask = floats?.astype(keys.dtype);
49
+ return ops.sdpa(query, keys, values, scale, mask ? "array" : "", mask ?? null);
50
+ },
51
+ });
52
+ return { sliding: bind(shared.sliding), full: bind(shared.full) };
53
+ }
54
+
55
+ export interface DrafterRowsStep { tokens: MlxArray; nextHidden: MlxArray; }
56
+
33
57
  export interface DrafterStep {
34
58
  /** Drafted token id. */
35
59
  token: number;
@@ -64,6 +88,7 @@ export class GemmaAssistantDrafter {
64
88
  readonly topK: number;
65
89
  readonly vocabPerCentroid: number;
66
90
  #w: Weights;
91
+ #owned: MlxArray[] = [];
67
92
  #blocks: Block[] = [];
68
93
  readonly useCentroids: boolean;
69
94
  #prePT: MlxArray; // pre_projection transposed [in, out]
@@ -84,73 +109,80 @@ export class GemmaAssistantDrafter {
84
109
  this.vocabPerCentroid = Math.floor(t.vocab_size / this.numCentroids);
85
110
  this.#w = w;
86
111
 
87
- const T = (name: string) => w.tensor(name);
88
- const transposed = (name: string): MlxArray => {
89
- const a = ops.transposeAxes(T(name), [1, 0]);
90
- const c = ops.contiguous(a);
91
- a.dispose();
92
- c.eval();
93
- return c;
94
- };
95
- this.#prePT = transposed("pre_projection.weight");
96
- this.#postPT = transposed("post_projection.weight");
97
- this.#embed = T("model.embed_tokens.weight");
98
- this.#norm = T("model.norm.weight");
99
- // Head variant: only the small drafters ship the centroid tensors.
100
- // The 12B/26B artifacts ship a tied embed_tokens head instead (their
101
- // config still declares num_centroids — ignore it, trust the tensors).
102
- this.useCentroids = w.has("masked_embedding.centroids.weight");
103
- if (this.useCentroids) {
104
- this.#centroidsT = transposed("masked_embedding.centroids.weight");
105
- const ord64 = T("masked_embedding.token_ordering");
106
- const ord32 = ord64.astype(Dtype.int32);
107
- this.#ordering = ops.reshape(ord32, [this.numCentroids, this.vocabPerCentroid]);
108
- this.#ordering.eval();
109
- ord32.dispose();
110
- }
112
+ const own = (array: MlxArray) => { this.#owned.push(array); return array; };
113
+ try {
114
+ const T = (name: string) => w.tensor(name);
115
+ // Keep the oracle's strided weight.T: a contiguous transpose changes
116
+ // M4 matmul rounding and allocates a second copy of every linear weight.
117
+ const transposed = (name: string): MlxArray => {
118
+ const c = own(ops.transposeAxes(T(name), [1, 0]));
119
+ c.eval();
120
+ return c;
121
+ };
122
+ this.#prePT = transposed("pre_projection.weight");
123
+ this.#postPT = transposed("post_projection.weight");
124
+ this.#embed = T("model.embed_tokens.weight");
125
+ this.#norm = T("model.norm.weight");
126
+ // Head variant: only the small drafters ship the centroid tensors.
127
+ // The 12B/26B artifacts ship a tied embed_tokens head instead (their
128
+ // config still declares num_centroids — ignore it, trust the tensors).
129
+ this.useCentroids = w.has("masked_embedding.centroids.weight");
130
+ if (this.useCentroids) {
131
+ this.#centroidsT = transposed("masked_embedding.centroids.weight");
132
+ const ord64 = T("masked_embedding.token_ordering");
133
+ const ord32 = ord64.astype(Dtype.int32);
134
+ this.#ordering = own(ops.reshape(ord32, [this.numCentroids, this.vocabPerCentroid]));
135
+ this.#ordering.eval();
136
+ ord32.dispose();
137
+ }
111
138
 
112
- const ropeP = t.rope_parameters ?? {};
113
- for (let i = 0; i < t.num_hidden_layers; i++) {
114
- const layerType = t.layer_types[i];
115
- const isFull = layerType === "full_attention";
116
- const headDim = isFull ? t.global_head_dim : t.head_dim;
117
- const rp = ropeP[layerType] ?? {};
118
- let ropeBase: number | null = rp.rope_theta ?? (isFull ? 1e6 : 1e4);
119
- let ropeFreqs: MlxArray | null = null;
120
- const partial = rp.partial_rotary_factor ?? 1.0;
121
- if (isFull && partial < 1.0) {
122
- // ProportionalRoPE freqs, matching the target's donor-K rope
123
- const rotated = Math.floor(headDim * partial);
124
- const n = headDim / 2;
125
- const freqs = new Float32Array(n).fill(Infinity);
126
- for (let k = 0; k < rotated / 2; k++)
127
- freqs[k] = Math.pow(ropeBase!, (2 * k) / headDim);
128
- ropeFreqs = MlxArray.fromFloat32(freqs, [n]);
129
- ropeBase = null;
139
+ const ropeP = t.rope_parameters ?? {};
140
+ for (let i = 0; i < t.num_hidden_layers; i++) {
141
+ const layerType = t.layer_types[i];
142
+ const isFull = layerType === "full_attention";
143
+ const headDim = isFull ? t.global_head_dim : t.head_dim;
144
+ const rp = ropeP[layerType] ?? {};
145
+ let ropeBase: number | null = rp.rope_theta ?? (isFull ? 1e6 : 1e4);
146
+ let ropeFreqs: MlxArray | null = null;
147
+ const partial = rp.partial_rotary_factor ?? 1.0;
148
+ if (isFull && partial < 1.0) {
149
+ // ProportionalRoPE freqs, matching the target's donor-K rope
150
+ const rotated = Math.floor(headDim * partial);
151
+ const n = headDim / 2;
152
+ const freqs = new Float32Array(n).fill(Infinity);
153
+ for (let k = 0; k < rotated / 2; k++)
154
+ freqs[k] = Math.pow(ropeBase!, (2 * k) / headDim);
155
+ ropeFreqs = own(MlxArray.fromFloat32(freqs, [n]));
156
+ ropeBase = null;
157
+ }
158
+ const p = `model.layers.${i}`;
159
+ this.#blocks.push({
160
+ layerType, headDim, ropeBase, ropeFreqs,
161
+ qProj: transposed(`${p}.self_attn.q_proj.weight`),
162
+ oProj: transposed(`${p}.self_attn.o_proj.weight`),
163
+ qNorm: T(`${p}.self_attn.q_norm.weight`),
164
+ inputNorm: T(`${p}.input_layernorm.weight`),
165
+ postAttnNorm: T(`${p}.post_attention_layernorm.weight`),
166
+ preFfNorm: T(`${p}.pre_feedforward_layernorm.weight`),
167
+ postFfNorm: T(`${p}.post_feedforward_layernorm.weight`),
168
+ gateProj: transposed(`${p}.mlp.gate_proj.weight`),
169
+ upProj: transposed(`${p}.mlp.up_proj.weight`),
170
+ downProj: transposed(`${p}.mlp.down_proj.weight`),
171
+ layerScalar: T(`${p}.layer_scalar`),
172
+ });
130
173
  }
131
- const p = `model.layers.${i}`;
132
- this.#blocks.push({
133
- layerType, headDim, ropeBase, ropeFreqs,
134
- qProj: transposed(`${p}.self_attn.q_proj.weight`),
135
- oProj: transposed(`${p}.self_attn.o_proj.weight`),
136
- qNorm: T(`${p}.self_attn.q_norm.weight`),
137
- inputNorm: T(`${p}.input_layernorm.weight`),
138
- postAttnNorm: T(`${p}.post_attention_layernorm.weight`),
139
- preFfNorm: T(`${p}.pre_feedforward_layernorm.weight`),
140
- postFfNorm: T(`${p}.post_feedforward_layernorm.weight`),
141
- gateProj: transposed(`${p}.mlp.gate_proj.weight`),
142
- upProj: transposed(`${p}.mlp.up_proj.weight`),
143
- downProj: transposed(`${p}.mlp.down_proj.weight`),
144
- layerScalar: T(`${p}.layer_scalar`),
145
- });
146
- }
174
+ } catch (error) { disposeResources(this.#owned.splice(0)); throw error; }
147
175
  }
148
176
 
149
177
  static async load(modelDir: string): Promise<GemmaAssistantDrafter> {
150
178
  const cfg = (await Bun.file(`${modelDir}/config.json`).json()) as Record<string, any>;
151
- return new GemmaAssistantDrafter(await Weights.open(modelDir), cfg);
179
+ const weights = await Weights.open(modelDir);
180
+ try { return new GemmaAssistantDrafter(weights, cfg); }
181
+ catch (error) { weights.dispose(); throw error; }
152
182
  }
153
183
 
184
+ dispose(): void { disposeResources([...this.#owned.splice(0), this.#w]); }
185
+
154
186
  /** f32-internal RMSNorm (reference _RMSNorm casts through float32). */
155
187
  #rms(x: MlxArray, weight: MlxArray): MlxArray {
156
188
  const f = x.astype(Dtype.float32);
@@ -161,49 +193,77 @@ export class GemmaAssistantDrafter {
161
193
  return back;
162
194
  }
163
195
 
164
- /** One draft step. All inputs (1,1,·); sharedKv holds the target's
165
- * donor K/V in chronological order. position = target's last position. */
166
- forward(
167
- lastTokenEmb: MlxArray, targetHidden: MlxArray,
168
- sharedKv: { sliding: [MlxArray, MlxArray]; full: [MlxArray, MlxArray] },
169
- position: number,
170
- ): DrafterStep {
171
- let h = ops.concatAxis([lastTokenEmb, targetHidden], 2); // (1,1,2*backbone)
172
- h = disposing(h, ops.matmul(h, this.#prePT)); // (1,1,256)
196
+ /** The request adapter consumes the same graph as a batch of one.
197
+ * Donors are chronological; position is the target's last position. */
198
+ forward(lastTokenEmb: MlxArray, targetHidden: MlxArray,
199
+ sharedKv: { sliding: [MlxArray, MlxArray]; full: [MlxArray, MlxArray] }, position: number): DrafterStep {
200
+ const result = this.forwardRows(lastTokenEmb, targetHidden, plainAssistantDonors(sharedKv, position), position);
201
+ try { return { token: ops.itemUint32(result.tokens), nextHidden: result.nextHidden }; }
202
+ catch (error) { result.nextHidden.dispose(); throw error; }
203
+ finally { result.tokens.dispose(); }
204
+ }
173
205
 
174
- for (const blk of this.#blocks) {
175
- const [k, v] = blk.layerType === "sliding_attention" ? sharedKv.sliding : sharedKv.full;
176
- const kLen = k.shape[2]!;
206
+ #sampleRows(h: MlxArray, B: number): MlxArray {
207
+ if (this.useCentroids) {
208
+ using scores = ops.matmul(h, this.#centroidsT!);
209
+ using flat = ops.reshape(scores, [B, this.numCentroids]);
210
+ using neg = ops.neg(flat);
211
+ using partition = ops.argpartitionAxis(neg, this.topK - 1, -1);
212
+ using top = partition.slice([0, 0], [B, this.topK]);
213
+ using topFlat = ops.reshape(top, [B * this.topK]);
214
+ using candidates = ops.takeAxis(this.#ordering!, topFlat, 0);
215
+ const count = this.topK * this.vocabPerCentroid;
216
+ using ids = ops.reshape(candidates, [B, count]);
217
+ using flatIds = ops.reshape(ids, [B * count]);
218
+ using embeddings = ops.takeAxis(this.#embed, flatIds, 0);
219
+ using matrix = ops.reshape(embeddings, [B, count, this.hidden]);
220
+ using vector = ops.reshape(h, [B, 1, this.hidden]);
221
+ // The oracle rounds each bf16 product before reduction. Matmul's
222
+ // accumulation changes candidate rankings on M1, even at identical h.
223
+ using products = ops.mul(matrix, vector);
224
+ using candidateScores = ops.sumAxis(products, -1, false);
225
+ using maximum = ops.maxAxis(candidateScores, -1, true);
226
+ using winners = ops.equal(candidateScores, maximum);
227
+ // Sparse full-vocabulary argmax chooses the lowest token ID on a tie.
228
+ // Apply that rule directly to the candidates without a vocab-size scatter.
229
+ using sentinel = ops.fromInt32([this.#embed.shape[0]!], []);
230
+ using eligible = ops.where(winners, ids, sentinel);
231
+ using selected = ops.minAxis(eligible, -1, false);
232
+ return selected.astype(Dtype.uint32);
233
+ }
234
+ using rows = ops.reshape(h, [B, this.hidden]);
235
+ using vector = B === 1 ? ops.reshape(h, [this.hidden, 1]) : ops.transposeAxes(rows, [1, 0]);
236
+ using scores = ops.matmul(this.#embed, vector);
237
+ using flat = B === 1 ? ops.reshape(scores, [1, this.#embed.shape[0]!]) : ops.transposeAxes(scores, [1, 0]);
238
+ return ops.argmaxAxis(flat, -1);
239
+ }
177
240
 
178
- // sliding-window additive mask over cached K positions
179
- let mask: MlxArray | null = null;
180
- if (blk.layerType === "sliding_attention") {
181
- const firstCached = position - kLen + 1;
182
- const windowStart = Math.max(0, position - this.slidingWindow + 1);
183
- const allowedFrom = Math.max(0, windowStart - firstCached);
184
- if (allowedFrom > 0) {
185
- const m = new Float32Array(kLen);
186
- for (let i = 0; i < allowedFrom; i++) m[i] = -1e9;
187
- const mf = MlxArray.fromFloat32(m, [1, 1, 1, kLen]);
188
- mask = mf.astype(k.dtype);
189
- mf.dispose();
190
- }
191
- }
241
+ /** Numerical graph only. Callers supply donor validity masks for different
242
+ * row positions; token selection remains on device across the draft chain. */
243
+ forwardRows(lastTokenEmb: MlxArray, targetHidden: MlxArray,
244
+ donors: AssistantDonors, position: number | MlxArray): DrafterRowsStep {
245
+ const B = lastTokenEmb.shape[0]!;
246
+ let h = ops.concatAxis([lastTokenEmb, targetHidden], 2); // (B,1,2*backbone)
247
+ h = disposing(h, ops.matmul(h, this.#prePT)); // (B,1,256)
192
248
 
249
+ for (const blk of this.#blocks) {
250
+ const donor = blk.layerType === "sliding_attention" ? donors.sliding : donors.full;
193
251
  const residual = h;
194
252
  let x = this.#rms(h, blk.inputNorm);
195
253
  // Q-only attention
196
254
  let q = ops.matmul(x, blk.qProj);
197
255
  x.dispose();
198
- q = disposing(q, ops.reshape(q, [1, 1, this.nHeads, blk.headDim]));
256
+ q = disposing(q, ops.reshape(q, [B, 1, this.nHeads, blk.headDim]));
199
257
  q = disposing(q, this.#rms(q, blk.qNorm));
200
258
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
201
- q = disposing(q, ops.rope(q, blk.headDim, blk.ropeBase, position, blk.ropeFreqs));
202
- let attn = ops.sdpa(q, k, v, Math.pow(blk.headDim, -0.5), mask ? "array" : "", mask);
259
+ q = disposing(q, typeof position === "number"
260
+ ? ops.rope(q, blk.headDim, blk.ropeBase, position, blk.ropeFreqs)
261
+ : ops.ropeDynamic(q, blk.headDim, blk.ropeBase, position, blk.ropeFreqs));
262
+ let attn = donor.attend(q, Math.pow(blk.headDim, -0.5),
263
+ blk.layerType === "sliding_attention" ? this.slidingWindow : null);
203
264
  q.dispose();
204
- mask?.dispose();
205
265
  attn = disposing(attn, ops.transposeAxes(attn, [0, 2, 1, 3]));
206
- attn = disposing(attn, ops.reshape(attn, [1, 1, this.nHeads * blk.headDim]));
266
+ attn = disposing(attn, ops.reshape(attn, [B, 1, this.nHeads * blk.headDim]));
207
267
  attn = disposing(attn, ops.matmul(attn, blk.oProj));
208
268
  attn = disposing(attn, this.#rms(attn, blk.postAttnNorm));
209
269
  h = ops.add(residual, attn);
@@ -231,59 +291,10 @@ export class GemmaAssistantDrafter {
231
291
 
232
292
  h = disposing(h, this.#rms(h, this.#norm));
233
293
 
234
- let token: number;
235
- if (this.useCentroids) {
236
- // centroid decode → drafted token (argmax-equivalent shortcut)
237
- const cScores = ops.matmul(h, this.#centroidsT!); // (1,1,2048)
238
- const flat = ops.reshape(cScores, [1, this.numCentroids]);
239
- cScores.dispose();
240
- const neg = ops.neg(flat);
241
- flat.dispose();
242
- const part = ops.argpartitionAxis(neg, this.topK - 1, -1);
243
- neg.dispose();
244
- const topIdx = part.slice([0, 0], [1, this.topK]); // (1, topK)
245
- part.dispose();
246
- const topFlat = ops.reshape(topIdx, [this.topK]);
247
- topIdx.dispose();
248
- const candIds = ops.takeAxis(this.#ordering!, topFlat, 0); // (topK, vpc) int32
249
- topFlat.dispose();
250
- const candFlat = ops.reshape(candIds, [this.topK * this.vocabPerCentroid]);
251
- candIds.dispose();
252
- const candEmb = ops.takeAxis(this.#embed, candFlat, 0); // (topK*vpc, hidden)
253
- const hVec = ops.reshape(h, [this.hidden, 1]);
254
- const scores = ops.matmul(candEmb, hVec); // (topK*vpc, 1)
255
- candEmb.dispose();
256
- hVec.dispose();
257
- const scoresFlat = ops.reshape(scores, [1, this.topK * this.vocabPerCentroid]);
258
- scores.dispose();
259
- const am = ops.argmaxAxis(scoresFlat, -1);
260
- scoresFlat.dispose();
261
- const winner = ops.itemUint32(am);
262
- am.dispose();
263
- const tokArr = candFlat.slice([winner], [winner + 1]);
264
- candFlat.dispose();
265
- const tokU32 = tokArr.astype(Dtype.uint32);
266
- tokArr.dispose();
267
- token = ops.itemUint32(tokU32);
268
- tokU32.dispose();
269
- } else {
270
- // tied-embedding head: token = argmax_v (embed[v] · h) over the
271
- // full vocab — the dense form the centroid head approximates.
272
- const V = this.#embed.shape[0]!;
273
- const hVec = ops.reshape(h, [this.hidden, 1]);
274
- const scores = ops.matmul(this.#embed, hVec); // (vocab, 1)
275
- hVec.dispose();
276
- const scoresFlat = ops.reshape(scores, [1, V]);
277
- scores.dispose();
278
- const am = ops.argmaxAxis(scoresFlat, -1);
279
- scoresFlat.dispose();
280
- token = ops.itemUint32(am);
281
- am.dispose();
282
- }
283
-
284
- const nextHidden = ops.matmul(h, this.#postPT); // (1,1,backbone)
294
+ const tokens = this.#sampleRows(h, B);
295
+ const nextHidden = ops.matmul(h, this.#postPT); // (B,1,backbone)
285
296
  h.dispose();
286
- return { token, nextHidden };
297
+ return { tokens, nextHidden };
287
298
  }
288
299
  }
289
300