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
@@ -17,11 +17,9 @@
17
17
  // trimmable mid-generation; a serve endpoint must not 500 mid-stream, so
18
18
  // we STOP SPECULATING and finish the generation with plain single-token
19
19
  // decode (bit-equivalent continuation — the target's own samples).
20
- // - Prompt-cache reuse is BYPASSED in v1 (fresh caches per spec request,
21
- // cachedTokens=0). mlx-lm composes spec with its LRU prompt cache
22
- // (target+draft caches per entry); wiring that through our PromptCache +
23
- // SSD tier is a tracked follow-up in the integration plan, not silently
24
- // absent.
20
+ // - Sources with checkpoint support use the injected prefix-cache service.
21
+ // Target and companion state share the existing RAM/SSD lifecycle. Other
22
+ // sources begin with fresh state until they supply a checkpoint codec.
25
23
  //
26
24
  // Grammar × spec (Phase C, the constrained verify walk — novel: NO runtime
27
25
  // serves both; mlx-lm has no grammar, oMLX no spec): the drafter runs FREE
@@ -40,14 +38,18 @@
40
38
  // false halts the generation mid-burst.
41
39
 
42
40
  import { MlxArray } from "../mlx/array";
41
+ import { sampleSpeculativeRows } from "../backends/mlx/speculative-round";
43
42
  import * as ops from "../mlx/ops";
44
43
  import { clearCache } from "../mlx/ffi";
45
44
  import { cleanupFailure, disposeResources } from "../engine/resources";
46
45
  import type { DisposableResource } from "../contracts/resources";
47
46
  import { assertMlxSpeculativeBinding, bindLegacySpeculativeModel, type MlxSpeculativeBinding } from "../backends/mlx/speculative";
48
47
  import type { RuntimeModel } from "../model/factory";
48
+ import { cloneKvCaches } from "../kv-store";
49
+ import { disposeAttachments, type CheckpointAttachment } from "../backends/mlx/checkpoint-state";
49
50
  import type { Cache } from "../model/gemma4";
50
- import { maybeQuantizeKv, withModelUsageFlush, withModelWiredLimit, type GenerateOptions, type GenerateStats } from "../generate";
51
+ import { withModelUsageFlush, withModelWiredLimit, type GenerateOptions, type GenerateStats } from "../generate";
52
+ import { createKvMaintenance } from "../backends/mlx/kv-maintenance";
51
53
  import { runtimeConfig, withRuntimeConfig } from "../runtime-config";
52
54
  import { makeSampler, makeStepSampler } from "../sampler";
53
55
  import type { OnToken } from "../serve/generation-gateway";
@@ -72,6 +74,13 @@ export interface SpecServeExtras {
72
74
  acceptedByPos: number[];
73
75
  }
74
76
 
77
+ export interface SpecRunServices {
78
+ readonly prefixCache?: import("../backends/mlx/checkpoint-state").MlxPrefixCache;
79
+ /** Mounted adapter revision, resolved while the execution lease is held. */
80
+ readonly cacheNamespace?: string;
81
+ readonly cloneState?: (caches: Cache[]) => Cache[];
82
+ }
83
+
75
84
  export async function specServeRun(
76
85
  model: RuntimeModel,
77
86
  provider: DraftProvider,
@@ -79,8 +88,9 @@ export async function specServeRun(
79
88
  promptIds: number[],
80
89
  options: GenerateOptions & { stopSequences?: string[] },
81
90
  onToken: OnToken,
91
+ services: SpecRunServices = {},
82
92
  ): Promise<GenerateStats> {
83
- return specRun(bindLegacySpeculativeModel(model, provider), numDraftTokens, promptIds, options, onToken);
93
+ return specRun(bindLegacySpeculativeModel(model, provider), numDraftTokens, promptIds, options, onToken, services);
84
94
  }
85
95
 
86
96
  /** The verifier consumes a bound target and source factory, independent of
@@ -91,9 +101,10 @@ export async function specRun(
91
101
  promptIds: number[],
92
102
  options: GenerateOptions & { stopSequences?: string[] },
93
103
  onToken: OnToken,
104
+ services: SpecRunServices = {},
94
105
  ): Promise<GenerateStats> {
95
106
  return withRuntimeConfig(binding.runtime ?? runtimeConfig(), () => {
96
- const run = () => specRunInner(binding, numDraftTokens, promptIds, options, onToken);
107
+ const run = () => specRunInner(binding, numDraftTokens, promptIds, options, onToken, services);
97
108
  return binding.memory
98
109
  ? withModelWiredLimit(binding.memory, () => withModelUsageFlush(binding.memory!, run))
99
110
  : run();
@@ -106,8 +117,10 @@ async function specRunInner(
106
117
  promptIds: number[],
107
118
  options: GenerateOptions & { stopSequences?: string[] },
108
119
  onToken: OnToken,
120
+ services: SpecRunServices = {},
109
121
  ): Promise<GenerateStats> {
110
122
  const maxTokens = options.maxTokens ?? 512;
123
+ const maintainKv = createKvMaintenance(options);
111
124
  const eos = options.eosTokenIds ?? [...binding.eosTokenIds];
112
125
  const sampler = makeSampler(options);
113
126
  const stepSampler = makeStepSampler(options, {
@@ -120,7 +133,9 @@ async function specRunInner(
120
133
  sampler,
121
134
  });
122
135
  const gamma = Math.max(1, numDraftTokens);
136
+ const trace = runtimeConfig().value("MLX_BUN_SPEC_TRACE") === "1";
123
137
  const prefillChunk = options.prefillChunkSize ??
138
+ binding.prefillPolicy?.chunkSize(promptIds.length) ??
124
139
  runtimeConfig().number("MLX_BUN_RD_PREFILL_CHUNK", PREFILL_CHUNK);
125
140
 
126
141
  // Allocated INSIDE the try below (2026-07-07 review): provider.open()
@@ -129,6 +144,7 @@ async function specRunInner(
129
144
  // and turned a config error into a per-request 500. loadContext now
130
145
  // probe-opens the pairing at startup, so a throw here is belt+suspenders.
131
146
  let caches: Cache[] = [];
147
+ let prefixRetain: (() => void) | undefined;
132
148
  let source: import("./source").DraftSource | null = null;
133
149
  let tapLayers: number[] | undefined;
134
150
  // Target final hidden [1,1,H] at the anchor position — the assistant source
@@ -175,16 +191,6 @@ async function specRunInner(
175
191
  const samplePos = async (logits1V: MlxArray, step: number): Promise<number> =>
176
192
  (await stepSampler.sample(logits1V, step)).token;
177
193
 
178
- /** The [1,V] logits row at position `pos` of a hidden window — batched
179
- * lm-head is applied by the caller ONCE; this slices its output. */
180
- const logitsRow = (logitsWindow: MlxArray, pos: number): MlxArray => {
181
- const V = logitsWindow.shape[logitsWindow.shape.length - 1]!;
182
- const sl = logitsWindow.slice([0, pos, 0], [1, pos + 1, V]);
183
- const flat = ops.reshape(sl, [1, V]);
184
- sl.dispose();
185
- return flat;
186
- };
187
-
188
194
  const forwardTokens = async (tokens: number[], taps?: number[]) => {
189
195
  const ids = ops.fromInt32(tokens, [1, tokens.length]);
190
196
  try { return await binding.forward(ids, caches, taps); }
@@ -197,25 +203,50 @@ async function specRunInner(
197
203
  if (!Number.isSafeInteger(prefillChunk) || prefillChunk < 1)
198
204
  throw new Error("speculative prefill chunk must be a positive integer");
199
205
  assertMlxSpeculativeBinding(binding);
200
- if (options.turboQuant || options.kvConfig?.length ||
201
- (options.kvBits && (options.kvBits !== 4 || options.quantizedKvStart !== 0)))
202
- throw new Error("speculative KV requires the qualified uniform KV4 start=0 policy");
206
+ if (options.kvConfig?.length ||
207
+ (options.turboQuant && (options.quantizedKvStart ?? 0) !== 0) ||
208
+ (options.kvBits && ((options.kvBits !== 4 && options.kvBits !== 8) || options.quantizedKvStart !== 0)))
209
+ throw new Error("speculative KV requires uniform KV4/KV8 or TurboQuant start=0");
203
210
  caches = binding.makeCache();
204
211
  const src = binding.openDraft(sampler, caches);
205
212
  source = src;
206
213
  tapLayers = src.tapLayers;
207
- const prefixBudget = Math.max(0, options.speculativeCacheBytes ?? 0);
208
- const prefix = src.prefillMode === "full" ? src.prefix : undefined;
209
- const prefixNamespace = JSON.stringify({ adapters: options.adapters ?? [],
210
- kvBits: options.kvBits ?? null, kvGroupSize: options.kvGroupSize ?? 64,
211
- quantizedKvStart: options.quantizedKvStart ?? null });
212
- const cached = prefix?.restore(promptIds, caches, prefixNamespace, prefixBudget) ?? 0;
214
+ const checkpoint = src.prefillMode === "full" ? src.checkpoint : undefined;
215
+ const prefixCache = checkpoint ? services.prefixCache : undefined;
216
+ const prefixNamespace = JSON.stringify({ method: checkpoint?.namespace,
217
+ adapters: services.cacheNamespace ?? options.adapters ?? [], kvBits: options.kvBits ?? null,
218
+ kvGroupSize: options.kvGroupSize ?? 64, quantizedKvStart: options.quantizedKvStart ?? null,
219
+ turboQuant: options.turboQuant ?? null });
220
+ let cached = 0;
221
+ const hit = prefixCache?.take(promptIds, prefixNamespace, options.cacheSessionId);
222
+ if (hit) {
223
+ try {
224
+ disposeResources(caches);
225
+ caches.splice(0, caches.length, ...hit.caches.splice(0));
226
+ prefixRetain = hit.retain;
227
+ hit.retain = undefined;
228
+ checkpoint!.restore(hit.tokens.length, hit.attachments![0]!);
229
+ cached = hit.tokens.length;
230
+ } finally {
231
+ disposeResources([...hit.caches, { dispose: () => disposeAttachments(hit.attachments) },
232
+ { dispose: () => hit.retain?.() }]);
233
+ }
234
+ }
213
235
  stats.cachedTokens = cached;
214
- const snapshotAt = prefix && prefixBudget > 0
215
- ? Math.min(options.snapshotAt ?? promptIds.length, promptIds.length - 1)
216
- : -1;
217
- if (prefix && prefixBudget > 0 && cached > 0 && snapshotAt <= cached)
218
- prefix.capture(promptIds.slice(0, cached), caches, prefixNamespace, prefixBudget);
236
+ const capture = (tokens: number[]) => {
237
+ let state: Cache[] = [];
238
+ let attachments: CheckpointAttachment[] = [];
239
+ try {
240
+ state = (services.cloneState ?? cloneKvCaches)(caches);
241
+ attachments.push(checkpoint!.capture(tokens.length));
242
+ prefixCache!.put(tokens, state, prefixNamespace, undefined, attachments, options.cacheSessionId);
243
+ state = []; attachments = [];
244
+ } finally {
245
+ disposeResources([...state, { dispose: () => disposeAttachments(attachments) }]);
246
+ }
247
+ };
248
+ const snapshotAt = prefixCache
249
+ ? Math.min(options.snapshotAt ?? promptIds.length, promptIds.length - 1) : -1;
219
250
  const seedSource = async (end: number) => {
220
251
  if (ctxParts.length === 1) prefillCtx = ctxParts[0]!;
221
252
  else if (ctxParts.length > 1) {
@@ -275,7 +306,7 @@ async function specRunInner(
275
306
  anchorHidden = h.slice([0, L - 1, 0], [1, L, H]); // position len-2
276
307
  }
277
308
  } finally { h.dispose(); } // no logits during the drain
278
- maybeQuantizeKv(caches, options);
309
+ maintainKv(caches);
279
310
  clearCache();
280
311
  pos += n;
281
312
  if (pos < end) await new Promise<void>((resolve) => setImmediate(resolve));
@@ -300,10 +331,10 @@ async function specRunInner(
300
331
  lastLogits = ops.reshape(lg, [1, V]);
301
332
  } finally { lg.dispose(); }
302
333
  } } finally { h.dispose(); }
303
- maybeQuantizeKv(caches, options);
334
+ maintainKv(caches);
304
335
  if (end === snapshotAt && snapshotAt > cached) {
305
336
  await seedSource(end);
306
- prefix!.capture(promptIds.slice(0, end), caches, prefixNamespace, prefixBudget);
337
+ capture(promptIds.slice(0, end));
307
338
  }
308
339
  clearCache();
309
340
  off = end;
@@ -416,7 +447,7 @@ async function specRunInner(
416
447
  );
417
448
  const d = drafts.length;
418
449
  if (d < 0 || d > n) throw new Error(`DraftSource returned ${d} drafts (contract: 0..${n})`);
419
- if (Bun.env.MLX_BUN_SPEC_TRACE === "1") {
450
+ if (trace) {
420
451
  console.error(
421
452
  `[SPEC_TRACE] round=${extras.rounds + 1} ` +
422
453
  `feed=${feed.join(",")} drafts=${drafts.join(",")}`,
@@ -460,39 +491,12 @@ async function specRunInner(
460
491
  // accept while it reproduces the draft. Sampling is sequential because
461
492
  // the processor history (and, in Phase C, the grammar mask) at position
462
493
  // i depends on the tokens accepted at positions < i.
463
- let kAccept = 0;
464
- let correction: number | null = null; // target's token at first mismatch (or bonus)
465
- let sawEos = false;
494
+ const [result] = await sampleSpeculativeRows([{
495
+ pending, step: stats.generatedTokens, remaining: maxTokens - stats.generatedTokens,
496
+ eosTokenIds: eos, sampling: stepSampler, grammarDone: () => grammar?.isTerminated ?? false,
497
+ }], [drafts], vLogits!);
498
+ const { accepted: kAccept, correction, sawEos, emitted, grammarDone } = result!.acceptance;
466
499
  let halted = false;
467
- const emitted: number[] = [];
468
- let grammarDone = false;
469
- for (let i = 0; i <= d; i++) {
470
- roundRow = logitsRow(vLogits!, i);
471
- const tok = await samplePos(roundRow, stats.generatedTokens + emitted.length);
472
- roundRow.dispose();
473
- roundRow = null;
474
- if (i < d && tok === drafts[i]) {
475
- kAccept++;
476
- // EOS is never content — even when it arrives as an ACCEPTED DRAFT
477
- // (the correction/bonus branch below always excluded it; this
478
- // branch pushed-then-broke, leaking the EOS through onToken —
479
- // caught by the 2026-07-07 live oracle gate: streams were
480
- // bit-identical, ours emitted one extra "content" token: <|eot_id|>).
481
- if (eos.includes(tok)) { sawEos = true; break; }
482
- emitted.push(tok);
483
- // grammar termination mid-burst truncates the round — nothing may
484
- // be sampled past a satisfied grammar (the all--inf guarantee).
485
- if (grammar?.isTerminated) { grammarDone = true; break; }
486
- if (stats.generatedTokens + emitted.length >= maxTokens) break;
487
- continue;
488
- }
489
- // mismatch (target replaces the draft) or bonus (i === n)
490
- correction = tok;
491
- if (!eos.includes(tok)) emitted.push(tok);
492
- else sawEos = true;
493
- if (grammar?.isTerminated) grammarDone = true;
494
- break;
495
- }
496
500
  vLogits!.dispose();
497
501
  vLogits = null;
498
502
  extras.accepted += kAccept;
@@ -575,7 +579,7 @@ async function specRunInner(
575
579
  } finally {
576
580
  const resources = [anchorHidden, lastLogits, prefillCtx, ...ctxParts,
577
581
  vLogits, roundRow, vHidden, vCtxML, ...caches, source, stepSampler,
578
- options.grammar, { dispose: clearCache }]
582
+ options.grammar, { dispose: () => prefixRetain?.() }, { dispose: clearCache }]
579
583
  .filter((resource): resource is DisposableResource => resource != null);
580
584
  if (failure) cleanupFailure(failure.error, () => disposeResources(resources));
581
585
  else disposeResources(resources);
@@ -23,6 +23,9 @@
23
23
  import type { MlxArray } from "../mlx/array";
24
24
  import type { Dtype } from "../mlx/ffi";
25
25
  import type { Cache } from "../model/gemma4";
26
+ import type { CheckpointAttachment } from "../backends/mlx/checkpoint-state";
27
+ import type { MlxDraftRows } from "../backends/mlx/speculative-round";
28
+ import type { PreparedStateChange } from "../contracts/resources";
26
29
 
27
30
  /** Numerical ports are backend-specific; returned arrays are caller-owned. */
28
31
  export interface DraftProjection {
@@ -33,11 +36,14 @@ export interface DraftProjection {
33
36
  logitsFromHidden(hidden: MlxArray): MlxArray;
34
37
  }
35
38
 
36
- export interface AssistantTarget {
37
- position(): number;
38
- embedScaled(token: number): MlxArray;
39
- /** Retained temporal views; the draft round disposes every returned array. */
40
- readDonors(): { sliding: [MlxArray, MlxArray]; full: [MlxArray, MlxArray] };
39
+ /** Read-only target state and embedding, independent of draft scheduling. */
40
+ export interface AssistantRowsTarget {
41
+ readonly hiddenSize: number;
42
+ embed(ids: MlxArray): MlxArray;
43
+ readDonors(): import("./drafter").AssistantDonors & {
44
+ readonly positions: readonly number[];
45
+ dispose(): void;
46
+ };
41
47
  }
42
48
 
43
49
  export interface QwenMtpTarget {
@@ -53,7 +59,7 @@ export interface QwenMtpTarget {
53
59
  export interface TargetView {
54
60
  /** Opaque identity for providers borrowing weights from one exact target. */
55
61
  readonly identity: object;
56
- readonly assistant?: AssistantTarget;
62
+ readonly assistantRows?: AssistantRowsTarget;
57
63
  readonly gemmaTaps?: { readonly layerCount: number; readonly projection: DraftProjection };
58
64
  readonly qwenMtp?: QwenMtpTarget;
59
65
  }
@@ -61,12 +67,13 @@ export interface TargetView {
61
67
  /** A per-request draft-token producer. Created per generation (owns its own
62
68
  * draft-side state), disposed by the serve loop's finally. */
63
69
  export interface DraftSource {
64
- /** Optional paired target/draft prefill state. Restore transfers matching
65
- * target state into the supplied array. Capture borrows evaluated state;
66
- * the source owns its retained snapshot. Only full-prefill sources opt in. */
67
- readonly prefix?: {
68
- restore(prompt: readonly number[], caches: Cache[], namespace: string, maxBytes: number): number;
69
- capture(tokens: number[], caches: Cache[], namespace: string, maxBytes: number): void;
70
+ /** Method-owned companion state, independent of retention and storage.
71
+ * Capture returns owned immutable views. Restore borrows its attachment;
72
+ * the source retains the views it needs before returning. */
73
+ readonly checkpoint?: {
74
+ readonly namespace: string;
75
+ capture(processedTokens: number): import("../backends/mlx/checkpoint-state").CheckpointAttachment;
76
+ restore(processedTokens: number, attachment: import("../backends/mlx/checkpoint-state").CheckpointAttachment): void;
70
77
  };
71
78
  /** Target prefill shape required by this source's oracle. Most mlx-lm
72
79
  * sources leave the final prompt token pending; native Colibri MTP starts
@@ -86,8 +93,9 @@ export interface DraftSource {
86
93
  /** Process the prompt (two-model: prefill the draft model's cache;
87
94
  * assistant/dflash: read the target's state — mostly a no-op, but DSpark
88
95
  * seeds H_ctx from `ctxML`, the tapped prefill context [1,Lp,m*H], present
89
- * iff tapLayers is set). */
90
- prefill(promptIds: number[], ctxML?: MlxArray): void | Promise<void>;
96
+ * iff tapLayers is set). A composed prefill may supply its exact processed
97
+ * token count, so a history source need not infer the target's boundary. */
98
+ prefill(promptIds: number[], ctxML?: MlxArray, processedTokens?: number): void | Promise<void>;
91
99
 
92
100
  /** Propose 0..n tokens (RETURN LENGTH IS AUTHORITATIVE — a source may
93
101
  * return fewer than n, e.g. DSpark's confidence-scheduled draft-length
@@ -132,12 +140,82 @@ export interface DraftSource {
132
140
  dispose(): void;
133
141
  }
134
142
 
143
+ export interface DraftRowSampling {
144
+ /** Borrow [B,V] log-probabilities; return owned [B] IDs on device. */
145
+ sample(logprobs: MlxArray, steps: readonly number[]): MlxArray;
146
+ }
147
+
148
+ /** Request-owned constraints can propose known continuations without changing
149
+ * their committed state. The target verifier still decides every output. */
150
+ export interface DraftRowConstraints {
151
+ propose(row: number, maxTokens: number): Promise<number[]>;
152
+ }
153
+
154
+ export interface DraftRowCheckpoint {
155
+ readonly processedTokens: number;
156
+ readonly attachment: CheckpointAttachment;
157
+ }
158
+
159
+ /** Method-owned state membership, independent of scheduling and cache tiers.
160
+ * Open/append borrow checkpoints; capture returns owned immutable state.
161
+ * Membership changes and capture occur only between committed rounds.
162
+ * The caller retains the provider's residency lease for the group's lifetime. */
163
+ export interface DraftRowGroup extends MlxDraftRows {
164
+ /** Consume externally chosen target inputs at a committed round boundary.
165
+ * Context and tokens are borrowed rectangular windows; lengths retain each
166
+ * row's consumed prefix, excluding right padding and the pending output. */
167
+ consume?(tokens: MlxArray, context: MlxArray, lengths: readonly number[]): void | Promise<void>;
168
+ readonly namespace: string;
169
+ readonly tapLayers: readonly number[];
170
+ readonly rowCount: number;
171
+ append(checkpoints: readonly DraftRowCheckpoint[]): void;
172
+ prepareAppend(checkpoints: readonly DraftRowCheckpoint[]): PreparedStateChange;
173
+ filterRows(keep: readonly number[]): void;
174
+ capture(row: number): DraftRowCheckpoint;
175
+ dispose(): void;
176
+ }
177
+
178
+ /** Draft preparation consumes each target chunk at the same batch geometry.
179
+ * Inputs are borrowed and contain only new tokens/context, with equal chunk
180
+ * lengths across rows. A null checkpoint appends a cold request. The provider
181
+ * owns alignment and retained hidden state; scheduling and persistence do not.
182
+ * Membership and capture occur between prefill calls. Capture returns owned
183
+ * state after at least one target token has been consumed for that row. */
184
+ export interface DraftPrefillGroup {
185
+ readonly namespace: string;
186
+ readonly prefillMode: NonNullable<DraftSource["prefillMode"]>;
187
+ readonly tapLayers: readonly number[];
188
+ readonly rowCount: number;
189
+ append(checkpoints: readonly (DraftRowCheckpoint | null)[]): void;
190
+ prepareAppend(checkpoints: readonly (DraftRowCheckpoint | null)[]): PreparedStateChange;
191
+ prefill(tokens: MlxArray, context?: MlxArray): void | Promise<void>;
192
+ /** Resolve borrowed backing before its external restore lease is released. */
193
+ materialize(): void;
194
+ filterRows(keep: readonly number[]): void;
195
+ capture(row: number): DraftRowCheckpoint;
196
+ dispose(): void;
197
+ }
198
+
199
+ export interface GroupedDraftProvider {
200
+ readonly supportsExternalTokens?: boolean;
201
+ /** Resolve persistence identity without allocating a draft row. */
202
+ checkpointNamespace?(): string;
203
+ /** Draft state remains valid when target forwards run under a mounted
204
+ * adapter context. Unqualified learned draft graphs leave this absent. */
205
+ readonly supportsTargetAdapters?: boolean;
206
+ openPrefill(options: { target: TargetView;
207
+ checkpoints: readonly (DraftRowCheckpoint | null)[] }): DraftPrefillGroup;
208
+ open(options: { target: TargetView; sampling: DraftRowSampling; constraints?: DraftRowConstraints;
209
+ checkpoints: readonly DraftRowCheckpoint[] }): DraftRowGroup;
210
+ }
211
+
135
212
  /** Server-lifetime owner of the draft machinery (the loaded draft model);
136
- * open() mints a per-request DraftSource. */
213
+ * open() mints a per-request DraftSource; grouped opens shared row state. */
137
214
  export interface DraftProvider {
138
215
  /** Human-readable id (registry id / path tail) for logs + cache namespacing. */
139
216
  readonly id: string;
140
217
  readonly weightsBytes: number;
218
+ readonly grouped?: GroupedDraftProvider;
141
219
  open(opts: {
142
220
  /** The request's sampler over logprobs [1,V] → token array [1] (the SAME
143
221
  * sampler as the target — mlx-lm parity; greedy drafting under a
@@ -0,0 +1,62 @@
1
+ import type { MlxAutoregressiveBinding } from "../backends/mlx/autoregressive";
2
+ import { MlxAutoregressiveDraftRows, type MlxDraftLayout } from "../backends/mlx/autoregressive-draft-rows";
3
+ import { prefillCacheLayout } from "../backends/mlx/cache-layout";
4
+ import { MlxStateRows } from "../backends/mlx/state-rows";
5
+ import { captureCacheAttachment, restoreCacheAttachment } from "../backends/mlx/cache-attachment";
6
+ import { applyStateChanges, cleanupFailure, disposeResources } from "../engine/resources";
7
+ import type { Cache } from "../model/gemma4-base";
8
+ import type { DraftPrefillGroup, DraftRowCheckpoint, DraftRowGroup, DraftRowSampling, GroupedDraftProvider } from "./source";
9
+ import type { PreparedStateChange } from "../contracts/resources";
10
+ import type { MlxArray } from "../mlx/array";
11
+
12
+ export interface StandaloneDraftRows extends DraftPrefillGroup {
13
+ draft: DraftRowGroup["draft"];
14
+ commit(accepted: readonly number[], context?: MlxArray): Promise<void>;
15
+ }
16
+
17
+ /** The standalone model supplies graph/state capabilities once. Target
18
+ * verification, scheduling and cache tiers use the existing provider ports. */
19
+ export function openStandaloneDraftRows(binding: MlxAutoregressiveBinding, namespace: string,
20
+ sampling: DraftRowSampling | null, checkpoints: readonly (DraftRowCheckpoint | null)[]): StandaloneDraftRows {
21
+ const empty = binding.makeCache(), layouts: MlxDraftLayout[] = [];
22
+ try { for (const cache of empty) layouts.push(prefillCacheLayout(cache) as MlxDraftLayout); }
23
+ catch (error) { disposeResources(layouts); throw error; }
24
+ finally { disposeResources(empty); }
25
+ const state = new MlxStateRows(layouts), rows = new MlxAutoregressiveDraftRows(binding.graph, state, sampling);
26
+ const prepareAppend = (checkpoints: readonly (DraftRowCheckpoint | null)[]): PreparedStateChange => {
27
+ const restored: Cache[][] = [], pending: Array<number | null> = [];
28
+ try {
29
+ for (const checkpoint of checkpoints) {
30
+ restored.push(checkpoint ? restoreCacheAttachment(checkpoint.attachment, binding.makeCache) : binding.makeCache());
31
+ pending.push(checkpoint && checkpoint.attachment.metadata.pendingLast !== false ? Number(checkpoint.attachment.metadata.pendingLast) : null);
32
+ }
33
+ return rows.prepareAppend(restored, pending);
34
+ } finally { disposeResources(restored.flat()); }
35
+ };
36
+ const append = (checkpoints: readonly (DraftRowCheckpoint | null)[]) => applyStateChanges([() => prepareAppend(checkpoints)]);
37
+ try { append(checkpoints); }
38
+ catch (error) { return cleanupFailure(error, () => rows.dispose()); }
39
+ return {
40
+ namespace, prefillMode: "tail-split", tapLayers: [],
41
+ get rowCount() { return state.rowCount; },
42
+ append, prepareAppend, prefill: tokens => rows.prefill(tokens), materialize: () => rows.materialize(),
43
+ filterRows: keep => rows.filterRows(keep), draft: rows.draft.bind(rows), commit: accepted => rows.commit(accepted),
44
+ capture(row): DraftRowCheckpoint {
45
+ const caches = state.extractRow(row), pendingLast = rows.pendingLast[row] ?? null;
46
+ try {
47
+ const processedTokens = caches[0]!.offset + Number(pendingLast !== null);
48
+ return { processedTokens, attachment: captureCacheAttachment("standalone-draft-v1", caches,
49
+ { processedTokens, pendingLast: pendingLast ?? false }) };
50
+ } finally { disposeResources(caches); }
51
+ },
52
+ dispose: () => rows.dispose(),
53
+ };
54
+ }
55
+
56
+ export function standaloneDraftGroups(binding: MlxAutoregressiveBinding, namespace: string): GroupedDraftProvider {
57
+ return {
58
+ checkpointNamespace: () => namespace,
59
+ open: options => openStandaloneDraftRows(binding, namespace, options.sampling, options.checkpoints),
60
+ openPrefill: options => openStandaloneDraftRows(binding, namespace, null, options.checkpoints),
61
+ };
62
+ }