mlx-bun 0.3.0 → 0.4.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 (150) hide show
  1. package/docs/README.md +4 -1
  2. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  3. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  4. package/docs/design/batching.md +1321 -49
  5. package/docs/design/decode-speed-program.md +331 -1163
  6. package/docs/design/generic-model-support.md +79 -3
  7. package/docs/design/kv-cache.md +198 -54
  8. package/docs/design/speculative-decoding.md +387 -24
  9. package/docs/design/turboquant.md +123 -13
  10. package/docs/design/unified-engine-frontier-plan.md +144 -142
  11. package/docs/planning/PRODUCT_ROADMAP.md +16 -0
  12. package/docs/planning/release-notes-v0.4.0.md +45 -0
  13. package/docs/reference/benchmarks.md +2457 -6
  14. package/docs/reference/cli.md +41 -2
  15. package/docs/reference/distribution.md +16 -9
  16. package/docs/reference/environment.md +55 -5
  17. package/docs/reference/models.md +13 -4
  18. package/docs/reference/server-api.md +79 -25
  19. package/docs/reference/server-config.md +178 -84
  20. package/docs/reference/troubleshooting.md +13 -21
  21. package/package.json +1 -1
  22. package/src/backends/mlx/assistant-target.ts +46 -0
  23. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  24. package/src/backends/mlx/batch-group.ts +368 -224
  25. package/src/backends/mlx/cache-attachment.ts +67 -0
  26. package/src/backends/mlx/cache-layout.ts +31 -0
  27. package/src/backends/mlx/checkpoint-state.ts +53 -0
  28. package/src/backends/mlx/continuation-persistence.ts +106 -0
  29. package/src/backends/mlx/continuation-request.ts +43 -0
  30. package/src/backends/mlx/continuation.ts +77 -0
  31. package/src/backends/mlx/draft-target.ts +7 -22
  32. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  33. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  34. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  35. package/src/backends/mlx/gateway-binding.ts +77 -18
  36. package/src/backends/mlx/kv-codec.ts +9 -0
  37. package/src/backends/mlx/kv-maintenance.ts +133 -0
  38. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  39. package/src/backends/mlx/model-serving.ts +1 -1
  40. package/src/backends/mlx/prefill-cohort.ts +89 -0
  41. package/src/backends/mlx/prefill-rows.ts +200 -0
  42. package/src/backends/mlx/prefill.ts +1 -1
  43. package/src/backends/mlx/projected-context-rows.ts +72 -0
  44. package/src/backends/mlx/request-state-policy.ts +41 -0
  45. package/src/backends/mlx/rollback.ts +41 -3
  46. package/src/backends/mlx/row-sampling.ts +49 -0
  47. package/src/backends/mlx/serial-executor.ts +35 -66
  48. package/src/backends/mlx/speculative-group.ts +359 -0
  49. package/src/backends/mlx/speculative-round.ts +161 -0
  50. package/src/backends/mlx/state-rows.ts +104 -0
  51. package/src/backends/mlx/state-views.ts +21 -1
  52. package/src/backends/mlx/target-layout-capability.ts +35 -0
  53. package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
  54. package/src/cli.ts +11 -36
  55. package/src/contracts/execution.ts +11 -0
  56. package/src/contracts/prefix-cache.ts +23 -0
  57. package/src/contracts/resources.ts +7 -0
  58. package/src/contracts/sampling.ts +23 -0
  59. package/src/contracts/scheduling.ts +18 -0
  60. package/src/engine/execution-plan.ts +18 -16
  61. package/src/engine/resources.ts +12 -1
  62. package/src/engine/scheduler.ts +10 -5
  63. package/src/fit.ts +29 -15
  64. package/src/generate.ts +77 -173
  65. package/src/inference/draft-acceptance.ts +41 -0
  66. package/src/inference/draft-output.ts +49 -0
  67. package/src/inference/rollback.ts +2 -2
  68. package/src/kv-scheme.ts +70 -12
  69. package/src/kv-store.ts +70 -16
  70. package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
  71. package/src/lab/paged-kv/paged-kv.ts +25 -11
  72. package/src/mlx/array.ts +2 -1
  73. package/src/model/artifact-identity.ts +73 -0
  74. package/src/model/batched-kv.ts +235 -0
  75. package/src/model/batched-mask.ts +12 -4
  76. package/src/model/batched-quant.ts +8 -2
  77. package/src/model/batched-quantized-kv.ts +144 -0
  78. package/src/model/batched-rotating-quant.ts +76 -15
  79. package/src/model/batched-rotating-state.ts +76 -0
  80. package/src/model/batched-rotating.ts +64 -22
  81. package/src/model/batched-row-storage.ts +52 -0
  82. package/src/model/batched-ssm.ts +52 -0
  83. package/src/model/batched-turboquant-kv.ts +93 -0
  84. package/src/model/compiled-decode.ts +7 -1
  85. package/src/model/decoded-kv-donor.ts +12 -0
  86. package/src/model/delayed-quantized-kv.ts +72 -0
  87. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  88. package/src/model/delayed-turboquant-kv.ts +55 -0
  89. package/src/model/full-prefill-padding.ts +56 -0
  90. package/src/model/full-prefill-row.ts +105 -0
  91. package/src/model/gemma4-base.ts +162 -80
  92. package/src/model/gemma4.ts +34 -26
  93. package/src/model/generated/gemma4-12b.ts +4 -1
  94. package/src/model/generated/gemma4-26b.ts +4 -1
  95. package/src/model/generated/gemma4-e4b.ts +4 -1
  96. package/src/model/glm52-cache.ts +80 -4
  97. package/src/model/glm52-mla.ts +12 -6
  98. package/src/model/glm52.ts +19 -40
  99. package/src/model/kv-attention-view.ts +67 -0
  100. package/src/model/minicpm5.ts +11 -7
  101. package/src/model/qwen3-delta-state.ts +74 -0
  102. package/src/model/qwen3-delta.ts +59 -36
  103. package/src/model/qwen3_5.ts +82 -42
  104. package/src/model/rotating-kv-donor.ts +35 -0
  105. package/src/model/rotating-kv-layout.ts +262 -0
  106. package/src/model/rotating-row-transaction.ts +35 -0
  107. package/src/model/speculative-rotating-kv.ts +101 -0
  108. package/src/model/ssm-prefill-padding.ts +39 -0
  109. package/src/model/trellis-linear.ts +2 -2
  110. package/src/model/turboquant-codec.ts +83 -0
  111. package/src/model/universal/dense.ts +14 -13
  112. package/src/preflight.ts +31 -15
  113. package/src/prompt-cache.ts +42 -44
  114. package/src/sampler.ts +36 -25
  115. package/src/serve/chat-stage.ts +2 -2
  116. package/src/serve/generation-gateway.ts +39 -30
  117. package/src/serve/model-host.ts +3 -4
  118. package/src/serve/request-plan.ts +4 -3
  119. package/src/serve/request-prep.ts +2 -4
  120. package/src/serve/text-completion-stage.ts +2 -2
  121. package/src/server.ts +64 -108
  122. package/src/spec/assistant-rows.ts +92 -0
  123. package/src/spec/assistant-source.ts +28 -18
  124. package/src/spec/cache-identity.ts +22 -0
  125. package/src/spec/deepspec-rows.ts +29 -0
  126. package/src/spec/deepspec-source.ts +12 -10
  127. package/src/spec/dflash-source.ts +28 -4
  128. package/src/spec/drafter.ts +153 -142
  129. package/src/spec/dspark/deepspec-module.ts +95 -120
  130. package/src/spec/dspark/module-dflash.ts +126 -170
  131. package/src/spec/glm52-mtp-graph.ts +29 -0
  132. package/src/spec/glm52-mtp-rows.ts +132 -0
  133. package/src/spec/glm52-mtp-source.ts +53 -136
  134. package/src/spec/glm52-mtp-state.ts +33 -0
  135. package/src/spec/ngram-source.ts +92 -20
  136. package/src/spec/projected-draft-rows.ts +73 -0
  137. package/src/spec/qwen-mtp-module.ts +171 -0
  138. package/src/spec/qwen-mtp-rows.ts +219 -0
  139. package/src/spec/qwen-mtp-source.ts +81 -226
  140. package/src/spec/qwen-mtp-state.ts +30 -0
  141. package/src/spec/serve-loop.ts +70 -68
  142. package/src/spec/source.ts +80 -15
  143. package/src/spec/two-model-rows.ts +61 -0
  144. package/src/spec/two-model.ts +43 -101
  145. package/src/ssd-cache.ts +33 -21
  146. package/src/ssd-durability.ts +11 -7
  147. package/src/web/app.js +57 -57
  148. package/src/backends/mlx/memory-guard.ts +0 -40
  149. package/src/spec/prefix-state.ts +0 -45
  150. /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
@@ -23,8 +23,12 @@ Two tiers, one contract:
23
23
 
24
24
  - **Tier-0 GENERIC** — one config-driven "universal" module per architecture
25
25
  *family*. Runs any covered checkpoint at **L1 parity** (bit-exact vs mlx-lm
26
- on this machine's GPU). Monolith path only: no compiled decode, no
27
- kv-quant. Slow, never broken. Built: `src/model/universal/` (dense
26
+ on this machine's GPU). Monolith path only, without compiled decode.
27
+ Standard attention now uses the shared plain/affine KV port; the softcap
28
+ branch retains plain KV. Qualified full-attention targets compose delayed
29
+ affine conversion with standalone and lookup drafting through the same
30
+ cache transaction interface; rotating layouts use that interface for
31
+ per-layer conversion and assistant donors. Built: `src/model/universal/` (dense
28
32
  llama-family). Design only: UniversalMoE.
29
33
  - **Tier-1 TARGETED** — a model *graduates*: dedicated or generated forward,
30
34
  L2 (mlx-optiq) composition where an optiq artifact exists, Lab experiments
@@ -200,7 +204,9 @@ Key choices:
200
204
  or missing tensors are a LOAD ERROR naming them, never a silently-wrong
201
205
  model.
202
206
  - **KV cache** — `KVCache`/`RotatingKVCache` unchanged; `layer_types` /
203
- `sliding_window` already normalized. No kv-quant at Tier-0.
207
+ `sliding_window` already normalized. Standard attention consumes the shared
208
+ cache-attention interface; qualified Llama uniform KV4/KV8 serving and
209
+ generated-cache coverage are described in batching.md.
204
210
  - **RuntimeModel surface** — the minimal contract `Qwen3Model` implements
205
211
  (config, weightsBytes, loraState, makeCache, forwardHidden,
206
212
  logitsFromHidden, forward, generate, loraTargets) so serving/eval/LoRA
@@ -336,6 +342,72 @@ design-relevant facts; phase logs live in PLAN.md at the named anchor.
336
342
  - Training reference model for segmented backward and ORPO
337
343
  (docs/design/orpo-training.md, orpo-training.md).
338
344
 
345
+ #### 6.2.1 MiniCPM5-2B investigation
346
+
347
+ Metadata and template inspection completed 2026-09-10. No weights were
348
+ downloaded and no GPU inference was run. This checkpoint is a candidate,
349
+ not an artifact with established parity or local performance results.
350
+
351
+ Start with the official [OpenBMB MLX artifact](https://huggingface.co/openbmb/MiniCPM5-2B-MLX),
352
+ revision `3d00c3da500debfe345e60e25a87ed2669f5b1ee`. Its config uses standard
353
+ Llama blocks, 42 layers, hidden size 2048, intermediate size 6144, 16 query
354
+ and 2 KV heads of dimension 128, untied embeddings, vocab 130560 and
355
+ RoPE theta 5000000. Quantization is affine 4-bit with group size 64.
356
+ Preserve both EOS IDs, `1` and `130073`, and the separate
357
+ `chat_template.jinja`. The release is text-only and Apache-2.0.
358
+
359
+ Current integration findings:
360
+
361
+ - `isMiniCPM5Config` in `src/model/support.ts` matches the older 24-layer,
362
+ hidden-1536 artifact. The 2B config fails that predicate and qualifies for
363
+ generic Llama through `genericArgsFor`; `familyProfile` therefore selects
364
+ the universal graph. Do not widen the predicate before auditing dedicated
365
+ execution, training and cache assumptions against this shape.
366
+ - A CPU-only smoke using the pinned config and template passed
367
+ `loadModelConfig`, `genericArgsFor`, `ChatTemplate.load` and rendering a
368
+ user → assistant tool call → tool result → user conversation. Explicit
369
+ thinking true/false both render correctly; the XML function call survives.
370
+ This checks rendering, not generated tool-call parsing or tokenizer parity.
371
+ - `resolveEnableThinking` applies the MiniCPM no-thinking default through
372
+ the same exact-shape predicate. It does not apply to 2B. With no explicit
373
+ option, this artifact's prompt ends at the assistant header; true appends
374
+ an open think tag, false appends a closed empty think block. Set this
375
+ explicitly in comparisons. Template support for disabling thinking does
376
+ not establish no-thinking quality.
377
+
378
+ Evaluation sequence:
379
+
380
+ 1. Pin an 8-bit or BF16 control alongside the official 4-bit candidate.
381
+ Establish tokenizer/prompt identity and same-machine logits against the
382
+ pinned mlx-lm oracle, then test multi-turn tool calls, reasoning splits,
383
+ EOS, cache reuse and batched continuation. Keep quantization quality
384
+ comparisons separate from same-artifact runtime parity.
385
+ 2. Compare with MiniCPM5-1B and Gemma e4b on held-out Dreaming segmentation,
386
+ extraction and source-grounded synthesis, plus bounded repository/tool
387
+ tasks. Score boundary/label accuracy and completed tasks, not just valid
388
+ JSON or XML. Use explicit thinking modes and equal token/time budgets.
389
+ 3. Record first-answer latency, complete task time, output/reasoning tokens,
390
+ repetition and truncation rates, memory and context scaling. Local
391
+ measurements belong only in `docs/reference/benchmarks.md`, with a named
392
+ quiet machine and the required serving benchmark procedure.
393
+ 4. Investigate the [released DSpark draft](https://huggingface.co/openbmb/MiniCPM5-2B-DSpark)
394
+ after ordinary inference passes. Its config declares `Qwen3DSparkModel`,
395
+ five draft layers, block size seven, and target taps `[1,10,20,30,39]`.
396
+ Existing Gemma DSpark support does not establish compatibility. Audit
397
+ the draft's architecture, target taps, weights and verifier before a
398
+ paired task-time comparison.
399
+
400
+ Evidence to use when designing the quality checks:
401
+ [OpenBMB's model card](https://huggingface.co/openbmb/MiniCPM5-2B) distinguishes
402
+ internally reproduced scores from Artificial Analysis results. The
403
+ [independent September 7 evaluation](https://artificialanalysis.ai/articles/openbmb-releases-minicpm5-2b)
404
+ supports strong capability for its size but reports substantial reasoning
405
+ token use and weak absolute terminal-task success. These evaluations do not
406
+ validate MLX 4-bit quality. [Upstream issue 374](https://github.com/OpenBMB/MiniCPM/issues/374)
407
+ reports repetition sensitivity in GGUF coding runs. That is an unreproduced
408
+ cross-runtime hypothesis for our sweep, not evidence of the same MLX failure
409
+ or a reason to change repetition defaults.
410
+
339
411
  ### 6.3 Qwen3 / Qwen3-MoE / Qwen3.5-family (3.6, 3.8) — `qwen3.ts`, `qwen3-moe.ts`, `qwen3_5.ts`
340
412
 
341
413
  - **Qwen3** (`model_type: qwen3`): dense GQA with per-head q/k RMSNorm
@@ -627,3 +699,7 @@ non-goals.
627
699
  - 2026-08-23 — This doc consolidated: audio-input-plan,
628
700
  diffusion-gemma-port, minicpm5-decode-megakernel, and the Colibri archive
629
701
  summarized here; ports become sections.
702
+
703
+
704
+ Non-Qwen grouped TurboQuant drafting, including assistant donor attention, uses
705
+ the shared cache interfaces. See [composition coverage](batching.md#turboquant-donor-attention-and-grouped-draft-composition).
@@ -3,7 +3,7 @@ status: landed
3
3
  axis: ON
4
4
  canonical-for: kv-cache
5
5
  plan-anchor: "Phase 18 — Concurrent / batched serving (slots) + parallel load benchmark `[~]` (2026-06-13)"
6
- last-verified: 2026-08-23
6
+ last-verified: 2026-09-09
7
7
  ---
8
8
 
9
9
  # KV cache — residency and layout
@@ -42,11 +42,10 @@ its KV encodes:
42
42
  The prompt cache tiers over the SSD store *inside* `take()` (structural
43
43
  `ColdTier` interface), so every consumer (serial lane, batch scheduler
44
44
  joiners, `/admin/drain`) gets both tiers through the same `take()`/`put()`
45
- calls. The default path does no persistence work on the token loop:
46
- spill/persist happen at request boundaries, evictions, idle sweeps, and
47
- explicit flushes. The opt-in `--generation-checkpoint N` path deliberately
48
- pauses at coarse, safe decode boundaries to atomically persist an in-flight
49
- generation (section 5.7). Paged KV
45
+ calls. Requests and evictions enqueue persistence work. The writer performs
46
+ it when the engine is idle; RAM reuse does not enqueue an extra write or wait
47
+ for the queue. Explicit flushes await the queued work. The opt-in `--generation-checkpoint N` path transfers owned snapshots at coarse decode boundaries and queues atomic
48
+ persistence without awaiting disk in decode (section 5.7). Paged KV
50
49
  (section 6) is a *layout* choice for the live tier and, in v1, bypasses the
51
50
  other two.
52
51
 
@@ -114,10 +113,10 @@ Two independent checks decide whether a model/scheme can batch
114
113
  `KVCache`, `RotatingKVCache`, `BatchableCache`, or (unless
115
114
  `MLX_BUN_BATCH_SSM=0`) `SSMCache`. Novel classes (TurboQuant, paged) fail
116
115
  this by construction.
117
- - `#kvBatchable()` — `KvScheme.batchable(config, canConvert)`: only the
118
- per-layer `affine-config` scheme batches, and only when every configured
119
- layer is a plain or rotating cache that can convert. Uniform `kvBits`
120
- and TurboQuant stay serial; a scheme-less gateway must never silently
116
+ - `#kvBatchable()` — `KvScheme.batchable(config, canConvert)`: per-layer
117
+ `affine-config` and uniform affine KV4/KV8 batch when their cache-owning
118
+ layers can convert. TurboQuant still requires merged-row layout support;
119
+ a scheme-less gateway must never silently
121
120
  drop quantization (`kv-scheme.ts` makes the capability probe mandatory
122
121
  for quantized schemes).
123
122
 
@@ -210,16 +209,23 @@ upstream is byte-capped now.
210
209
  **`take(prompt, ns)` — prefix sharing.** Finds the entry with the longest
211
210
  usable common prefix (usable = common prefix capped at `prompt.length - 1`,
212
211
  so at least one token is forwarded; an entry longer than the match must be
213
- trimmable). Trimmable entries are cloned zero-copy (`cloneKvCaches`: slice
214
- views of live arrays), trimmed, and returned with a ref-counted `retain`
215
- share; the donor stays in place, so N agents can share one prefill. An exact
216
- hit on a Qwen hybrid entry containing recurrent SSM state transfers the donor
217
- itself to bound resident snapshots; the request puts its extended state back
218
- on completion. With an SSD tier, `take` persists that exact boundary before
219
- transferring ownership. An already-durable prefix needs only an index check.
220
- The extended recurrent state cannot trim back to the consumed boundary, so
221
- keeping its SSD copy preserves later exact prompt repeats. A failed required
222
- store leaves the donor and backing owned by the cache and refuses the transfer.
212
+ trimmable). Every RAM hit lends zero-copy views (`cloneKvCaches` and
213
+ `cloneAttachments`) with a ref-counted backing share; the donor remains in
214
+ place. This includes exact-boundary recurrent and method state. Only
215
+ trimmable state may be shortened. Decode extends its own views without
216
+ changing the retained checkpoint.
217
+
218
+ SSD is an eventually consistent persistence layer. RAM reuse never writes
219
+ to SSD or waits for durability. Background writes retain immutable snapshot
220
+ views and yield between tensor writes; explicit flush and graceful shutdown
221
+ wait for persistence. A pending or failed write does not prevent RAM reuse.
222
+ The existing byte cap controls retained donors and LRU eviction.
223
+
224
+ The native sharing gate checks logical checkpoint bytes after repeated decode
225
+ and after SSD restore. It passes on both development Macs; the M1 long-prefix
226
+ case also preserves three independent 512-token continuations. The queued
227
+ writer test retains snapshot state after both donor and borrower disposal,
228
+ and explicit flush remains pending until the write completes.
223
229
 
224
230
  **Tier order inside `take()`.** The cold tier is consulted with
225
231
  `find(prompt, ns)` (index-only, no I/O) and wins only with a *strictly*
@@ -293,8 +299,9 @@ full prefill (GEMV-vs-GEMM reduction order). Both tiers share it.
293
299
  `PromptCache` (section 4); restart persistence comes from a debounced
294
300
  write-behind snapshot after each put, not only from spill-on-evict.
295
301
  - **D3 — Own page-aligned format, not safetensors.** `src/kv-store.ts`
296
- writes `MLXBUNKV2\n`-magic files whose current layout is the **v3
297
- streaming writer**: header sized up front with fixed-width tensor
302
+ writes `MLXBUNKV2\n`-magic files with a streaming writer: ordinary KV
303
+ retains format v3; checkpoints with method companion state use v4.
304
+ The header is sized up front with fixed-width tensor
298
305
  hashes, tensors materialized/hashed/written one at a time at 16 KiB
299
306
  aligned offsets, header patched last (peak host transient = one
300
307
  tensor). Header carries `formatVersion`, `modelId`,
@@ -344,10 +351,13 @@ full prefill (GEMV-vs-GEMM reduction order). Both tiers share it.
344
351
  `--memory-budget` is unchanged (admission already assumes worst-case
345
352
  bf16). `--paged-kv` warns that the tier sees nothing (section 6).
346
353
  - **D6 — In-flight resume is explicit.** `--generation-checkpoint <tokens>`
347
- requires `--ssd-cache` and `--batch 1`. It is off by default because each
348
- checkpoint synchronizes and writes the live KV, trading decode speed and
349
- SSD writes for bounded lost work. An identical request resumes; a changed
350
- prompt or sampling policy starts normally.
354
+ requires `--ssd-cache` and supports eligible shared ordinary execution. It
355
+ is off by default. Captured states queue for SSD persistence; an explicit
356
+ flush establishes durability of queued work (section 5.7). An identical request resumes; a changed
357
+ prompt or sampling policy starts normally. Ordinary adapter requests use the
358
+ same continuation hooks under their existing execution context. The checkpoint
359
+ identity includes the ordered adapter IDs and content-revision namespace; a
360
+ base-model request or replacement adapter cannot restore that checkpoint.
351
361
 
352
362
  ### 5.2 Restore is a streamed copy (supersedes the original zero-copy plan)
353
363
 
@@ -392,22 +402,21 @@ Every per-tensor flush step is real GPU-stream and JS-thread work
392
402
  between decode tokens taxed decode at long context, so:
393
403
 
394
404
  - `saveKvCacheAsync` / `SsdCacheStore.storeAsync` accept a per-step runner.
395
- The server passes `gateway.runExclusive`, so the idle decision and the
396
- blocking MLX readback are one atomic engine turn. The earlier
405
+ The server passes `gateway.runWhenIdle`, so the idle decision and the
406
+ blocking MLX readback are one atomic engine turn. Background work waits
407
+ without registering a serial waiter or draining active batches. The earlier
397
408
  `gateway.onIdle()` check had a check/use race: a request could begin after
398
409
  the check and overlap the next tensor sync. A request arriving mid-flush
399
- previously could run between tensor writes. Serial generation now first
400
- drains the durability coordinator outside the engine lock, so pending
401
- snapshot clones release their buffers before the next prefill.
410
+ can run between tensor writes. Neither execution lane drains pending
411
+ durability work before generation.
402
412
  - `GenerationGateway.busy` covers both lanes (serial mutex held/awaited or
403
413
  batch rows active/pending).
404
414
  - `MLX_BUN_SSD_WRITEBEHIND=0` disables write-behind snapshots entirely
405
415
  (kill switch + paired-A/B lever; restart survival then degrades to
406
416
  spill-on-evict).
407
417
 
408
- Serial requests may wait for outstanding snapshots to finish. The drain
409
- runs before acquiring the generation lock because the writer needs that
410
- same lock. Continuous batching retains per-tensor scheduling.
418
+ Explicit flush and shutdown drain outstanding writes outside the generation
419
+ lock because the writer needs that same lock. Ordinary requests do not.
411
420
 
412
421
  The RAM cache's configured byte cap is only an upper bound. Serial serving
413
422
  checks MLX's live allocation count against the smaller of admission's usable
@@ -446,7 +455,7 @@ the write — the server implied durability it did not have.
446
455
  - `schedule(tokens, ns)` records a *dirty* key that includes the exact
447
456
  token sequence (two same-ns, same-length conversations cannot cancel
448
457
  each other) and arms the debounce.
449
- - An attempt checks SSD coverage under `gateway.runExclusive` before cloning.
458
+ - An attempt checks SSD coverage under `gateway.runWhenIdle` before cloning.
450
459
  Otherwise it snapshots the RAM entry (`findExact` + `cloneKvCaches`) and
451
460
  enqueues it; a busy gateway re-arms;
452
461
  a dropped or failed store leaves the key dirty so it is retryable.
@@ -495,13 +504,128 @@ IDs and every sampling/KV policy field. Grammar, token-fill, media, paged KV,
495
504
  and logprob requests are not checkpointed in v1 because they carry additional
496
505
  state or response data that cannot yet be reconstructed by token replay.
497
506
 
507
+ Ordinary continuation adds an optional request port to
508
+ that same ordinary driver. Checkpoint lookup, keys and persistence are shared
509
+ with the compatibility executor; the numerical driver supplies owned row
510
+ snapshots at an emitted-prefix/pending-token boundary. Restored rows complete
511
+ preparation without a target forward, before merging cold prefill rows, and
512
+ recover sampler seed, history and sample index. The server binding qualifies
513
+ ordinary requests through supported row-cache layouts, including recurrent Qwen
514
+ and full-attention Llama/MiniCPM as well as Gemma4. It transfers owned row
515
+ snapshots to a bounded persistence coordinator without awaiting disk in decode.
516
+ Tensor serialization and file maintenance use the existing idle gate. Completion
517
+ invalidates reuse immediately and queues cleanup after any in-flight write;
518
+ new attempts cannot be erased by older cleanup. Cancellation retains eventual
519
+ persistence, and explicit flush reports failed or pending durability. A hard
520
+ process exit may lose queued intervals. Plain greedy retains the ordinary
521
+ vectorized sampler when no history-dependent processors are active. Supported KV layouts retain their existing affine, per-layer and full-attention
522
+ TurboQuant conversion policy. Requests remain without
523
+ media, grammar, fill, paging, logprobs or speculative methods. Native tests
524
+ compare interrupted/restored runs at the same batch geometry; arbitrary
525
+ cohort changes are not an exact numerical identity guarantee. The HTTP fixture
526
+ checks serial B1, shared B1 and shared B4 with identical seeded requests and
527
+ held arrivals, and inspects the generated checkpoint before restart.
528
+ When checkpointing is enabled, the existing total spill queue budget is split
529
+ between prompt spills and continuation snapshots. Flush duration and pending/failed
530
+ statistics cover both queues.
531
+
498
532
  Only the newest successful checkpoint for a request is retained. The old file
499
- remains valid until the new file's fsync+rename completes; a crash mid-write
500
- therefore loses at most one interval. Normal completion removes the checkpoint.
533
+ remains valid until the new file's fsync+rename completes. Queue backpressure
534
+ can leave more than one interval undurable; a crash loses work since the latest
535
+ successful write. Normal completion immediately invalidates the checkpoint and
536
+ queues its disk removal.
501
537
  Client cancellation throws through the serial token callback and preserves the
502
538
  last checkpoint, so terminating a long-lived client does not erase its restart
503
539
  point.
504
540
 
541
+ Fresh-model compiled continuation preserves the device sampler's uint32 pending
542
+ register. The Gemma model materializes its computed FP32 RoPE frequencies once
543
+ before compiled graph capture: a restored request may skip the prefill that
544
+ normally evaluates those constants. Capturing their unevaluated construction
545
+ graph can change arithmetic on M1. The forced-token continuation regression
546
+ compares a warmed model and a fresh model with cloned state, including every
547
+ active cache byte and full logits across delayed KV conversion. Live delayed
548
+ row layouts publish compact state through their extraction interface before
549
+ persistence; they do not need storage codecs for the live wrapper itself.
550
+
551
+ ### Method companion state
552
+
553
+ `PrefixCache<State, Attachment>` is the shared consumer contract for ordinary
554
+ and batched execution. It has no scheduling or storage-tier operations. The
555
+ MLX binding supplies target cache layers plus optional `CheckpointAttachment`
556
+ records containing a method schema, scalar metadata and tensor snapshots.
557
+ The method owns their meaning and alignment; the shared cache owns retention.
558
+ Method and drafter compatibility must be part of the namespace before a
559
+ producer publishes these records. Storage does not interpret draft state as
560
+ extra target-model layers.
561
+
562
+ RAM accounting, eviction, idle demotion and the bounded SSD spill queue include
563
+ companion tensors. Checkpoints containing them require an exact prefix
564
+ boundary and preserve shorter fallback entries. Write-behind captures companion
565
+ views together with target-cache views under the existing exclusive boundary.
566
+ The writer persists them in the same atomic file and the same per-tensor write
567
+ loop. Restore copies and verifies them through the existing tensor reader.
568
+ Ordinary files retain v3 compatibility; older readers reject v4 files rather
569
+ than restore an incomplete checkpoint.
570
+
571
+ The storage implementation is in the working tree. Synthetic gates cover RAM
572
+ ownership, divergent-prefix fallback, SSD eviction, fresh-process restore,
573
+ corruption, queue drops/failures and durability on both development Macs.
574
+ Qwen MTP now uses this interface for prefill snapshots; its private store and
575
+ separate budget option are removed. `DraftSource.checkpoint` supplies the draft
576
+ KV and pending true hidden row. The namespace includes a SHA-256 of draft
577
+ configuration and shard bytes, resolved at provider load, plus the mounted
578
+ adapter revision and KV policy. Model loading resolves that digest through
579
+ `artifactIdentity`, independently of inference and scheduling. A memo key includes
580
+ the configuration seed, sorted shard names, resolved paths, device/inode, size,
581
+ and nanosecond modification/change times. Unchanged revisions reuse the exact
582
+ existing SHA-256; a miss reads every shard byte. Same-size writes with restored
583
+ mtime and changed symlink targets invalidate the memo. Rechecking revisions after
584
+ hashing prevents persisting a digest under a revision that changed during reading.
585
+
586
+ `ArtifactIdentityStore` owns memo retention and persistence. Its file adapter uses
587
+ `~/.cache/mlx-bun/artifact-identities/`, keeps RAM hits and queues atomic file writes
588
+ without awaiting them in model loading. Missing, malformed or unwritable memo
589
+ storage does not prevent computing the identity. It stores only digests, not
590
+ weights, prompts or KV. Qwen MTP, standalone, assistant, DeepSpec and DSpark use
591
+ the same identity service; existing namespaces and SSD entries remain compatible.
592
+ Tests cover byte identity, no weight reads after a fresh-store memo hit, file
593
+ mutation, configuration changes, symlink replacement and write failure.
594
+
595
+ Native bf16/KV4 gates on M1 Max and M4 Pro
596
+ preserve live target/companion state hashes, emitted IDs and acceptance decisions
597
+ through repeated RAM reuse and SSD cache/provider restart. SSD allocation
598
+ padding is excluded from the live-state comparison. Generated-output acceptance
599
+ is described below; long-conversation timing remains R17 work.
600
+
601
+ The unreleased shared method now publishes completed row state before
602
+ retirement through the same `PrefixCache.put` port. It records only inputs
603
+ retained by committed target/draft rounds; an unprocessed correction or bonus
604
+ token is absent from the key. The target row layout and draft provider supply
605
+ their snapshots independently. Failed and cancelled consumers publish no
606
+ generated checkpoint. Persistence continues through the existing queue.
607
+
608
+ M1 and M4 native bf16, affine KV4 and fused TurboQuant K8/V3 tests cover four active
609
+ rows, early retirement, exact sampled-ID coverage, unchanged retained bytes
610
+ after sibling and follow-up work, shorter-prefix selection after an edit, and
611
+ matching RAM/SSD continuation and acceptance. Existing paired-prefill tests
612
+ also pass. A second native gate records actual sampled IDs for an echo tool
613
+ turn with thinking off/on, passes them through the production output parsers,
614
+ and renders the subsequent tool-result request with the same chat template.
615
+ Both published checkpoints are exact token prefixes of that next request.
616
+ A production HTTP gate now passes on both Macs for bf16, KV4 and fused K8/V3, with
617
+ thinking off/on. The next tool-result request reuses generated content in RAM.
618
+ After a durable flush, a separate server process restores the first turn's
619
+ SSD state and reproduces the same response, logprobs and acceptance. The test
620
+ copies only the first turn's flushed files before running the RAM continuation,
621
+ so no later prefill checkpoint can explain the restart hit. All six cases
622
+ pass, including the actual wire `reasoning` alias and template options.
623
+ The five-file change is adopted alongside the captured prefill-policy fix.
624
+ Native acceptance of that final composition, long saved Kanban replay,
625
+ pressure and complete next-turn timing remain open.
626
+ It does not resolve the older serial prototype's bf16 comparison with fresh
627
+ full prefill, whose operation shapes differ from a generated history.
628
+
505
629
  ### 5.8 Invalidation
506
630
 
507
631
  Compatibility key = `configFingerprint(config)` + `KvScheme.cacheKey` +
@@ -555,10 +679,10 @@ already shipped, so what transfers is: (a) padded-batch waste removal —
555
679
  today's batched buffer width tracks the longest live row
556
680
  (`mergeKVRows`/`extendKVRows`), so a short row cohabiting with a long one
557
681
  pays the long one's KV; and (b) block-level copy-on-write prefix sharing.
558
- Neither is in v1. **At batch=1 paging buys nothing** — gather-before-attend
559
- is bandwidth tax on a bandwidth-bound decode. v1 ships the
560
- correctness-proven block-manager abstraction at serial scope as
561
- scaffolding for the follow-ups, not as a speed win.
682
+ Persistent pools now stay separate per request under the shared executor.
683
+ Attention still gathers and pads a temporary batch tensor; block-level prefix
684
+ sharing and direct paged attention remain open. At batch one, the gather adds
685
+ a copy. Paging remains optional and does not imply a speed improvement.
562
686
 
563
687
  This is neither of the two paging rejections already on record:
564
688
  paged KV as a *prompt-cache substitute* (rejected 2026-07-07 — `take()`'s
@@ -584,8 +708,8 @@ greedy trajectory paged-on vs paged-off).
584
708
  `[block, 0, within, 0]` — the incoming `[1,H,l,D]` piece matches the
585
709
  destination slice directly.
586
710
  - **`PagedKVCache implements Cache`** — standalone (not a `KVCache`
587
- subclass, the TurboQuant reasoning: every `instanceof` gate excludes it
588
- and routes to the monolith forward). `updateAndFetch` splits the
711
+ subclass). The cache-layout binding supplies `PagedKvRows` for dynamic
712
+ membership, masks and per-row positions. `updateAndFetch` splits the
589
713
  incoming `L` along block boundaries, allocates tail blocks as reached,
590
714
  then gathers occupied blocks with `ops.takeAxis(pool, blockTable, 0)` →
591
715
  transpose `[1,0,2,3]` → reshape `[1,H,nb·bs,D]` → slice to `offset`. Only
@@ -599,7 +723,8 @@ greedy trajectory paged-on vs paged-off).
599
723
  prompt + `max_tokens`, so exhaustion is unreachable absent an
600
724
  accounting bug — it exists as a tripwire, never silent truncation.
601
725
  - **Wiring** — `maybePageKv(cache, options, capacityTokens)`
602
- (`generate.ts`) mirrors `maybeQuantizeKv`'s post-construction in-place
726
+ (`backends/mlx/request-state-policy.ts`, re-exported by `generate.ts`)
727
+ mirrors `maybeQuantizeKv`'s post-construction in-place
603
728
  swap but runs *once before prefill*: paging changes layout, not
604
729
  arithmetic, so there is no "convert when populated" trigger. Only fresh
605
730
  (`offset === 0`) plain `KVCache` entries are replaced; rotating layers
@@ -610,32 +735,34 @@ greedy trajectory paged-on vs paged-off).
610
735
 
611
736
  ### 6.3 Gates (explicit refusals, never silent downgrades)
612
737
 
613
- - Startup (`server.ts`): `--paged-kv` with `--batch N>1`, any `--kv-quant`
738
+ - Startup (`server.ts`): `--paged-kv` with any `--kv-quant`
614
739
  (affine or TurboQuant), `--draft-model`, or a non-`gemma4*` model exits
615
740
  with a clear message; `--paged-kv-block-size` must be a positive
616
741
  integer; `--ssd-cache` alongside it warns that the tier sees nothing.
617
- With no explicit `--batch`, the CLI pins `--batch 1` (default is 8) and
618
- says so. Env `MLX_BUN_PAGED_KV=1` is equivalent to the flag.
742
+ Paging preserves the selected batch size. Env `MLX_BUN_PAGED_KV=1`
743
+ is equivalent to the flag.
619
744
  - Request scope: media (vision/audio) and LoRA-adapter requests strip the
620
745
  flag and run the plain cache path (v1 non-goal cells, never a 400). One
621
746
  effective `pagedKv` value per request keeps the strip and the
622
747
  prompt-cache bypass coherent.
623
- - Serial lane: paged requests skip `take`/`put` and the boundary
748
+ - Both execution modes: paged requests skip `take`/`put` and the boundary
624
749
  snapshot; caches are disposed on completion. Spec eligibility also
625
750
  excludes `options.pagedKv` (belt on top of the startup refusal).
626
751
  - Compiled decode: `CompiledDecode.supports()` excludes `PagedKVCache`
627
752
  automatically (not one of the four supported classes) — a
628
753
  data-dependent block-list length is the shape shapeless replay already
629
754
  broke on.
630
- - Batch lane: unreachable by construction (startup pins serial); the
631
- `RequestShape` gate to add when batched paging lands is noted here.
755
+ - Shared execution: `PagedKvRows` owns independent block pools and immutable
756
+ row snapshots. A request-state policy creates the storage and supplies no
757
+ reusable-prefix store. Its opaque compatibility key keeps incompatible
758
+ layouts in separate cohorts without making scheduling inspect paging flags.
632
759
 
633
760
  ### 6.4 Follow-ups (dependency order) and open items
634
761
 
635
- 1. Batched integration — `PagedKVCache` into the scheduler's inner union;
636
- block-count admission replacing byte projection; join/filter/extract
637
- allocating and freeing blocks instead of pad+concat. This is where the
638
- padded-batch-waste win lives. Tracked under the plan anchor (Phase 18
762
+ 1. Direct paged attention and block sharing — row integration now uses the
763
+ shared layout interface with independent per-request pools. Attention still
764
+ gathers padded contiguous inputs. Measure block-level shared arenas and
765
+ direct reads before replacing this compatibility implementation. Tracked under the plan anchor (Phase 18
639
766
  S3+).
640
767
  2. Block-level CoW prefix sharing — refcounted block table, fork on
641
768
  divergent write; extends `PromptCache`'s entry-level ref-counting down
@@ -678,3 +805,20 @@ greedy trajectory paged-on vs paged-off).
678
805
  - Paged KV v1 (serial, Gemma4, bf16, gather-then-SDPA) landed with
679
806
  bit-exact gates; flag rows in cli.md / server-config.md (landing date
680
807
  not recorded in the source doc, which still reads "in progress").
808
+
809
+ ## Minimum reusable offsets
810
+
811
+ Physical trimmability does not prove that an older prefix has a valid
812
+ numerical representation. Caches may expose `minimumReusableOffset` after
813
+ an irreversible transition, such as delayed TurboQuant conversion. Prefix
814
+ selection requires the matched offset to meet every layer's bound. A
815
+ trimmable descendant supersedes an ancestor only when it can serve that
816
+ ancestor's offset; otherwise the earlier donor remains.
817
+
818
+ Cloning and per-row extraction preserve this state metadata. New SSD
819
+ headers record it, and indexing, durable-prefix checks and restore use the
820
+ same rule as RAM. Legacy TurboQuant headers without a recorded boundary
821
+ are conservatively reusable from their full stored offset only. Other
822
+ legacy cache kinds retain their existing interpretation. This changes
823
+ reuse eligibility, not scheduling, quantization arithmetic or memory
824
+ admission policy.