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
@@ -1,48 +1,61 @@
1
1
  import type { ExecutionCapabilities, ExecutionFeatures, ExecutionRequirements, ResolvedExecution } from "../contracts/execution";
2
2
 
3
- /** Preserve the shipped support matrix. A configured draft still forces serial
4
- * placement even when this request must use the ordinary AR method. */
3
+ /** Select the request method, then place it using executor capabilities. */
5
4
  export function resolveExecution(
6
5
  request: ExecutionRequirements, capabilities: ExecutionCapabilities,
7
6
  features: ExecutionFeatures = { pagedKv: false, fill: false },
8
7
  ): ResolvedExecution {
9
8
  const reasons: string[] = [];
10
- const continuousExclusions = [
9
+ const sharedRequestExclusions = [
11
10
  [!capabilities.continuous, "continuous-unavailable"],
12
- [capabilities.method !== "autoregressive", "method-requires-serial"],
13
- [request.hasVision, "media-requires-serial"],
14
- [request.hasAdapters, "adapters-require-serial"],
15
- [request.wantsLogprobs, "logprobs-require-serial"],
16
- [request.userSeed, "explicit-seed-requires-serial"],
11
+ [request.hasVision && !capabilities.mediaBatch, "media-requires-serial"],
12
+ [request.hasAdapters && !capabilities.adapterBatch, "adapters-require-serial"],
17
13
  [request.kvQuant && !capabilities.quantizedBatch, "kv-scheme-requires-serial"],
18
- [request.turboQuant, "turbo-kv-requires-serial"],
19
- [request.hasDraft, "configured-draft-requires-serial"],
14
+ [request.turboQuant && !capabilities.turboQuantBatch, "turbo-kv-requires-serial"],
20
15
  [request.hasGrammar && !capabilities.grammarBatch, "grammar-batching-disabled"],
21
- [features.pagedKv, "paged-kv-requires-serial"],
16
+ [features.pagedKv && !capabilities.pagedBatch, "paged-kv-requires-serial"],
17
+ ] as const;
18
+ const sharedAdapterMethod = capabilities.sharedSpeculativeAdapters === true &&
19
+ !sharedRequestExclusions.some(([excluded]) => excluded) &&
20
+ capabilities.groupedMethods?.includes("speculative") === true;
21
+ const grammarProposals = features.grammarJump === true && request.hasGrammar && !request.hasDraft && !request.hasVision &&
22
+ (!request.wantsLogprobs || capabilities.speculativeLogprobs === true) && !features.pagedKv && capabilities.method === "autoregressive" &&
23
+ capabilities.sharedGrammarProposals === true && capabilities.groupedMethods?.includes("speculative") === true &&
24
+ (!request.hasAdapters || sharedAdapterMethod) && (!request.kvQuant || capabilities.speculativeKvQuant === true) &&
25
+ (!request.turboQuant || capabilities.speculativeTurboQuant === true) &&
26
+ !sharedRequestExclusions.some(([excluded]) => excluded);
27
+ const speculative = grammarProposals || (capabilities.method === "autoregressive" && request.hasDraft &&
28
+ !request.hasVision && (!request.hasAdapters || sharedAdapterMethod) && (!request.wantsLogprobs || capabilities.speculativeLogprobs === true) &&
29
+ (!request.kvQuant || capabilities.speculativeKvQuant === true) &&
30
+ (!request.turboQuant || capabilities.speculativeTurboQuant === true) && !features.pagedKv);
31
+ const method = speculative ? "speculative" : capabilities.method;
32
+ const continuousExclusions = [sharedRequestExclusions[0],
33
+ [!(capabilities.groupedMethods ?? ["autoregressive"]).includes(method), "method-requires-serial"],
34
+ ...sharedRequestExclusions.slice(1),
22
35
  ] as const;
23
36
  for (const [excluded, reason] of continuousExclusions) if (excluded) reasons.push(reason);
24
37
  const mechanism = reasons.length ? "serial" : "continuous";
25
- const speculative = capabilities.method === "autoregressive" && request.hasDraft &&
26
- !request.hasVision && !request.hasAdapters && !request.wantsLogprobs &&
27
- (!request.kvQuant || capabilities.speculativeKvQuant === true) &&
28
- !request.turboQuant && !features.pagedKv;
29
38
  if (request.hasDraft && !speculative) reasons.push("draft-incompatible-with-request");
30
- const method = speculative ? "speculative" : capabilities.method;
31
39
  const pagedKv = features.pagedKv && !request.hasVision && !request.hasAdapters;
32
40
  if (features.pagedKv && !pagedKv) reasons.push("paged-kv-bypassed-for-media-or-adapters");
33
- const promptCache = method !== "speculative" && !request.hasVision && !pagedKv;
34
- const fill = features.fill && method === "autoregressive" && mechanism === "serial" &&
35
- !request.hasDraft && !request.hasVision && !request.userSeed && !request.hasGrammar &&
36
- !request.wantsLogprobs && !request.kvQuant && !request.turboQuant;
41
+ const promptCache = method !== "speculative" && (!request.hasVision ||
42
+ (mechanism === "continuous" && request.hasPreparedPrefixIdentity === true && capabilities.mediaPrefixCache === true));
43
+ const sharedFill = mechanism === "continuous" && capabilities.sharedFill === true && !pagedKv;
44
+ const speculativeEcho = method === "speculative" && mechanism === "continuous" &&
45
+ capabilities.sharedSpeculativeEcho === true;
46
+ const fill = features.fill && !request.hasVision && !request.hasGrammar && !request.wantsLogprobs &&
47
+ (speculativeEcho || (method === "autoregressive" && (mechanism === "serial" || sharedFill) &&
48
+ !request.hasDraft && (!request.userSeed || sharedFill)));
49
+ // Cache-format eligibility belongs to the method's append binding.
37
50
  if (features.fill && !fill) reasons.push("fill-incompatible-with-request");
38
- const checkpoint = capabilities.checkpoints && method === "autoregressive" &&
39
- mechanism === "serial" && promptCache && !request.hasGrammar &&
51
+ const checkpoint = capabilities.checkpoints && method === "autoregressive" && !request.hasVision &&
52
+ (mechanism === "serial" || capabilities.sharedCheckpoints === true) && promptCache && !pagedKv && !request.hasGrammar &&
40
53
  !features.fill && !request.wantsLogprobs;
41
54
  const compiledDecode = features.compiledDecode === true && capabilities.compiledDecode === true &&
42
- method === "autoregressive" && !request.hasAdapters && !pagedKv;
55
+ method === "autoregressive" && !request.hasAdapters && !pagedKv && !(sharedFill && fill);
43
56
  if (features.compiledDecode && !compiledDecode) reasons.push("compiled-decode-unavailable-for-request");
44
- const grammarJump = features.grammarJump === true && request.hasGrammar &&
45
- method === "autoregressive" && mechanism === "serial" && !request.wantsLogprobs;
57
+ const grammarJump = grammarProposals || (features.grammarJump === true && request.hasGrammar &&
58
+ method === "autoregressive" && mechanism === "serial" && !request.wantsLogprobs);
46
59
  if (features.grammarJump && request.hasGrammar && !grammarJump)
47
60
  reasons.push("grammar-jump-incompatible-with-request");
48
61
  return Object.freeze({ method, mechanism, pagedKv, promptCache, checkpoint, fill,
@@ -0,0 +1,44 @@
1
+ interface Task {
2
+ run(): Promise<void>;
3
+ reject(error: unknown): void;
4
+ }
5
+
6
+ /** Work whose owner requires an execution boundary, without owning a decode
7
+ * row. The driver supplies the lease; this queue owns waiting and cancellation.
8
+ * Once started, work settles before its caller can release native resources. */
9
+ export class ExecutionTasks {
10
+ #pending: Task[] = [];
11
+ get pending(): number { return this.#pending.length; }
12
+
13
+ enqueue<T>(work: () => Promise<T>, cancellation?: Cancellation): Promise<T> {
14
+ if (cancellation?.reason !== undefined) return Promise.reject(new GenerationCancelled(cancellation.reason));
15
+ return new Promise<T>((resolve, reject) => {
16
+ let unsubscribe = () => {};
17
+ const cleanup = () => unsubscribe();
18
+ const task: Task = {
19
+ async run() {
20
+ cleanup();
21
+ try { resolve(await work()); } catch (error) { reject(error); }
22
+ },
23
+ reject(error) { cleanup(); reject(error); },
24
+ };
25
+ this.#pending.push(task);
26
+ unsubscribe = cancellation?.subscribe(reason => {
27
+ const index = this.#pending.indexOf(task);
28
+ if (index < 0) return;
29
+ this.#pending.splice(index, 1);
30
+ task.reject(new GenerationCancelled(reason));
31
+ }) ?? (() => {});
32
+ if (!this.#pending.includes(task)) unsubscribe();
33
+ });
34
+ }
35
+
36
+ async advance(): Promise<void> { await this.#pending.shift()?.run(); }
37
+
38
+ rejectAll(error: unknown): void {
39
+ const tasks = this.#pending; this.#pending = [];
40
+ for (const task of tasks) task.reject(error);
41
+ }
42
+ }
43
+ import type { Cancellation } from "../contracts/generation";
44
+ import { GenerationCancelled } from "./cancellation";
@@ -0,0 +1,13 @@
1
+ import type { PrefixCache } from "../contracts/prefix-cache";
2
+
3
+ /** Compose numerical identity with a cache without exposing its storage tiers. */
4
+ export function namespacedCache<State, Attachment>(cache: PrefixCache<State, Attachment>,
5
+ namespace: (base: string) => string): PrefixCache<State, Attachment> {
6
+ return {
7
+ closeSession: id => cache.closeSession?.(id),
8
+ reclaim: () => cache.reclaim?.(),
9
+ ...(cache.prefetch ? { prefetch: (tokens: number[], ns = "", sessionId?: string) => cache.prefetch!(tokens, namespace(ns), sessionId) } : {}),
10
+ take: (tokens, ns = "", sessionId) => cache.take(tokens, namespace(ns), sessionId),
11
+ put: (tokens, state, ns = "", retain, attachments, sessionId) => cache.put(tokens, state, namespace(ns), retain, attachments, sessionId),
12
+ };
13
+ }
@@ -1,4 +1,4 @@
1
- import type { DisposableResource, ResourceOwner } from "../contracts/resources";
1
+ import type { DisposableResource, PreparedStateChange, ResourceOwner } from "../contracts/resources";
2
2
 
3
3
  export function ownResource<T>(value: T, release: (value: T) => void): ResourceOwner<T> {
4
4
  let held: { value: T } | undefined = { value };
@@ -35,6 +35,17 @@ export function cleanupFailure(error: unknown, cleanup: () => void): never {
35
35
  throw error;
36
36
  }
37
37
 
38
+ /** Complete every preparation before publishing any owner's replacement.
39
+ * Old state is released only after all owners have committed. */
40
+ export function applyStateChanges(prepare: readonly (() => PreparedStateChange)[]): void {
41
+ const changes: PreparedStateChange[] = [];
42
+ try {
43
+ for (const build of prepare) changes.push(build());
44
+ for (const change of changes) change.commit();
45
+ } catch (error) { return cleanupFailure(error, () => disposeResources(changes.splice(0))); }
46
+ disposeResources(changes);
47
+ }
48
+
38
49
  export function withResource<T, R>(owner: Pick<ResourceOwner<T>, "borrow" | "close">, use: (value: T) => R): R {
39
50
  try { return use(owner.borrow()); }
40
51
  catch (error) { cleanupFailure(error, () => owner.close()); }
@@ -18,8 +18,8 @@ export async function driveExecutionGroup(
18
18
  try {
19
19
  while (!group.closed) {
20
20
  group.pruneCancelled();
21
- const held = group.admissionHeld;
22
- if (!group.active && !group.preparing && (held || !group.queued)) {
21
+ let held = group.admissionHeld;
22
+ if (!group.active && !group.preparing && (held || (!group.queued && !group.pendingTasks))) {
23
23
  release();
24
24
  await group.waitForWork();
25
25
  continue;
@@ -28,17 +28,42 @@ export async function driveExecutionGroup(
28
28
  if (!execution && group.acquireExecution) execution = await group.acquireExecution();
29
29
  if (group.closed) break;
30
30
 
31
+ // One preparation task per iteration. An exclusive model mutation
32
+ // keeps its existing drain priority; preparation cannot prolong it.
33
+ if (!held && group.pendingTasks && group.advanceTask) {
34
+ await group.advanceTask();
35
+ if (group.closed) break;
36
+ group.pruneCancelled();
37
+ held = group.admissionHeld;
38
+ }
39
+
31
40
  if (!group.preparing && !held && group.queued && group.active < group.maxActive)
32
41
  group.admitNext();
42
+ while (group.preparing && group.canPrepareMore && !held && group.queued &&
43
+ group.active + (group.preparingRows ?? 1) < group.maxActive && group.canBurst() &&
44
+ (group.preparingTokens ?? 0) + (group.nextPreparationTokens ?? 0) <= (group.maxPreparationTokens ?? Infinity)) {
45
+ if (!group.admitNext()) break;
46
+ }
47
+ const mixed = group.preparing && group.active ? group.mixedPreparation : undefined;
48
+ if (mixed && group.advanceMixed) {
49
+ // Reserve the method's running-token demand first; prompt work fills
50
+ // the rest. A budget smaller than one token per row still progresses.
51
+ const budget = Math.max(group.maxIterationTokens ?? Infinity,
52
+ mixed.runningTokens + mixed.minimumPreparationTokens);
53
+ await group.advanceMixed(budget);
54
+ lastYield = clock.now();
55
+ await clock.yield();
56
+ continue;
57
+ }
33
58
  if (group.preparing) {
34
59
  const activeBefore = group.active;
35
60
  await group.advancePreparation();
36
- // A backend may emit its first output while preparing the first row.
37
- // Flush a lone admission before decode; queued short admissions still
38
- // group together. Re-enter policy after yielding to observe new work,
61
+ // A bounded preparation may publish output before its state work ends.
62
+ // Flush it before the next work unit; queued short admissions still
63
+ // form a group across yields. Re-enter policy to observe new work,
39
64
  // cancellation, admission holds and shutdown.
40
- if (yieldAfterPreparation && !activeBefore && !group.preparing &&
41
- group.active === 1 && !group.queued) {
65
+ if (group.preparationPublishedOutput ||
66
+ (yieldAfterPreparation && !activeBefore && !group.preparing && group.active === 1 && !group.queued)) {
42
67
  lastYield = clock.now();
43
68
  await clock.yield();
44
69
  continue;
@@ -20,7 +20,7 @@
20
20
  // sources in priority order, clamps what they return, and counts. It never
21
21
  // touches MLX, the tokenizer, or the template. Row COMPILATION is
22
22
  // ./schema-rows.ts.
23
- import { flagOn, runtimeNumber, runtimeValue } from "../runtime-config";
23
+ import { runtimeFlag, runtimeNumber, runtimeValue } from "../runtime-config";
24
24
  import { EchoSource, type EchoConfig } from "./echo-index";
25
25
  import type {
26
26
  Proposal, ProposalOrigin, ProposalPolicy, ProposalSource, TokenView,
@@ -153,7 +153,7 @@ export function fillEchoConfig(): FillEchoConfig {
153
153
  * the engine's fill path. Off by default (the invariant it checks is the
154
154
  * easiest bug in the feature, and the check costs a JS compare). */
155
155
  export function fillTraceEnabled(): boolean {
156
- return flagOn("MLX_BUN_FILL_TRACE", false) || fillTracePath() !== null;
156
+ return runtimeFlag("MLX_BUN_FILL_TRACE", false) || fillTracePath() !== null;
157
157
  }
158
158
 
159
159
  /** MLX_BUN_FILL_TRACE=<file.jsonl> — additionally append one record per
@@ -319,12 +319,23 @@ export class FillSession {
319
319
  /** Feed one SAMPLED token, then ask the sources what follows. `budget` is
320
320
  * the caller's remaining allowance (max_tokens − generated). */
321
321
  push(tokenId: number, budget: number = Number.POSITIVE_INFINITY): Proposal | null {
322
+ this.observe(tokenId);
323
+ return this.propose(budget);
324
+ }
325
+
326
+ /** Record sampled output independently of when the method requests proposals. */
327
+ observe(tokenId: number): void {
322
328
  this.stats.decodeSteps++;
323
329
  this.#append([tokenId]);
330
+ }
331
+
332
+ /** Select a continuation after a committed output boundary. Methods may
333
+ * request only verified proposals while retaining ordinary sampled policy. */
334
+ propose(budget: number = Number.POSITIVE_INFINITY, policy?: ProposalPolicy): Proposal | null {
324
335
  this.#budget = budget;
325
336
  for (const source of this.#sources) {
326
337
  const proposal = source.propose(this);
327
- if (!proposal) continue;
338
+ if (!proposal || (policy && proposal.policy !== policy)) continue;
328
339
  if (proposal.branchStop) this.stats.branchStops++;
329
340
  const clamped = this.#clamp(proposal.ids, budget, proposal.origin === "echo");
330
341
  if (!clamped) continue;
package/src/fit.ts CHANGED
@@ -13,12 +13,12 @@ import type { ModelConfig } from "./config";
13
13
  import type { MemoryPlan } from "./admission-plan";
14
14
  import {
15
15
  kvBytesAt,
16
- kvGeometry,
17
16
  kvQuantBytesPerElement,
17
+ sdpaFallbackBytes,
18
18
  type KvSchemeOptions,
19
19
  } from "./kv-scheme";
20
20
 
21
- export { kvBytesAt, kvQuantBytesPerElement } from "./kv-scheme";
21
+ export { kvBytesAt, kvQuantBytesPerElement, sdpaFallbackBytes } from "./kv-scheme";
22
22
  export type FitKvScheme = KvSchemeOptions;
23
23
 
24
24
  /** Decode-efficiency vs theoretical bandwidth ceiling, measured on the
@@ -161,23 +161,37 @@ export function fit(
161
161
  kvScheme?: FitKvScheme,
162
162
  ): FitReport {
163
163
  const usable = usableBytes ?? machine.ramBytes * WIRED_FRACTION;
164
- const transient = Math.min(chunk, ctx) * TRANSIENT_PER_TOKEN;
164
+ // Prefill transient = the calibrated per-chunk-token constant + the
165
+ // SDPA-fallback scores tensor (heads × min(chunk, ctx) × ctx × 2 B) for
166
+ // attention layers whose head dim MLX cannot fuse (kv-scheme.ts). The
167
+ // second term is what grows with context on Qwen3.x (head_dim 256) and
168
+ // Gemma 4 (256/512) and was unbilled before 2026-09-08.
169
+ const transientAt = (n: number) =>
170
+ Math.min(chunk, n) * TRANSIENT_PER_TOKEN + sdpaFallbackBytes(config, chunk, n);
171
+ const transient = transientAt(ctx);
165
172
  const kv = kvBytesAt(config, ctx, kvScheme);
166
173
  const total = weightsBytes + kv + transient;
167
174
 
168
- // solve max context: weights + kv(ctx) + transient ≤ usable.
169
- // Below the window both KV terms are linear in ctx; above it the
170
- // sliding term saturates and only full-attention layers keep growing.
171
- const g = kvGeometry(config, kvScheme);
172
- const fixed = weightsBytes + chunk * TRANSIENT_PER_TOKEN + g.linearStateBytes;
175
+ // solve max context: weights + kv(n) + transient(n) ≤ usable. The same
176
+ // formula `fits` uses, so the solved ceiling always fits. It is monotone
177
+ // in n (KV linear, sliding saturating at the window, scores quadratic
178
+ // below the chunk then linear), so bisect over [0, maxPositionEmbeddings].
179
+ const totalAt = (n: number) => weightsBytes + kvBytesAt(config, n, kvScheme) + transientAt(n);
173
180
  let maxCtx = 0;
174
- if (usable > fixed) {
175
- const budget = usable - fixed;
176
- const linear = Math.floor(budget / (g.fullBytesPerToken + g.slidingBytesPerToken));
177
- maxCtx = linear <= g.window
178
- ? linear
179
- : Math.floor((budget - g.slidingBytesPerToken * g.window) / g.fullBytesPerToken);
180
- maxCtx = Math.min(maxCtx, config.text.maxPositionEmbeddings);
181
+ {
182
+ let lo = 0;
183
+ let hi = Math.max(0, Math.floor(config.text.maxPositionEmbeddings));
184
+ if (totalAt(hi) <= usable) lo = hi;
185
+ else {
186
+ // invariant: totalAt(lo) ≤ usable (lo = 0 may itself not fit → 0)
187
+ if (totalAt(0) > usable) hi = 0;
188
+ while (hi - lo > 1) {
189
+ const mid = Math.floor((lo + hi) / 2);
190
+ if (totalAt(mid) <= usable) lo = mid;
191
+ else hi = mid;
192
+ }
193
+ }
194
+ maxCtx = lo;
181
195
  }
182
196
 
183
197
  // decode reads all weights + the KV cache once per token — except MoE