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
@@ -10,7 +10,11 @@
10
10
  // shared maybeQuantizeKv path).
11
11
 
12
12
  import type { ModelConfig } from "../config";
13
+ import { quantizedAppendAttention } from "./quantized-append-attention";
14
+ import { gatedDeltaState } from "./qwen3-delta-state";
13
15
  import type { Weights } from "../weights";
16
+ import { runtimeFlag } from "../runtime-config";
17
+ import { mapTokenGroups, type TokenGroup } from "./token-groups";
14
18
  import { MlxArray } from "../mlx/array";
15
19
  import { Dtype, deviceArchitecture } from "../mlx/ffi";
16
20
  import * as ops from "../mlx/ops";
@@ -23,7 +27,6 @@ import {
23
27
  disposing,
24
28
  KVCache,
25
29
  LoraState,
26
- QuantizedKVCache,
27
30
  QuantizedEmbedding,
28
31
  QuantizedLinear,
29
32
  quantizedSdpa,
@@ -36,8 +39,8 @@ import { materializeCopy } from "../mlx/materialize";
36
39
  import { gatedDeltaUpdate, SSMCache } from "./qwen3-delta";
37
40
  import type { QwenConvolution } from "./qwen-conv";
38
41
  import {
39
- activeMrope, applyInterleavedRope, buildMropePositions, mropeInvFreq,
40
- setActiveMrope, type MropeRequestState,
42
+ applyInterleavedRope, buildMropePositions, mropeInvFreq,
43
+ type MropeRequestState, type MropeForwardState,
41
44
  } from "./qwen3-mrope";
42
45
 
43
46
  const PREFIX = "language_model";
@@ -155,16 +158,20 @@ export class GatedDeltaNet {
155
158
  this.normWeight = weights.tensor(`${prefix}.norm.weight`);
156
159
  }
157
160
 
158
- #convolve(qkv: MlxArray, state: MlxArray): [MlxArray, MlxArray] {
159
- if (this.convolution) return this.convolution(qkv, state, this.convWeight);
160
- const [B, S, D] = qkv.shape as [number, number, number];
161
- const nKeep = this.convKernel - 1;
161
+ #convolve(qkv: MlxArray, state: MlxArray, rowLengths?: readonly number[]): [MlxArray, MlxArray] {
162
+ if (this.convolution) {
163
+ const result = this.convolution(qkv, state, this.convWeight);
164
+ if (!rowLengths) return result;
165
+ using input = ops.concatAxis([state, qkv], 1);
166
+ const tail = this.#convTail(input, qkv.shape[1]!, rowLengths);
167
+ result[1].dispose();
168
+ return [result[0], tail];
169
+ }
170
+ const [, S, D] = qkv.shape as [number, number, number];
162
171
  const input = ops.concatAxis([state, qkv], 1);
163
172
  // MLX copy and contiguous can both alias the whole prefill buffer.
164
173
  // Materialize only the tail so cache residency follows its logical size.
165
- const view = input.slice([0, S, 0], [B, S + nKeep, D]);
166
- const tail = materializeCopy(view);
167
- view.dispose();
174
+ const tail = this.#convTail(input, S, rowLengths);
168
175
  const conv = ops.conv1d(input, this.convWeight, 1, 0, 1, D);
169
176
  input.dispose();
170
177
  const out = compiledSilu(conv);
@@ -172,7 +179,19 @@ export class GatedDeltaNet {
172
179
  return [out, tail];
173
180
  }
174
181
 
175
- forward(x: MlxArray, cache: SSMCache, independentRows = false): MlxArray {
182
+ #convTail(input: MlxArray, processed: number, rowLengths?: readonly number[]): MlxArray {
183
+ const [B, , D] = input.shape as [number, number, number];
184
+ const nKeep = this.convKernel - 1;
185
+ if (rowLengths) {
186
+ using indices = ops.fromInt32(rowLengths.flatMap(length =>
187
+ Array.from({ length: nKeep }, (_, position) => length + position)), [B, nKeep, 1]);
188
+ return ops.takeAlongAxis(input, indices, 1);
189
+ }
190
+ using view = input.slice([0, processed, 0], [B, processed + nKeep, D]);
191
+ return materializeCopy(view);
192
+ }
193
+
194
+ forward(x: MlxArray, cache: SSMCache, independentRows = false, mask?: MlxArray | null): MlxArray {
176
195
  const [B, S] = x.shape as [number, number, number];
177
196
  const convDim = this.keyDim * 2 + this.valueDim;
178
197
  const nKeep = this.convKernel - 1;
@@ -194,7 +213,7 @@ export class GatedDeltaNet {
194
213
  }
195
214
 
196
215
  const t0 = prof ? performance.now() : 0;
197
- const qkv = this.inProjQkv.forward(x, independentRows); // [B,S,convDim]
216
+ let qkv = this.inProjQkv.forward(x, independentRows); // [B,S,convDim]
198
217
  let z = this.inProjZ.forward(x, independentRows);
199
218
  z = disposing(z, ops.reshape(z, [B, S, this.numVHeads, this.headVDim]));
200
219
  const b = this.inProjB.forward(x, independentRows); // [B,S,numVHeads]
@@ -202,10 +221,19 @@ export class GatedDeltaNet {
202
221
  if (prof) { ops.evalAll([qkv, z, b, a]); prof.proj = (prof.proj ?? 0) + performance.now() - t0; }
203
222
  const tc = prof ? performance.now() : 0; // [B,S,numVHeads]
204
223
 
205
- // Causal depthwise conv with the conv-state prefix (B=1: no ssm mask).
224
+ // The model shares one padding mask across recurrent layers. Direct layer
225
+ // callers can request the same cache-owned mask without a model wrapper.
226
+ using localMask = mask === undefined ? cache.prefillPadding?.makeMask(S) ?? null : null;
227
+ const ssmMask = mask ?? localMask;
228
+ if (ssmMask) {
229
+ using expanded = ops.reshape(ssmMask, [B, S, 1]);
230
+ using zero = MlxArray.fromBytesCopy(new Uint8Array(qkv.dtype === Dtype.float32 ? 4 : 2), [], qkv.dtype);
231
+ qkv = disposing(qkv, ops.where(expanded, qkv, zero));
232
+ }
233
+ // Convolution keeps each row's last real-token tail, including empty rows.
206
234
  const convState =
207
235
  cache.conv ?? ops.zeros([B, nKeep, convDim], x.dtype);
208
- const [convOut, newConv] = this.#convolve(qkv, convState);
236
+ const [convOut, newConv] = this.#convolve(qkv, convState, cache.prefillPadding?.convolutionLengths(S));
209
237
  if (!cache.conv) convState.dispose();
210
238
  if (spec) spec.qkv = qkv;
211
239
  else qkv.dispose();
@@ -236,7 +264,7 @@ export class GatedDeltaNet {
236
264
  const tk = prof ? performance.now() : 0;
237
265
 
238
266
  const [out, newState] = gatedDeltaUpdate(
239
- q, k, v, a, b, this.aLog, this.dtBias, cache.recurrent,
267
+ q, k, v, a, b, this.aLog, this.dtBias, cache.recurrent, ssmMask,
240
268
  );
241
269
  if (prof) { ops.evalAll([out]); prof.kernel = (prof.kernel ?? 0) + performance.now() - tk; }
242
270
  const to = prof ? performance.now() : 0;
@@ -275,18 +303,21 @@ export class GatedDeltaNet {
275
303
  * recurrent state is BIT-EXACTLY what a forward over only the accepted
276
304
  * prefix would have produced. Output projections (z / out_proj) are state-
277
305
  * free and skipped: only the states matter here. */
278
- #replaySpecPrefix(cache: SSMCache, keep: number): void {
306
+ #replaySpecPrefix(cache: SSMCache, keep: number | readonly number[]): void {
279
307
  const r = cache.specRound;
280
- if (!r || !r.qkv || !r.a || !r.b) throw new Error("GatedDeltaNet replay without recorded round inputs");
308
+ if (!r || !r.qkv || !r.a || !r.b)
309
+ throw new Error("GatedDeltaNet replay without recorded round inputs");
281
310
  const B = r.qkv.shape[0]!;
311
+ const count = typeof keep === "number" ? keep : Math.max(...keep);
312
+ const rowLengths = typeof keep === "number" ? undefined : keep;
282
313
  const convDim = this.keyDim * 2 + this.valueDim;
283
314
  const nKeep = this.convKernel - 1;
284
315
 
285
- const qkvPfxView = r.qkv.slice([0, 0, 0], [B, keep, convDim]);
316
+ const qkvPfxView = r.qkv.slice([0, 0, 0], [B, count, convDim]);
286
317
  const qkvPfx = ops.contiguous(qkvPfxView);
287
318
  qkvPfxView.dispose();
288
319
  const convState = cache.conv ?? ops.zeros([B, nKeep, convDim], qkvPfx.dtype);
289
- const [convOut, newConv] = this.#convolve(qkvPfx, convState);
320
+ const [convOut, newConv] = this.#convolve(qkvPfx, convState, rowLengths);
290
321
  if (!cache.conv) convState.dispose();
291
322
  qkvPfx.dispose();
292
323
  cache.conv?.dispose();
@@ -296,36 +327,45 @@ export class GatedDeltaNet {
296
327
  convOut, [this.keyDim, 2 * this.keyDim], -1,
297
328
  ) as [MlxArray, MlxArray, MlxArray];
298
329
  convOut.dispose();
299
- let q = ops.reshape(qFlat, [B, keep, this.numKHeads, this.headKDim]);
330
+ let q = rowLengths ? null : ops.reshape(qFlat, [B, count, this.numKHeads, this.headKDim]);
300
331
  qFlat.dispose();
301
- let k = ops.reshape(kFlat, [B, keep, this.numKHeads, this.headKDim]);
332
+ let k = ops.reshape(kFlat, [B, count, this.numKHeads, this.headKDim]);
302
333
  kFlat.dispose();
303
- const v = disposing(vFlat, ops.reshape(vFlat, [B, keep, this.numVHeads, this.headVDim]));
334
+ const v = disposing(vFlat, ops.reshape(vFlat, [B, count, this.numVHeads, this.headVDim]));
304
335
  const invScale = Math.pow(this.headKDim, -0.5);
305
- q = disposing(q, ops.rmsNorm(q, null, 1e-6));
306
- q = disposing(q, ops.mulScalar(q, invScale * invScale));
336
+ if (q) {
337
+ q = disposing(q, ops.rmsNorm(q, null, 1e-6));
338
+ q = disposing(q, ops.mulScalar(q, invScale * invScale));
339
+ }
307
340
  k = disposing(k, ops.rmsNorm(k, null, 1e-6));
308
341
  k = disposing(k, ops.mulScalar(k, invScale));
309
342
 
310
- const aPfxView = r.a.slice([0, 0, 0], [B, keep, this.numVHeads]);
343
+ const aPfxView = r.a.slice([0, 0, 0], [B, count, this.numVHeads]);
311
344
  const aPfx = ops.contiguous(aPfxView);
312
345
  aPfxView.dispose();
313
- const bPfxView = r.b.slice([0, 0, 0], [B, keep, this.numVHeads]);
346
+ const bPfxView = r.b.slice([0, 0, 0], [B, count, this.numVHeads]);
314
347
  const bPfx = ops.contiguous(bPfxView);
315
348
  bPfxView.dispose();
316
349
 
317
- const [y, newState] = gatedDeltaUpdate(
318
- q, k, v, aPfx, bPfx, this.aLog, this.dtBias, cache.recurrent,
319
- );
320
- q.dispose();
350
+ let newState: MlxArray;
351
+ if (rowLengths) {
352
+ // Unequal prefixes need a row-length-aware recurrence. The state-only
353
+ // kernel omits query/output work. Uniform replay retains its measured
354
+ // kernel until the separate optimization demonstrates a serving win.
355
+ newState = gatedDeltaState(k, v, aPfx, bPfx, this.aLog, this.dtBias, cache.recurrent, rowLengths);
356
+ } else {
357
+ const [output, state] = gatedDeltaUpdate(q!, k, v, aPfx, bPfx, this.aLog, this.dtBias, cache.recurrent);
358
+ output.dispose(); newState = state;
359
+ }
360
+ q?.dispose();
321
361
  k.dispose();
322
362
  v.dispose();
323
363
  aPfx.dispose();
324
364
  bPfx.dispose();
325
- y.dispose(); // only the state advance matters on the rollback path
326
365
  cache.recurrent?.dispose();
327
366
  cache.recurrent = newState;
328
- cache.advance(keep);
367
+ if (typeof keep === "number") cache.advance(keep);
368
+ else cache.advanceRows(keep);
329
369
  }
330
370
 
331
371
  private rmsNormGated(hidden: MlxArray, gate: MlxArray): MlxArray {
@@ -369,7 +409,8 @@ export class Qwen3Attention {
369
409
  this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
370
410
  }
371
411
 
372
- forward(x: MlxArray, mask: Mask, cache: Cache, independentRows = false): MlxArray {
412
+ forward(x: MlxArray, mask: Mask, cache: Cache, independentRows = false,
413
+ mrope: MropeForwardState | null = null): MlxArray {
373
414
  const [B, L] = x.shape as [number, number, number];
374
415
 
375
416
  // q_proj emits 2× head_dim per head → split into queries + gate.
@@ -396,11 +437,10 @@ export class Qwen3Attention {
396
437
  // position as ropeOffsetArr (rows have different prompt lengths); the
397
438
  // dynamic-offset kernel is the same fast::rope, bit-exact vs the static
398
439
  // form (tests/unit/compile.test.ts). Serial lane: scalar offset, unchanged.
399
- // Vision requests (serial lane only) install activeMrope for the current
400
- // forward: 3D interleaved positions via the manual apply — the reference
440
+ // Media forwards supply their own 3D interleaved positions. The reference
401
441
  // never uses the fused fast-rope kernel when position_ids are supplied,
402
442
  // so this IS the oracle's own arithmetic. Text-only stays on ops.rope.
403
- const mr = activeMrope;
443
+ const mr = mrope;
404
444
  const offArr = (cache as { ropeOffsetArr?: MlxArray }).ropeOffsetArr;
405
445
  if (mr) {
406
446
  q = disposing(q, applyInterleavedRope(q, mr));
@@ -415,11 +455,18 @@ export class Qwen3Attention {
415
455
  }
416
456
 
417
457
  let attn: MlxArray;
418
- if (cache instanceof QuantizedKVCache) {
419
- const [keys, values] = cache.updateAndFetchQuantized(k, v);
458
+ const quantized = cache.quantizedAttention;
459
+ if (cache.attentionState) {
460
+ const view = cache.attentionState.appendAndFetch(k, v);
461
+ k.dispose(); v.dispose();
462
+ try { attn = view.attend(q, this.scale, mask, independentRows); } finally { view.dispose(); }
463
+ } else if (quantized) {
464
+ const [keys, values] = quantized.updateAndFetchQuantized(k, v);
420
465
  k.dispose();
421
466
  v.dispose();
422
- attn = quantizedSdpa(q, keys, values, this.scale, mask, cache.groupSize, cache.bits);
467
+ attn = independentRows && L > 1
468
+ ? quantizedAppendAttention(q, keys, values, this.scale, quantized.groupSize, quantized.bits)
469
+ : quantizedSdpa(q, keys, values, this.scale, mask, quantized.groupSize, quantized.bits);
423
470
  disposeTriple(keys);
424
471
  disposeTriple(values);
425
472
  } else {
@@ -517,23 +564,27 @@ export class Qwen3Layer {
517
564
  this.postAttnNorm = new RMSNorm(weights.tensor(`${prefix}.post_attention_layernorm.weight`), config.text.rmsNormEps);
518
565
  }
519
566
 
520
- forward(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false): MlxArray {
521
- const xn = this.inputNorm.forward(x);
522
- const r = this.isLinear
523
- ? this.linearAttn!.forward(xn, cache as SSMCache, independentRows)
524
- : this.selfAttn!.forward(xn, faMask, cache, independentRows);
525
- xn.dispose();
526
- const h = ops.add(x, r);
527
- r.dispose();
528
- const hn = this.postAttnNorm.forward(h);
567
+ forward(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false, ssmMask?: MlxArray | null,
568
+ mrope: MropeForwardState | null = null): MlxArray {
569
+ using h = this.forwardAttn(x, faMask, cache, independentRows, ssmMask, mrope);
570
+ return this.forwardMlp(h, independentRows);
571
+ }
572
+
573
+ forwardAttn(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false, ssmMask?: MlxArray | null,
574
+ mrope: MropeForwardState | null = null): MlxArray {
575
+ using xn = this.inputNorm.forward(x);
576
+ using r = this.isLinear
577
+ ? this.linearAttn!.forward(xn, cache as SSMCache, independentRows, ssmMask)
578
+ : this.selfAttn!.forward(xn, faMask, cache, independentRows, mrope);
579
+ return ops.add(x, r);
580
+ }
581
+
582
+ forwardMlp(h: MlxArray, independentRows = false): MlxArray {
583
+ using hn = this.postAttnNorm.forward(h);
529
584
  // RMSNorm allocates an aligned row-contiguous output. Pass that layout
530
585
  // proof so Trellis can match native small-prefill matmul without an eval.
531
- const m = this.mlp.forward(hn, true, independentRows);
532
- hn.dispose();
533
- const out = ops.add(h, m);
534
- h.dispose();
535
- m.dispose();
536
- return out;
586
+ using m = this.mlp.forward(hn, true, independentRows);
587
+ return ops.add(h, m);
537
588
  }
538
589
  }
539
590
 
@@ -627,10 +678,16 @@ export class Qwen35Model {
627
678
  return null;
628
679
  }
629
680
  return {
630
- maxChunkSize: (state: readonly Cache[]) => qwenAppendChunkSize(state[0]!.offset),
681
+ affineKvBits: [4, 8],
682
+ turboQuantFormats: [{ kBits: 8, vBits: 3 }],
683
+ maxChunkSize: (state: readonly Cache[], rows = 1) => rows === 1 ? qwenAppendChunkSize(state[0]!.offset) : 1,
631
684
  forwardHidden: (ids: MlxArray, cache: Cache[]): MlxArray => {
685
+ // Wider cohorts retain their ordinary one-position numerical graph.
686
+ // The specialized multi-position append remains qualified at B=1.
687
+ if (ids.shape.length === 2 && ids.shape[0]! > 1 && ids.shape[1] === 1)
688
+ return this.forwardHidden(ids, cache);
632
689
  if (ids.shape.length !== 2 || ids.shape[0] !== 1 || ids.shape[1]! > 4)
633
- throw new Error("Qwen committed-token append supports one row and at most four positions");
690
+ throw new Error("Qwen committed-token append supports one position per shared row or up to four positions at B=1");
634
691
  const hidden = this.embed.encode(ids);
635
692
  return this.forwardLayers(hidden, cache, true);
636
693
  },
@@ -642,13 +699,62 @@ export class Qwen35Model {
642
699
  return this.forwardLayers(h, cache);
643
700
  }
644
701
 
702
+ /** Borrowed request positions, including different positions for each row. */
703
+ forwardHiddenAtPositions(ids: MlxArray, cache: Cache[], positions: MlxArray): MlxArray {
704
+ return this.forwardLayers(this.embed.encode(ids), cache, false, positions);
705
+ }
706
+
707
+ forwardEmbeddingsAtPositions(embeds: MlxArray, cache: Cache[], positions: MlxArray): MlxArray {
708
+ return this.forwardLayers(ops.contiguous(embeds), cache, false, positions);
709
+ }
710
+
711
+ /** Attention and DeltaNet retain disjoint row state. Only tokenwise MLP
712
+ * work is eligible for packing; verification can preserve its geometry. */
713
+ forwardHiddenMixed(work: readonly TokenGroup[]): MlxArray[] {
714
+ if (work.length === 1 && !work[0]!.captureLayer) return [this.forwardHidden(work[0]!.ids, work[0]!.cache)];
715
+ const groups: Array<{ h: MlxArray; mask?: Mask; ssmMask?: MlxArray | null }> = [];
716
+ const results: MlxArray[] = [];
717
+ const pack = runtimeFlag("MLX_BUN_MIXED_PACKED_MLP", true);
718
+ const bounded = work.some(group => group.ids.shape[1]! > TRELLIS_MATVEC_MAX_M);
719
+ try {
720
+ for (const { ids, cache } of work) {
721
+ const group: typeof groups[number] = { h: this.embed.encode(ids) }; groups.push(group);
722
+ group.mask = cache[this.faIdx]!.makeMask(ids.shape[1]!, null);
723
+ group.ssmMask = (cache[0] as SSMCache).prefillPadding?.makeMask(ids.shape[1]!);
724
+ }
725
+ for (const [i, layer] of this.layers.entries()) {
726
+ const mids: MlxArray[] = [];
727
+ try {
728
+ for (const [row, group] of groups.entries())
729
+ mids.push(layer.forwardAttn(group.h, group.mask!, work[row]!.cache[i]!, false, group.ssmMask));
730
+ const outputs = mapTokenGroups(work, mids, hidden => layer.forwardMlp(hidden), pack);
731
+ for (const [row, group] of groups.entries()) { group.h.dispose(); group.h = outputs[row]!; }
732
+ // Materialize every layer's recurrent tail as well as the residual.
733
+ // It is not a dependency of h and otherwise pins prefill buffers.
734
+ if (bounded) {
735
+ const caches = work.map(group => group.cache[i]!);
736
+ const state = caches.map(cache => cache.state());
737
+ try { ops.evalAll([...groups.map(group => group.h), ...state.flat()]); }
738
+ finally { for (const [row, cache] of caches.entries())
739
+ if (cache.stateNeedsDispose) for (const value of state[row]!) value.dispose(); }
740
+ }
741
+ for (const [row, group] of groups.entries()) work[row]!.captureLayer?.(i, group.h);
742
+ } finally { for (const mid of mids) mid.dispose(); }
743
+ }
744
+ for (const [row, group] of groups.entries()) {
745
+ const hidden = this.finalNorm.forward(group.h); results.push(hidden);
746
+ work[row]!.captureLayer?.(this.layers.length, hidden);
747
+ }
748
+ return results;
749
+ } catch (error) { for (const result of results) result.dispose(); throw error; }
750
+ finally { for (const group of groups) { group.h.dispose(); group.mask?.arr?.dispose(); group.ssmMask?.dispose(); } }
751
+ }
752
+
645
753
  /** Active vision mRoPE request state (serial lane; set by the generation
646
754
  * gateway around a vision request's run, null for text-only — which keeps
647
- * the bit-exact fast-rope path). While set, forwardLayers installs the
648
- * per-forward interleaved cos/sin consumed by every full-attn layer. */
755
+ * the bit-exact fast-rope path). The explicit serial executor scopes this value.
756
+ * Shared media forwards pass request-owned positions directly. */
649
757
  mrope: MropeRequestState | null = null;
650
- /** Serving's memory-pressure check, scoped to the serial request. */
651
- prefillMemoryGuard: (() => void) | null = null;
652
758
  #mropeInvFreq: MlxArray | null = null;
653
759
 
654
760
  /** Vision prefill: spliced input embeddings [1, L, H] (image features
@@ -682,23 +788,23 @@ export class Qwen35Model {
682
788
  tap.captured.set(i, copy);
683
789
  }
684
790
 
685
- protected forwardLayers(h0: MlxArray, cache: Cache[], independentRows = false): MlxArray {
791
+ protected forwardLayers(h0: MlxArray, cache: Cache[], independentRows = false,
792
+ positions?: MlxArray): MlxArray {
686
793
  const L = h0.shape[1]!;
687
794
  // One full-attention mask shared by all full layers (same offset); linear
688
795
  // layers see no ssm mask at B=1.
689
796
  const faMask = cache[this.faIdx]!.makeMask(L, null);
797
+ using ssmMask = (cache[0] as SSMCache).prefillPadding?.makeMask(L) ?? null;
690
798
  // Vision requests: one interleaved-mRoPE cos/sin table per forward,
691
799
  // shared by all 12 full-attention layers (positions are layer-invariant).
692
800
  let mropeFwd: ReturnType<typeof buildMropePositions> | null = null;
693
- if (this.mrope) {
801
+ if (positions || this.mrope) {
694
802
  const t = this.config.text;
695
803
  const ropeDims = Math.trunc(t.headDim * t.partialRotaryFactor);
696
804
  const base = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
697
805
  this.#mropeInvFreq ??= mropeInvFreq(ropeDims, base);
698
- mropeFwd = buildMropePositions(
699
- this.mrope, cache[this.faIdx]!.offset, L, this.#mropeInvFreq, ropeDims,
700
- );
701
- setActiveMrope(mropeFwd);
806
+ mropeFwd = positions ? { posIds: positions, invFreq: this.#mropeInvFreq, rotaryDims: ropeDims }
807
+ : buildMropePositions(this.mrope!, cache[this.faIdx]!.offset, L, this.#mropeInvFreq, ropeDims);
702
808
  }
703
809
  let h: MlxArray | null = h0;
704
810
  const prof = (globalThis as Record<string, unknown>).__deltaProf as
@@ -706,14 +812,13 @@ export class Qwen35Model {
706
812
  | undefined;
707
813
  try {
708
814
  for (let i = 0; i < this.layers.length; i++) {
709
- if (L > 1) this.prefillMemoryGuard?.();
710
815
  const tl = prof ? performance.now() : 0;
711
- const next = this.layers[i]!.forward(h, faMask, cache[i]!, independentRows);
816
+ const next = this.layers[i]!.forward(h, faMask, cache[i]!, independentRows, ssmMask, mropeFwd);
712
817
  h.dispose();
713
818
  h = next;
714
- const mlp = this.layers[i]!.mlp;
715
- if (L > TRELLIS_MATVEC_MAX_M &&
716
- [mlp.gate, mlp.up, mlp.down].some((p) => p instanceof TrellisLinear && !p.fallback)) {
819
+ if (L > TRELLIS_MATVEC_MAX_M) {
820
+ // Affine prefills also need bounded evaluation: a retained prefix
821
+ // history can leave too little room for the entire deferred graph.
717
822
  // Materialize the cache outputs too. In particular, the tiny
718
823
  // copied conv tail is not a dependency of `h`; leaving it lazy
719
824
  // pins the whole prefill conv buffer until the chunk ends.
@@ -732,10 +837,7 @@ export class Qwen35Model {
732
837
  h = null; // consumed — the finally must not double-free
733
838
  return out;
734
839
  } finally {
735
- if (mropeFwd) {
736
- setActiveMrope(null);
737
- mropeFwd.posIds.dispose();
738
- }
840
+ if (mropeFwd && !positions) mropeFwd.posIds.dispose();
739
841
  // A mid-loop layer throw must not strand the mask array or the
740
842
  // in-flight [1,L,H] residual (2026-08-18 review).
741
843
  faMask.arr?.dispose();
@@ -0,0 +1,35 @@
1
+ import type { KvDonorAttention } from "./gemma4-base";
2
+ import { BatchedRotatingState } from "./batched-rotating-state";
3
+ import { BatchedRotatingQuantCache } from "./batched-rotating-quant";
4
+ import { plainRowStorage, quantizedRowStorage, temporalStorageView } from "./batched-row-storage";
5
+ import { quantizedDonorAttention } from "./kv-attention-view";
6
+ import { decodedKvDonorAttention } from "./decoded-kv-donor";
7
+ import type { RotatingRing } from "./rotating-row-transaction";
8
+
9
+ /** Capture the same chronological donor rectangle for plain and encoded rings. */
10
+ export function captureRotatingDonorAttention(ring: RotatingRing): KvDonorAttention {
11
+ const position = new BatchedRotatingState(ring.maxSize, []);
12
+ position.restore(ring.positionSnapshot);
13
+ const offsets = position.offsets.map((_, row) => position.validOffset(row));
14
+ const ends = offsets.map((offset, row) => position.activeLength - (position.offsets[row]! - offset));
15
+ // Pending prefill keeps a shared physical rectangle, including when mixed
16
+ // rows are captured separately. Cropping from each row's valid end would
17
+ // give siblings different widths and can discard an earlier valid window.
18
+ const from = position.hasPendingPadding ? 0 : Math.max(0, position.activeLength - ring.maxSize);
19
+ const range = { from, to: position.activeLength };
20
+ const validity = { offsets,
21
+ starts: position.leftPad.map((pad, row) => Math.max(0, pad - from, ends[row]! - from - ring.maxSize)),
22
+ ends: ends.map(end => Math.max(0, end - from)) };
23
+ if (ring instanceof BatchedRotatingQuantCache) {
24
+ const keys = temporalStorageView(quantizedRowStorage, ring.keys!, position, range);
25
+ let values: import("../mlx/ops").QuantizedTensor;
26
+ try { values = temporalStorageView(quantizedRowStorage, ring.values!, position, range); }
27
+ catch (error) { quantizedRowStorage.dispose(keys); throw error; }
28
+ return quantizedDonorAttention(keys, values, validity, ring.groupSize, ring.bits);
29
+ }
30
+ const keys = temporalStorageView(plainRowStorage, ring.keys!, position, range);
31
+ let values: import("../mlx/array").MlxArray;
32
+ try { values = temporalStorageView(plainRowStorage, ring.values!, position, range); }
33
+ catch (error) { keys.dispose(); throw error; }
34
+ return decodedKvDonorAttention({ keys, values, ...validity });
35
+ }