mlx-bun 0.0.10 → 0.0.12

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 (216) hide show
  1. package/README.md +107 -32
  2. package/THIRD_PARTY_LICENSES.md +10 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/colibri-glm52-port.md +1479 -0
  10. package/docs/design/compat-cli-surface-design.md +4 -3
  11. package/docs/design/decode-speed-program.md +172 -0
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +371 -0
  14. package/docs/design/dspark-speculative-decoding.md +167 -5
  15. package/docs/design/faithful-l1-consolidation.md +310 -0
  16. package/docs/design/generic-model-support.md +8 -5
  17. package/docs/design/grammar-spec-batching-integration.md +301 -0
  18. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  19. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  20. package/docs/design/omlx-adoption-map.md +14 -6
  21. package/docs/design/orpo-training.md +34 -30
  22. package/docs/design/paged-kv-cache.md +214 -0
  23. package/docs/design/parallel-slots.md +82 -49
  24. package/docs/design/parity-tier-dag.md +43 -33
  25. package/docs/design/pre-colibri-stabilization.md +329 -0
  26. package/docs/design/repo-cleanup-plan.md +189 -0
  27. package/docs/design/runtime-isolation.md +19 -3
  28. package/docs/design/segmented-backward-training.md +3 -2
  29. package/docs/design/ssd-kv-cold-tier.md +130 -0
  30. package/docs/design/structured-output.md +484 -0
  31. package/docs/design/the-dreaming-master-plan.md +2 -3
  32. package/docs/design/turboquant-kv.md +235 -0
  33. package/docs/design/unified-engine-frontier-plan.md +774 -0
  34. package/docs/design/web-chat-beat-matrix.md +315 -0
  35. package/docs/design/web-chat-redesign.md +1112 -0
  36. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  37. package/docs/investigations/dspark-handoff.md +182 -16
  38. package/docs/investigations/indexshare-performance-spike.md +357 -0
  39. package/docs/investigations/lab-build-journal.md +2 -2
  40. package/docs/investigations/openwiki-evaluation.md +249 -0
  41. package/docs/planning/IDEAS.md +14 -0
  42. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  43. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  44. package/docs/planning/chat-ui-vision.md +50 -0
  45. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  46. package/docs/planning/release-notes-v0.0.11.md +239 -0
  47. package/docs/planning/release-notes-v0.0.12.md +80 -0
  48. package/docs/reference/cli.md +63 -18
  49. package/docs/reference/distribution.md +37 -6
  50. package/docs/reference/embedding.md +40 -16
  51. package/docs/reference/features-matrix.md +99 -0
  52. package/docs/reference/library-api.md +69 -20
  53. package/docs/reference/memory.md +26 -6
  54. package/docs/reference/models.md +60 -3
  55. package/docs/reference/orpo-quickstart.md +3 -4
  56. package/docs/reference/server-api.md +717 -24
  57. package/docs/reference/server-config.md +265 -129
  58. package/docs/reference/training.md +24 -16
  59. package/package.json +5 -3
  60. package/src/anthropic.ts +67 -44
  61. package/src/audio/conformer.ts +647 -0
  62. package/src/audio/decode.ts +133 -0
  63. package/src/audio/features.ts +202 -0
  64. package/src/audio/transcode.ts +67 -0
  65. package/src/chat-template.ts +155 -10
  66. package/src/cli.ts +483 -113
  67. package/src/config.ts +116 -24
  68. package/src/diffusion/diffusion-generate.ts +14 -8
  69. package/src/download.ts +143 -12
  70. package/src/eval/README.md +7 -6
  71. package/src/eval/ifeval.ts +63 -120
  72. package/src/eval/kl.ts +59 -3
  73. package/src/eval/quality-db.ts +2 -2
  74. package/src/eval/runner.ts +17 -8
  75. package/src/eval/tasks/ifeval.ts +167 -31
  76. package/src/expert-io.ts +391 -0
  77. package/src/expert-offload.ts +15 -2
  78. package/src/expert-residency.ts +1134 -0
  79. package/src/expert-usage.ts +415 -0
  80. package/src/flags.ts +13 -0
  81. package/src/generate.ts +438 -35
  82. package/src/grammar.ts +585 -0
  83. package/src/hf-push.ts +1 -1
  84. package/src/hub-rest.ts +242 -0
  85. package/src/index.ts +14 -2
  86. package/src/jobs/runner.ts +37 -18
  87. package/src/kv-store.ts +586 -93
  88. package/src/lora.ts +85 -14
  89. package/src/md-modules.d.ts +42 -0
  90. package/src/media-fetch.ts +426 -0
  91. package/src/memory/rest.ts +245 -0
  92. package/src/memory/vault.ts +74 -0
  93. package/src/mlx/array.ts +149 -19
  94. package/src/mlx/autograd.ts +53 -16
  95. package/src/mlx/checkpoint.ts +46 -13
  96. package/src/mlx/custom-vjp.ts +83 -35
  97. package/src/mlx/ffi.ts +27 -3
  98. package/src/mlx/metal-kernel.ts +7 -4
  99. package/src/mlx/ops.ts +153 -23
  100. package/src/mlx/turboquant-ops.ts +831 -0
  101. package/src/mlx/turboquant-tables.ts +237 -0
  102. package/src/model/batched-mask.ts +62 -1
  103. package/src/model/batched-quant.ts +206 -0
  104. package/src/model/batched-rotating-quant.ts +346 -0
  105. package/src/model/batched-rotating.ts +46 -1
  106. package/src/model/compiled-decode.ts +4 -15
  107. package/src/model/factory.ts +103 -6
  108. package/src/model/gemma4-base.ts +395 -39
  109. package/src/model/gemma4.ts +122 -33
  110. package/src/model/generated/gemma4-12b.ts +9 -13
  111. package/src/model/generated/gemma4-26b.ts +10 -5
  112. package/src/model/generated/gemma4-e4b.ts +14 -9
  113. package/src/model/glm52-atlas.ts +511 -0
  114. package/src/model/glm52-cache.ts +706 -0
  115. package/src/model/glm52-config.ts +262 -0
  116. package/src/model/glm52-container.ts +305 -0
  117. package/src/model/glm52-coupling.ts +388 -0
  118. package/src/model/glm52-dsa.ts +602 -0
  119. package/src/model/glm52-expert-layout.ts +253 -0
  120. package/src/model/glm52-layout.ts +152 -0
  121. package/src/model/glm52-memory.ts +473 -0
  122. package/src/model/glm52-mla.ts +786 -0
  123. package/src/model/glm52-moe.ts +223 -0
  124. package/src/model/glm52-pilot.ts +537 -0
  125. package/src/model/glm52-reference.ts +445 -0
  126. package/src/model/glm52-residency.ts +557 -0
  127. package/src/model/glm52-resident-weights.ts +135 -0
  128. package/src/model/glm52-streamed-experts.ts +453 -0
  129. package/src/model/glm52-streamed-metal.ts +610 -0
  130. package/src/model/glm52-weights.ts +503 -0
  131. package/src/model/glm52.ts +1153 -0
  132. package/src/model/minicpm5.ts +58 -102
  133. package/src/model/paged-kv.ts +253 -0
  134. package/src/model/qwen3-delta.ts +76 -15
  135. package/src/model/qwen3-moe.ts +404 -0
  136. package/src/model/qwen3.ts +9 -8
  137. package/src/model/qwen3_5.ts +88 -23
  138. package/src/model/support.ts +19 -1
  139. package/src/model/universal/dense.ts +40 -5
  140. package/src/model/universal/rope.ts +36 -21
  141. package/src/native/expert_io.c +523 -0
  142. package/src/native-pack.ts +6 -4
  143. package/src/pi-web.ts +1299 -82
  144. package/src/preflight.ts +27 -8
  145. package/src/prompt-cache.ts +293 -33
  146. package/src/quantize/quantizer.ts +16 -1
  147. package/src/registry.ts +57 -2
  148. package/src/responses.ts +62 -49
  149. package/src/serve/batch-scheduler.ts +1050 -78
  150. package/src/serve/generation-gateway.ts +252 -24
  151. package/src/serve/isolate.ts +417 -0
  152. package/src/serve/lane-registry.ts +44 -0
  153. package/src/serve/session-search.ts +326 -0
  154. package/src/server.ts +1701 -175
  155. package/src/spec/assistant-source.ts +150 -0
  156. package/src/spec/deepspec-source.ts +143 -0
  157. package/src/spec/dflash-source.ts +124 -0
  158. package/src/spec/dspark/ab-stats.ts +136 -0
  159. package/src/spec/dspark/calibration.ts +112 -0
  160. package/src/spec/dspark/deepspec-module.ts +801 -0
  161. package/src/spec/dspark/generate-dflash.ts +72 -20
  162. package/src/spec/dspark/loader.ts +23 -0
  163. package/src/spec/dspark/module-dflash.ts +278 -36
  164. package/src/spec/dspark/quantize-drafter.ts +91 -0
  165. package/src/spec/generate.ts +59 -25
  166. package/src/spec/glm52-mtp-source.ts +392 -0
  167. package/src/spec/ngram-source.ts +132 -0
  168. package/src/spec/serve-loop.ts +617 -0
  169. package/src/spec/source.ts +118 -0
  170. package/src/spec/two-model.ts +171 -0
  171. package/src/ssd-cache.ts +111 -35
  172. package/src/tokenizer.ts +288 -2
  173. package/src/tool-approvals.ts +131 -0
  174. package/src/tool-call.ts +438 -22
  175. package/src/train/flash-cce.ts +2 -2
  176. package/src/train/job.ts +0 -12
  177. package/src/train/lora-params.ts +17 -6
  178. package/src/train/trainer.ts +60 -30
  179. package/src/vision/prompt.ts +217 -48
  180. package/src/web/app.html +1197 -1791
  181. package/src/web/app.js +5694 -0
  182. package/src/web/icon.svg +13 -0
  183. package/src/web/manifest.webmanifest +24 -0
  184. package/src/web/src/adapters-panel.ts +288 -0
  185. package/src/web/src/api.ts +57 -0
  186. package/src/web/src/assistant.ts +485 -0
  187. package/src/web/src/chat.ts +1050 -0
  188. package/src/web/src/composer.ts +959 -0
  189. package/src/web/src/dataset.ts +132 -0
  190. package/src/web/src/finetune.ts +267 -0
  191. package/src/web/src/hub.ts +428 -0
  192. package/src/web/src/main.ts +98 -0
  193. package/src/web/src/markdown.ts +0 -0
  194. package/src/web/src/memory-panel.ts +572 -0
  195. package/src/web/src/model-picker.ts +159 -0
  196. package/src/web/src/palette.ts +346 -0
  197. package/src/web/src/protocol.ts +62 -0
  198. package/src/web/src/quantize.ts +167 -0
  199. package/src/web/src/rag.ts +274 -0
  200. package/src/web/src/sessions.ts +469 -0
  201. package/src/web/src/shell.ts +804 -0
  202. package/src/web/src/status.ts +300 -0
  203. package/src/web/src/ui-catalog.ts +92 -0
  204. package/src/web/sw.js +71 -0
  205. package/src/web/vendor/README.md +61 -0
  206. package/src/web/vendor/hljs-LICENSE +29 -0
  207. package/src/web/vendor/hljs-theme.css +59 -0
  208. package/src/web/vendor/hljs.js +26 -0
  209. package/src/web-tools.ts +54 -2
  210. package/src/weights.ts +82 -19
  211. package/src/model/fused-decode-kernel.ts +0 -312
  212. package/src/model/fused-geglu-kernel.ts +0 -198
  213. package/src/model/fused-mlp-kernel.ts +0 -169
  214. package/src/model/fused-swiglu-kernel.ts +0 -240
  215. package/src/model/steel-linear-kernel.ts +0 -96
  216. package/src/status-page.html +0 -497
@@ -0,0 +1,356 @@
1
+ # Audio input — design & implementation plan (gemma-4 audio tower)
2
+
3
+ Status: **A0–A4 DONE** (2026-07-07: served end to end on e4b, oracle-EXACT
4
+ greedy offline AND over HTTP; A5 bench + 12B coverage open).
5
+ Scope: audio **understanding** — audio-in, text-out through the chat API.
6
+ TTS / STT-as-an-endpoint / STS stay out of scope (see
7
+ docs/design/omlx-adoption-map.md); §5 lists non-goals explicitly.
8
+
9
+ ## 0. TL;DR
10
+
11
+ e4b ships a 12-block Conformer audio tower that we already have on disk —
12
+ the `optiq_vision.safetensors` sidecar in the local e4b OptiQ-4bit snapshot
13
+ contains 752 `audio_tower.*`/`embed_audio.*` tensors, and its `config.json`
14
+ carries the full `audio_config` plus the audio token ids. mlx-lm strips all
15
+ of this and rejects audio requests, so (as with vision) **the oracle is
16
+ optiq's internal machinery**, which is complete end-to-end but not exposed
17
+ by optiq's own serve frontend. The implementation is a fourth tower next to
18
+ `src/vision/{siglip,embedder,diffusion-vision}.ts`, reusing every existing
19
+ multimodal seam: sidecar lazy-load, `<|placeholder|>` splice, merged
20
+ embeddings into `forwardEmbeddings`, serial-lane routing. New primitives
21
+ needed: a `mlx_conv2d` binding (SSCP subsampler) and a small TS-side
22
+ mel-spectrogram extractor (USM pipeline). Everything else is pattern-match
23
+ on the vision port.
24
+
25
+ ## 1. What the ancestors do (surveyed 2026-07-06, pinned oracle venv)
26
+
27
+ ### mlx-lm 0.31.3 — nothing, and actively strips
28
+
29
+ - `models/gemma3n.py:602` and `models/gemma4.py:55` `sanitize()` pop
30
+ `audio_tower`, `embed_audio` (and the vision keys) from the weights at
31
+ load. The upstream capability is deliberately discarded.
32
+ - `server.py:118–150` `process_message_content()` raises
33
+ `ValueError("Only 'text' content type is supported.")` for any non-text
34
+ content part — `input_audio` requests hard-fail.
35
+ - No mel/spectrogram/feature code anywhere in the package; no mlx-vlm /
36
+ mlx-audio / mlx-whisper installed in the oracle venv.
37
+
38
+ **Parity-tree consequence:** there is no mlx-lm arm for audio. Like the
39
+ vision port, correctness gates come from optiq's internals; anything no
40
+ ancestor does (e.g. ever batching audio requests) is novel territory →
41
+ KL + quality gates, not bit-parity (see three-tier doctrine in PLAN.md).
42
+
43
+ ### mlx-optiq — complete machinery, unexposed at the serve layer
44
+
45
+ All under `optiq/vlm/_mlxvlm/models/gemma4/` in the oracle venv:
46
+
47
+ - **Feature extraction** (`audio_feature_extractor.py`,
48
+ `Gemma4AudioFeatureExtractor`) — USM pipeline: 16 kHz mono input,
49
+ frame 20 ms / hop 10 ms, magnitude STFT, 128 HTK-scale mel bins over
50
+ 0–8 kHz, `log(mel + 1e-3)` floor, optional per-bin mean/stddev
51
+ normalization; `preemphasis=0.0`, `fft_overdrive=False`, `dither=0.0`
52
+ by default. Runs CPU-side (numpy). Output
53
+ `input_features [B,T,128]` + validity mask.
54
+ - **Tower** (`audio.py`, `AudioEncoder`) —
55
+ `SubSampleConvProjection`: two Conv2d stages (channels `(128, 32)`,
56
+ each 2× time stride) then linear to `hidden_size=1024`; then
57
+ **12 Conformer blocks**: FF (residual_weight 0.5) → chunked local
58
+ attention (chunk 12, left context 13 chunks, right 0, logit softcap
59
+ 50.0, invalid logits −1e9) → depthwise light Conv1d (kernel 5) → FF →
60
+ clamp + RMSNorm; then output projection to `output_proj_dims=1536`.
61
+ `use_clipped_linears=True`: linears clamp activations to recorded
62
+ per-tensor `input_min/max`/`output_min/max` ranges (those stats ship in
63
+ the sidecar — confirmed present in our local e4b sidecar).
64
+ - **Embedder** (`merge.py`, `MultimodalEmbedder`) — RMSNormNoScale +
65
+ bias-free Linear into text hidden size; embeddings scattered into the
66
+ input sequence at `audio_token_id` positions via `masked_scatter`
67
+ (`gemma4.py get_input_embeddings`).
68
+ - **Tokens & splicing** (`processing_gemma4.py:264–299`) —
69
+ `<|audio|>` expands to `boa + audio_token×n + eoa` with
70
+ `n = min(ceil(duration_ms / 40), 750)` (40 ms/token, ≤30 s ≙ 750 soft
71
+ tokens). Ids (confirmed in our local e4b config.json):
72
+ `boa=256000`, `audio=258881`, `eoa=258883`.
73
+ - **Request parsing** (`processing_gemma4.py:690`) — accepts OpenAI-style
74
+ parts `{"audio", "input_audio", "audio_url"}`.
75
+ - **Sidecar & quant policy** — `vlm/sidecar.py` extracts
76
+ `audio_tower.*`/`embed_audio.*` into the same `optiq_vision.safetensors`
77
+ as vision, kept **bf16**; `backends/mlx_backend.py:491` skips
78
+ `audio_tower.` in the quantization predicate. Calibration recipe (and a
79
+ worked example of driving the audio path programmatically):
80
+ `calibration/datasets.py load_audio_calibration` (LibriSpeech clean).
81
+ - **The gap:** `vlm/gemma4/frontend.py` only wires **vision** into optiq
82
+ serve; audio works only through the internal model class. Golden
83
+ generation therefore scripts the internal model directly (the
84
+ calibration loader shows exactly how) — same move as the vision port.
85
+
86
+ ## 2. What we already have (verified on this machine, 2026-07-06)
87
+
88
+ - **e4b OptiQ-4bit** (snapshot `fcdb12…`): sidecar contains **752 audio
89
+ tensors** (`audio_tower.*` + `embed_audio.*`, incl. clipped-linear
90
+ min/max stats); `config.json` has `audio_config`
91
+ (`model_type=gemma4_audio`, 12 layers, hidden 1024, heads 8, conv
92
+ kernel 5, subsampling channels (128,32), logit cap 50, residual 0.5,
93
+ `output_proj_dims=1536`, `use_clipped_linears=true`) plus
94
+ `audio_token_id/boa_token_id/eoa_token_id`; `tokenizer_config.json` has
95
+ `audio_token: "<|audio|>"`. **No download needed — audio-ready today.**
96
+ - **12B OptiQ-4bit**: `audio_config` present but the local sidecar holds
97
+ only **1** audio tensor → needs a sidecar rebuild via optiq's
98
+ `build_vision_sidecar` (its selective-download path pulls only the
99
+ audio shards, not the full base model).
100
+ - **26B-A4B, DiffusionGemma, bf16 assistants**: no `audio_config` — no
101
+ audio for these, ever (architecture, not a porting gap).
102
+ - **Vision pipeline is the template** — every seam already exists:
103
+ content-part detection `src/server.ts:2234`, extraction + splice + merge
104
+ `src/vision/prompt.ts`, lazy tower `src/server.ts:326–350`, sidecar
105
+ detection `src/registry.ts:487` / `src/config.ts:287`, embeddings
106
+ prefill `src/generate.ts:468` → `forwardEmbeddings`
107
+ `src/model/gemma4.ts:876–896` (per-layer-input id zeroing + bidir mask).
108
+ - **FFI gaps**: `mlx_conv1d` is bound (`src/mlx/ffi.ts:173`, Qwen3.5
109
+ DeltaNet, depthwise) but there is **no conv2d and no FFT** binding.
110
+ SSCP needs conv2d; mel extraction needs an FFT (resolved TS-side, §3.1).
111
+
112
+ ## 3. Design
113
+
114
+ ### 3.1 New files (mirror of `src/vision/`)
115
+
116
+ - **`src/audio/decode.ts`** — WAV parser (PCM16/24/32/float32) → mono
117
+ mix-down → resample to 16 kHz (windowed-sinc or linear; gate vs oracle
118
+ resampler on the fixture). Non-WAV formats (mp3/m4a/flac/ogg): shell out
119
+ to macOS `afconvert` (built into darwin, our only platform) to produce
120
+ WAV — zero new dependencies. **Duration (and thus soft-token count) is
121
+ computed from decoded samples**, so format never affects splicing.
122
+ - **`src/audio/features.ts`** — USM mel extractor, verbatim-semantics port
123
+ of `audio_feature_extractor.py` (megakernel copy-verbatim methodology:
124
+ match first, optimize never — this is ~100×128 floats, not a perf
125
+ path). STFT frame 320 samples / hop 160 / FFT 512
126
+ (`fft_overdrive=False`), 128 HTK mel bins 0–8 kHz, `log(x + 1e-3)`,
127
+ per-bin normalization from shipped stats (open Q3). Implemented as a
128
+ small radix-2 real FFT in TS, f64 accumulate → f32 out, CPU-side —
129
+ exactly where optiq does it (numpy), keeping the mlx graph clean.
130
+ *Considered and rejected for v1*: binding `mlx_fft_rfft` and doing it
131
+ on-device — new binding surface for no perf need.
132
+ - **`src/audio/conformer.ts`** — the tower, bf16 mlx graph:
133
+ - **needs `mlx_conv2d` binding** — read the full mlx.h signature first
134
+ (trailing-optional-param → shifted stream arg hazard, CLAUDE.md).
135
+ - SSCP: Conv2d(1→128) → Conv2d(128→32), each 2× time stride → linear
136
+ to 1024 (exact shapes read from the sidecar weights).
137
+ - 12 × ConformerBlock as in §1; chunked local attention implemented as
138
+ an explicit dense additive mask (≤750 frames pre-subsample, ~188
139
+ post — dense is trivially cheap at this size) with logit softcap 50
140
+ and −1e9 invalid fill.
141
+ - Clipped linears: clamp to shipped `input_min/max`/`output_min/max` —
142
+ parity depends on this, port exactly (toggle test in A2).
143
+ - Output proj → 1536, then `embed_audio` (RMSNormNoScale + Linear →
144
+ text hidden), **pre-divided by embed_scale** — same convention as
145
+ vision towers (`forwardEmbeddings` re-multiplies).
146
+ - **`src/audio/prompt.ts` folded into a shared multimodal prompt
147
+ builder** — recommendation: generalize `src/vision/prompt.ts`
148
+ (`buildVisionPrompt` → `buildMultimodalPrompt`) rather than add a
149
+ parallel audio-only path, because one message can legally carry images
150
+ AND audio and both must splice in document order. Vision-only and
151
+ audio-only fall out as special cases; the vision goldens gate the
152
+ refactor (spliced ids must stay byte-identical).
153
+
154
+ ### 3.2 Server surface
155
+
156
+ - Accept OpenAI content parts:
157
+ `{"type":"input_audio","input_audio":{"data":"<b64>","format":"wav"|"mp3"}}`
158
+ (OpenAI canonical) plus optiq's `{"type":"audio"}` / `{"type":"audio_url"}`
159
+ aliases. `hasAudio` detection lands next to `hasImages`
160
+ (`src/server.ts:2234`).
161
+ - `getAudioTower(ctx)` lazy-loads next to `getVisionTower`
162
+ (`src/server.ts:326–350`) — same sidecar file, separate tower object;
163
+ text-only sessions never pay for it. Auto-enable on
164
+ `audio_config` + sidecar audio tensors present, mirroring vision —
165
+ **no new flags** (target surface: zero CLI delta).
166
+ - Routing: audio requests take the **serial lane**, same rule as vision —
167
+ embeddings-prefill doesn't batch, and `promptEmbeddings` already
168
+ excludes the pre-warmed prompt cache (`src/generate.ts:468`). Batching
169
+ is a mode, not a fallback; batched audio has no oracle → if we ever
170
+ want it, it goes through KL + quality gates as new territory.
171
+ - Anthropic-protocol endpoint: the Anthropic messages API has no audio
172
+ input block type → clean 400 with a pointer to the OpenAI endpoint;
173
+ documented.
174
+ - **Docs land in the SAME commit** (standing rule):
175
+ `docs/reference/server-api.md` (content-part schema, ≤30 s / 750
176
+ soft-token cap, 16 kHz mono internal format),
177
+ `docs/reference/features-matrix.md` (audio row incl. serial-lane
178
+ note), README capability line, `cli.md` only if a flag appears
179
+ (target: none).
180
+
181
+ ### 3.3 Open questions — RESOLVED against the oracle sources (2026-07-07)
182
+
183
+ 1. **LM attention semantics for audio tokens: strictly CAUSAL.**
184
+ `language.py:484–545 _make_masks`: the blockwise bidirectional overlay
185
+ keys off `is_vision = (mm_token_type_ids == 1) | (== 2)` only — audio
186
+ (type 3) never enters a block. Stronger: `use_bidirectional_vision`
187
+ requires `not has_audio_tokens`, so **any audio token in the prompt
188
+ disables the vision overlay entirely** (oracle comment: "Audio spans
189
+ are sequential; keep mixed image+audio prompts causal"). mlx-bun rule:
190
+ audio runs get NO bidir mask, and a mixed image+audio request drops
191
+ the image bidir mask too — pass no bidir mask at all in that case.
192
+ 2. **Per-layer-input zeroing: yes, audio included.** `gemma4.py:92–99`
193
+ zeroes per-layer ids for the union mask
194
+ `(ids==image_token) | (ids==audio_token) | (ids==video_token)`.
195
+ Extend the existing image-mask zeroing in
196
+ `src/model/gemma4.ts:876–896` to the combined multimodal mask.
197
+ 3. **Per-bin mel stats: constructor defaults, no normalization.**
198
+ `processing_gemma4.py:919–934`: standard HF checkpoints ship no
199
+ `feature_extractor` key (our e4b snapshot has no
200
+ processor_config.json at all) → `Gemma4AudioFeatureExtractor()` with
201
+ defaults; "the USM parameters are fixed for all Gemma 4 models".
202
+ `per_bin_mean/stddev = None` → that branch is dead for us. Also
203
+ pinned there: `audio_seq_length` default 750, `audio_ms_per_token`
204
+ default 40.
205
+ 4. **boa/eoa are splice-side.** e4b `chat_template.jinja:301,334` emits
206
+ bare `<|audio|>` for audio parts (both content shapes); the
207
+ boa + audio×n + eoa expansion is processor-side — same ownership as
208
+ vision's boi/eoi. Splice in our prompt builder, not the template.
209
+
210
+ ### 3.4 Parity strategy (tree doctrine)
211
+
212
+ Oracle = optiq internal gemma4 model in the pinned venv, driven by
213
+ `scripts/regen-audio-goldens.py` (modeled on their calibration loader;
214
+ remember `enable_thinking` pinning for any templated prompt). Tiers follow
215
+ the e4b vision precedent exactly:
216
+
217
+ - **T0 model-free** (CI, no weights): WAV decode + mel features vs
218
+ oracle-dumped golden (f32 tolerance ~1e-5 — DSP is deterministic);
219
+ spliced ids **exact**; soft-token count **exact**.
220
+ - **T1 golden tower**: `audio_tower`+`embed_audio` output vs oracle,
221
+ rel-RMSE gate (expect vision-like ~1% from bf16 composition; calibrate
222
+ the threshold from the first honest run, don't invent it).
223
+ - **T2 e2e greedy** (`tests/e4b-audio.test.ts`): first-N greedy prefix
224
+ match vs oracle + grounded-output check (speech fixture → transcript
225
+ keyword present; chirp fixture → tone/beep mentioned).
226
+ - **T3 serve**: curl e2e with base64 `input_audio` against a running
227
+ server (manual/gated; Josh runs servers).
228
+
229
+ Fixtures (tracked in `fixtures/`, tiny): (a) 1.6 s synthesized chirp WAV
230
+ (~50 KB, generated by a checked-in deterministic script), (b) ~2 s public
231
+ domain speech clip (LibriSpeech clean sample, ~64 KB). Goldens:
232
+ `goldens/e4b-audio.json` manifest tracked, `.bin` blobs regenerable and
233
+ untracked (goldens/README.md convention).
234
+
235
+ ### 3.5 Perf & memory
236
+
237
+ Expectation (to be *measured*, not asserted): the tower is tiny —
238
+ ≤750 mel frames → ~≤188 post-subsample positions × 12 layers × hidden
239
+ 1024 bf16 — so added TTFT should be dominated by CPU feature extraction
240
+ plus one extra graph eval; sidecar RSS delta is the 752 bf16 tensors.
241
+ Every number that gets quoted comes from preflight-gated `benchmark.sh`
242
+ cells (tower ms, TTFT delta vs text-only, peak RSS delta) and lands in
243
+ `benchmarks/RESULTS.md`.
244
+
245
+ ## 4. Work plan
246
+
247
+ **Phase A0 — groundwork**
248
+ - [ ] Bind `mlx_conv2d` (full header signature; CPU/GPU sanity test vs
249
+ oracle conv output)
250
+ - [ ] `scripts/regen-audio-goldens.py` + fixtures; dump mel golden,
251
+ tower-output golden, spliced ids, greedy ids
252
+ - [ ] Resolve open questions §3.3 (write the answers into this doc)
253
+ - Exit: goldens on disk; masking/zeroing semantics documented.
254
+
255
+ **Phase A1 — decode + features**
256
+ - [ ] `src/audio/decode.ts` (WAV + mono + resample; `afconvert` fallback)
257
+ - [ ] `src/audio/features.ts` USM mel port
258
+ - Exit: T0 green (mel within tolerance; token counts + spliced ids exact).
259
+
260
+ **Phase A2 — tower**
261
+ - [x] `src/audio/conformer.ts` loads `audio_tower.*`/`embed_audio.*` from
262
+ `optiq_vision.safetensors`; forward matches golden — rel-RMSE **2.4e-8**
263
+ (bit-exact: the tower runs f32 activations over bf16 weights because
264
+ the oracle feeds f32 mel and mlx promotes; the residual is 1 f32 ulp
265
+ from the test's /embed_scale·embed_scale roundtrip). Sidecar conv
266
+ weights are ALREADY MLX layout [C_out,kH,kW,C_in] — no transpose.
267
+ Golden reference point: embed_audio output BEFORE the /embed_scale
268
+ pre-division; features() returns pre-divided (vision convention).
269
+ - [x] Clipped-linear toggle test (off → 90% rel-RMSE divergence, on → gate;
270
+ in tests/e4b-audio-tower.test.ts)
271
+ - Exit: T1 rel-RMSE gate green. ✅ (gate 1e-6, ~40× margin)
272
+
273
+ **Phase A3 — prompt + LM integration**
274
+ - [x] Generalize prompt builder (images + audio, document order):
275
+ `buildMultimodalPrompt` in src/vision/prompt.ts; `<|audio|>` →
276
+ `boa + audio×n + eoa`; `buildVisionPrompt` kept as a byte-identical
277
+ wrapper (vision goldens green, e4b-vision 3/3).
278
+ - [x] Merged embeddings + masks; per-layer id zeroing per Q2 decoupled from
279
+ the bidir overlay (`forwardEmbeddings` gained an optional `multimodal`
280
+ zeroing mask; generate() `multimodalMask` option). Merge numerics are
281
+ oracle-exact: the divide happens AFTER the bf16 cast with a weak
282
+ (bf16) embed_scale scalar — `AudioTower.features(preDivide=false)` +
283
+ astype + div, mirroring `features.astype(bf16) / embed_scale` (mlx
284
+ weak scalars take the array dtype; divide-in-f32-then-cast is up to a
285
+ bf16 ulp off and fails the exact gate).
286
+ - Exit: T2 green — and stronger than planned: the FULL greedy stream +
287
+ decoded text match the oracle EXACTLY for both fixtures (the tower is
288
+ bit-exact, so no prefix allowance was needed; generate()'s
289
+ promptEmbeddings path prefills in one forward exactly like the golden
290
+ script — no tail-split gap). `tests/e4b-audio.test.ts`.
291
+
292
+ **Phase A4 — serve + docs**
293
+ - [x] `src/server.ts` audio branch, lazy tower, serial-lane routing,
294
+ mixed image+audio request handled. `hasAudio` detection over the
295
+ three part shapes; `makeAudioLoader`/`getAudioTower` mirror vision
296
+ (same sidecar, zero flags, auto-enable on `audio_config` +
297
+ sidecar) — but a request WITH audio on a towerless model is an
298
+ explicit 400 naming the model, never a silent text-only degrade.
299
+ The gateway's `Vision` payload generalized to
300
+ `{embeddings, imageMask?, multimodalMask?}` (bidirMask stays null
301
+ whenever audio is present, §3.3 Q1); `shape.hasVision = !!payload`
302
+ routes every media request serial and skips the prompt cache —
303
+ verified live: `/stats` batch.submitted_rows stays 0 for audio
304
+ while a concurrent text request advances it (batch default 8).
305
+ Non-WAV containers (mp3/m4a/flac/ogg/aiff) transcode via
306
+ `afconvert` (src/audio/transcode.ts, RIFF-sniffed content-based
307
+ passthrough, temp files under os.tmpdir(), clear 400 on failure);
308
+ >30 s clips truncate at 480 000 samples in decodeAudio — the
309
+ oracle feature extractor's own default (truncation=True,
310
+ max_length=480_000) verified in audio_feature_extractor.py:302.
311
+ /v1/messages (Anthropic) rejects smuggled audio blocks with a 400
312
+ pointing to the OpenAI endpoint (protocol has no audio block).
313
+ - [x] server-api.md (schema + curl + caps + 400s + lane note) /
314
+ features-matrix.md (audio row + footnote: mlx-lm strips it, optiq
315
+ never serves it — we expose it) / README capability line, same
316
+ change set; Anthropic endpoint 400 documented.
317
+ - [x] capability discovery (post-merge CodeRabbit follow-up, 2026-07-07):
318
+ `audio` flag mirrors `vision` on every surface — /v1/models served
319
+ row, /library rows (`audioCapable` in src/registry.ts:
320
+ `has_audio_config AND has_audio_tower`, header-only sidecar scan),
321
+ and the pi ws `ready` frame. The predicate checks TENSORS, not just
322
+ config: the 12B stub sidecar (audio_config + embed_audio-only) reads
323
+ audio=false, and `makeAudioLoader` header-checks the sidecar so the
324
+ runtime flag can't lie either. Discovery-only in pi land — pi's
325
+ `Model.input` type has no "audio" modality to declare.
326
+ - Exit: T3 green — `tests/e4b-audio-serve.test.ts`
327
+ (MLX_BUN_TEST_AUDIO_SERVE=1): base64 WAV over
328
+ /v1/chat/completions returns the EXACT golden transcription
329
+ ("The quick brown fox jumps over the lazy dog.", 83 prompt /
330
+ 11 completion tokens); m4a transcode path transcribes; malformed
331
+ part, undecodable bytes, and Anthropic audio block all 400.
332
+ docs-map hygiene green. ✅ 2026-07-07
333
+
334
+ **Phase A5 — bench + coverage**
335
+ - [ ] benchmark.sh cells → RESULTS.md (tower ms, TTFT delta, RSS delta)
336
+ - [ ] 12B audio cell: rebuild its sidecar via optiq `build_vision_sidecar`
337
+ (selective download), own goldens — per-model doctrine, every
338
+ (model, quant) cell gets its own validation or an explicit deferral
339
+ - [ ] features-matrix: audio×batching = serial documented
340
+ - Exit: numbers curated; e4b cell validated; 12B validated or deferred
341
+ with reason.
342
+
343
+ Sizing: A0–A4 ≈ 3–4 focused sessions (the tower is the only genuinely new
344
+ model code; everything else is seam reuse); A5 ≈ 1.
345
+
346
+ ## 5. Non-goals (v1)
347
+
348
+ - TTS, STS, and `/v1/audio/transcriptions`-style endpoints — a
349
+ transcription shim over this same path is a cheap follow-up, noted, not
350
+ committed.
351
+ - Streaming/real-time audio input.
352
+ - >30 s audio: cap at 750 soft tokens exactly as the oracle does.
353
+ - Video (`mm_token_type_ids=2`) — same seams, explicitly later.
354
+ - Audio for 26B-A4B / DiffusionGemma / bf16 assistants (no
355
+ `audio_config` in those snapshots — architectural, not deferred work).
356
+ - Batched audio prefill (novel territory; serial lane only in v1).
@@ -1,6 +1,19 @@
1
1
  # Batching perf path — promote continuous batching out of compat mode
2
2
 
3
- Status: P5 + two gate fixes LANDED (2026-07-02, same day); rest PLANNED.
3
+ Status: P5 + two gate fixes LANDED (2026-07-02, same day). Since then
4
+ (2026-07-03, executed via grammar-spec-batching-integration.md Phases D/E):
5
+ the **P3 KV-budget admission slice LANDED** (`--kv-budget` aggregate
6
+ projection gate, queue-don't-OOM,
7
+ `/stats.batch.{pending_rows,kv_bytes,kv_budget_bytes}`,
8
+ tests/batch-kv-budget.test.ts) and the **P0 bench harness LANDED** as
9
+ `scripts/bench-feature-matrix.ts` (the six-cell composition matrix;
10
+ `scripts/bench-serving-load.ts` stays a client-only stack-vs-stack tool).
11
+ Also 2026-07-03: **Tier-0 universal models (plain full-attention, e.g.
12
+ Llama) now batch** — per-row RoPE ported (`UniversalRope.applyDynamic`)
13
+ and gated token-exact vs mlx-lm B=2
14
+ (tests/batched-decode-parity.test.ts "Llama 3B Tier-0"); maskArray +
15
+ sliding universal archs stay serial (unvalidated cells). Rest PLANNED —
16
+ P0 COMPLETED 2026-07-04: extend-join (full-attention; own mlx-lm extend oracle) + vectorized all-greedy sampling (BIT-equal A/B).
4
17
 
5
18
  **Headline result (M1 Max 32 GB, loaded machine, 4 concurrent × 128 tok,
6
19
  median of 3, wall-clock aggregate; oMLX 0.4.5.dev1, same OptiQ snapshots):**
@@ -56,7 +69,9 @@ the 2026-07-02 oMLX report (Claude artifact) and `~/.omlx` bench scripts.
56
69
  Design stance: the batch lane grows the **same parity-tier DAG as the
57
70
  serial lane** (docs/design/parity-tier-dag.md). `--l1 --batch N` stays
58
71
  bit-exact mlx-lm-B=N compat mode forever (the oracle checkpoint); the
59
- default/`--l3` batch path becomes the optimized one. `--batch` remains a
72
+ default batch path becomes the optimized one. (`--l3` was deleted
73
+ 2026-07-05 — docs/design/unified-engine-frontier-plan.md §6; output-changing
74
+ experiments are now env-flag Lab items.) `--batch` remains a
60
75
  mode switch, not a load fallback.
61
76
 
62
77
  ## 1. Current state
@@ -69,7 +84,9 @@ decode, per-row eviction (`filterKVRows` / `BatchedRotatingCache.filter`),
69
84
  per-row failure containment, drain-on-serial-waiter, `clearCache` every
70
85
  256 steps. Bit-parity with mlx-lm B=N verified at B=2 across all 4 model
71
86
  paths (`tests/batched-decode-parity.test.ts`, `tests/batched-rotating.test.ts`,
72
- `tests/batch-scheduler.test.ts`).
87
+ `tests/batch-scheduler.test.ts`) — plus, since 2026-07-03, Tier-0
88
+ universal plain-full-attention archs (Llama 3B, token-exact vs mlx-lm B=2;
89
+ see header).
73
90
 
74
91
  `src/serve/generation-gateway.ts` (`GenerationGateway.willBatch`) routes;
75
92
  one `AsyncMutex` is the GPU/`loraState` exclusion domain. The batch lane
@@ -81,22 +98,21 @@ is deliberately compat mode: it calls `model.forwardHidden` /
81
98
  | Lever | Serial mechanism | Why batch can't use it today |
82
99
  | --- | --- | --- |
83
100
  | Quantized KV (`kv_config`) | `maybeQuantizeKv` (`src/generate.ts:101`) | `QuantizedKVCache` (`src/model/gemma4-base.ts:419`) has no dynamic-B ops (no merge/filter/extend/temporalView over the packed/scales/biases triples); `willBatch` hard-gates `kvQuant` → serial |
84
- | Perf-kernel fused decode SDPA | dispatched from `quantizedSdpa` (`gemma4-base.ts:1560`) | kernel M=1-specialized: `fused-decode-kernel.ts:262` rejects `B !== 1 \|\| L !== 1`, and requires empty mask (batched decode needs per-row left-pad validity) |
85
- | Fused N-tiled SDPA (`--fused-decode`) | `quantizedSdpaTiled` | needs 2-D mask; batched masks are 4-D `[B,1,1,S]` (`buildBatchedDecodeMask`); quantized-only anyway |
101
+ | ~~Perf-kernel fused decode SDPA~~ | ~~dispatched from `quantizedSdpa`~~ | **DELETED 2026-07-05** (kernel + `--perf-kernel` removed; unified-engine-frontier-plan.md §6) — no longer a lever, batched or serial |
102
+ | ~~Fused N-tiled SDPA (`--fused-decode`)~~ | ~~`quantizedSdpaTiled`~~ | **DELETED 2026-07-05** (measured 1.00×; unified-engine-frontier-plan.md §6) |
86
103
  | Compiled decode | `CompiledDecode.for()` (`generate.ts:518-525`) | trace is `[1,1]`; B changes every join/evict |
87
104
  | Prompt-cache prefix reuse | `PromptCache.take/put` in `runGeneration` | bypassed by design in v1; every batched row solo-prefills (`cachedTokens: 0`) |
88
105
  | Vectorized sampling | n/a | `#step` samples per row (B slice+sample+concat graphs per token) |
89
106
 
90
107
  Two more gap sources specific to the benchmark:
91
108
 
92
- - **Qwen3.5 never batches at all**: `SSMCache` (`src/model/qwen3-delta.ts`)
93
- has no merge/filter, so the capability gate routes every request serial.
94
- The Qwen3.5 concurrency loss is pure serial queueing. mlx-lm batches
95
- Mamba-family state via `ArraysCache` — oracle exists.
109
+ - **Qwen3.5 never batches at all** — **FIXED by P5 (2026-07-02, header
110
+ item 1)**: `SSMCache.mergeRows`/`filter` landed; Qwen3.5 batches
111
+ token-exact vs mlx-lm B=2.
96
112
  - Joins re-merge the whole batch (O(B·S) slice storm, `#mergeJoiner`)
97
113
  instead of mlx-lm's `extend`.
98
114
 
99
- ## 2. Gap analysis: quantized KV + fused kernels at B>1
115
+ ## 2. Gap analysis: quantized KV at B>1
100
116
 
101
117
  - `QuantizedKVCache` already stores `[B,H,S,D']` triples; missing is
102
118
  dynamic-B surgery. New `src/model/batched-quant.ts`:
@@ -113,23 +129,30 @@ Two more gap sources specific to the benchmark:
113
129
  - Correctness fallback: `quantizedSdpaUnfused` (`gemma4-base.ts:1273`) is
114
130
  already B-generic with `mask.mode === "array"`; needs the batched mask
115
131
  broadcast as `[B,1,1,1,N]` for the 5-D GQA score reshape.
116
- - Perf kernel at B>1: (1) drop the `B !== 1` gate (keep `L !== 1`);
117
- (2) grid gains a B axis (`[TG_THREADS, H·B, G]`), `SCORE_ROW` offsets
118
- index `((b·KV + kv)·N + n)`; (3) replace the empty-mask requirement with
119
- a per-row `leftPad` int32 `[B]` input — each row's n-loop starts at
120
- `max(blockStart, leftPad[b])`, so pad bytes are never read.
132
+ - ~~Perf kernel at B>1~~ — OBSOLETE: the perf kernel was deleted 2026-07-05
133
+ (unified-engine-frontier-plan.md §6); `quantizedSdpaUnfused` is the
134
+ quant-batched path, full stop.
121
135
  - RoPE per-row positions already solved (`ropeOffsetArr`, the S1b.1 fix).
122
136
 
123
137
  ## 3. Phases
124
138
 
125
139
  ### P0 — Baseline + engine quick wins (no new numerics) [S–M, 2–4 days]
126
- - Write `scripts/bench-serving-load.ts` (N concurrent vs live server,
127
- aggregate tok/s + TTFT p50/p95). Re-measure vs oMLX **with `--batch 4`
128
- actually on** to size the true engine gap vs the serial-queueing gap.
140
+ - Bench harness — **DONE 2026-07-03**: landed as
141
+ `scripts/bench-feature-matrix.ts` (six-cell composition matrix over live
142
+ SSE, TTFT p50/p95 + aggregate tok/s; integration-plan Phase E).
143
+ `scripts/bench-serving-load.ts` exists as the client-only stack-vs-stack
144
+ tool and stays one. Clean-machine run remains Josh-gated.
129
145
  - Land `extend` join (full-attention twin + `BatchedRotatingCache.extend`);
130
146
  oracle: extend `scripts/gen-batched-dynamic-golden.py` (mlx-lm `extend`).
147
+ **LANDED 2026-07-04** for full-attention layers (`extendKVRows`,
148
+ mlx-lm `BatchKVCache.extend` semantics; own oracle
149
+ `scripts/gen-batched-extend-golden.py`, token-for-token CPM + Llama;
150
+ `MLX_BUN_BATCH_EXTEND=0` = re-merge). `BatchedRotatingCache.extend`
151
+ (sliding layers on Gemma joins) remains open.
131
152
  - Vectorize the homogeneous-sampler fast path in `#step` (all-greedy →
132
- one `ops.argmaxAxis` on `[B,V]`); mixed rows keep the loop.
153
+ one `ops.argmaxAxis` on `[B,V]`); mixed rows keep the loop. **LANDED
154
+ 2026-07-04** (BIT-equal A/B, `MLX_BUN_BATCH_VEC_SAMPLE=0`;
155
+ tests/batch-vec-sample.test.ts).
133
156
  - Exit: gated suites unchanged; B=4 baseline recorded; joins not O(B·S).
134
157
 
135
158
  ### P1 — Quantized KV at B>1 [M–L, 1–1.5 wk]
@@ -144,19 +167,20 @@ Two more gap sources specific to the benchmark:
144
167
  (3) KL vs batched-bf16 ≤ serial quant-vs-bf16 KL; 6-task eval unchanged;
145
168
  (4) `--l1 --batch N` suites untouched.
146
169
 
147
- ### P2 — Perf kernel at B>1 + fallback matrix [M, ~1 wk]
148
- - Kernel edits per §2; dispatch when every row supported, else unfused.
149
- - Fallback matrix documented in server-config.md.
150
- - Batched frozen-oracle envelope (B=2/4 `freeze-perf-oracle` variant,
151
- ≥56/64 per row, `tests/perf-kernel-oracle.test.ts` pattern).
152
- - Exit: dispatch counter proves engagement; envelope green; measured
153
- uplift at B=4 vs P1 unfused on a clean machine; if <5%, ship the
154
- fallback matrix only and close honestly.
170
+ ### P2 — Perf kernel at B>1 + fallback matrix [OBSOLETE — kernel deleted 2026-07-05]
171
+ The perf kernel (and `--perf-kernel` / its frozen-oracle tests) was deleted
172
+ 2026-07-05 (unified-engine-frontier-plan.md §6): it never beat the L1
173
+ baseline in a paired A/B, so there is nothing to port to B>1. The planned
174
+ "batched frozen-oracle envelope" gate is obsolete with it. Quant-batched
175
+ decode stays on `quantizedSdpaUnfused` (P1).
155
176
 
156
177
  ### P3 — Admission, prompt cache, adapters, defaults [M–L, 1–1.5 wk]
157
- - KV-budget admission: `projectKvBytes(model, promptLen, maxTokens)`
158
- beside `fit()`; `--kv-budget`, `--prompt-concurrency`;
159
- `/stats.batch.{pending_rows,kv_bytes,kv_budget_bytes}`.
178
+ - KV-budget admission — **LANDED 2026-07-03** (integration-plan Phase D):
179
+ `--kv-budget <GB>` caps the batch's aggregate projected KV
180
+ (prompt+max_tokens per row, window-capped); over-budget joiners QUEUE
181
+ (FIFO), never OOM; a single request over the budget alone rejects with
182
+ an actionable error; `/stats.batch.{pending_rows,kv_bytes,kv_budget_bytes}`;
183
+ gated by tests/batch-kv-budget.test.ts. `--prompt-concurrency` still open.
160
184
  - Prompt-cache reuse under batching: `take` at admit, forward uncached
161
185
  tail; on finish/evict `extract(row)` → solo caches → `put`. Quantized
162
186
  entries are non-trimmable mid-group — match on full-prefix or STEP
@@ -204,17 +228,16 @@ Two more gap sources specific to the benchmark:
204
228
  (per-row fold; Qwen3.5's default repetition penalty had been routing every
205
229
  request serial). Exit numbers in the header table.
206
230
 
207
- Ordering: P1→P2 strictly ordered; P4 and P5 independent (P5 first if the
208
- Qwen3.5 headline matters most). Total ~5–6 wk serial, ~4 wk with P4/P5
231
+ Ordering: P4 and P5 independent (P5 first if the Qwen3.5 headline matters
232
+ most); P2 dropped (kernel deleted). Total ~4–5 wk serial, ~3 wk with P4/P5
209
233
  parallel.
210
234
 
211
235
  ## 4. Risks
212
236
 
213
237
  - **No external oracle for quant-batched** — mitigated by the P1 parity
214
238
  ladder; `--l1 --batch N` stays the always-reachable bit-exact checkpoint.
215
- - **Metal kernel at B>1**: leftPad start-index never touches pad bytes;
216
- no atomics (deterministic); gate = batched frozen-oracle envelope +
217
- dispatch assertions + A/B vs unfused.
239
+ - ~~Metal kernel at B>1~~ — risk retired with P2 (perf kernel deleted
240
+ 2026-07-05).
218
241
  - **Memory**: B× quant caches + prompt cache + weights on 24 GB — P3
219
242
  admission is the backstop; mirror mlx-lm's
220
243
  `trim_to(total − activeBatchBytes)` so LRU and live batch share one
@@ -1,6 +1,17 @@
1
1
  # Batching v2 — state map, debt, and the gap plan
2
2
 
3
- Status: **PLAN** (written 2026-07-01, read-only audit — no code changed)
3
+ Status: **PLAN, largely EXECUTED** (written 2026-07-01 as a read-only
4
+ audit; §1's state map is that day's snapshot). Landed since: steps 1–3
5
+ (hotfix bundle, failure/fairness, engine hygiene — 2026-07-01); step 8
6
+ SSM batched path (2026-07-02, superseded by batching-perf-path.md P5 —
7
+ token-exact vs mlx-lm B=2); step 5 KV-budget admission (2026-07-03,
8
+ executed as grammar-spec-batching-integration.md Phase D:
9
+ `--kv-budget`, queue-don't-OOM, `/stats.batch` fields,
10
+ tests/batch-kv-budget.test.ts — the `--prompt-concurrency` half still
11
+ open); step 7's e1+e3 (min_p/XTC + penalties/logit_bias batch via
12
+ per-row logits processors, 2026-07-02, batching-perf-path.md header
13
+ item 2; e2 logprobs still routes serial). Still open: step 4 (extend
14
+ join), step 6 (prompt-cache reuse), 7's e2, 9 (rolling), 10 (deferred).
4
15
  Scope: the `--batch N` continuous-batching engine (`src/serve/batch-scheduler.ts`,
5
16
  `src/serve/generation-gateway.ts`, `src/model/batched-mask.ts`,
6
17
  `src/model/batched-rotating.ts`, the `server.ts` wiring).
@@ -178,6 +189,9 @@ endpoints silently don't participate in. Specific debt, ranked:
178
189
  (mirror mlx-lm server.py:371-374) in `willBatch` — batchable iff every
179
190
  `makeCache()` element is `KVCache | RotatingKVCache` — plus the doc
180
191
  correction. The real SSM batch path is item (h), later. *(confidence 95)*
192
+ **RESOLVED**: gate landed 2026-07-01; the full SSM batch path landed
193
+ 2026-07-02 (batching-perf-path.md P5) — server-config's Qwen3.5 claim is
194
+ true again.
181
195
 
182
196
  - **D2 — `#admit` materializes full-prompt logits.**
183
197
  batch-scheduler.ts:173-174 runs `logitsFromHidden(h)` on the **whole**
@@ -498,14 +512,14 @@ step a gate.
498
512
 
499
513
  | # | Item | Effort | Test gate | Notes |
500
514
  | --- | --- | --- | --- | --- |
501
- | 1 | **Hotfix bundle**: D2 slice-before-head; D1 capability gate + server-config.md correction; D3 route curve/`/signal`(/adapter-unmount) through `gateway.runExclusive`; stale comment server.ts:960 | **S** | gated batch suites unchanged; fast suite; new unit: willBatch(Qwen)=false | no numerics change anywhere |
502
- | 2 | **D5/D6 failure + fairness**: per-row onToken containment (evict row, keep batch); `#admit` try/finally; drain-on-serial-waiter | **S–M** | new gated test: throwing onToken evicts one row, siblings complete; drain test (serial completes under sustained batchable load) | mlx-lm `remove` semantics |
503
- | 3 | **(d) engine hygiene**: chunked+interleaved admit, pipelined decode, clearCache cadence | **M** | KL suite + mlx-lm goldens UNCHANGED; bench-serving-load A/B (clean machine, Josh-gated for quotables) | closes perf-review backlog #6 |
504
- | 4 | **(a) `BatchedKVCache` + `extend`** (full + rotating) | **M** | extended dynamic golden (mlx-lm `extend` driven); existing suites green | new files only — no gemma4-base.ts conflict |
505
- | 5 | **(c) KV-budget admission + `--prompt-concurrency`** | **M** | projection unit tests; gated queueing test; `/stats` fields | depends on 3 (interleaved admit) for prompt-concurrency semantics |
506
- | 6 | **(b) prompt-cache reuse under batching** | **M–L** | `cached_tokens>0` gated test; warm==cold row parity; serial suite | depends on 4 (`extract`) + 5 (byte interplay) |
507
- | 7 | **(e) gate widening**: e1 min_p/XTC (S) → e2 logprobs (M) → e3 penalties/logit_bias (M) | **S+M+M** | per-feature: batched==serial teacher-forced/deterministic-seed tests | e1 waits for the seed fix to merge |
508
- | 8 | **(h) SSM batched path** (Qwen3.5) | **M–L** | mlx-lm B=N golden for a Qwen3.5 model (ArraysCache oracle) | un-does the item-1 gate for Qwen; doc row flips back |
515
+ | 1 | ✅ **DONE 2026-07-01** — **Hotfix bundle**: D2 slice-before-head; D1 capability gate + server-config.md correction; D3 route curve/`/signal`(/adapter-unmount) through `gateway.runExclusive`; stale comment server.ts:960 | **S** | gated batch suites unchanged; fast suite; new unit: willBatch(Qwen)=false | no numerics change anywhere |
516
+ | 2 | ✅ **DONE 2026-07-01** — **D5/D6 failure + fairness**: per-row onToken containment (evict row, keep batch); `#admit` try/finally; drain-on-serial-waiter | **S–M** | new gated test: throwing onToken evicts one row, siblings complete; drain test (serial completes under sustained batchable load) | mlx-lm `remove` semantics |
517
+ | 3 | ✅ **DONE 2026-07-01** — **(d) engine hygiene**: chunked+interleaved admit, pipelined decode, clearCache cadence | **M** | KL suite + mlx-lm goldens UNCHANGED; bench-serving-load A/B (clean machine, Josh-gated for quotables) | closes perf-review backlog #6 |
518
+ | 4 | **(a) `BatchedKVCache` + `extend`** (full + rotating) — **still open** | **M** | extended dynamic golden (mlx-lm `extend` driven); existing suites green | new files only — no gemma4-base.ts conflict |
519
+ | 5 | ✅ **DONE 2026-07-03** (admission half; `--prompt-concurrency` still open) — **(c) KV-budget admission** — executed as grammar-spec-batching-integration.md Phase D | **M** | projection unit tests; gated queueing test (tests/batch-kv-budget.test.ts); `/stats` fields | depends on 3 (interleaved admit) for prompt-concurrency semantics |
520
+ | 6 | **(b) prompt-cache reuse under batching** — **still open** | **M–L** | `cached_tokens>0` gated test; warm==cold row parity; serial suite | depends on 4 (`extract`) + 5 (byte interplay) |
521
+ | 7 | **(e) gate widening**: ✅ e1 min_p/XTC + e3 penalties/logit_bias **DONE 2026-07-02** (per-row logits processors, batching-perf-path.md header item 2); e2 logprobs **still open** (routes serial) | **S+M+M** | per-feature: batched==serial teacher-forced/deterministic-seed tests | e1 waits for the seed fix to merge |
522
+ | 8 | ✅ **DONE 2026-07-02** — **(h) SSM batched path** (Qwen3.5) — landed as batching-perf-path.md P5, token-exact vs mlx-lm B=2 | **M–L** | mlx-lm B=N golden for a Qwen3.5 model (ArraysCache oracle) | un-does the item-1 gate for Qwen; doc row flips back |
509
523
  | 9 | **(g) docs pass** | **S** | doc-vs-code re-audit (this file's §1 as the checklist) | rolling — update per landing, final sweep here |
510
524
  | 10 | **(f) quantized-KV batched** | **L** | KL + 6-task + density benchmark — design doc first | DEFERRED — needs a concrete density case |
511
525
 
@@ -166,7 +166,8 @@ future-proof empty table only.
166
166
  - **Stage 2 — train `memory-bucket` LoRA, only if Stage 0/1 miss.** Data from lucien.db, one
167
167
  example per (chunk, candidate-bucket) binary decision (gold = belongs?), shortlist regime =
168
168
  serve regime. Recipe = `chunk-finetune.ts` (sft, rank 16, lr 1e-5, seq 8192,
169
- `MLX_BUN_PERF_KERNEL=0 MLX_BUN_FUSED_GELU=0`, no train-flash); adapter →
169
+ no train-flash; no env-flag sanitization needed since the 2026-07-05 kernel
170
+ deletions); adapter →
170
171
  `~/.cache/mlx-bun/adapters/memory-bucket`; pick checkpoints by the F1 judge, not val loss.
171
172
  **GPU-gated.**
172
173