mlx-bun 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +58 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  5. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  6. package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
  7. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  8. package/docs/design/batching.md +1535 -50
  9. package/docs/design/decode-speed-program.md +394 -1171
  10. package/docs/design/generic-model-support.md +195 -3
  11. package/docs/design/kv-cache.md +538 -225
  12. package/docs/design/speculative-decoding.md +552 -50
  13. package/docs/design/turboquant.md +140 -17
  14. package/docs/design/unified-engine-frontier-plan.md +259 -150
  15. package/docs/planning/PRODUCT_ROADMAP.md +21 -6
  16. package/docs/planning/public-readiness-handoff.md +244 -0
  17. package/docs/planning/public-readiness.md +149 -0
  18. package/docs/reference/benchmarks.md +3537 -1112
  19. package/docs/reference/cli.md +102 -2
  20. package/docs/reference/distribution.md +45 -13
  21. package/docs/reference/environment.md +159 -13
  22. package/docs/reference/library-api.md +18 -4
  23. package/docs/reference/memory.md +1 -1
  24. package/docs/reference/models.md +28 -7
  25. package/docs/reference/server-api.md +257 -33
  26. package/docs/reference/server-config.md +372 -151
  27. package/docs/reference/troubleshooting.md +13 -21
  28. package/package.json +2 -2
  29. package/src/audio/audiotoolbox.ts +112 -0
  30. package/src/audio/conformer.ts +8 -0
  31. package/src/audio/mic-capture.ts +98 -0
  32. package/src/audio/silero-vad.ts +379 -0
  33. package/src/audio/whisper-decode.ts +774 -0
  34. package/src/audio/whisper-fast.ts +732 -0
  35. package/src/audio/whisper-format.ts +73 -0
  36. package/src/audio/whisper-mel.ts +185 -0
  37. package/src/audio/whisper-timing.ts +274 -0
  38. package/src/audio/whisper-tokenizer.ts +239 -0
  39. package/src/audio/whisper-transcribe.ts +410 -0
  40. package/src/backends/mlx/assistant-target.ts +46 -0
  41. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  42. package/src/backends/mlx/autoregressive.ts +22 -1
  43. package/src/backends/mlx/batch-group.ts +464 -236
  44. package/src/backends/mlx/cache-attachment.ts +67 -0
  45. package/src/backends/mlx/cache-layout.ts +31 -0
  46. package/src/backends/mlx/checkpoint-state.ts +53 -0
  47. package/src/backends/mlx/continuation-persistence.ts +107 -0
  48. package/src/backends/mlx/continuation-request.ts +43 -0
  49. package/src/backends/mlx/continuation.ts +77 -0
  50. package/src/backends/mlx/draft-target.ts +7 -22
  51. package/src/backends/mlx/encoder-cache.ts +25 -0
  52. package/src/backends/mlx/fill-append.ts +21 -8
  53. package/src/backends/mlx/fill-group.ts +497 -0
  54. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  55. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  56. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  57. package/src/backends/mlx/gateway-binding.ts +122 -19
  58. package/src/backends/mlx/kv-codec.ts +9 -0
  59. package/src/backends/mlx/kv-maintenance.ts +161 -0
  60. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  61. package/src/backends/mlx/mixed-iteration.ts +62 -0
  62. package/src/backends/mlx/model-prompt.ts +46 -32
  63. package/src/backends/mlx/model-serving.ts +5 -4
  64. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  65. package/src/backends/mlx/prefill-cohort.ts +100 -0
  66. package/src/backends/mlx/prefill-policy.ts +26 -0
  67. package/src/backends/mlx/prefill-rows.ts +244 -0
  68. package/src/backends/mlx/prefill.ts +1 -1
  69. package/src/backends/mlx/projected-context-rows.ts +72 -0
  70. package/src/backends/mlx/prompt-input.ts +29 -0
  71. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  72. package/src/backends/mlx/request-state-policy.ts +54 -0
  73. package/src/backends/mlx/rollback.ts +45 -4
  74. package/src/backends/mlx/row-sampling.ts +49 -0
  75. package/src/backends/mlx/serial-executor.ts +51 -73
  76. package/src/backends/mlx/speculative-group.ts +438 -0
  77. package/src/backends/mlx/speculative-round.ts +161 -0
  78. package/src/backends/mlx/speculative.ts +24 -7
  79. package/src/backends/mlx/state-rows.ts +104 -0
  80. package/src/backends/mlx/state-views.ts +21 -1
  81. package/src/backends/mlx/target-layout-capability.ts +35 -0
  82. package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
  83. package/src/cli.ts +333 -36
  84. package/src/contracts/execution.ts +21 -0
  85. package/src/contracts/object-cache.ts +8 -0
  86. package/src/contracts/pi-provider.ts +1 -1
  87. package/src/contracts/pi.ts +3 -1
  88. package/src/contracts/prefix-cache.ts +33 -0
  89. package/src/contracts/resources.ts +7 -0
  90. package/src/contracts/sampling.ts +23 -0
  91. package/src/contracts/scheduling.ts +27 -0
  92. package/src/engine/execution-plan.ts +38 -25
  93. package/src/engine/execution-tasks.ts +44 -0
  94. package/src/engine/namespaced-cache.ts +13 -0
  95. package/src/engine/resources.ts +12 -1
  96. package/src/engine/scheduler.ts +32 -7
  97. package/src/fill/fill-session.ts +14 -3
  98. package/src/fit.ts +29 -15
  99. package/src/generate.ts +105 -195
  100. package/src/grammar.ts +13 -0
  101. package/src/inference/draft-acceptance.ts +41 -0
  102. package/src/inference/draft-output.ts +49 -0
  103. package/src/inference/prefill.ts +6 -0
  104. package/src/inference/rollback.ts +2 -2
  105. package/src/kv-scheme.ts +92 -12
  106. package/src/kv-store.ts +183 -93
  107. package/src/lab/README.md +5 -1
  108. package/src/lab/paged-kv/paged-attention.ts +117 -0
  109. package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
  110. package/src/lab/paged-kv/paged-kv.ts +126 -46
  111. package/src/md-modules.d.ts +5 -0
  112. package/src/mlx/array.ts +39 -1
  113. package/src/mlx/ffi.ts +19 -0
  114. package/src/mlx/normalized-argmax.ts +116 -0
  115. package/src/mlx/ops.ts +97 -10
  116. package/src/model/artifact-identity.ts +73 -0
  117. package/src/model/batched-kv.ts +235 -0
  118. package/src/model/batched-mask.ts +12 -4
  119. package/src/model/batched-quant.ts +8 -2
  120. package/src/model/batched-quantized-kv.ts +144 -0
  121. package/src/model/batched-rotating-quant.ts +76 -15
  122. package/src/model/batched-rotating-state.ts +76 -0
  123. package/src/model/batched-rotating.ts +64 -22
  124. package/src/model/batched-row-storage.ts +52 -0
  125. package/src/model/batched-ssm.ts +52 -0
  126. package/src/model/batched-turboquant-kv.ts +94 -0
  127. package/src/model/compiled-decode.ts +7 -1
  128. package/src/model/decoded-kv-donor.ts +12 -0
  129. package/src/model/delayed-quantized-kv.ts +72 -0
  130. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  131. package/src/model/delayed-turboquant-kv.ts +57 -0
  132. package/src/model/factory.ts +26 -0
  133. package/src/model/fingerprint.ts +12 -0
  134. package/src/model/full-prefill-padding.ts +56 -0
  135. package/src/model/full-prefill-row.ts +105 -0
  136. package/src/model/gemma4-base.ts +183 -82
  137. package/src/model/gemma4.ts +116 -28
  138. package/src/model/generated/gemma4-12b.ts +4 -1
  139. package/src/model/generated/gemma4-26b.ts +4 -1
  140. package/src/model/generated/gemma4-e4b.ts +4 -1
  141. package/src/model/glm52-cache.ts +80 -4
  142. package/src/model/glm52-mla.ts +12 -6
  143. package/src/model/glm52.ts +19 -40
  144. package/src/model/kv-attention-view.ts +70 -0
  145. package/src/model/minicpm5.ts +11 -7
  146. package/src/model/profile.ts +29 -4
  147. package/src/model/quantized-append-attention.ts +31 -0
  148. package/src/model/qwen3-delta-state.ts +74 -0
  149. package/src/model/qwen3-delta.ts +59 -36
  150. package/src/model/qwen3-mrope.ts +7 -9
  151. package/src/model/qwen3_5.ts +177 -75
  152. package/src/model/rotating-kv-donor.ts +35 -0
  153. package/src/model/rotating-kv-layout.ts +262 -0
  154. package/src/model/rotating-row-transaction.ts +35 -0
  155. package/src/model/speculative-rotating-kv.ts +101 -0
  156. package/src/model/ssm-prefill-padding.ts +39 -0
  157. package/src/model/support.ts +8 -0
  158. package/src/model/token-groups.ts +66 -0
  159. package/src/model/trellis-linear.ts +4 -4
  160. package/src/model/turboquant-codec.ts +89 -0
  161. package/src/model/universal/dense.ts +14 -13
  162. package/src/model/whisper.ts +435 -0
  163. package/src/native/mic_capture.swift +92 -0
  164. package/src/native-pack.ts +6 -5
  165. package/src/pi-web.ts +9 -1
  166. package/src/preflight.ts +31 -15
  167. package/src/prompt-cache.ts +306 -86
  168. package/src/runtime-config.ts +1 -5
  169. package/src/safetensors.ts +9 -2
  170. package/src/sampler.ts +37 -25
  171. package/src/serve/audio-routes.ts +323 -0
  172. package/src/serve/chat-request.ts +2 -0
  173. package/src/serve/chat-stage.ts +19 -14
  174. package/src/serve/completion-executor.ts +9 -1
  175. package/src/serve/discovery-routes.ts +17 -1
  176. package/src/serve/generated-token-history.ts +57 -0
  177. package/src/serve/generation-gateway.ts +88 -38
  178. package/src/serve/model-binding.ts +7 -1
  179. package/src/serve/model-host.ts +3 -4
  180. package/src/serve/preparation.ts +3 -3
  181. package/src/serve/prompt-response-trace.ts +14 -0
  182. package/src/serve/request-plan.ts +6 -3
  183. package/src/serve/request-prep.ts +40 -25
  184. package/src/serve/text-completion-stage.ts +2 -2
  185. package/src/serve/transcription-server.ts +64 -0
  186. package/src/serve/transcription-service.ts +502 -0
  187. package/src/server.ts +185 -261
  188. package/src/spec/assistant-rows.ts +93 -0
  189. package/src/spec/assistant-source.ts +28 -18
  190. package/src/spec/cache-identity.ts +22 -0
  191. package/src/spec/deepspec-rows.ts +29 -0
  192. package/src/spec/deepspec-source.ts +12 -10
  193. package/src/spec/dflash-source.ts +28 -4
  194. package/src/spec/drafter.ts +153 -142
  195. package/src/spec/dspark/deepspec-module.ts +95 -120
  196. package/src/spec/dspark/module-dflash.ts +126 -170
  197. package/src/spec/glm52-mtp-graph.ts +29 -0
  198. package/src/spec/glm52-mtp-rows.ts +132 -0
  199. package/src/spec/glm52-mtp-source.ts +54 -136
  200. package/src/spec/glm52-mtp-state.ts +33 -0
  201. package/src/spec/ngram-source.ts +115 -21
  202. package/src/spec/projected-draft-rows.ts +73 -0
  203. package/src/spec/qwen-mtp-module.ts +171 -0
  204. package/src/spec/qwen-mtp-rows.ts +230 -0
  205. package/src/spec/qwen-mtp-source.ts +84 -226
  206. package/src/spec/qwen-mtp-state.ts +30 -0
  207. package/src/spec/serve-loop.ts +73 -69
  208. package/src/spec/source.ts +93 -15
  209. package/src/spec/two-model-rows.ts +62 -0
  210. package/src/spec/two-model.ts +45 -103
  211. package/src/ssd-cache.ts +107 -36
  212. package/src/ssd-durability.ts +29 -27
  213. package/src/storage/host-buffer.ts +24 -0
  214. package/src/storage/kv-writer.ts +83 -0
  215. package/src/storage/kv-writer.worker.js +264 -0
  216. package/src/storage/retention-policy.ts +37 -0
  217. package/src/tiered-prompt-cache.ts +45 -0
  218. package/src/vision/embedder.ts +8 -0
  219. package/src/vision/prompt.ts +101 -65
  220. package/src/vision/qwen3vl-prompt.ts +101 -64
  221. package/src/vision/qwen3vl-tower.ts +11 -0
  222. package/src/vision/siglip.ts +8 -0
  223. package/src/web/app.html +12 -0
  224. package/src/web/app.js +217 -57
  225. package/src/web/src/chat.ts +4 -1
  226. package/src/web/src/composer.ts +3 -0
  227. package/src/web/src/voice.ts +141 -0
  228. package/src/web/sw.js +1 -1
  229. package/src/backends/mlx/memory-guard.ts +0 -40
  230. package/src/spec/prefix-state.ts +0 -45
  231. /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
@@ -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-12
7
7
  ---
8
8
 
9
9
  # KV cache — residency and layout
@@ -24,29 +24,32 @@ and open items.
24
24
  Code: `src/model/gemma4-base.ts` (Cache interface + the K/V cache family),
25
25
  `src/model/batched-*.ts`, `src/lab/paged-kv/paged-kv.ts`, `src/prompt-cache.ts`,
26
26
  `src/kv-store.ts`, `src/ssd-cache.ts`, `src/ssd-durability.ts`,
27
+ `src/tiered-prompt-cache.ts`, `src/storage/kv-writer.ts`,
27
28
  `src/kv-scheme.ts`, `src/generate.ts` (`maybeQuantizeKv` / `maybePageKv`),
28
29
  `src/server.ts` (wiring, `/stats`, `POST /admin/cache/flush`),
29
30
  `src/serve/generation-gateway.ts`, `src/serve/batch-scheduler.ts`.
30
31
 
31
32
  ## 1. Layering
32
33
 
33
- Residency is a three-tier stack, each tier keyed by the exact token prefix
34
- its KV encodes:
34
+ Active generation owns mutable cache containers. Published checkpoints belong
35
+ to one logical cache, with RAM and SSD storage. On Apple Silicon the GPU and
36
+ CPU access unified memory; publication does not require a GPU-to-CPU copy.
37
+ Each checkpoint is keyed by the exact token prefix its state encodes:
35
38
 
36
39
  | tier | owner | unit | key |
37
40
  |---|---|---|---|
38
41
  | live caches | one generation (serial lane or a batch row) | `Cache[]`, one per layer | — |
39
- | RAM prompt cache | `PromptCache` (`src/prompt-cache.ts`) | whole prefix entry (tokens + `Cache[]`) | tokens + adapter ns |
42
+ | RAM prompt cache | `TieredPromptCache` over `PromptCache` | whole prefix entry (tokens + `Cache[]`) | tokens + adapter ns |
40
43
  | SSD cold tier | `SsdCacheStore` (`src/ssd-cache.ts`) over `src/kv-store.ts` files | one `.mlxkv` file per entry | tokens + ns + model/scheme/tokenizer fingerprint |
41
44
 
42
45
  The prompt cache tiers over the SSD store *inside* `take()` (structural
43
46
  `ColdTier` interface), so every consumer (serial lane, batch scheduler
44
47
  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
48
+ calls. The cache queues persistence on publication or eviction. A dedicated
49
+ CPU worker writes evaluated immutable buffers while inference continues.
50
+ Persistence does not evict RAM; only cache residency policy does. RAM reuse
51
+ does not enqueue an extra write or wait 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
52
+ persistence without awaiting disk in decode (section 5.7). Paged KV
50
53
  (section 6) is a *layout* choice for the live tier and, in v1, bypasses the
51
54
  other two.
52
55
 
@@ -114,10 +117,10 @@ Two independent checks decide whether a model/scheme can batch
114
117
  `KVCache`, `RotatingKVCache`, `BatchableCache`, or (unless
115
118
  `MLX_BUN_BATCH_SSM=0`) `SSMCache`. Novel classes (TurboQuant, paged) fail
116
119
  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
120
+ - `#kvBatchable()` — `KvScheme.batchable(config, canConvert)`: per-layer
121
+ `affine-config` and uniform affine KV4/KV8 batch when their cache-owning
122
+ layers can convert. TurboQuant still requires merged-row layout support;
123
+ a scheme-less gateway must never silently
121
124
  drop quantization (`kv-scheme.ts` makes the capability probe mandatory
122
125
  for quantized schemes).
123
126
 
@@ -210,34 +213,48 @@ upstream is byte-capped now.
210
213
  **`take(prompt, ns)` — prefix sharing.** Finds the entry with the longest
211
214
  usable common prefix (usable = common prefix capped at `prompt.length - 1`,
212
215
  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.
216
+ trimmable). Every RAM hit lends zero-copy views (`cloneKvCaches` and
217
+ `cloneAttachments`) with a ref-counted backing share; the donor remains in
218
+ place. This includes exact-boundary recurrent and method state. Only
219
+ trimmable state may be shortened. Decode extends its own views without
220
+ changing the retained checkpoint.
221
+
222
+ SSD is an eventually consistent persistence layer. RAM reuse never writes
223
+ to SSD or waits for durability. Background writes retain immutable snapshot
224
+ views while a CPU worker packs, hashes and writes them; explicit flush and
225
+ graceful shutdown wait for persistence. A pending or failed write does not prevent RAM reuse.
226
+ The byte target controls LRU eviction. With SSD enabled, an unwritten victim
227
+ stays resident until storage commits; the target is soft while writes are
228
+ pending or failing.
229
+
230
+ The native sharing gate checks logical checkpoint bytes after repeated decode
231
+ and after SSD restore. It passes on both development Macs; the M1 long-prefix
232
+ case also preserves three independent 512-token continuations. The queued
233
+ writer test retains snapshot state after both donor and borrower disposal,
234
+ and explicit flush remains pending until the write completes.
223
235
 
224
236
  **Tier order inside `take()`.** The cold tier is consulted with
225
237
  `find(prompt, ns)` (index-only, no I/O) and wins only with a *strictly*
226
238
  longer usable prefix; then `restore(handle)` materializes it and the
227
239
  divergent tail is trimmed. A cold entry that would need a trim but is
228
240
  untrimmable (wrapped ring, SSM) is disposed loudly and the RAM candidate
229
- (or a fresh prefill) serves. `hits`/`misses` count RAM candidacy only.
241
+ (or a fresh prefill) serves. Tiered restore immediately publishes shared views
242
+ in RAM so another request can reuse them before the borrower finishes.
243
+ `hits`/`misses` count RAM candidacy only.
230
244
 
231
245
  **`put(tokens, caches, ns, retain)` — supersession + eviction.** Exact
232
246
  duplicates (same tokens) are replaced regardless of trimmability. Strict
233
247
  prefix-ancestors in the same ns are superseded without spill — but only
234
248
  when the *new* entry is fully trimmable; an untrimmable new entry (wrapped
235
249
  ring) can only serve exact-length matches, so shorter ancestors (the
236
- prompt-boundary snapshot) must survive. Then LRU-evict until under cap,
237
- spilling each evictee to the cold tier first. An entry larger than the cap
238
- is spilled and disposed, never stored. `onPut` fires after every
239
- successful put — the server hangs the write-behind scheduler there so both
240
- lanes' entries persist.
250
+ prompt-boundary snapshot) must survive. `TieredPromptCache` selects the
251
+ least recently used RAM entry when over budget. If it lacks durable SSD
252
+ coverage, the cache queues a write and retains it until that write succeeds.
253
+ It does not evict a hotter entry merely because that entry is already on SSD.
254
+ An oversized checkpoint follows the same persist-before-demotion rule.
255
+ Without SSD, the RAM-only cache discards evicted or oversized entries.
256
+ `TieredPromptCache.put` also owns proactive persistence; `onPut` remains an
257
+ independent observer for generated-token history.
241
258
 
242
259
  **Prompt-boundary snapshot.** Serial lane (`server.ts`) and batch
243
260
  scheduler (`snapshotAt`) both clone-and-put a trim-free *prompt-only*
@@ -251,12 +268,11 @@ always an exact prefix of the next rendering.
251
268
  **Namespaces.** `ns` = the adapter spec (`adapters.join("+")`, `""` = base).
252
269
  KV computed under one adapter never seeds another's prefill.
253
270
 
254
- **Bypasses.** Media (vision/audio) requests skip the cache (soft tokens are
255
- identical placeholder ids → false hits), as do paged-KV requests (v1) and
256
- the speculative lane (v1). Merged batch rows are not re-put on finish;
257
- never-merged lone rows put their adopted caches back zero-copy, and
258
- merged rows are extracted per row (`#extractAndPut`) before `filter()`
259
- mutates the batched inners.
271
+ **Bypasses.** Media and optional paged-KV requests retain their documented
272
+ cache qualification rules. Shared speculative execution publishes target and
273
+ method state together where supported; method attachments preserve the exact
274
+ boundary needed for reuse. Merged batch rows are extracted before retirement
275
+ changes the batched state. The cache owns the extracted checkpoint.
260
276
 
261
277
  **Idle demotion (`demoteIdle`).** Entries unused past a threshold spill to
262
278
  the cold tier and free their GPU arrays; the prefix stays reachable via
@@ -281,20 +297,18 @@ full prefill (GEMV-vs-GEMM reduction order). Both tiers share it.
281
297
 
282
298
  ### 5.1 Decisions
283
299
 
284
- - **D1 — Whole-prefix-entry spill, not paged blocks.** Single-user serving
285
- with a prefix-entry cache that grows per conversation. Per-block hashing
286
- and save-queue bookkeeping during decode is where oMLX's measured ~20%
287
- steady-state tax came from; ours does zero work on the token loop
288
- (prefix tokens are the key). Partial-prefix reuse works without paging:
289
- restore + `trim()`. The economics that justify the tier are
290
- SSD-vs-recompute, not SSD-vs-RAM: at 12B prefill rates a 30k agent
291
- context is minutes of GPU compute against a ~1 s NVMe restore.
292
- - **D2 — One store, two tiers.** The cold tier is bound *into*
293
- `PromptCache` (section 4); restart persistence comes from a debounced
294
- write-behind snapshot after each put, not only from spill-on-evict.
300
+ - **D1 — Whole-prefix-entry storage.** A checkpoint stores a complete
301
+ conversation boundary. This reuses the existing cache codecs and avoids
302
+ per-token storage bookkeeping, but duplicates shared attention history
303
+ between files. Block deduplication is a separate layout optimization to
304
+ measure against this baseline; recurrent state still needs exact boundaries.
305
+ - **D2 — One store, two tiers.** `TieredPromptCache` owns RAM residency and
306
+ the SSD backend. Proactive persistence after publication provides restart
307
+ survival without waiting for an eviction.
295
308
  - **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
309
+ writes `MLXBUNKV2\n`-magic files with a streaming writer: ordinary KV
310
+ retains format v3; checkpoints with method companion state use v4.
311
+ The header is sized up front with fixed-width tensor
298
312
  hashes, tensors materialized/hashed/written one at a time at 16 KiB
299
313
  aligned offsets, header patched last (peak host transient = one
300
314
  tensor). Header carries `formatVersion`, `modelId`,
@@ -344,10 +358,13 @@ full prefill (GEMV-vs-GEMM reduction order). Both tiers share it.
344
358
  `--memory-budget` is unchanged (admission already assumes worst-case
345
359
  bf16). `--paged-kv` warns that the tier sees nothing (section 6).
346
360
  - **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.
361
+ requires `--ssd-cache` and supports eligible shared ordinary execution. It
362
+ is off by default. Captured states queue for SSD persistence; an explicit
363
+ flush establishes durability of queued work (section 5.7). An identical request resumes; a changed
364
+ prompt or sampling policy starts normally. Ordinary adapter requests use the
365
+ same continuation hooks under their existing execution context. The checkpoint
366
+ identity includes the ordered adapter IDs and content-revision namespace; a
367
+ base-model request or replacement adapter cannot restore that checkpoint.
351
368
 
352
369
  ### 5.2 Restore is a streamed copy (supersedes the original zero-copy plan)
353
370
 
@@ -367,9 +384,9 @@ ordering is unchanged (the server's `coldTier.restore` returns
367
384
  `() => {}`). Comments in `prompt-cache.ts` that still say "zero-copy
368
385
  mmap" describe the superseded mechanism.
369
386
 
370
- The write side is the mirror: hashing and writing from a zero-copy view
371
- of the contiguous mlx buffer (`rawBytesView`), no JS-heap copy at any
372
- point.
387
+ The asynchronous writer reads evaluated native storage directly. Contiguous
388
+ tensors need no packing copy. Strided tensors are packed on the CPU, one
389
+ tensor at a time, to omit capacity padding without a GPU `contiguous` call.
373
390
 
374
391
  ### 5.3 `find()` returns partial matches, gated on trimmability
375
392
 
@@ -384,57 +401,52 @@ Store-side supersession mirrors `PromptCache.put`: exact duplicates
384
401
  replaced; prefix-ancestors superseded only when the new entry is
385
402
  trimmable.
386
403
 
387
- ### 5.4 Write-behind scheduling contract: own each tensor step
388
-
389
- Every per-tensor flush step is real GPU-stream and JS-thread work
390
- (`ops.contiguous` on the decode stream, a synchronous eval for
391
- `rawBytesView`, a synchronous multi-MB `writeSync`). Interleaving those
392
- between decode tokens taxed decode at long context, so:
393
-
394
- - `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
397
- `gateway.onIdle()` check had a check/use race: a request could begin after
398
- 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.
402
- - `GenerationGateway.busy` covers both lanes (serial mutex held/awaited or
403
- batch rows active/pending).
404
- - `MLX_BUN_SSD_WRITEBEHIND=0` disables write-behind snapshots entirely
405
- (kill switch + paired-A/B lever; restart survival then degrades to
406
- spill-on-evict).
407
-
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.
411
-
412
- The RAM cache's configured byte cap is only an upper bound. Serial serving
413
- checks MLX's live allocation count against the smaller of admission's usable
414
- memory and Metal's recommended working set, reserving prefill workspace and
415
- the prompt's KV footprint for growth/copy-on-write overlap. Pressure relief
416
- persists LRU entries synchronously before disposing them, avoiding deferred
417
- spill clones that would keep the memory resident. The selected request cache
418
- remains owned by the request; older entries remain reusable from SSD. Qwen
419
- checks between prefill layers, and serial decode checks every 256 tokens.
420
- If no reclaimable entries remain, the guard raises a request error instead
421
- of intentionally proceeding over that budget. In-flight checkpoints survive.
422
-
423
- ### 5.5 Bounded retention: `SpillQueue` (`kv-store.ts`)
424
-
425
- All three producers — eviction spills, idle demotions, write-behind
426
- snapshots — go through one serial, byte-capped queue. Pending clones pin
427
- their entries' GPU buffers until the generation-locked flush gets a turn; without
428
- a cap, sustained traffic (gate starved, evictions ongoing) made resident
429
- memory = prompt-cache cap + every queued clone. Policy: cap default 2 GiB
430
- (`MLX_BUN_SSD_SPILL_QUEUE_GB`; `0` keeps only the newest + in-flight
431
- clone); over cap the *oldest* not-in-flight item drops and its clones are
432
- disposed immediately; the item just enqueued is never its own victim
433
- (soft cap). A dropped spill is a future cache miss, never a wrong result.
434
- Clones are disposed on every settle path (fulfil, reject, drop). The
435
- snapshot timer never holds batch admission: while rows are active it
436
- re-arms instead of grabbing the exclusive (entries are immutable; a late
437
- snapshot is equally valid).
404
+ ### 5.4 Background persistence and RAM residency
405
+
406
+ `TieredPromptCache` owns RAM residency, SSD writes and durable-coverage checks.
407
+ `put` materializes the checkpoint before publication. `saveKvCacheAsync`
408
+ uses the existing codecs to describe the file and obtains host-visible native
409
+ pointers, shapes and strides on the MLX owner thread. Its optional preparation
410
+ runner does not cover disk I/O. No MLX handles cross into the writer.
411
+
412
+ `KvWriter` embeds a dedicated Bun CPU worker in source and compiled builds.
413
+ The worker packs strided layouts, hashes tensor bytes, writes a temporary file,
414
+ fsyncs it and renames it atomically. It imports no MLX or scheduler code and
415
+ needs no generation lock. Contiguous tensors use direct buffer views; packing
416
+ scratch is reused across strided tensors. The cache owns the retained state,
417
+ and the queued write holds shared views through its completion. Those views
418
+ share immutable storage rather than copying the entire checkpoint.
419
+
420
+ The owner thread still prepares native views and updates the SSD metadata
421
+ index after completion. A background worker removes blocking payload I/O from
422
+ that thread; it does not make shared memory bandwidth or snapshot preparation
423
+ free. Measure those costs with concurrent decode before claiming a speedup.
424
+
425
+ Writing a copy does not remove RAM residency. Byte pressure chooses the actual
426
+ LRU entry. If its SSD copy is missing, it queues persistence and waits for the
427
+ completion callback to retry eviction. Failed writes leave the RAM entry
428
+ usable and dirty. Age-based eviction follows the same rule and runs without
429
+ acquiring the scheduler's generation lock. Neither policy refuses requests or
430
+ changes context, batch size or sampling settings.
431
+
432
+ `MLX_BUN_SSD_WRITEBEHIND=0` disables proactive persistence after publication;
433
+ eviction still queues persistence before removing RAM. Explicit flush and
434
+ shutdown await outstanding writes. A hard exit can lose uncommitted writes.
435
+
436
+ ### 5.5 Persistence queue and capacity
437
+
438
+ Normal prefix persistence uses one serial `SpillQueue` with no independent
439
+ byte-drop policy. RAM ownership and pending write views share the checkpoint;
440
+ queue completion releases only the writer's reference. RAM cache capacity
441
+ and age policy decide demotion. They do not cancel an unwritten snapshot to
442
+ reduce the queue's accounting. While the oldest victim is writing, RAM can
443
+ exceed its target. A storage failure keeps that victim resident and retryable.
444
+
445
+ The optional interrupted-generation coordinator still uses the generic
446
+ bounded queue for supersedable intervals. `MLX_BUN_SSD_SPILL_QUEUE_GB`
447
+ (default 2 GiB) now controls that queue only. It does not limit retained prompt
448
+ history. SSD capacity is independent: no capacity eviction without an explicit
449
+ positive `--ssd-cache-max`.
438
450
 
439
451
  ### 5.6 Durability boundary (`src/ssd-durability.ts`)
440
452
 
@@ -445,17 +457,16 @@ the write — the server implied durability it did not have.
445
457
 
446
458
  - `schedule(tokens, ns)` records a *dirty* key that includes the exact
447
459
  token sequence (two same-ns, same-length conversations cannot cancel
448
- each other) and arms the debounce.
449
- - An attempt checks SSD coverage under `gateway.runExclusive` before cloning.
460
+ each other) and arms a next-tick attempt.
461
+ - An attempt checks committed SSD coverage before cloning.
450
462
  Otherwise it snapshots the RAM entry (`findExact` + `cloneKvCaches`) and
451
- enqueues it; a busy gateway re-arms;
452
- a dropped or failed store leaves the key dirty so it is retryable.
463
+ enqueues it without consulting scheduler activity. A failed store leaves
464
+ the key dirty so it is retryable.
453
465
  A snapshot that vanished from RAM is "stored" only if the SSD index
454
466
  already covers the prefix (`hasDurablePrefix`), else "missing" and still
455
467
  dirty. A second flush cannot clear that failure without durable coverage.
456
468
  - `flush()` cancels timers, awaits in-flight attempts and `drain()`, then
457
- forces each dirty record version once, one at a time (so the queue cap
458
- cannot drop a boundary snapshot while a large final snapshot is in flight).
469
+ forces each dirty record version once, one at a time.
459
470
  A replacement scheduled during a write receives its own attempt. After
460
471
  all writes settle, the coordinator checks missing ancestors against the
461
472
  committed SSD index again: a later trimmable descendant may now cover a
@@ -495,13 +506,161 @@ IDs and every sampling/KV policy field. Grammar, token-fill, media, paged KV,
495
506
  and logprob requests are not checkpointed in v1 because they carry additional
496
507
  state or response data that cannot yet be reconstructed by token replay.
497
508
 
509
+ Ordinary continuation adds an optional request port to
510
+ that same ordinary driver. Checkpoint lookup, keys and persistence are shared
511
+ with the compatibility executor; the numerical driver supplies owned row
512
+ snapshots at an emitted-prefix/pending-token boundary. Restored rows complete
513
+ preparation without a target forward, before merging cold prefill rows, and
514
+ recover sampler seed, history and sample index. The server binding qualifies
515
+ ordinary requests through supported row-cache layouts, including recurrent Qwen
516
+ and full-attention Llama/MiniCPM as well as Gemma4. It transfers owned row
517
+ snapshots to a bounded persistence coordinator without awaiting disk in decode.
518
+ Snapshot preparation runs on the owner thread; payload serialization and
519
+ writes use the CPU worker. Completion
520
+ invalidates reuse immediately and queues cleanup after any in-flight write;
521
+ new attempts cannot be erased by older cleanup. Cancellation retains eventual
522
+ persistence, and explicit flush reports failed or pending durability. A hard
523
+ process exit may lose queued intervals. Plain greedy retains the ordinary
524
+ vectorized sampler when no history-dependent processors are active. Supported KV layouts retain their existing affine, per-layer and full-attention
525
+ TurboQuant conversion policy. Requests remain without
526
+ media, grammar, fill, paging, logprobs or speculative methods. Native tests
527
+ compare interrupted/restored runs at the same batch geometry; arbitrary
528
+ cohort changes are not an exact numerical identity guarantee. The HTTP fixture
529
+ checks serial B1, shared B1 and shared B4 with identical seeded requests and
530
+ held arrivals, and inspects the generated checkpoint before restart.
531
+ The configured spill queue budget applies to continuation snapshots; normal
532
+ prefix persistence follows the RAM cache residency policy. Flush duration and pending/failed
533
+ statistics cover both queues.
534
+
498
535
  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.
536
+ remains valid until the new file's fsync+rename completes. Queue backpressure
537
+ can leave more than one interval undurable; a crash loses work since the latest
538
+ successful write. Normal completion immediately invalidates the checkpoint and
539
+ queues its disk removal.
501
540
  Client cancellation throws through the serial token callback and preserves the
502
541
  last checkpoint, so terminating a long-lived client does not erase its restart
503
542
  point.
504
543
 
544
+ Fresh-model compiled continuation preserves the device sampler's uint32 pending
545
+ register. The Gemma model materializes its computed FP32 RoPE frequencies once
546
+ before compiled graph capture: a restored request may skip the prefill that
547
+ normally evaluates those constants. Capturing their unevaluated construction
548
+ graph can change arithmetic on M1. The forced-token continuation regression
549
+ compares a warmed model and a fresh model with cloned state, including every
550
+ active cache byte and full logits across delayed KV conversion. Live delayed
551
+ row layouts publish compact state through their extraction interface before
552
+ persistence; they do not need storage codecs for the live wrapper itself.
553
+
554
+ ### Method companion state
555
+
556
+ `PrefixCache<State, Attachment>` is the shared consumer contract for ordinary
557
+ and batched execution. It has no scheduling or storage-tier operations. The
558
+ MLX binding supplies target cache layers plus optional `CheckpointAttachment`
559
+ records containing a method schema, scalar metadata and tensor snapshots.
560
+ The method owns their meaning and alignment; the shared cache owns retention.
561
+ Method and drafter compatibility must be part of the namespace before a
562
+ producer publishes these records. Storage does not interpret draft state as
563
+ extra target-model layers.
564
+
565
+ RAM accounting, eviction, idle demotion and SSD persistence include
566
+ companion tensors. Checkpoints containing them require an exact prefix
567
+ boundary and preserve shorter fallback entries. Write-behind captures companion
568
+ views together with target-cache views under the existing exclusive boundary.
569
+ The writer persists them in the same atomic file and the same per-tensor write
570
+ loop. Restore copies and verifies them through the existing tensor reader.
571
+ Ordinary files retain v3 compatibility; older readers reject v4 files rather
572
+ than restore an incomplete checkpoint.
573
+
574
+ The storage implementation is in the working tree. Synthetic gates cover RAM
575
+ ownership, divergent-prefix fallback, SSD eviction, fresh-process restore,
576
+ corruption, queue drops/failures and durability on both development Macs.
577
+ Qwen MTP now uses this interface for prefill snapshots; its private store and
578
+ separate budget option are removed. `DraftSource.checkpoint` supplies the draft
579
+ KV and pending true hidden row. The namespace includes a SHA-256 of draft
580
+ configuration and shard bytes, resolved at provider load, plus the mounted
581
+ adapter revision and KV policy. Model loading resolves that digest through
582
+ `artifactIdentity`, independently of inference and scheduling. A memo key includes
583
+ the configuration seed, sorted shard names, resolved paths, device/inode, size,
584
+ and nanosecond modification/change times. Unchanged revisions reuse the exact
585
+ existing SHA-256; a miss reads every shard byte. Same-size writes with restored
586
+ mtime and changed symlink targets invalidate the memo. Rechecking revisions after
587
+ hashing prevents persisting a digest under a revision that changed during reading.
588
+
589
+ `ArtifactIdentityStore` owns memo retention and persistence. Its file adapter uses
590
+ `~/.cache/mlx-bun/artifact-identities/`, keeps RAM hits and queues atomic file writes
591
+ without awaiting them in model loading. Missing, malformed or unwritable memo
592
+ storage does not prevent computing the identity. It stores only digests, not
593
+ weights, prompts or KV. Qwen MTP, standalone, assistant, DeepSpec and DSpark use
594
+ the same identity service; existing namespaces and SSD entries remain compatible.
595
+ Tests cover byte identity, no weight reads after a fresh-store memo hit, file
596
+ mutation, configuration changes, symlink replacement and write failure.
597
+
598
+ Native bf16/KV4 gates on M1 Max and M4 Pro
599
+ preserve live target/companion state hashes, emitted IDs and acceptance decisions
600
+ through repeated RAM reuse and SSD cache/provider restart. SSD allocation
601
+ padding is excluded from the live-state comparison. Generated-output acceptance
602
+ is described below; long-conversation timing remains R17 work.
603
+
604
+ The shared method publishes completed row state before
605
+ retirement through the same `PrefixCache.put` port. It records only inputs
606
+ retained by committed target/draft rounds; an unprocessed correction or bonus
607
+ token is absent from the key. The target row layout and draft provider supply
608
+ their snapshots independently. Failed and cancelled consumers publish no
609
+ generated checkpoint. Persistence continues through the existing queue.
610
+
611
+ Generated BPE IDs are not necessarily the canonical encoding of their decoded
612
+ text. `GeneratedTokenHistory` retains bounded text/ID provenance from published
613
+ prefixes and recovered SSD headers. Request preparation uses the original IDs
614
+ when the rendered conversation contains that exact text prefix, encodes only
615
+ the remaining suffix without another BOS, and checks that the combined IDs
616
+ decode to the complete rendered request. Edited history or an unsafe decoder
617
+ boundary keeps canonical tokenization. KV lookup still requires exact IDs and
618
+ the existing execution namespace; no state is relabeled under different IDs.
619
+ The template's primer length is measured canonically and applied to the actual
620
+ history-preserving token sequence. This can change input token counts, and
621
+ therefore continuation logits, relative to re-encoding all generated text.
622
+ Logit parity remains a contract for the same input IDs and execution shape.
623
+ The stable-boundary probe includes both text and tool replies followed by a
624
+ new user turn. Qwen can omit an empty thinking primer when rendering a prior
625
+ tool turn; a text-only probe retained three extra tokens in the small-model
626
+ HTTP fixture and prevented recurrent-cache reuse. The corrected probe keeps
627
+ the last shared prefix. It never rewrites existing KV under the changed text.
628
+ Republishing an identical token sequence refreshes its existing provenance
629
+ entry without decoding the whole conversation again. Equality covers every ID;
630
+ equal text from different IDs still replaces provenance. This avoids repeated
631
+ text conversion during request completion and preserves the retention limit.
632
+
633
+ Cache pressure maintenance uses 85% of the smaller of the recommended device
634
+ working set and an explicit allocator limit as a residency target. It includes
635
+ known weight bytes when file-backed weights have not appeared in MLX active
636
+ allocation. `PrefixCache.reclaim` asks the cache to demote LRU donors. With SSD,
637
+ only durable donors can leave RAM; pending writes remain queued. A selected
638
+ request retains its own views. Shared buffer ownership is not counted twice as
639
+ RAM plus queued data. Lookup, prefill boundaries and periodic decode maintenance
640
+ call this interface; decode checks every 256 steps. It does not constrain
641
+ request admission, context, sampling or batch size.
642
+
643
+ M1 and M4 native bf16, affine KV4 and fused TurboQuant K8/V3 tests cover four active
644
+ rows, early retirement, exact sampled-ID coverage, unchanged retained bytes
645
+ after sibling and follow-up work, shorter-prefix selection after an edit, and
646
+ matching RAM/SSD continuation and acceptance. Existing paired-prefill tests
647
+ also pass. A second native gate records actual sampled IDs for an echo tool
648
+ turn with thinking off/on, passes them through the production output parsers,
649
+ and renders the subsequent tool-result request with the same chat template.
650
+ Both published checkpoints are exact token prefixes of that next request.
651
+ A production HTTP gate now passes on both Macs for bf16, KV4 and fused K8/V3, with
652
+ thinking off/on. The next tool-result request reuses generated content in RAM.
653
+ After a durable flush, a separate server process restores the first turn's
654
+ SSD state and reproduces the same response, logprobs and acceptance. The test
655
+ copies only the first turn's flushed files before running the RAM continuation,
656
+ so no later prefill checkpoint can explain the restart hit. All six cases
657
+ pass, including the actual wire `reasoning` alias and template options.
658
+ The five-file change is adopted alongside the captured prefill-policy fix.
659
+ Native acceptance of that final composition, long saved Kanban replay,
660
+ pressure and complete next-turn timing remain open.
661
+ It does not resolve the older serial prototype's bf16 comparison with fresh
662
+ full prefill, whose operation shapes differ from a generated history.
663
+
505
664
  ### 5.8 Invalidation
506
665
 
507
666
  Compatibility key = `configFingerprint(config)` + `KvScheme.cacheKey` +
@@ -538,117 +697,254 @@ the model's prototype cache before opening the tensor mmap.
538
697
  ever lands.
539
698
  - Block-granular dedup for multi-user shared prefixes stays out of scope
540
699
  (D1); revisit only with the paged follow-ups in 6.4.
541
- - Stale comments: `prompt-cache.ts` still describes restore as zero-copy
542
- mmap (5.2).
543
-
544
- ## 6. Optional paged KV (`src/lab/paged-kv/paged-kv.ts`, `--paged-kv`)
545
-
546
- Default off; with the flag unset no paged code executes and the plain
547
- `KVCache`/`RotatingKVCache` path is byte-identical.
548
-
549
- ### 6.1 What it buys here, honestly
550
-
551
- vLLM's PagedAttention solves contiguous worst-case per-sequence
552
- reservation and allocator fragmentation for a multi-tenant server.
553
- mlx-bun is one process on unified memory with byte-budget admission
554
- already shipped, so what transfers is: (a) padded-batch waste removal —
555
- today's batched buffer width tracks the longest live row
556
- (`mergeKVRows`/`extendKVRows`), so a short row cohabiting with a long one
557
- 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.
562
-
563
- This is neither of the two paging rejections already on record:
564
- paged KV as a *prompt-cache substitute* (rejected 2026-07-07 — `take()`'s
565
- zero-copy clones already share physically) and paged *blocks* as the SSD
566
- spill granularity (D1). It is the rung-3 allocation abstraction from
567
- docs/design/batching.md.
568
-
569
- No external oracle exists (mlx-lm's `cache.py` has no paged cache); the
570
- gate is mlx-bun's own plain `KVCache`, valid because the claim is storage
571
- equivalence (same bytes, different arrangement), so the bar is bit-exact
572
- (tol 0): `tests/paged-kv.test.ts` (model-free: block-boundary crossing,
573
- free-list reuse after trim, typed exhaustion, gathered fetch vs a plain
574
- reference, dispose) and `tests/paged-kv-parity.test.ts` (weights-gated
575
- greedy trajectory paged-on vs paged-off).
576
-
577
- ### 6.2 Mechanism
578
-
579
- - **`BlockPool`** — per-layer arena: K and V pool tensors
580
- `[numBlocks, H_kv, blockSize, headDim]` (V may differ in head dim),
581
- `ops.zeros`-allocated so they are mlx-owned end to end (no host-pointer
582
- alignment/dtor hazards). LIFO free list; `alloc()` throws the typed
583
- `PagedPoolExhausted`; writes are `ops.sliceUpdate` at
584
- `[block, 0, within, 0]` — the incoming `[1,H,l,D]` piece matches the
585
- destination slice directly.
586
- - **`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
589
- incoming `L` along block boundaries, allocates tail blocks as reached,
590
- then gathers occupied blocks with `ops.takeAxis(pool, blockTable, 0)` →
591
- transpose `[1,0,2,3]` → reshape `[1,H,nb·bs,D]` → slice to `offset`. Only
592
- existing bound ops — no `mlx_gather` FFI binding was needed. `makeMask`
593
- is `KVCache.makeMask`'s logic; `trim(n)` rewinds `offset` and frees
594
- now-unoccupied tail blocks (stale bytes past `offset` are never read —
595
- the `KVCache` padding invariant, block-shaped). Block table is a
596
- host-side `number[]`; all intermediates are bound and disposed.
597
- - Pool lazily allocates on the first write (head count/dims/dtype from
598
- the first k/v pair) and is sized once from `capacityTokens` =
599
- prompt + `max_tokens`, so exhaustion is unreachable absent an
600
- accounting bug — it exists as a tripwire, never silent truncation.
601
- - **Wiring** — `maybePageKv(cache, options, capacityTokens)`
602
- (`generate.ts`) mirrors `maybeQuantizeKv`'s post-construction in-place
603
- swap but runs *once before prefill*: paging changes layout, not
604
- arithmetic, so there is no "convert when populated" trigger. Only fresh
605
- (`offset === 0`) plain `KVCache` entries are replaced; rotating layers
606
- keep their scheme — mixed paged-full + rotating-sliding is the supported
607
- shape. Default block size 256 = `KVCache.STEP`, so v1's growth
608
- granularity is a permutation of today's into reusable slots, not a new
609
- tuning axis.
610
-
611
- ### 6.3 Gates (explicit refusals, never silent downgrades)
612
-
613
- - Startup (`server.ts`): `--paged-kv` with `--batch N>1`, any `--kv-quant`
614
- (affine or TurboQuant), `--draft-model`, or a non-`gemma4*` model exits
615
- with a clear message; `--paged-kv-block-size` must be a positive
616
- 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.
619
- - Request scope: media (vision/audio) and LoRA-adapter requests strip the
620
- flag and run the plain cache path (v1 non-goal cells, never a 400). One
621
- effective `pagedKv` value per request keeps the strip and the
622
- prompt-cache bypass coherent.
623
- - Serial lane: paged requests skip `take`/`put` and the boundary
624
- snapshot; caches are disposed on completion. Spec eligibility also
625
- excludes `options.pagedKv` (belt on top of the startup refusal).
626
- - Compiled decode: `CompiledDecode.supports()` excludes `PagedKVCache`
627
- automatically (not one of the four supported classes) — a
628
- data-dependent block-list length is the shape shapeless replay already
629
- broke on.
630
- - Batch lane: unreachable by construction (startup pins serial); the
631
- `RequestShape` gate to add when batched paging lands is noted here.
632
-
633
- ### 6.4 Follow-ups (dependency order) and open items
634
-
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
639
- S3+).
640
- 2. Block-level CoW prefix sharing — refcounted block table, fork on
641
- divergent write; extends `PromptCache`'s entry-level ref-counting down
642
- to blocks.
643
- 3. Quantized paged blocks — dtype-parametric block descriptor sharing one
644
- block-index space.
645
- 4. Fused paged-attention Metal kernel (optiq's `sdpa_2pass_paged` is the
646
- port source) — kills the per-step gather copy.
647
- - Perf disclosure: paged-on vs paged-off decode/prefill on the quiet
648
- reference machine has not been recorded in `docs/reference/benchmarks.md`
649
- (expected: a small decode regression at batch=1, to be reported plainly).
650
- - `PagedKVCache` has no `signature()` (2.4) and no kv-store codec; both
651
- are prerequisites for any prompt-cache or SSD integration.
700
+
701
+ ### 5.10 Comparison with other serving caches
702
+
703
+ Reviewed 2026-09-12 against published designs, not private provider internals.
704
+
705
+ | System | Algorithm and ownership | Application here |
706
+ | --- | --- | --- |
707
+ | [Mooncake / Kimi](https://github.com/kvcache-ai/Mooncake), [SSD offload](https://kvcache-ai.github.io/Mooncake/design/store/ssd-offload.html) | A cache service owns RAM/SSD placement. Background offload records a disk replica on successful write; the same lookup can restore from SSD. | One cache API with independent storage work. No application-managed demotion. |
708
+ | [SGLang HiCache](https://docs.sglang.io/docs/advanced_features/hicache_design) | A radix tree shares token spans and tracks memory placement. Supports proactive, selective and eviction-triggered writes, plus storage prefetch. Decode output can populate storage. | Keep output-produced checkpoints. Evaluate block sharing and prefetch after the persistence correction. |
709
+ | [LMCache MP](https://docs.lmcache.ai/mp/) | Independent store, eviction and prefetch controllers. Watermark-triggered RAM eviction uses LRU; asynchronous storage adapters handle lower tiers. | Separate write completion from residency policy. Keep the storage interface independent of inference. |
710
+ | [vLLM prefix caching](https://docs.vllm.ai/en/latest/design/prefix_caching/) | Token blocks use hashes including their preceding prefix. Unused blocks are evicted by recency. | Shared prefix blocks could reduce whole-entry duplication. |
711
+ | [Anthropic prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) | Public API documents exact-prefix reuse and expiration. | The same avoided-prefill benefit; this documentation does not establish its internal storage policy. |
712
+
713
+ The baseline storage unit is a complete checkpoint. Repeated conversation
714
+ snapshots duplicate attention history on SSD. Block deduplication in §5.11
715
+ preserves the existing state-kind contract: Qwen recurrent state and method
716
+ attachments cannot be trimmed to arbitrary earlier tokens. They need valid
717
+ checkpoint boundaries alongside any shared attention blocks. RAM LRU is the
718
+ initial policy; §5.11 records the measured frequency-weighting, prefetch and
719
+ block-layout alternatives.
720
+
721
+ ### 5.11 Cache expansion program
722
+
723
+ Authorized scope: implement and measure all five opportunities from §5.10.
724
+ Baseline is `c953f2f`, with an independent CPU writer and one RAM/SSD cache.
725
+ Preserve existing cache files and explicit serial support. Storage format,
726
+ residency policy, transfer work and attention kernels remain independently
727
+ selectable for paired comparisons; select defaults from measured results.
728
+
729
+ | Step | Implementation | Measurement and completion |
730
+ | --- | --- | --- |
731
+ | C1 | Immutable content-addressed blocks shared by checkpoint manifests. Attention token spans share storage; recurrent and method state preserve exact boundaries. | Same restored bytes/continuations, atomic publication and shared-block lifetime; bytes written and retained across growing/divergent conversations. |
732
+ | C2 | Cache-owned asynchronous reads and preparation-time prefetch with promotion into the existing RAM tier. | Same selected prefix and continuation; restore/TTFT, event-loop delay and concurrent decode against synchronous restore. |
733
+ | C3 | Pluggable retention policy, with LRU baseline and an alternative using reuse frequency, state size and saved processing cost. | Replay identical access traces; compare misses, avoided prefill, eviction/reload traffic and peak residency. |
734
+ | C4 | Segmented transfers and bounded packing scratch, preserving tensor layout contracts. | Copied bytes, temporary memory, write/read time and inference overlap on the same payload. |
735
+ | C5 | Direct paged-attention Metal execution through cache/attention interfaces, including shared row layouts and quantized representations. | Same-B numerical controls, B1/B>1 prefill/decode, gather/padding allocations and complete-request timing. |
736
+
737
+ Implementation is in `storage/kv-writer.worker.js` (CPU transfers), `kv-store`
738
+ (codec reconstruction), `PromptCache.prefetch` (request preparation),
739
+ `storage/retention-policy` (RAM choices), and the paged attention/layout modules.
740
+ Format 5 manifests reference SHA-256 content blocks. Block boundaries reset per
741
+ leading row so growing token axes preserve earlier head blocks. Blocks are
742
+ committed before their manifest; garbage collection is serialized with writes
743
+ and removes only content no remaining manifest references. Explicit SSD caps
744
+ account unique committed blocks rather than charging every checkpoint for a
745
+ shared copy. Existing format 3/4 files remain readable.
746
+
747
+ Restore reads use a separate worker queue and page-aligned host allocations.
748
+ After reads finish, the owner thread transfers allocation ownership to MLX
749
+ through a native destructor; no JavaScript callback runs on GPU completion.
750
+ Cache codecs are shared with synchronous restore. Plain KV still uses its
751
+ established capacity-growth operation; the async transport does not change
752
+ cached values or recurrent/checkpoint boundaries.
753
+
754
+ Use the saved Kanban token histories and seeded synthetic branches to separate
755
+ cache-policy effects from model-generated changes in task length. Existing
756
+ native oracles remain authoritative. Each implementation records paired M4
757
+ measurements and applicable M1 correctness; a slower arm stays explicit rather
758
+ than being promoted on architectural grounds alone. Raw reports stay under
759
+ `reports/`; published measurements belong in benchmarks.md.
760
+
761
+ The bounded C1–C5 implementation and measurements are complete. Shared blocks
762
+ trade write/read latency for space; segmentation removes packing copies for
763
+ contiguous spans. Asynchronous transport overlaps ongoing decode, while a lone
764
+ restart request still pays restore completion. Cost-size retention loses on the
765
+ saved Kanban boundary replay; direct paged attention has shape-dependent wins
766
+ and regressions. The measured selection keeps whole files and LRU, enables
767
+ asynchronous preparation, and leaves direct attention and block storage explicit.
768
+ [Measurements and reproduction](../reference/benchmarks.md#cache-expansion-c1c5-storage-restore-retention-and-paged-attention)
769
+ include the negative results and limits. Full long-task retention acceptance
770
+ and the paging extensions below remain separate work.
771
+
772
+ ### 5.12 Session checkpoint index
773
+
774
+ The cache owns a second index: application session ID plus numerical namespace
775
+ maps to its latest immutable checkpoint. Numerical/content identity remains
776
+ independent, so session and anonymous requests can share the same state.
777
+ The request carries a session hint through the existing cache port's lookup,
778
+ preparation and publication calls. Ordinary, speculative and paged methods
779
+ forward the hint; scheduling does not own the index or residency decisions.
780
+
781
+ A matching resident checkpoint bypasses candidate scans. For an evicted known
782
+ checkpoint, the SSD exact index selects its manifest before asynchronous
783
+ preparation adopts state through the existing codecs. Hashes accelerate SSD
784
+ selection; the selected token sequence still establishes identity. A missing
785
+ or incompatible session checkpoint uses ordinary prefix matching, allowing
786
+ history edits, branches and retries without rejecting the request.
787
+
788
+ New publications advance the session's reference. Explicit sessions also retain
789
+ ordinary generated checkpoints below the anonymous publication threshold.
790
+ RAM eviction first considers
791
+ entries with no session references, then applies the configured policy to
792
+ referenced entries when needed. This is a preference, not an unlimited memory
793
+ pin. Request-scoped preparation interests retain their existing short-lived
794
+ ownership. Closing a session removes its references without deleting cached
795
+ state; clearing the cache removes the session index. Session metadata is
796
+ process-local, and restart rebuilds associations through ordinary SSD hits.
797
+
798
+ The current API still accepts full histories. This change removes candidate
799
+ search and supplies retention information; it does not claim delta-only input,
800
+ zero tokenization, or automatic reconstruction from a session label. The Pi
801
+ provider passes the session identity its application already owns.
802
+ The index, native retention and fixed-input M4 HTTP comparisons are complete.
803
+ Lookup and reload work decrease; warmed model throughput is effectively
804
+ unchanged. The full Kanban task now completes with all follow-up session hits
805
+ and a durable final flush; independent app acceptance retains two defects.
806
+ [Measurements](../reference/benchmarks.md#session-checkpoint-index-c6) preserve
807
+ both the benefits and the limits. Existing idle-demotion settings remain in
808
+ effect; session preference changes budget/pressure victim selection.
809
+
810
+
811
+ ### 5.13 Exact tensor objects in the shared cache
812
+
813
+ `ObjectCache<Value>` exposes exact-key lookup and ownership transfer separately
814
+ from `PrefixCache` matching. `PromptCache.objects` uses the same resident records,
815
+ byte accounting, retention policy, pressure callback and SSD durability queue.
816
+ An object has an empty conversation history, no target KV layers and a private
817
+ namespace containing its full key. Its versioned tensor attachment owns the
818
+ payload. It never supplies a language-model prefix. No synthetic language token
819
+ IDs or second RAM budget are introduced.
820
+
821
+ RAM reads lend immutable native views. Evicting the resident entry cannot
822
+ invalidate a request's borrowed tensors. A cold read uses the existing async
823
+ serializer with an empty target-layer schema, outside the execution queue;
824
+ publication joins the same RAM policy. Cache clear invalidates pending restore
825
+ publication. A newer resident publication wins over an older in-flight restore.
826
+ The durability coordinator includes attachment-only objects; RAM eviction waits
827
+ for the shared writer to finish as it does for conversation checkpoints.
828
+
829
+ The Qwen encoder adapter hashes exact preprocessed pixel bytes, grid dimensions
830
+ and its schema revision. A SHA-256 of the actual loaded encoder tensors supplies weight identity,
831
+ computed once under native preparation. The containing SSD store also supplies
832
+ configuration, tokenizer and native numerical identity. Different image bytes or grids miss;
833
+ repeated media can reuse the tensor across request histories. Prompt rendering,
834
+ video timestamps, mRoPE positions, sampling and scheduling remain independent.
835
+ The producer retains its own result while storage takes a separate native view.
836
+ The object lookup finishes before the prompt builder enters the native queue.
837
+ Preprocessing and video extraction are not cached by this change.
838
+
839
+ Gemma's encoder-free and SigLIP vision towers and its audio tower use the same
840
+ `EncoderCache` adapter. Each tower hashes its immutable weight names, shapes,
841
+ dtypes, bytes and computation settings once. Image/WAV content hashes and a
842
+ producer revision identify individual objects. Vision retains its existing
843
+ pre-divided features; audio retains raw float32 features, then each prompt casts
844
+ them to its embedding dtype before dividing by the embedding scale. That order
845
+ is unchanged. CPU image preprocessing and audio mel extraction precede the
846
+ native execution lease, as do RAM/SSD lookups. The producer never caches chat
847
+ tokens, bidirectional masks or positions. Custom vision encoders without an
848
+ immutable `cacheIdentity` continue to encode normally.
849
+
850
+ [vLLM's encoder cache](https://docs.vllm.ai/en/v0.18.0/api/vllm/v1/core/encoder_cache_manager/)
851
+ also identifies individual media items by hash and retains encoder outputs
852
+ across requests. Here the existing cache owns both RAM residency and queued
853
+ SSD persistence on unified memory. The bounded Qwen screen and integrated
854
+ acceptance pass on M4; both-machine native/HTTP checks and fresh-process SSD
855
+ reuse preserve their recorded outputs. [Measurements](../reference/benchmarks.md#qwen-encoder-reuse-through-shared-ram-and-ssd-storage-2026-09-13)
856
+ include the one-time weight-fingerprint cost. Gemma prepared inputs and mixed
857
+ RAM/SSD HTTP reuse pass both-machine identity checks. Its M4 ABBA preserves all
858
+ 168 paired responses/usage and improves every measured workload; first-use
859
+ cost and changed image overlap are recorded in the
860
+ [Gemma measurements](../reference/benchmarks.md#gemma-encoder-reuse-through-shared-ram-and-ssd-storage-2026-09-13).
861
+ Prepared media conversation state uses the same cache, as described below.
862
+
863
+
864
+ ### 5.14 Prepared media prefix identity
865
+
866
+ The Gemma producer can supply a versioned `prefixIdentity` containing every
867
+ media item's exact input/encoder identity, its causal policy, and the rendered
868
+ token prefix through the last media span. The ordinary cache namespace combines
869
+ this identity with the adapter namespace. Consequently, matching histories
870
+ include identical media and preceding tokens; a changed media set or a change
871
+ from image-only bidirectional attention to mixed causal input cannot collide.
872
+ Appending later text leaves the identity stable.
873
+
874
+ By default, the shared Gemma binding uses ordinary
875
+ RAM/SSD lookup, generated snapshots, prefetch and persistence. The prepared-input
876
+ interface receives the cache's starting position. An uncached input performs
877
+ its atomic media prefill; a restored prefix resumes text after all media through
878
+ the model's ordinary forward method. Scheduling and sampling are unchanged.
879
+ Qwen supplies the same opaque identity with prepared image/video hashes,
880
+ rendered timestamps and all mRoPE positions through its final media span. Its
881
+ resumed text forward uses the current request positions and preserves the
882
+ recurrent state. Recurrent layers still require an exact retained prefix;
883
+ explicit serial prefix reuse remains unqualified. `MLX_BUN_MEDIA_PREFIX_CACHE=0`
884
+ selects encoder-only reuse for comparisons.
885
+
886
+ The existing minimum reusable offset still identifies a precision transition.
887
+ If delayed quantization occurs after an atomic media prefill, its cached state
888
+ can serve a later conversation turn but cannot rewind before that conversion.
889
+ A shorter request recomputes normally. Start-zero affine/TurboQuant and bf16
890
+ retain their applicable prefix-trimming behavior. This is cache selection,
891
+ not request admission or a memory refusal.
892
+
893
+
894
+ ## 6. Optional paged KV
895
+
896
+ `PagedKVCache` stores full-attention planes in per-request block pools.
897
+ `PagedKvRows` composes those pools through the existing row-state interface.
898
+ Scheduling sees request state and attention ports, not block tables or codecs.
899
+ Flags, defaults and supported combinations are documented in
900
+ [server-config.md](../reference/server-config.md).
901
+
902
+ ### 6.1 Storage and ownership
903
+
904
+ Pools hold bf16 planes or affine packed/scales/biases planes. New tokens alone
905
+ are quantized and written into their destination blocks. Logical block tables
906
+ map sequence positions to physical slots. Trimming returns unused tail blocks;
907
+ restored prefixes can grow beyond their original request's capacity.
908
+
909
+ Published attention views, cache borrowers and persistence jobs own independent
910
+ MLX handles. A subsequent functional append cannot overwrite their buffers.
911
+ The backend's `paged-cache-codec` preserves block tables, encoded planes and
912
+ layout metadata through the common RAM/SSD cache. `namespacedCache` separates
913
+ paged numerical settings from ordinary prefixes. Neither persistence nor a
914
+ successful write removes a RAM donor.
915
+
916
+ The current pools are separate per request. Their snapshots share immutable
917
+ MLX storage, but divergent writes can detach a whole pool. Shared physical
918
+ arenas with per-block copy-on-write remain a further optimization.
919
+
920
+ ### 6.2 Attention
921
+
922
+ The established attention arm gathers blocks and uses stock SDPA. Its bf16
923
+ storage-layout contract remains bit-exact against plain KVCache.
924
+
925
+ The optional direct Metal arm reads block tables for short causal queries.
926
+ Each SIMD group processes one query/head/sequence partition, retaining a
927
+ softmax maximum, denominator and partial value vector. A second kernel merges
928
+ those partials. Affine variants unpack KV4/KV8 values in the read operation;
929
+ no full-history dequantization or contiguous KV gather is materialized.
930
+ Shared rows retain their own lengths, so direct attention does not pad KV to
931
+ the longest row. Long prefill and explicit masks use the established SDPA
932
+ shape dispatch through the same attention view.
933
+
934
+ This changes reduction order. Direct attention is Lab, with numerical tolerance
935
+ checks against the same encoded values; it is not a bit-exact mlx-lm claim.
936
+ Tests cover unequal K/V dimensions, grouped heads, partial blocks, multiple
937
+ queries, row retirement, immutable views and restored growth. Performance
938
+ measurements retain both short-context losses and long-context wins in
939
+ [benchmarks.md](../reference/benchmarks.md).
940
+
941
+ ### 6.3 Remaining extensions
942
+
943
+ TurboQuant page encoding, paged speculative transactions, media/adapter paging
944
+ and shared physical arenas are outside the implemented bf16/affine page layout.
945
+ Existing non-paged TurboQuant and speculative RAM/SSD state remain supported
946
+ through their own codecs and attention interfaces. Extending a page codec or
947
+ kernel does not require a second scheduler.
652
948
 
653
949
  ## History
654
950
 
@@ -678,3 +974,20 @@ greedy trajectory paged-on vs paged-off).
678
974
  - Paged KV v1 (serial, Gemma4, bf16, gather-then-SDPA) landed with
679
975
  bit-exact gates; flag rows in cli.md / server-config.md (landing date
680
976
  not recorded in the source doc, which still reads "in progress").
977
+
978
+ ## Minimum reusable offsets
979
+
980
+ Physical trimmability does not prove that an older prefix has a valid
981
+ numerical representation. Caches may expose `minimumReusableOffset` after
982
+ an irreversible transition, such as delayed TurboQuant conversion. Prefix
983
+ selection requires the matched offset to meet every layer's bound. A
984
+ trimmable descendant supersedes an ancestor only when it can serve that
985
+ ancestor's offset; otherwise the earlier donor remains.
986
+
987
+ Cloning and per-row extraction preserve this state metadata. New SSD
988
+ headers record it, and indexing, durable-prefix checks and restore use the
989
+ same rule as RAM. Legacy TurboQuant headers without a recorded boundary
990
+ are conservatively reusable from their full stored offset only. Other
991
+ legacy cache kinds retain their existing interpretation. This changes
992
+ reuse eligibility, not scheduling, quantization arithmetic or memory
993
+ admission policy.