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,12 +10,16 @@ import type { SpeculativeTransaction } from "../../inference/rollback";
10
10
  import { bindCacheRollback } from "./rollback";
11
11
  import type { GraphDescriptor } from "../../inference/graph";
12
12
  import { bindLegacyDraftTarget } from "./draft-target";
13
+ import type { MlxForwardWork } from "./mixed-iteration";
14
+ import type { PrefillPolicy } from "../../inference/prefill";
15
+ import { resolveMlxPrefillPolicy } from "./prefill-policy";
13
16
 
14
17
  /** Bound target operations and draft construction for one speculative run.
15
18
  * A replacement graph supplies this entire port, including any hidden taps.
16
19
  * The verifier never needs a RuntimeModel or an artifact-family check. */
17
20
  export interface MlxSpeculativeBinding {
18
21
  readonly runtime?: RuntimeConfig;
22
+ readonly prefillPolicy?: PrefillPolicy;
19
23
  readonly memory?: MlxModelMemory;
20
24
  readonly descriptor: GraphDescriptor;
21
25
  readonly eosTokenIds: readonly number[];
@@ -23,27 +27,29 @@ export interface MlxSpeculativeBinding {
23
27
  makeCache(): Cache[];
24
28
  openDraft(sampler: Sampler, caches: Cache[]): DraftSource;
25
29
  bindRollback(caches: Cache[]): SpeculativeTransaction;
26
- forward(ids: MlxArray, caches: Cache[], tapLayers?: number[]):
30
+ forward(ids: MlxArray, caches: Cache[], tapLayers?: number[], work?: MlxForwardWork):
27
31
  Promise<{ hidden: MlxArray; ctxML: MlxArray | null }>;
28
32
  projectLogits(hidden: MlxArray): MlxArray;
29
33
  /** Establish the implementation's verify kernel context; restore on close. */
30
34
  pinVerify?(): { close(): void };
31
35
  }
32
36
 
37
+ export type MlxSpeculativeTargetBinding = Omit<MlxSpeculativeBinding, "openDraft">;
38
+
33
39
  /** Legacy mutable tap/kernel fields require the gateway's exclusive lease. */
34
- export function bindLegacySpeculativeModel(model: RuntimeModel, provider: DraftProvider): MlxSpeculativeBinding {
40
+ export function bindSpeculativeTargetModel(model: RuntimeModel): MlxSpeculativeTargetBinding {
35
41
  const runtime = runtimeConfig();
36
42
  return {
37
43
  runtime,
44
+ prefillPolicy: resolveMlxPrefillPolicy(model.config, runtime),
38
45
  memory: model,
39
46
  descriptor: Object.freeze({ id: `legacy-spec:${model.config.modelType}`, backend: "mlx",
40
47
  graphAbi: "mlx-hidden-bsh-v1", stateAbi: "legacy-cache-array-v1", artifact: "legacy-resident-model" }),
41
48
  eosTokenIds: model.config.eosTokenIds,
42
49
  prefillTailSplit: runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true),
43
50
  makeCache: model.makeCache.bind(model),
44
- openDraft: (sampler, caches) => provider.open({ sampler, target: bindLegacyDraftTarget(model, caches) }),
45
51
  bindRollback: bindCacheRollback,
46
- forward: (ids, caches, tapLayers) => legacyForwardWithTaps(model, ids, caches, tapLayers),
52
+ forward: (ids, caches, tapLayers, work) => legacyForwardWithTaps(model, ids, caches, tapLayers, work),
47
53
  projectLogits: model.logitsFromHidden.bind(model),
48
54
  ...("setSpecKernelPinned" in model ? {
49
55
  pinVerify() {
@@ -54,6 +60,12 @@ export function bindLegacySpeculativeModel(model: RuntimeModel, provider: DraftP
54
60
  };
55
61
  }
56
62
 
63
+ /** Serial compatibility binds draft construction separately from target work. */
64
+ export function bindLegacySpeculativeModel(model: RuntimeModel, provider: DraftProvider): MlxSpeculativeBinding {
65
+ return { ...bindSpeculativeTargetModel(model),
66
+ openDraft: (sampler, caches) => provider.open({ sampler, target: bindLegacyDraftTarget(model, caches) }) };
67
+ }
68
+
57
69
  export function assertMlxSpeculativeBinding(binding: MlxSpeculativeBinding): void {
58
70
  const descriptor = binding.descriptor;
59
71
  if (descriptor.backend !== "mlx" || descriptor.graphAbi !== "mlx-hidden-bsh-v1" ||
@@ -71,8 +83,10 @@ async function legacyForwardWithTaps(
71
83
  ids: MlxArray,
72
84
  caches: Cache[],
73
85
  tapLayers: number[] | undefined,
86
+ work?: MlxForwardWork,
74
87
  ): Promise<{ hidden: MlxArray; ctxML: MlxArray | null }> {
75
88
  if (!tapLayers) {
89
+ if (work) return { hidden: await work(ids, caches), ctxML: null };
76
90
  const asyncModel = model as RuntimeModel & {
77
91
  forwardHiddenAsync?: (
78
92
  ids: MlxArray,
@@ -88,10 +102,13 @@ async function legacyForwardWithTaps(
88
102
  const m = model;
89
103
  const previousTap = m.hiddenTap;
90
104
  const cap = new Map<number, MlxArray>();
91
- m.hiddenTap = { layers: new Set(tapLayers), captured: cap };
105
+ const layers = new Set(tapLayers);
106
+ if (!work) m.hiddenTap = { layers, captured: cap };
92
107
  let hidden: MlxArray | null = null;
93
108
  try {
94
- hidden = model.forwardHidden(ids, caches);
109
+ hidden = work ? await work(ids, caches, { captureLayer(layer, h) {
110
+ if (layers.has(layer)) cap.set(layer, ops.contiguous(h));
111
+ } }) : model.forwardHidden(ids, caches);
95
112
  const perLayer = tapLayers.map((li) => {
96
113
  const a = cap.get(li);
97
114
  if (!a) throw new Error(`spec tap: layer ${li} not captured`);
@@ -109,6 +126,6 @@ async function legacyForwardWithTaps(
109
126
  // are already gone (cap cleared, hidden nulled).
110
127
  hidden?.dispose();
111
128
  for (const [, a] of cap) a.dispose();
112
- m.hiddenTap = previousTap;
129
+ if (!work) m.hiddenTap = previousTap;
113
130
  }
114
131
  }
@@ -0,0 +1,104 @@
1
+ import type { BatchableCache, Cache } from "../../model/gemma4-base";
2
+ import { applyStateChanges, cleanupFailure, disposeResources } from "../../engine/resources";
3
+ import type { PreparedStateChange } from "../../contracts/resources";
4
+
5
+ /** Preserve the graph's layout capabilities when an empty sibling is made. */
6
+ export interface MlxRowLayout<Layout extends BatchableCache> extends BatchableCache {
7
+ makeEmptyBatch(): Layout;
8
+ }
9
+
10
+ /** Layer layouts own tensor geometry. This collection only applies the same
11
+ * row membership operation to every layer. It has no scheduling, sampling,
12
+ * checkpoint-key or persistence policy. */
13
+ export class MlxStateRows<Layout extends MlxRowLayout<Layout> = BatchableCache> {
14
+ /** Borrowed by the bound target graph; collection ownership stays here. */
15
+ readonly caches: Layout[];
16
+ #rows = 0;
17
+
18
+ /** Takes ownership of empty, compatible layer layouts. */
19
+ constructor(layouts: readonly Layout[]) { this.caches = [...layouts]; }
20
+ get rowCount(): number { return this.#rows; }
21
+ /** Retain an immutable batch boundary without extracting or materializing
22
+ * individual rows. Layer layouts decide how to share their backing arrays. */
23
+ clone(): MlxStateRows<Layout> {
24
+ const layouts: Layout[] = [];
25
+ try {
26
+ for (const source of this.caches) {
27
+ const layout = source.makeEmptyBatch(); layouts.push(layout);
28
+ layout.mergeRows([source]);
29
+ }
30
+ const result = new MlxStateRows(layouts); result.#rows = this.#rows;
31
+ return result;
32
+ } catch (error) { return cleanupFailure(error, () => disposeResources(layouts)); }
33
+ }
34
+ /** Prepare an initial cohort with one merge per layer. */
35
+ mergeRows(rows: readonly (readonly Cache[])[]): void {
36
+ const next: Layout[] = [];
37
+ try {
38
+ for (let layer = 0; layer < this.caches.length; layer++) {
39
+ const layout = this.caches[layer]!.makeEmptyBatch(); next.push(layout);
40
+ layout.mergeRows(rows.map(row => row[layer]!));
41
+ }
42
+ } catch (error) { return cleanupFailure(error, () => disposeResources(next)); }
43
+ const previous = this.caches.splice(0, this.caches.length, ...next);
44
+ this.#rows = rows.length; disposeResources(previous);
45
+ }
46
+
47
+ /** Borrow a prepared request's state. Build every replacement before
48
+ * releasing the active group, so a failed admission leaves it intact. */
49
+ append(row: readonly Cache[]): void {
50
+ applyStateChanges([() => this.prepareAppend(row)]);
51
+ }
52
+
53
+ /** Stage membership so a method can compose it with companion-state admission. */
54
+ prepareAppend(row: readonly Cache[]): PreparedStateChange {
55
+ return this.prepareAppendMany([row]);
56
+ }
57
+
58
+ /** Stage several admissions with one membership operation per layer. */
59
+ prepareAppendMany(rows: readonly (readonly Cache[])[]): PreparedStateChange {
60
+ const next: Layout[] = [];
61
+ try {
62
+ for (let layer = 0; layer < this.caches.length; layer++) {
63
+ const layout = this.caches[layer]!;
64
+ const merged = layout.makeEmptyBatch();
65
+ next.push(merged);
66
+ merged.mergeRows([...(this.#rows ? [layout] : []), ...rows.map(row => row[layer]!)]);
67
+ }
68
+ } catch (error) { return cleanupFailure(error, () => disposeResources(next)); }
69
+ return {
70
+ commit: () => {
71
+ const previous = this.caches.splice(0, this.caches.length, ...next.splice(0));
72
+ this.#rows += rows.length;
73
+ next.push(...previous);
74
+ },
75
+ dispose: () => disposeResources(next.splice(0)),
76
+ };
77
+ }
78
+
79
+ /** The method calls this only between committed steps. A layer failure
80
+ * invalidates the group; its caller discards the state. */
81
+ filterRows(keep: readonly number[]): void {
82
+ if (!keep.length) { this.clear(); return; }
83
+ for (const layout of this.caches) layout.filterRows(keep);
84
+ this.#rows = keep.length;
85
+ }
86
+
87
+ /** Returns independent request state for a cache publisher or a new group. */
88
+ extractRow(row: number): Cache[] {
89
+ const state: Cache[] = [];
90
+ try { for (const layout of this.caches) state.push(layout.extractRow(row)); return state; }
91
+ catch (error) { return cleanupFailure(error, () => disposeResources(state)); }
92
+ }
93
+
94
+ clear(): void {
95
+ const next: Layout[] = [];
96
+ try { for (const layout of this.caches) next.push(layout.makeEmptyBatch()); }
97
+ catch (error) { return cleanupFailure(error, () => disposeResources(next)); }
98
+ const previous = this.caches.splice(0, this.caches.length, ...next);
99
+ this.#rows = 0;
100
+ disposeResources(previous);
101
+ }
102
+
103
+ dispose(): void { this.#rows = 0; disposeResources(this.caches); }
104
+ }
@@ -1,7 +1,8 @@
1
1
  import type { StateView } from "../../contracts/resources";
2
2
  import { disposeResources, ownResource } from "../../engine/resources";
3
3
  import type { MlxArray } from "../../mlx/array";
4
- import type { Cache } from "../../model/gemma4-base";
4
+ import { isBatchableCache, type Cache } from "../../model/gemma4-base";
5
+ import { cloneKvCaches, type CacheCodecProvider } from "../../kv-store";
5
6
 
6
7
  /** Compatibility boundary for legacy state(). Numerical code consumes a lease,
7
8
  * never the legacy ownership marker. No copy, evaluation or fence is added. */
@@ -24,3 +25,22 @@ export function leaseCacheStates(caches: readonly Cache[]): StateView<MlxArray>
24
25
  throw error;
25
26
  }
26
27
  }
28
+
29
+ /** Precision/storage transitions constrain reuse independently of scheduling. */
30
+ export function minimumReusableOffset(caches: readonly Pick<Cache, "minimumReusableOffset">[]): number {
31
+ return Math.max(0, ...caches.map(cache => cache.minimumReusableOffset ?? 0));
32
+ }
33
+
34
+ /** Snapshot an adopted single-row state into persistence-compatible caches.
35
+ * Row layouts own compact extraction; ordinary serial caches use their codec. */
36
+ export function cloneSingleRowState(caches: readonly Cache[], codecs?: CacheCodecProvider): Cache[] {
37
+ const owned: Cache[] = [];
38
+ try {
39
+ for (const cache of caches) owned.push(...(isBatchableCache(cache)
40
+ ? [cache.extractRow(0)] : cloneKvCaches([cache], codecs)));
41
+ return owned;
42
+ } catch (error) {
43
+ try { disposeResources(owned); } catch (cleanupError) { throw new AggregateError([error, cleanupError], "single-row snapshot failed"); }
44
+ throw error;
45
+ }
46
+ }
@@ -0,0 +1,35 @@
1
+ import { Glm52Cache } from "../../model/glm52-cache";
2
+ import { SpeculativeRotatingKVCache } from "../../model/speculative-rotating-kv";
3
+ import { RotatingAffineLayout, SpeculativeRotatingAffineLayout } from "../../model/rotating-kv-layout";
4
+ import type { Cache } from "../../model/gemma4-base";
5
+ import { KVCache, QuantizedKVCache, TurboQuantKVCache, RotatingKVCache, RotatingQuantizedKVCache } from "../../model/gemma4-base";
6
+ import { DelayedRotatingQuantizedKVCache } from "../../model/delayed-rotating-quantized-kv";
7
+ import { DelayedQuantizedKVCache } from "../../model/delayed-quantized-kv";
8
+ import { DelayedTurboQuantKVCache } from "../../model/delayed-turboquant-kv";
9
+ import { SSMCache } from "../../model/qwen3-delta";
10
+ import { BatchedSSMCache } from "../../model/batched-ssm";
11
+ import { BatchedKVCache } from "../../model/batched-kv";
12
+ import { BatchedQuantizedKVCache } from "../../model/batched-quantized-kv";
13
+ import { BatchedTurboQuantKVCache } from "../../model/batched-turboquant-kv";
14
+
15
+ export type TargetRowLayout = Glm52Cache | DelayedRotatingQuantizedKVCache | RotatingAffineLayout | SpeculativeRotatingKVCache | BatchedKVCache | BatchedQuantizedKVCache | BatchedTurboQuantKVCache |
16
+ BatchedSSMCache | DelayedTurboQuantKVCache | DelayedQuantizedKVCache;
17
+
18
+ /** The storage binding advertises a row transaction factory. The gateway can
19
+ * compose supported model graphs without identifying their model family. */
20
+ export function targetRowLayoutFactory(cache: Cache): (() => TargetRowLayout) | undefined {
21
+ if (cache instanceof Glm52Cache) return () => cache.makeEmptyBatch();
22
+ if (cache instanceof DelayedRotatingQuantizedKVCache)
23
+ return () => new DelayedRotatingQuantizedKVCache(cache.maxSize, cache.groupSize, cache.bits, cache.start, cache.maintain, undefined, true);
24
+ if (cache instanceof RotatingAffineLayout || cache instanceof RotatingQuantizedKVCache)
25
+ return () => new SpeculativeRotatingAffineLayout(cache.maxSize, cache.groupSize, cache.bits);
26
+ if (cache instanceof RotatingKVCache || cache instanceof SpeculativeRotatingKVCache) return () => new SpeculativeRotatingKVCache(cache.maxSize);
27
+ if (cache instanceof DelayedTurboQuantKVCache || cache instanceof DelayedQuantizedKVCache ||
28
+ cache instanceof BatchedTurboQuantKVCache || cache instanceof BatchedKVCache ||
29
+ cache instanceof BatchedQuantizedKVCache || cache instanceof BatchedSSMCache) return () => cache.makeEmptyBatch();
30
+ if (cache instanceof TurboQuantKVCache) return () => new BatchedTurboQuantKVCache(cache.kBits, cache.vBits, cache.fusedDecode);
31
+ if (cache instanceof SSMCache) return () => new BatchedSSMCache();
32
+ if (cache instanceof QuantizedKVCache) return () => new BatchedQuantizedKVCache(cache.groupSize, cache.bits);
33
+ if (cache instanceof KVCache) return () => new BatchedKVCache();
34
+ return undefined;
35
+ }
@@ -0,0 +1,173 @@
1
+ import type { MlxArray } from "../../mlx/array";
2
+ import * as ops from "../../mlx/ops";
3
+ import type { BatchableCache, Cache, Mask } from "../../model/gemma4-base";
4
+ import { KvTensorRows } from "./kv-tensor-rows";
5
+ import { leaseCacheStates, minimumReusableOffset } from "./state-views";
6
+ import { withResource, disposeResources } from "../../engine/resources";
7
+ import { cloneKvCaches } from "../../kv-store";
8
+
9
+ /** Encoded layouts retain physical row alignment across precision changes. */
10
+ export interface TransitionedKvLayout extends BatchableCache {
11
+ alignRows?(leftPad: readonly number[]): void;
12
+ }
13
+ export interface SpeculativeTransitionedKvLayout extends TransitionedKvLayout {
14
+ specRoundBegin(): void;
15
+ specRoundCommit(): void;
16
+ specRoundRollback(keep: number | readonly number[]): void;
17
+ }
18
+ export interface KvRowTransition<Layout extends TransitionedKvLayout> {
19
+ readonly signature: string;
20
+ readonly conversionOffset?: number;
21
+ maintain(rows: Cache[]): void;
22
+ converted(row: Cache): boolean;
23
+ makeLayout(): Layout;
24
+ packRows?(layout: Layout, rows: readonly Cache[]): void;
25
+ prepareRows?(rows: Cache[]): Cache[];
26
+ extractRow?(row: Cache): Cache;
27
+ rollbackRow?(row: Cache, before: number, keep: number, preserveColumns: boolean): void;
28
+ }
29
+
30
+
31
+ export interface TransitioningKvPositions {
32
+ readonly rowOffsets: readonly number[];
33
+ readonly leftPad: readonly number[];
34
+ readonly offset: number;
35
+ readonly batchSize: number | null;
36
+ readonly ropeOffsetArr: MlxArray | undefined;
37
+ sync(rows: readonly Cache[], preserve: boolean): void;
38
+ filterRows(keep: readonly number[]): void;
39
+ makeMask(tokens: number, window: number | null): Mask;
40
+ dispose(): void;
41
+ }
42
+ export class FullKvPositions extends KvTensorRows implements TransitioningKvPositions {
43
+ sync(rows: readonly Cache[], preserve: boolean): void {
44
+ this.mergeRows(preserve
45
+ ? [{ planes: [], rowOffsets: rows.map(row => row.offset), leftPad: [...this.leftPad] }]
46
+ : rows.map(row => ({ planes: [], rowOffsets: [row.offset], leftPad: [0] })));
47
+ }
48
+ }
49
+
50
+ /** Owns membership, committed positions and precision transitions. Codec and
51
+ * attention adapters own tensor access; scheduling and persistence use the
52
+ * existing batch and checkpoint contracts. Row conversion never examines an
53
+ * uncommitted speculative suffix. */
54
+ export abstract class TransitioningKvRows<Layout extends TransitionedKvLayout> implements BatchableCache {
55
+ protected readonly positions: TransitioningKvPositions;
56
+ protected rows: Cache[] = [];
57
+ protected packed?: Layout;
58
+ #prefilling = false;
59
+
60
+ readonly stateNeedsDispose = true;
61
+ constructor(readonly transition: KvRowTransition<Layout>, row?: Cache, positions: TransitioningKvPositions = new FullKvPositions()) {
62
+ this.positions = positions;
63
+ if (row) { this.rows = transition.prepareRows?.([row]) ?? [row]; this.syncPositions(); }
64
+ }
65
+ signature(): string { return this.transition.signature; }
66
+ get prefillMaintenance() { return this; }
67
+ beginPrefill(): void { this.#prefilling = true; }
68
+ commitPrefill(indices: readonly number[]): void {
69
+ if (this.packed) return;
70
+ for (const index of indices) {
71
+ const row = [this.rows[index]!];
72
+ try { this.transition.maintain(row); }
73
+ finally { this.rows[index] = row[0]!; }
74
+ }
75
+ }
76
+ endPrefill(): void { this.#prefilling = false; }
77
+ get minimumReusableOffset(): number { return this.packed?.minimumReusableOffset ?? minimumReusableOffset(this.rows); }
78
+ get rowOffsets(): readonly number[] { return this.packed?.rowOffsets ?? this.positions.rowOffsets; }
79
+ get leftPad(): readonly number[] { return this.packed?.leftPad ?? this.positions.leftPad; }
80
+ get offset(): number { return this.packed?.offset ?? this.positions.offset; }
81
+ maxAppendTokens(): number {
82
+ if (this.packed) return this.packed.maxAppendTokens?.() ?? Infinity;
83
+ const end = this.transition.conversionOffset;
84
+ if (end === undefined) return Infinity;
85
+ return Math.min(Infinity, ...this.rowOffsets.map((offset, row) =>
86
+ offset < end && !this.transition.converted(this.rows[row]!) ? end - offset : Infinity));
87
+ }
88
+ get batchSize(): number | null { return this.packed?.batchSize ?? this.positions.batchSize; }
89
+ get ropeOffsetArr(): MlxArray | undefined { return this.packed ? this.packed.ropeOffsetArr : this.positions.ropeOffsetArr; }
90
+ bytesPerToken(): number { return this.packed?.bytesPerToken?.() ?? Math.max(0, ...this.rows.map(row => row.bytesPerToken?.() ?? 0)); }
91
+ projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
92
+ makeMask(tokens: number, window: number | null): Mask { return (this.packed ?? this.positions).makeMask(tokens, window); }
93
+ isTrimmable(): boolean { return this.packed?.isTrimmable() ?? this.rows.every(row => row.isTrimmable()); }
94
+ trim(count: number): void {
95
+ if (this.packed) this.packed.trim(count);
96
+ else { for (const row of this.rows) row.trim(count); this.syncPositions(true); }
97
+ }
98
+ protected syncPositions(preserve = false): void { this.positions.sync(this.rows, preserve); }
99
+ protected advance(): void {
100
+ if (this.packed) return;
101
+ if (!this.#prefilling) this.transition.maintain(this.rows);
102
+ if (this.rows.length && this.rows.every(row => this.transition.converted(row))) {
103
+ const packed = this.transition.makeLayout();
104
+ try {
105
+ if (this.transition.packRows) this.transition.packRows(packed, this.rows);
106
+ else packed.mergeRows(this.rows);
107
+ packed.alignRows?.(this.positions.leftPad);
108
+ }
109
+ catch (error) { packed.dispose(); throw error; }
110
+ const previous = this.rows; this.rows = []; this.packed = packed;
111
+ this.positions.dispose(); disposeResources(previous);
112
+ }
113
+ }
114
+ abstract updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray];
115
+ abstract makeEmptyBatch(): TransitioningKvRows<Layout>;
116
+ mergeRows(rows: readonly Cache[]): void {
117
+ const owned: Cache[] = [];
118
+ try {
119
+ for (const row of rows) {
120
+ if (row instanceof TransitioningKvRows) {
121
+ for (let index = 0; index < row.batchSize!; index++) owned.push(row.extractRow(index));
122
+ } else owned.push(...cloneKvCaches([row]));
123
+ }
124
+ } catch (error) { disposeResources(owned); throw error; }
125
+ let prepared: Cache[];
126
+ try { prepared = this.transition.prepareRows?.(owned) ?? owned; }
127
+ catch (error) { disposeResources(owned); throw error; }
128
+ this.dispose(); this.rows = prepared; this.syncPositions();
129
+ }
130
+ filterRows(keep: readonly number[]): void {
131
+ if (this.packed) { this.packed.filterRows(keep); return; }
132
+ const previous = this.rows; this.rows = keep.map(index => previous[index]!);
133
+ disposeResources(previous.filter((_, index) => !keep.includes(index))); this.positions.filterRows(keep);
134
+ }
135
+ extractRow(row: number): Cache { return this.packed?.extractRow(row) ?? (this.transition.extractRow?.(this.rows[row]!) ?? cloneKvCaches([this.rows[row]!])[0]!); }
136
+ state(): MlxArray[] {
137
+ return withResource(leaseCacheStates(this.packed ? [this.packed] : this.rows), arrays => {
138
+ const views: MlxArray[] = [];
139
+ try { for (const array of arrays) views.push(ops.contiguous(array)); return views; }
140
+ catch (error) { disposeResources(views); throw error; }
141
+ });
142
+ }
143
+ dispose(): void {
144
+ this.#prefilling = false;
145
+ const previous = this.rows; this.rows = [];
146
+ this.packed?.dispose(); this.packed = undefined; this.positions.dispose(); disposeResources(previous);
147
+ }
148
+ }
149
+
150
+ /** Rollback is a method capability, separate from ordinary cache membership. */
151
+ export abstract class SpeculativeTransitioningKvRows<Layout extends SpeculativeTransitionedKvLayout> extends TransitioningKvRows<Layout> {
152
+ #beforeRound?: number[];
153
+ specRoundBegin(): void {
154
+ this.advance(); this.#beforeRound = [...this.rowOffsets]; this.packed?.specRoundBegin();
155
+ }
156
+ specRoundCommit(): void {
157
+ this.packed?.specRoundCommit(); this.#beforeRound = undefined; this.advance();
158
+ }
159
+ specRoundRollback(keep: number | readonly number[]): void {
160
+ if (this.packed) this.packed.specRoundRollback(keep);
161
+ else {
162
+ for (const [index, row] of this.rows.entries()) {
163
+ const count = typeof keep === "number" ? keep : keep[index]!;
164
+ const before = this.#beforeRound![index]!;
165
+ if (this.transition.rollbackRow) this.transition.rollbackRow(row, before, count, this.rows.length > 1);
166
+ else row.trim(row.offset - before - count);
167
+ }
168
+ this.syncPositions(true);
169
+ }
170
+ this.#beforeRound = undefined; this.advance();
171
+ }
172
+ override dispose(): void { this.#beforeRound = undefined; super.dispose(); }
173
+ }