mlx-bun 0.4.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 (158) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +55 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  5. package/docs/design/batching.md +220 -7
  6. package/docs/design/decode-speed-program.md +121 -66
  7. package/docs/design/generic-model-support.md +116 -0
  8. package/docs/design/kv-cache.md +380 -211
  9. package/docs/design/speculative-decoding.md +165 -26
  10. package/docs/design/turboquant.md +20 -7
  11. package/docs/design/unified-engine-frontier-plan.md +120 -13
  12. package/docs/planning/PRODUCT_ROADMAP.md +5 -6
  13. package/docs/planning/public-readiness-handoff.md +244 -0
  14. package/docs/planning/public-readiness.md +149 -0
  15. package/docs/reference/benchmarks.md +3463 -3489
  16. package/docs/reference/cli.md +62 -1
  17. package/docs/reference/distribution.md +29 -4
  18. package/docs/reference/environment.md +104 -8
  19. package/docs/reference/library-api.md +18 -4
  20. package/docs/reference/memory.md +1 -1
  21. package/docs/reference/models.md +17 -5
  22. package/docs/reference/server-api.md +181 -11
  23. package/docs/reference/server-config.md +232 -105
  24. package/package.json +2 -2
  25. package/src/audio/audiotoolbox.ts +112 -0
  26. package/src/audio/conformer.ts +8 -0
  27. package/src/audio/mic-capture.ts +98 -0
  28. package/src/audio/silero-vad.ts +379 -0
  29. package/src/audio/whisper-decode.ts +774 -0
  30. package/src/audio/whisper-fast.ts +732 -0
  31. package/src/audio/whisper-format.ts +73 -0
  32. package/src/audio/whisper-mel.ts +185 -0
  33. package/src/audio/whisper-timing.ts +274 -0
  34. package/src/audio/whisper-tokenizer.ts +239 -0
  35. package/src/audio/whisper-transcribe.ts +410 -0
  36. package/src/backends/mlx/autoregressive.ts +22 -1
  37. package/src/backends/mlx/batch-group.ts +111 -27
  38. package/src/backends/mlx/cache-layout.ts +3 -3
  39. package/src/backends/mlx/continuation-persistence.ts +5 -4
  40. package/src/backends/mlx/encoder-cache.ts +25 -0
  41. package/src/backends/mlx/fill-append.ts +21 -8
  42. package/src/backends/mlx/fill-group.ts +497 -0
  43. package/src/backends/mlx/gateway-binding.ts +56 -12
  44. package/src/backends/mlx/kv-maintenance.ts +30 -2
  45. package/src/backends/mlx/mixed-iteration.ts +62 -0
  46. package/src/backends/mlx/model-prompt.ts +46 -32
  47. package/src/backends/mlx/model-serving.ts +4 -3
  48. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  49. package/src/backends/mlx/prefill-cohort.ts +15 -4
  50. package/src/backends/mlx/prefill-policy.ts +26 -0
  51. package/src/backends/mlx/prefill-rows.ts +57 -13
  52. package/src/backends/mlx/prompt-input.ts +29 -0
  53. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  54. package/src/backends/mlx/request-state-policy.ts +18 -5
  55. package/src/backends/mlx/rollback.ts +6 -3
  56. package/src/backends/mlx/serial-executor.ts +16 -7
  57. package/src/backends/mlx/speculative-group.ts +105 -26
  58. package/src/backends/mlx/speculative.ts +24 -7
  59. package/src/backends/mlx/target-layout-capability.ts +1 -1
  60. package/src/backends/mlx/transitioning-kv-rows.ts +8 -0
  61. package/src/cli.ts +322 -0
  62. package/src/contracts/execution.ts +10 -0
  63. package/src/contracts/object-cache.ts +8 -0
  64. package/src/contracts/pi-provider.ts +1 -1
  65. package/src/contracts/pi.ts +3 -1
  66. package/src/contracts/prefix-cache.ts +13 -3
  67. package/src/contracts/scheduling.ts +9 -0
  68. package/src/engine/execution-plan.ts +23 -12
  69. package/src/engine/execution-tasks.ts +44 -0
  70. package/src/engine/namespaced-cache.ts +13 -0
  71. package/src/engine/scheduler.ts +22 -2
  72. package/src/fill/fill-session.ts +14 -3
  73. package/src/generate.ts +28 -22
  74. package/src/grammar.ts +13 -0
  75. package/src/inference/prefill.ts +6 -0
  76. package/src/kv-scheme.ts +22 -0
  77. package/src/kv-store.ts +115 -79
  78. package/src/lab/README.md +5 -1
  79. package/src/lab/paged-kv/paged-attention.ts +117 -0
  80. package/src/lab/paged-kv/paged-kv-rows.ts +33 -5
  81. package/src/lab/paged-kv/paged-kv.ts +108 -42
  82. package/src/md-modules.d.ts +5 -0
  83. package/src/mlx/array.ts +37 -0
  84. package/src/mlx/ffi.ts +19 -0
  85. package/src/mlx/normalized-argmax.ts +116 -0
  86. package/src/mlx/ops.ts +97 -10
  87. package/src/model/batched-turboquant-kv.ts +6 -5
  88. package/src/model/delayed-quantized-kv.ts +3 -3
  89. package/src/model/delayed-rotating-quantized-kv.ts +1 -1
  90. package/src/model/delayed-turboquant-kv.ts +6 -4
  91. package/src/model/factory.ts +26 -0
  92. package/src/model/fingerprint.ts +12 -0
  93. package/src/model/full-prefill-row.ts +1 -1
  94. package/src/model/gemma4-base.ts +25 -6
  95. package/src/model/gemma4.ts +82 -2
  96. package/src/model/kv-attention-view.ts +4 -1
  97. package/src/model/profile.ts +29 -4
  98. package/src/model/quantized-append-attention.ts +31 -0
  99. package/src/model/qwen3-mrope.ts +7 -9
  100. package/src/model/qwen3_5.ts +99 -37
  101. package/src/model/support.ts +8 -0
  102. package/src/model/token-groups.ts +66 -0
  103. package/src/model/trellis-linear.ts +2 -2
  104. package/src/model/turboquant-codec.ts +6 -0
  105. package/src/model/whisper.ts +435 -0
  106. package/src/native/mic_capture.swift +92 -0
  107. package/src/native-pack.ts +6 -5
  108. package/src/pi-web.ts +9 -1
  109. package/src/prompt-cache.ts +265 -43
  110. package/src/runtime-config.ts +1 -5
  111. package/src/safetensors.ts +9 -2
  112. package/src/sampler.ts +5 -4
  113. package/src/serve/audio-routes.ts +323 -0
  114. package/src/serve/chat-request.ts +2 -0
  115. package/src/serve/chat-stage.ts +17 -12
  116. package/src/serve/completion-executor.ts +9 -1
  117. package/src/serve/discovery-routes.ts +17 -1
  118. package/src/serve/generated-token-history.ts +57 -0
  119. package/src/serve/generation-gateway.ts +52 -11
  120. package/src/serve/model-binding.ts +7 -1
  121. package/src/serve/preparation.ts +3 -3
  122. package/src/serve/prompt-response-trace.ts +14 -0
  123. package/src/serve/request-plan.ts +2 -0
  124. package/src/serve/request-prep.ts +38 -21
  125. package/src/serve/transcription-server.ts +64 -0
  126. package/src/serve/transcription-service.ts +502 -0
  127. package/src/server.ts +132 -164
  128. package/src/spec/assistant-rows.ts +1 -0
  129. package/src/spec/deepspec-rows.ts +1 -1
  130. package/src/spec/dflash-source.ts +1 -1
  131. package/src/spec/glm52-mtp-source.ts +1 -0
  132. package/src/spec/ngram-source.ts +27 -5
  133. package/src/spec/projected-draft-rows.ts +2 -2
  134. package/src/spec/qwen-mtp-rows.ts +11 -0
  135. package/src/spec/qwen-mtp-source.ts +3 -0
  136. package/src/spec/serve-loop.ts +5 -3
  137. package/src/spec/source.ts +14 -1
  138. package/src/spec/two-model-rows.ts +1 -0
  139. package/src/spec/two-model.ts +2 -2
  140. package/src/ssd-cache.ts +76 -17
  141. package/src/ssd-durability.ts +20 -22
  142. package/src/storage/host-buffer.ts +24 -0
  143. package/src/storage/kv-writer.ts +83 -0
  144. package/src/storage/kv-writer.worker.js +264 -0
  145. package/src/storage/retention-policy.ts +37 -0
  146. package/src/tiered-prompt-cache.ts +45 -0
  147. package/src/vision/embedder.ts +8 -0
  148. package/src/vision/prompt.ts +101 -65
  149. package/src/vision/qwen3vl-prompt.ts +101 -64
  150. package/src/vision/qwen3vl-tower.ts +11 -0
  151. package/src/vision/siglip.ts +8 -0
  152. package/src/web/app.html +12 -0
  153. package/src/web/app.js +160 -0
  154. package/src/web/src/chat.ts +4 -1
  155. package/src/web/src/composer.ts +3 -0
  156. package/src/web/src/voice.ts +141 -0
  157. package/src/web/sw.js +1 -1
  158. /package/docs/{planning → archive/planning}/release-notes-v0.4.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-09-09
6
+ last-verified: 2026-09-12
7
7
  ---
8
8
 
9
9
  # KV cache — residency and layout
@@ -24,27 +24,31 @@ 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. 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
+ 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
48
52
  persistence without awaiting disk in decode (section 5.7). Paged KV
49
53
  (section 6) is a *layout* choice for the live tier and, in v1, bypasses the
50
54
  other two.
@@ -217,9 +221,11 @@ changing the retained checkpoint.
217
221
 
218
222
  SSD is an eventually consistent persistence layer. RAM reuse never writes
219
223
  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.
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.
223
229
 
224
230
  The native sharing gate checks logical checkpoint bytes after repeated decode
225
231
  and after SSD restore. It passes on both development Macs; the M1 long-prefix
@@ -232,18 +238,23 @@ and explicit flush remains pending until the write completes.
232
238
  longer usable prefix; then `restore(handle)` materializes it and the
233
239
  divergent tail is trimmed. A cold entry that would need a trim but is
234
240
  untrimmable (wrapped ring, SSM) is disposed loudly and the RAM candidate
235
- (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.
236
244
 
237
245
  **`put(tokens, caches, ns, retain)` — supersession + eviction.** Exact
238
246
  duplicates (same tokens) are replaced regardless of trimmability. Strict
239
247
  prefix-ancestors in the same ns are superseded without spill — but only
240
248
  when the *new* entry is fully trimmable; an untrimmable new entry (wrapped
241
249
  ring) can only serve exact-length matches, so shorter ancestors (the
242
- prompt-boundary snapshot) must survive. Then LRU-evict until under cap,
243
- spilling each evictee to the cold tier first. An entry larger than the cap
244
- is spilled and disposed, never stored. `onPut` fires after every
245
- successful put — the server hangs the write-behind scheduler there so both
246
- 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.
247
258
 
248
259
  **Prompt-boundary snapshot.** Serial lane (`server.ts`) and batch
249
260
  scheduler (`snapshotAt`) both clone-and-put a trim-free *prompt-only*
@@ -257,12 +268,11 @@ always an exact prefix of the next rendering.
257
268
  **Namespaces.** `ns` = the adapter spec (`adapters.join("+")`, `""` = base).
258
269
  KV computed under one adapter never seeds another's prefill.
259
270
 
260
- **Bypasses.** Media (vision/audio) requests skip the cache (soft tokens are
261
- identical placeholder ids → false hits), as do paged-KV requests (v1) and
262
- the speculative lane (v1). Merged batch rows are not re-put on finish;
263
- never-merged lone rows put their adopted caches back zero-copy, and
264
- merged rows are extracted per row (`#extractAndPut`) before `filter()`
265
- 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.
266
276
 
267
277
  **Idle demotion (`demoteIdle`).** Entries unused past a threshold spill to
268
278
  the cold tier and free their GPU arrays; the prefix stays reachable via
@@ -287,17 +297,14 @@ full prefill (GEMV-vs-GEMM reduction order). Both tiers share it.
287
297
 
288
298
  ### 5.1 Decisions
289
299
 
290
- - **D1 — Whole-prefix-entry spill, not paged blocks.** Single-user serving
291
- with a prefix-entry cache that grows per conversation. Per-block hashing
292
- and save-queue bookkeeping during decode is where oMLX's measured ~20%
293
- steady-state tax came from; ours does zero work on the token loop
294
- (prefix tokens are the key). Partial-prefix reuse works without paging:
295
- restore + `trim()`. The economics that justify the tier are
296
- SSD-vs-recompute, not SSD-vs-RAM: at 12B prefill rates a 30k agent
297
- context is minutes of GPU compute against a ~1 s NVMe restore.
298
- - **D2 — One store, two tiers.** The cold tier is bound *into*
299
- `PromptCache` (section 4); restart persistence comes from a debounced
300
- 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.
301
308
  - **D3 — Own page-aligned format, not safetensors.** `src/kv-store.ts`
302
309
  writes `MLXBUNKV2\n`-magic files with a streaming writer: ordinary KV
303
310
  retains format v3; checkpoints with method companion state use v4.
@@ -377,9 +384,9 @@ ordering is unchanged (the server's `coldTier.restore` returns
377
384
  `() => {}`). Comments in `prompt-cache.ts` that still say "zero-copy
378
385
  mmap" describe the superseded mechanism.
379
386
 
380
- The write side is the mirror: hashing and writing from a zero-copy view
381
- of the contiguous mlx buffer (`rawBytesView`), no JS-heap copy at any
382
- 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.
383
390
 
384
391
  ### 5.3 `find()` returns partial matches, gated on trimmability
385
392
 
@@ -394,56 +401,52 @@ Store-side supersession mirrors `PromptCache.put`: exact duplicates
394
401
  replaced; prefix-ancestors superseded only when the new entry is
395
402
  trimmable.
396
403
 
397
- ### 5.4 Write-behind scheduling contract: own each tensor step
398
-
399
- Every per-tensor flush step is real GPU-stream and JS-thread work
400
- (`ops.contiguous` on the decode stream, a synchronous eval for
401
- `rawBytesView`, a synchronous multi-MB `writeSync`). Interleaving those
402
- between decode tokens taxed decode at long context, so:
403
-
404
- - `saveKvCacheAsync` / `SsdCacheStore.storeAsync` accept a per-step runner.
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
408
- `gateway.onIdle()` check had a check/use race: a request could begin after
409
- the check and overlap the next tensor sync. A request arriving mid-flush
410
- can run between tensor writes. Neither execution lane drains pending
411
- durability work before generation.
412
- - `GenerationGateway.busy` covers both lanes (serial mutex held/awaited or
413
- batch rows active/pending).
414
- - `MLX_BUN_SSD_WRITEBEHIND=0` disables write-behind snapshots entirely
415
- (kill switch + paired-A/B lever; restart survival then degrades to
416
- spill-on-evict).
417
-
418
- Explicit flush and shutdown drain outstanding writes outside the generation
419
- lock because the writer needs that same lock. Ordinary requests do not.
420
-
421
- The RAM cache's configured byte cap is only an upper bound. Serial serving
422
- checks MLX's live allocation count against the smaller of admission's usable
423
- memory and Metal's recommended working set, reserving prefill workspace and
424
- the prompt's KV footprint for growth/copy-on-write overlap. Pressure relief
425
- persists LRU entries synchronously before disposing them, avoiding deferred
426
- spill clones that would keep the memory resident. The selected request cache
427
- remains owned by the request; older entries remain reusable from SSD. Qwen
428
- checks between prefill layers, and serial decode checks every 256 tokens.
429
- If no reclaimable entries remain, the guard raises a request error instead
430
- of intentionally proceeding over that budget. In-flight checkpoints survive.
431
-
432
- ### 5.5 Bounded retention: `SpillQueue` (`kv-store.ts`)
433
-
434
- All three producers — eviction spills, idle demotions, write-behind
435
- snapshots — go through one serial, byte-capped queue. Pending clones pin
436
- their entries' GPU buffers until the generation-locked flush gets a turn; without
437
- a cap, sustained traffic (gate starved, evictions ongoing) made resident
438
- memory = prompt-cache cap + every queued clone. Policy: cap default 2 GiB
439
- (`MLX_BUN_SSD_SPILL_QUEUE_GB`; `0` keeps only the newest + in-flight
440
- clone); over cap the *oldest* not-in-flight item drops and its clones are
441
- disposed immediately; the item just enqueued is never its own victim
442
- (soft cap). A dropped spill is a future cache miss, never a wrong result.
443
- Clones are disposed on every settle path (fulfil, reject, drop). The
444
- snapshot timer never holds batch admission: while rows are active it
445
- re-arms instead of grabbing the exclusive (entries are immutable; a late
446
- 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`.
447
450
 
448
451
  ### 5.6 Durability boundary (`src/ssd-durability.ts`)
449
452
 
@@ -454,17 +457,16 @@ the write — the server implied durability it did not have.
454
457
 
455
458
  - `schedule(tokens, ns)` records a *dirty* key that includes the exact
456
459
  token sequence (two same-ns, same-length conversations cannot cancel
457
- each other) and arms the debounce.
458
- - An attempt checks SSD coverage under `gateway.runWhenIdle` before cloning.
460
+ each other) and arms a next-tick attempt.
461
+ - An attempt checks committed SSD coverage before cloning.
459
462
  Otherwise it snapshots the RAM entry (`findExact` + `cloneKvCaches`) and
460
- enqueues it; a busy gateway re-arms;
461
- 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.
462
465
  A snapshot that vanished from RAM is "stored" only if the SSD index
463
466
  already covers the prefix (`hasDurablePrefix`), else "missing" and still
464
467
  dirty. A second flush cannot clear that failure without durable coverage.
465
468
  - `flush()` cancels timers, awaits in-flight attempts and `drain()`, then
466
- forces each dirty record version once, one at a time (so the queue cap
467
- cannot drop a boundary snapshot while a large final snapshot is in flight).
469
+ forces each dirty record version once, one at a time.
468
470
  A replacement scheduled during a write receives its own attempt. After
469
471
  all writes settle, the coordinator checks missing ancestors against the
470
472
  committed SSD index again: a later trimmable descendant may now cover a
@@ -513,7 +515,8 @@ recover sampler seed, history and sample index. The server binding qualifies
513
515
  ordinary requests through supported row-cache layouts, including recurrent Qwen
514
516
  and full-attention Llama/MiniCPM as well as Gemma4. It transfers owned row
515
517
  snapshots to a bounded persistence coordinator without awaiting disk in decode.
516
- Tensor serialization and file maintenance use the existing idle gate. Completion
518
+ Snapshot preparation runs on the owner thread; payload serialization and
519
+ writes use the CPU worker. Completion
517
520
  invalidates reuse immediately and queues cleanup after any in-flight write;
518
521
  new attempts cannot be erased by older cleanup. Cancellation retains eventual
519
522
  persistence, and explicit flush reports failed or pending durability. A hard
@@ -525,8 +528,8 @@ compare interrupted/restored runs at the same batch geometry; arbitrary
525
528
  cohort changes are not an exact numerical identity guarantee. The HTTP fixture
526
529
  checks serial B1, shared B1 and shared B4 with identical seeded requests and
527
530
  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
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
530
533
  statistics cover both queues.
531
534
 
532
535
  Only the newest successful checkpoint for a request is retained. The old file
@@ -559,7 +562,7 @@ Method and drafter compatibility must be part of the namespace before a
559
562
  producer publishes these records. Storage does not interpret draft state as
560
563
  extra target-model layers.
561
564
 
562
- RAM accounting, eviction, idle demotion and the bounded SSD spill queue include
565
+ RAM accounting, eviction, idle demotion and SSD persistence include
563
566
  companion tensors. Checkpoints containing them require an exact prefix
564
567
  boundary and preserve shorter fallback entries. Write-behind captures companion
565
568
  views together with target-cache views under the existing exclusive boundary.
@@ -598,13 +601,45 @@ through repeated RAM reuse and SSD cache/provider restart. SSD allocation
598
601
  padding is excluded from the live-state comparison. Generated-output acceptance
599
602
  is described below; long-conversation timing remains R17 work.
600
603
 
601
- The unreleased shared method now publishes completed row state before
604
+ The shared method publishes completed row state before
602
605
  retirement through the same `PrefixCache.put` port. It records only inputs
603
606
  retained by committed target/draft rounds; an unprocessed correction or bonus
604
607
  token is absent from the key. The target row layout and draft provider supply
605
608
  their snapshots independently. Failed and cancelled consumers publish no
606
609
  generated checkpoint. Persistence continues through the existing queue.
607
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
+
608
643
  M1 and M4 native bf16, affine KV4 and fused TurboQuant K8/V3 tests cover four active
609
644
  rows, early retirement, exact sampled-ID coverage, unchanged retained bytes
610
645
  after sibling and follow-up work, shorter-prefix selection after an edit, and
@@ -662,120 +697,254 @@ the model's prototype cache before opening the tensor mmap.
662
697
  ever lands.
663
698
  - Block-granular dedup for multi-user shared prefixes stays out of scope
664
699
  (D1); revisit only with the paged follow-ups in 6.4.
665
- - Stale comments: `prompt-cache.ts` still describes restore as zero-copy
666
- mmap (5.2).
667
-
668
- ## 6. Optional paged KV (`src/lab/paged-kv/paged-kv.ts`, `--paged-kv`)
669
-
670
- Default off; with the flag unset no paged code executes and the plain
671
- `KVCache`/`RotatingKVCache` path is byte-identical.
672
-
673
- ### 6.1 What it buys here, honestly
674
-
675
- vLLM's PagedAttention solves contiguous worst-case per-sequence
676
- reservation and allocator fragmentation for a multi-tenant server.
677
- mlx-bun is one process on unified memory with byte-budget admission
678
- already shipped, so what transfers is: (a) padded-batch waste removal —
679
- today's batched buffer width tracks the longest live row
680
- (`mergeKVRows`/`extendKVRows`), so a short row cohabiting with a long one
681
- pays the long one's KV; and (b) block-level copy-on-write prefix sharing.
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.
686
-
687
- This is neither of the two paging rejections already on record:
688
- paged KV as a *prompt-cache substitute* (rejected 2026-07-07 — `take()`'s
689
- zero-copy clones already share physically) and paged *blocks* as the SSD
690
- spill granularity (D1). It is the rung-3 allocation abstraction from
691
- docs/design/batching.md.
692
-
693
- No external oracle exists (mlx-lm's `cache.py` has no paged cache); the
694
- gate is mlx-bun's own plain `KVCache`, valid because the claim is storage
695
- equivalence (same bytes, different arrangement), so the bar is bit-exact
696
- (tol 0): `tests/paged-kv.test.ts` (model-free: block-boundary crossing,
697
- free-list reuse after trim, typed exhaustion, gathered fetch vs a plain
698
- reference, dispose) and `tests/paged-kv-parity.test.ts` (weights-gated
699
- greedy trajectory paged-on vs paged-off).
700
-
701
- ### 6.2 Mechanism
702
-
703
- - **`BlockPool`** — per-layer arena: K and V pool tensors
704
- `[numBlocks, H_kv, blockSize, headDim]` (V may differ in head dim),
705
- `ops.zeros`-allocated so they are mlx-owned end to end (no host-pointer
706
- alignment/dtor hazards). LIFO free list; `alloc()` throws the typed
707
- `PagedPoolExhausted`; writes are `ops.sliceUpdate` at
708
- `[block, 0, within, 0]` — the incoming `[1,H,l,D]` piece matches the
709
- destination slice directly.
710
- - **`PagedKVCache implements Cache`** — standalone (not a `KVCache`
711
- subclass). The cache-layout binding supplies `PagedKvRows` for dynamic
712
- membership, masks and per-row positions. `updateAndFetch` splits the
713
- incoming `L` along block boundaries, allocates tail blocks as reached,
714
- then gathers occupied blocks with `ops.takeAxis(pool, blockTable, 0)` →
715
- transpose `[1,0,2,3]` → reshape `[1,H,nb·bs,D]` → slice to `offset`. Only
716
- existing bound ops — no `mlx_gather` FFI binding was needed. `makeMask`
717
- is `KVCache.makeMask`'s logic; `trim(n)` rewinds `offset` and frees
718
- now-unoccupied tail blocks (stale bytes past `offset` are never read —
719
- the `KVCache` padding invariant, block-shaped). Block table is a
720
- host-side `number[]`; all intermediates are bound and disposed.
721
- - Pool lazily allocates on the first write (head count/dims/dtype from
722
- the first k/v pair) and is sized once from `capacityTokens` =
723
- prompt + `max_tokens`, so exhaustion is unreachable absent an
724
- accounting bug — it exists as a tripwire, never silent truncation.
725
- - **Wiring** — `maybePageKv(cache, options, capacityTokens)`
726
- (`backends/mlx/request-state-policy.ts`, re-exported by `generate.ts`)
727
- mirrors `maybeQuantizeKv`'s post-construction in-place
728
- swap but runs *once before prefill*: paging changes layout, not
729
- arithmetic, so there is no "convert when populated" trigger. Only fresh
730
- (`offset === 0`) plain `KVCache` entries are replaced; rotating layers
731
- keep their scheme — mixed paged-full + rotating-sliding is the supported
732
- shape. Default block size 256 = `KVCache.STEP`, so v1's growth
733
- granularity is a permutation of today's into reusable slots, not a new
734
- tuning axis.
735
-
736
- ### 6.3 Gates (explicit refusals, never silent downgrades)
737
-
738
- - Startup (`server.ts`): `--paged-kv` with any `--kv-quant`
739
- (affine or TurboQuant), `--draft-model`, or a non-`gemma4*` model exits
740
- with a clear message; `--paged-kv-block-size` must be a positive
741
- integer; `--ssd-cache` alongside it warns that the tier sees nothing.
742
- Paging preserves the selected batch size. Env `MLX_BUN_PAGED_KV=1`
743
- is equivalent to the flag.
744
- - Request scope: media (vision/audio) and LoRA-adapter requests strip the
745
- flag and run the plain cache path (v1 non-goal cells, never a 400). One
746
- effective `pagedKv` value per request keeps the strip and the
747
- prompt-cache bypass coherent.
748
- - Both execution modes: paged requests skip `take`/`put` and the boundary
749
- snapshot; caches are disposed on completion. Spec eligibility also
750
- excludes `options.pagedKv` (belt on top of the startup refusal).
751
- - Compiled decode: `CompiledDecode.supports()` excludes `PagedKVCache`
752
- automatically (not one of the four supported classes) — a
753
- data-dependent block-list length is the shape shapeless replay already
754
- broke on.
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.
759
-
760
- ### 6.4 Follow-ups (dependency order) and open items
761
-
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
766
- S3+).
767
- 2. Block-level CoW prefix sharing — refcounted block table, fork on
768
- divergent write; extends `PromptCache`'s entry-level ref-counting down
769
- to blocks.
770
- 3. Quantized paged blocks — dtype-parametric block descriptor sharing one
771
- block-index space.
772
- 4. Fused paged-attention Metal kernel (optiq's `sdpa_2pass_paged` is the
773
- port source) — kills the per-step gather copy.
774
- - Perf disclosure: paged-on vs paged-off decode/prefill on the quiet
775
- reference machine has not been recorded in `docs/reference/benchmarks.md`
776
- (expected: a small decode regression at batch=1, to be reported plainly).
777
- - `PagedKVCache` has no `signature()` (2.4) and no kv-store codec; both
778
- 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.
779
948
 
780
949
  ## History
781
950