mlx-bun 0.0.13 → 0.2.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 (224) hide show
  1. package/README.md +31 -21
  2. package/bin/mlx-bun.mjs +1 -1
  3. package/docs/{design → archive}/hlg-sampling.md +3 -3
  4. package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
  5. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
  6. package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
  7. package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
  8. package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
  9. package/docs/design/colibri-glm52-port.md +1 -1
  10. package/docs/design/compat-cli-surface-design.md +1 -1
  11. package/docs/design/decode-speed-program.md +1 -1
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +1 -1
  14. package/docs/design/dspark-speculative-decoding.md +53 -3
  15. package/docs/design/grammar-spec-batching-integration.md +1 -1
  16. package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
  17. package/docs/design/orpo-dynamic-lambda.md +1 -1
  18. package/docs/design/orpo-future-enhancements.md +1 -1
  19. package/docs/design/orpo-training.md +2 -2
  20. package/docs/design/parity-tier-dag.md +1 -1
  21. package/docs/design/repo-cleanup-plan.md +46 -4
  22. package/docs/design/segmented-backward-training.md +2 -2
  23. package/docs/design/ssd-kv-cold-tier.md +127 -0
  24. package/docs/design/turboquant-weights.md +467 -0
  25. package/docs/design/w4a16-compute-precision-spike.md +1 -1
  26. package/docs/design/web-chat-redesign.md +5 -5
  27. package/docs/planning/chunk-task-roadmap.md +1 -1
  28. package/docs/planning/curve-sampler-research-plan.md +4 -4
  29. package/docs/planning/memory-docs-and-dag-plan.md +1 -1
  30. package/docs/planning/release-notes-v0.2.0.md +49 -0
  31. package/docs/planning/video-series.md +421 -0
  32. package/docs/reference/cli.md +15 -4
  33. package/docs/reference/distribution.md +1 -0
  34. package/docs/reference/features-matrix.md +3 -2
  35. package/docs/reference/library-api.md +41 -1
  36. package/docs/reference/models.md +33 -0
  37. package/docs/reference/server-api.md +69 -14
  38. package/docs/reference/server-config.md +57 -33
  39. package/package.json +2 -2
  40. package/src/anthropic.ts +92 -3
  41. package/src/cli.ts +131 -36
  42. package/src/eval/kl.ts +27 -20
  43. package/src/eval/runner.ts +23 -4
  44. package/src/eval/tasks/bfcl.ts +2 -1
  45. package/src/eval/tasks/gsm8k.ts +2 -1
  46. package/src/eval/tasks/hashhop.ts +2 -1
  47. package/src/eval/tasks/humaneval.ts +5 -4
  48. package/src/eval/tasks/ifeval.ts +2 -1
  49. package/src/eval/tasks/mmlu.ts +2 -1
  50. package/src/expert-io.ts +3 -1
  51. package/src/expert-offload.ts +3 -1
  52. package/src/expert-trace.ts +3 -1
  53. package/src/fit.ts +24 -43
  54. package/src/flags.ts +7 -9
  55. package/src/generate.ts +107 -92
  56. package/src/grammar.ts +2 -1
  57. package/src/harness-pi.ts +1 -1
  58. package/src/index.ts +20 -0
  59. package/src/jobs/job-entry.ts +3 -2
  60. package/src/kv-scheme.ts +199 -0
  61. package/src/kv-store.ts +370 -205
  62. package/src/media-fetch.ts +19 -3
  63. package/src/memory/model.ts +2 -1
  64. package/src/memory/vault.ts +2 -1
  65. package/src/memory-plan.ts +18 -0
  66. package/src/mlx/ffi.ts +8 -11
  67. package/src/mlx/ops.ts +31 -6
  68. package/src/model/batched-mask.ts +1 -1
  69. package/src/model/batched-quant.ts +1 -1
  70. package/src/model/batched-rotating-quant.ts +43 -97
  71. package/src/model/batched-rotating-state.ts +73 -0
  72. package/src/model/batched-rotating.ts +56 -105
  73. package/src/model/batched-row-storage.ts +127 -0
  74. package/src/model/compiled-decode.ts +3 -2
  75. package/src/model/factory.ts +44 -49
  76. package/src/model/gemma4-base.ts +163 -10
  77. package/src/model/generated/gemma4-12b.ts +50 -146
  78. package/src/model/generated/gemma4-26b.ts +32 -92
  79. package/src/model/generated/gemma4-e4b.ts +26 -74
  80. package/src/model/glm52-cache.ts +19 -9
  81. package/src/model/glm52-memory.ts +28 -1
  82. package/src/model/minicpm5.ts +2 -1
  83. package/src/model/profile.ts +433 -0
  84. package/src/model/qwen3-delta.ts +98 -2
  85. package/src/model/qwen3-mrope.ts +252 -0
  86. package/src/model/qwen3_5.ts +217 -25
  87. package/src/native/frame_extract.swift +140 -0
  88. package/src/native-pack.ts +13 -6
  89. package/src/pi-web.ts +6 -5
  90. package/src/prompt-cache.ts +2 -2
  91. package/src/quantize/atomic-output.ts +38 -0
  92. package/src/quantize/calibration.ts +2 -1
  93. package/src/quantize/config-writer.ts +6 -0
  94. package/src/quantize/index.ts +28 -1
  95. package/src/quantize/job.ts +11 -0
  96. package/src/quantize/quantizer.ts +265 -100
  97. package/src/quantize/rotate.ts +603 -0
  98. package/src/quantize/safetensors-writer.ts +79 -1
  99. package/src/quantize/sensitivity.ts +41 -59
  100. package/src/quantize/weight-transform.ts +292 -0
  101. package/src/responses.ts +102 -2
  102. package/src/runtime-config.ts +67 -0
  103. package/src/sampler.ts +272 -0
  104. package/src/serve/admin-routes.ts +184 -0
  105. package/src/serve/aux-routes.ts +132 -0
  106. package/src/serve/batch-scheduler.ts +200 -87
  107. package/src/serve/completion-executor.ts +346 -0
  108. package/src/serve/completion-sink.ts +157 -0
  109. package/src/serve/discovery-routes.ts +205 -0
  110. package/src/serve/generation-gateway.ts +116 -109
  111. package/src/serve/kv-budget.ts +14 -0
  112. package/src/serve/lab-routes.ts +338 -0
  113. package/src/serve/model-admin-routes.ts +153 -0
  114. package/src/serve/prompt-response-trace.ts +147 -0
  115. package/src/serve/request-plan.ts +158 -0
  116. package/src/serve/static-routes.ts +101 -0
  117. package/src/server.ts +704 -1400
  118. package/src/spec/dflash-source.ts +2 -1
  119. package/src/spec/dspark/module-dflash.ts +1 -1
  120. package/src/spec/glm52-mtp-source.ts +8 -0
  121. package/src/spec/qwen-mtp-source.ts +431 -0
  122. package/src/spec/serve-loop.ts +36 -68
  123. package/src/ssd-cache.ts +17 -14
  124. package/src/ssd-durability.ts +195 -0
  125. package/src/train/flash-cce.ts +12 -11
  126. package/src/train/forward.ts +32 -1
  127. package/src/train/loss.ts +2 -1
  128. package/src/train/segmented.ts +4 -3
  129. package/src/train/trainer.ts +3 -2
  130. package/src/vision/preprocess.ts +1 -1
  131. package/src/vision/prompt.ts +47 -1
  132. package/src/vision/qwen3vl-preprocess.ts +409 -0
  133. package/src/vision/qwen3vl-prompt.ts +196 -0
  134. package/src/vision/qwen3vl-tower.ts +513 -0
  135. package/src/vision/video-frames.ts +148 -0
  136. package/src/web/app.js +10 -3
  137. package/src/web/src/chat.ts +12 -5
  138. package/src/weights.ts +81 -26
  139. /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
  140. /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
  141. /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
  142. /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
  143. /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
  144. /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
  145. /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
  146. /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
  147. /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
  148. /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
  149. /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
  150. /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
  151. /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
  152. /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
  153. /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
  154. /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
  155. /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
  156. /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
  157. /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
  158. /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
  159. /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
  160. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
  161. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
  162. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
  163. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
  164. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
  165. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
  166. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
  167. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
  168. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
  169. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
  170. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
  171. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
  172. /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
  173. /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
  174. /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
  175. /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
  176. /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
  177. /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
  178. /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
  179. /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
  180. /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
  181. /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
  182. /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
  183. /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
  184. /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
  185. /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
  186. /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
  187. /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
  188. /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
  189. /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
  190. /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
  191. /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
  192. /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
  193. /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
  194. /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
  195. /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
  196. /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
  197. /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
  198. /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
  199. /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
  200. /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
  201. /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
  202. /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
  203. /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
  204. /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
  205. /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
  206. /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
  207. /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
  208. /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
  209. /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
  210. /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
  211. /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
  212. /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
  213. /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
  214. /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
  215. /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
  216. /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
  217. /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
  218. /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
  219. /package/docs/{planning → archive/planning}/journal.md +0 -0
  220. /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
  221. /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
  222. /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
  223. /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
  224. /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
@@ -1,10 +1,11 @@
1
- // Generation gateway — the seam between the server's request handlers and the
2
- // two execution lanes: the serial single-queue path (today's default) and the
3
- // continuous-batching scheduler (`--batch N`). The handler builds an onToken
4
- // closure (its own StopMatcher + tool router + SSE stream) and calls run();
5
- // the gateway routes it to the right lane. Per-row SSE fan-out falls out for
6
- // free — each request keeps its own onToken/stream and the scheduler just
7
- // invokes the right row's onToken.
1
+ // GenerationGateway is the scheduling seam between CompletionExecutor and
2
+ // the inference runtime. It declares one mechanism per request: the preserved
3
+ // strict/dedicated serial executor, or the continuous scheduler (`--batch N`).
4
+ // The scheduler itself chooses its B=1 fast path or B=N step from its active
5
+ // row count; placement does not predict a batch size. The executor owns each
6
+ // request's semantic token sink and passes its callback plus one immutable
7
+ // placement into run(). Each request keeps its own callback, so the scheduler
8
+ // can fan tokens out to the matching response stream.
8
9
  //
9
10
  // The two lanes are MUTUALLY EXCLUSIVE on the GPU (and on shared model state
10
11
  // like loraState, which generate() mutates per-generation assuming a serialized
@@ -34,16 +35,23 @@
34
35
  // back on finish (merged rows' KV is not extracted — their entries age out).
35
36
 
36
37
  import { MlxArray } from "../mlx/array";
37
- import * as ops from "../mlx/ops";
38
38
  import type { RuntimeModel } from "../model/factory";
39
39
  import { DiffusionGemmaModel } from "../model/diffusion-gemma";
40
- import { KVCache, RotatingKVCache, isBatchableCache } from "../model/gemma4-base";
40
+ import {
41
+ KVCache,
42
+ RotatingKVCache,
43
+ isBatchableCache,
44
+ isPlainKvCache,
45
+ isRotatingPlainCache,
46
+ } from "../model/gemma4-base";
41
47
  import { SSMCache } from "../model/qwen3-delta";
42
48
  import { UniversalDenseModel } from "../model/universal/dense";
43
49
  import type { GenerateOptions, GenerateStats, TokenLogprobs } from "../generate";
44
- import type { KvQuantSpec, TurboQuantScheme } from "../config";
45
- import { makeSampler, makeLogitsProcessors, toLogprobs } from "../sampler";
50
+ import type { KvScheme } from "../kv-scheme";
51
+ import { makeStepSampler } from "../sampler";
46
52
  import { BatchScheduler, type RowPromptCache } from "./batch-scheduler";
53
+ import { runtimeValue } from "../runtime-config";
54
+ import type { PromptResponseTrace } from "./prompt-response-trace";
47
55
 
48
56
  /** Async mutex: acquire() resolves to a release fn; releases run FIFO. */
49
57
  class AsyncMutex {
@@ -83,6 +91,9 @@ export type Vision = {
83
91
  * per-layer-input id zeroing. Absent on the legacy vision-only shape,
84
92
  * where zeroing falls back to imageMask. */
85
93
  multimodalMask?: MlxArray;
94
+ /** Qwen3.5/3.8 vision: the request's mRoPE positions + decode delta,
95
+ * installed on the model for exactly this serial run (server-side). */
96
+ mrope?: import("../model/qwen3-mrope").MropeRequestState;
86
97
  };
87
98
 
88
99
  /** Per-token sink: returning `false` halts this generation (stop sequence).
@@ -100,17 +111,19 @@ export type SerialRun = (
100
111
  options: GenerateOptions & { stopSequences?: string[] },
101
112
  onToken: OnToken,
102
113
  vision?: Vision,
114
+ trace?: PromptResponseTrace,
103
115
  ) => Promise<GenerateStats>;
104
116
 
105
117
  /** What the batchable decision needs from a request (cheap to compute). */
106
118
  export interface RequestShape {
107
- hasVision: boolean;
108
- hasAdapters: boolean;
109
- hasRepetitionPenalty: boolean;
119
+ readonly hasVision: boolean;
120
+ readonly hasAdapters: boolean;
121
+ readonly hasRepetitionPenalty: boolean;
110
122
  /** The user explicitly set `seed` (reproducibility) — not the random default. */
111
- userSeed: boolean;
112
- /** KV quantization is active (kvConfig/kvBits) — batched is bf16-only in v1. */
113
- kvQuant: boolean;
123
+ readonly userSeed: boolean;
124
+ /** KV quantization is active. Per-layer configs may use the continuous
125
+ * scheduler; uniform schemes use the serial mechanism. */
126
+ readonly kvQuant: boolean;
114
127
  /** TurboQuant is active (docs/design/turboquant-kv.md). Solo-only in v1:
115
128
  * TurboQuantKVCache is a novel Cache implementation (not a KVCache/
116
129
  * RotatingKVCache subclass), so #modelCachesBatchable() already excludes
@@ -118,32 +131,42 @@ export interface RequestShape {
118
131
  * BELT on top of that automatic BRACES, an explicit refusal at the
119
132
  * request-shape level so a turbo request never reaches the scheduler even
120
133
  * before any cache conversion has happened. Both layers exist on purpose. */
121
- turboQuant: boolean;
134
+ readonly turboQuant: boolean;
122
135
  /** Any of the mlx-lm sampler/processor extensions is active: min_p, XTC,
123
136
  * logit_bias, presence/frequency penalty. INFORMATIONAL ONLY: since the
124
- * batched lane grew per-row samplers/logits processors these all batch —
125
- * willBatch() deliberately does not gate on this field (see its
126
- * repetitionPenalty note). Kept so /stats and lane tracing can show what
137
+ * continuous scheduler grew per-row samplers/logits processors, these all
138
+ * compose. Placement deliberately does not gate on this field (see the
139
+ * repetition-penalty note in #supportsContinuous). Kept so /stats and
140
+ * scheduling traces can show what
127
141
  * a request carries. */
128
- hasLogitsExtras: boolean;
142
+ readonly hasLogitsExtras: boolean;
129
143
  /** A grammar controller compiled for this request (response_format /
130
144
  * guided_*). Degrade-path requests (compile failed → prompt injection)
131
145
  * have NO controller and stay batchable — the injection already happened
132
146
  * at the prompt level. B0 routes grammar to serial; B1 makes it batchable
133
147
  * (per-row matchers), with MLX_BUN_GRAMMAR_BATCH=0 forcing the B0 serial
134
148
  * fallback as the A/B + kill switch. */
135
- hasGrammar: boolean;
149
+ readonly hasGrammar: boolean;
136
150
  /** The request asked for logprobs/top_logprobs capture. Batch-lane logprobs
137
151
  * are deferred (the scheduler's per-row sampler doesn't capture or read
138
152
  * back logprob arrays yet), so these route to the serial lane like the
139
153
  * other mlx-lm request extensions above. */
140
- wantsLogprobs: boolean;
154
+ readonly wantsLogprobs: boolean;
141
155
  /** A draft model is configured (serve --draft-model). Server-level and
142
156
  * upstream-parity: mlx_lm.server sets is_batchable = (draft is None), so
143
157
  * every request routes serial while a draft is mounted — speculation is a
144
158
  * B=1 latency optimization, batching a throughput one; they are different
145
- * modes by design (grammar-spec-batching-integration.md). */
146
- hasDraft: boolean;
159
+ * mechanisms by design (grammar-spec-batching-integration.md). */
160
+ readonly hasDraft: boolean;
161
+ }
162
+
163
+ export type GenerationMechanism = "serial" | "continuous";
164
+
165
+ /** One immutable scheduling declaration for one exact request shape.
166
+ * `continuous` means scheduler admission, not that another row is present. */
167
+ export interface GenerationPlacement {
168
+ readonly shape: RequestShape;
169
+ readonly mechanism: GenerationMechanism;
147
170
  }
148
171
 
149
172
  export class GenerationGateway {
@@ -156,12 +179,8 @@ export class GenerationGateway {
156
179
  #serialWaiters = 0;
157
180
  /** Lazy, memoized: can this model's caches do the dynamic-B ops? */
158
181
  #cacheBatchable: boolean | null = null;
159
- /** Lazy, memoized: can the server's kv scheme run under batching?
160
- * (Phase 3.1: kvConfig whose every configured layerIdx is a plain
161
- * full-attention KVCache — uniform kvBits and rotating-layer configs
162
- * stay serial. The scheme is a server-lifetime constant, so one memo.) */
182
+ /** Lazy, memoized: can the configured KV scheme convert every named cache? */
163
183
  #kvSchemeBatchable: boolean | null = null;
164
-
165
184
  constructor(
166
185
  private readonly model: RuntimeModel,
167
186
  batch: number,
@@ -170,9 +189,9 @@ export class GenerationGateway {
170
189
  kvBudgetBytes?: number;
171
190
  /** The server-wide KV scheme (server.ts kvScheme) — threaded to the
172
191
  * scheduler at construction when batchable (Phase 3.1). turboQuant is
173
- * never threaded to the scheduler (always solo-only — see willBatch);
192
+ * never threaded to the scheduler (always solo-only — see placement);
174
193
  * it's here only so the gateway can see it's active. */
175
- kvScheme?: { kvBits?: number; kvConfig?: KvQuantSpec[]; turboQuant?: TurboQuantScheme };
194
+ kvScheme?: KvScheme;
176
195
  /** The server's prompt cache (Phase 3.2): batch-lane joiners take()
177
196
  * the longest usable prefix at admission; never-merged rows put()
178
197
  * back on finish. Safe to share with the serial lane — both use it
@@ -260,7 +279,7 @@ export class GenerationGateway {
260
279
  !a.maskArray && !a.layerTypes?.includes("sliding_attention");
261
280
  return this.#cacheBatchable;
262
281
  }
263
- const ssmOk = process.env.MLX_BUN_BATCH_SSM !== "0";
282
+ const ssmOk = runtimeValue("MLX_BUN_BATCH_SSM") !== "0";
264
283
  const proto = this.model.makeCache(); // fresh caches hold no buffers
265
284
  this.#cacheBatchable = proto.every(
266
285
  (c) =>
@@ -284,37 +303,36 @@ export class GenerationGateway {
284
303
  #kvBatchable(): boolean {
285
304
  if (this.#kvSchemeBatchable === null) {
286
305
  const scheme = this.opts.kvScheme;
287
- if (!scheme || (!scheme.kvBits && !scheme.kvConfig?.length)) {
288
- // Only consulted when shape.kvQuant is SET: a kv-quant request with
289
- // no scheme threaded to the gateway cannot be applied by the
290
- // scheduler — batching it would SILENTLY DROP the quantization (the
291
- // exact composition bug optiq serve ships). Route serial.
306
+ if (scheme?.kind !== "affine-config") {
292
307
  this.#kvSchemeBatchable = false;
293
- } else if (scheme.kvBits || !scheme.kvConfig?.length) {
294
- this.#kvSchemeBatchable = false; // uniform bits: serial
295
308
  } else {
296
309
  const proto = this.model.makeCache();
297
- this.#kvSchemeBatchable = scheme.kvConfig.every(
298
- (e) => proto[e.layerIdx] instanceof KVCache ||
299
- proto[e.layerIdx] instanceof RotatingKVCache,
300
- );
301
- for (const c of proto) c.dispose();
310
+ try {
311
+ this.#kvSchemeBatchable = scheme.batchable(
312
+ this.model.config,
313
+ (layerIdx) =>
314
+ isPlainKvCache(proto[layerIdx]) || isRotatingPlainCache(proto[layerIdx]),
315
+ );
316
+ } finally {
317
+ for (const cache of proto) cache.dispose();
318
+ }
302
319
  }
303
320
  }
304
321
  return this.#kvSchemeBatchable;
305
322
  }
306
323
 
307
- /** Decide whether a request joins the batch or runs serially. */
308
- willBatch(shape: RequestShape): boolean {
324
+ /** Whether the declared execution composition is implemented by the
325
+ * continuous scheduler. This is a support check only: it never rewrites
326
+ * MTP/drafting, KV, TurboQuant, grammar, adapters, or sampling. */
327
+ #supportsContinuous(shape: RequestShape): boolean {
309
328
  // DiffusionGemma is non-autoregressive — the batch scheduler assumes the AR
310
329
  // KV-cache decode path, so it always runs serially through generate().
311
330
  if (this.model instanceof DiffusionGemmaModel) return false;
312
331
  if (!this.batchingEnabled) return false;
313
332
  if (!this.#modelCachesBatchable()) return false;
314
333
  // repetitionPenalty / logits extras (min_p, XTC, logit_bias,
315
- // presence/frequency penalties) batch: the per-row sampler folds in the
316
- // same makeSampler/makeLogitsProcessors closures the serial lane runs,
317
- // over a per-row device-side token history. (Load-bearing beyond opt-in
334
+ // presence/frequency penalties) batch through the same StepSampler and
335
+ // per-row device history as the serial lane. (Load-bearing beyond opt-in
318
336
  // knobs: some models — Qwen3.5 — ship a default repetition penalty in
319
337
  // generation_config.json, which used to route EVERY request serial.)
320
338
  return (
@@ -336,20 +354,35 @@ export class GenerationGateway {
336
354
  // Grammar: B1 makes it batchable (per-row matchers) unless the kill
337
355
  // switch forces serial. MLX_BUN_GRAMMAR_BATCH=0 = B0 behavior (serial),
338
356
  // the A/B + kill lever for the new code, house style.
339
- !(shape.hasGrammar && process.env.MLX_BUN_GRAMMAR_BATCH === "0")
357
+ !(shape.hasGrammar && runtimeValue("MLX_BUN_GRAMMAR_BATCH") === "0")
340
358
  );
341
359
  }
342
360
 
361
+ place(shape: RequestShape): GenerationPlacement {
362
+ const frozenShape = Object.freeze(shape);
363
+ return Object.freeze({
364
+ shape: frozenShape,
365
+ mechanism: this.#supportsContinuous(frozenShape) ? "continuous" : "serial",
366
+ });
367
+ }
368
+
343
369
  /** Run `fn` with exclusive ownership of the GPU + shared model state (the
344
370
  * serial lane's lock). THE single mutual-exclusion domain: the serial
345
371
  * generation path, the curve /generate + /signal endpoints, and adapter
346
372
  * mount/unmount all come through here, so nothing runs concurrently with
347
373
  * batched decode steps (or with each other). Registers as a serial waiter
348
374
  * so the scheduler drains (stops admitting) until `fn` has run. */
349
- async runExclusive<T>(fn: () => Promise<T>): Promise<T> {
375
+ async runExclusive<T>(
376
+ fn: () => Promise<T>,
377
+ trace?: PromptResponseTrace,
378
+ ): Promise<T> {
350
379
  this.#serialWaiters++;
380
+ const closeAdmission = trace?.begin("engine.admission_wait", {
381
+ mechanism: "serial",
382
+ });
351
383
  try {
352
384
  const release = await this.#mutex.acquire();
385
+ closeAdmission?.();
353
386
  try {
354
387
  return await fn();
355
388
  } finally {
@@ -369,8 +402,14 @@ export class GenerationGateway {
369
402
  onToken: OnToken,
370
403
  vision: Vision | undefined,
371
404
  shape: RequestShape,
405
+ placement: GenerationPlacement,
372
406
  signal?: AbortSignal,
407
+ trace?: PromptResponseTrace,
373
408
  ): Promise<GenerateStats> {
409
+ if (placement.shape !== shape) {
410
+ options.grammar?.dispose();
411
+ throw new Error("generation placement does not belong to this request shape");
412
+ }
374
413
  try {
375
414
  signal?.throwIfAborted();
376
415
  } catch (e) {
@@ -379,7 +418,7 @@ export class GenerationGateway {
379
418
  options.grammar?.dispose();
380
419
  throw e;
381
420
  }
382
- if (!this.willBatch(shape)) {
421
+ if (placement.mechanism === "serial") {
383
422
  // Serial decode is an unbroken microtask chain (FFI + generator
384
423
  // resumes): without a periodic macrotask hop the event loop starves for
385
424
  // the WHOLE generation — /stats, /health, and new-connection accepts
@@ -404,75 +443,43 @@ export class GenerationGateway {
404
443
  };
405
444
  return this.runExclusive(async () => {
406
445
  signal?.throwIfAborted();
407
- const stats = await this.serialRun(promptIds, options, hoppingOnToken, vision);
446
+ const stats = await this.serialRun(
447
+ promptIds, options, hoppingOnToken, vision, trace,
448
+ );
408
449
  signal?.throwIfAborted();
409
450
  return stats;
410
- })
451
+ }, trace)
411
452
  // Covers a serial waiter aborted before serialRun takes ownership.
412
453
  // generate() also disposes defensively; the operation is idempotent.
413
454
  .finally(() => options.grammar?.dispose());
414
455
  }
415
456
 
416
- // Per-row sampler, mirroring generate()'s sampleStep: logits processors
417
- // (logit_bias / repetition / presence / frequency penalties — the same
418
- // device-side closures the serial lane runs) fold in over a per-row token
419
- // history seeded with the prompt, then the sampler. Greedy (temperature 0)
420
- // is argmax; temp>0 uses this request's own seed → independent per-row RNG.
421
- const sampler = makeSampler(options);
422
- const processors = makeLogitsProcessors(options);
423
- // Device-side history (prompt + generated), maintained only when a
424
- // processor needs it — generate()'s pushHistory, one per row. The sample
425
- // closure is called once per sampled token in order, so extending it
426
- // inside the closure keeps history exact.
427
- let history: MlxArray | null = null;
428
- if (processors.length > 0) history = ops.fromInt32(promptIds, [promptIds.length]);
429
- const sample = (logits1V: MlxArray, step: number): MlxArray => {
430
- let cur = logits1V; // caller-owned; only intermediates are disposed here
431
- for (const p of processors) {
432
- const next = p(history, cur);
433
- if (cur !== logits1V) cur.dispose();
434
- cur = next;
435
- }
436
- // Grammar mask (B1): after the standard processors, before the sampler —
437
- // same "grammar has the final say" ordering as serial sampleStep.
438
- // Precondition: the scheduler has awaited ready() before invoking this
439
- // closure (the mask for this step is materialized). applyMask is sync.
440
- if (options.grammar) {
441
- const masked = options.grammar.applyMask(cur);
442
- if (cur !== logits1V) cur.dispose();
443
- cur = masked;
444
- }
445
- const lp = toLogprobs(cur);
446
- if (cur !== logits1V) cur.dispose();
447
- const tok = sampler(lp, step);
448
- lp.dispose();
449
- if (history) {
450
- const t1 = ops.reshape(tok, [1]);
451
- const prev = history;
452
- history = ops.concatAxis([prev, t1], 0);
453
- prev.dispose();
454
- t1.dispose();
455
- }
456
- return tok;
457
- };
457
+ // The scheduler owns grammar ready/accept sequencing. StepSampler owns the
458
+ // shared processors -> mask -> logprobs -> sample -> history contract.
459
+ const stepSampler = makeStepSampler(options, {
460
+ tokenRepresentation: "device",
461
+ grammarWait: "external",
462
+ historyUpdate: "after-sample",
463
+ initialHistory: promptIds,
464
+ });
465
+ const sample = (logits1V: MlxArray, step: number): MlxArray =>
466
+ stepSampler.sample(logits1V, step).token;
458
467
 
459
468
  let st;
460
469
  this.#rowsSubmitted++;
470
+ const closeAdmission = trace?.begin("engine.admission_wait", {
471
+ mechanism: "continuous",
472
+ });
461
473
  try {
462
474
  st = await this.#ensureScheduler().submit({
463
475
  promptIds,
464
476
  maxTokens: options.maxTokens ?? 512,
465
477
  eosTokenIds: options.eosTokenIds ?? this.model.config.eosTokenIds,
466
478
  sample,
467
- // Vectorized-sampling eligibility: mirrors makeSampler's greedy branch
468
- // (temperature 0, no curve override) with nothing per-row in the way
469
- // (no processors, no grammar mask).
470
- plainGreedy:
471
- (options.temperature ?? 0) === 0 &&
472
- !options.curve &&
473
- processors.length === 0 &&
474
- !options.grammar,
479
+ plainGreedy: stepSampler.isPlainGreedy,
475
480
  onToken,
481
+ onAdmitted: closeAdmission,
482
+ trace,
476
483
  ...(signal ? { signal } : {}),
477
484
  // B1: pass the per-row grammar controller through. The scheduler drives
478
485
  // accept/ready/terminate; this gateway OWNS disposal (finally below)
@@ -483,8 +490,8 @@ export class GenerationGateway {
483
490
  ...(options.snapshotAt !== undefined ? { snapshotAt: options.snapshotAt } : {}),
484
491
  });
485
492
  } finally {
486
- history?.dispose();
487
- history = null;
493
+ closeAdmission?.();
494
+ stepSampler.dispose();
488
495
  // The scheduler never owns per-row grammar state.
489
496
  options.grammar?.dispose();
490
497
  }
@@ -507,7 +514,7 @@ export class GenerationGateway {
507
514
  // scheduler (solo rows convert at serial chunk boundaries, then
508
515
  // merge as quantized triples). Only threaded when the scheme
509
516
  // passed #kvBatchable — otherwise those requests never reach here.
510
- kvConfig: this.#kvBatchable() ? this.opts.kvScheme?.kvConfig : undefined,
517
+ kvScheme: this.#kvBatchable() ? this.opts.kvScheme : undefined,
511
518
  promptCache: this.opts.promptCache,
512
519
  lock: { acquire: () => this.#mutex.acquire() },
513
520
  // Drain: pause admission while any serial-lane request waits; the
@@ -0,0 +1,14 @@
1
+ import type { ModelConfig } from "../config";
2
+ import { kvBytesAt, KvScheme } from "../kv-scheme";
3
+
4
+ /** Project one batch row's worst-case KV bytes using the same per-layer
5
+ * scheme that the server-wide admission ceiling uses. */
6
+ export function batchRowKvBytes(
7
+ config: ModelConfig,
8
+ promptTokens: number,
9
+ maxTokens: number,
10
+ scheme?: KvScheme,
11
+ ): number {
12
+ const tokens = promptTokens + maxTokens;
13
+ return scheme ? scheme.bytesAt(config, tokens) : kvBytesAt(config, tokens);
14
+ }