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.
- package/README.md +57 -32
- package/docs/README.md +58 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +1535 -50
- package/docs/design/decode-speed-program.md +394 -1171
- package/docs/design/generic-model-support.md +195 -3
- package/docs/design/kv-cache.md +538 -225
- package/docs/design/speculative-decoding.md +552 -50
- package/docs/design/turboquant.md +140 -17
- package/docs/design/unified-engine-frontier-plan.md +259 -150
- package/docs/planning/PRODUCT_ROADMAP.md +21 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3537 -1112
- package/docs/reference/cli.md +102 -2
- package/docs/reference/distribution.md +45 -13
- package/docs/reference/environment.md +159 -13
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +28 -7
- package/docs/reference/server-api.md +257 -33
- package/docs/reference/server-config.md +372 -151
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +464 -236
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +107 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +122 -19
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +161 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +5 -4
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +100 -0
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +244 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +54 -0
- package/src/backends/mlx/rollback.ts +45 -4
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +51 -73
- package/src/backends/mlx/speculative-group.ts +438 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
- package/src/cli.ts +333 -36
- package/src/contracts/execution.ts +21 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +33 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/engine/execution-plan.ts +38 -25
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +32 -7
- package/src/fill/fill-session.ts +14 -3
- package/src/fit.ts +29 -15
- package/src/generate.ts +105 -195
- package/src/grammar.ts +13 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/prefill.ts +6 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +92 -12
- package/src/kv-store.ts +183 -93
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
- package/src/lab/paged-kv/paged-kv.ts +126 -46
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +39 -1
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +94 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +57 -0
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +183 -82
- package/src/model/gemma4.ts +116 -28
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +70 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +177 -75
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +4 -4
- package/src/model/turboquant-codec.ts +89 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +306 -86
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +37 -25
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +19 -14
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +88 -38
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/model-host.ts +3 -4
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +6 -3
- package/src/serve/request-prep.ts +40 -25
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +185 -261
- package/src/spec/assistant-rows.ts +93 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +115 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +230 -0
- package/src/spec/qwen-mtp-source.ts +84 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +73 -69
- package/src/spec/source.ts +93 -15
- package/src/spec/two-model-rows.ts +62 -0
- package/src/spec/two-model.ts +45 -103
- package/src/ssd-cache.ts +107 -36
- package/src/ssd-durability.ts +29 -27
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +217 -57
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
package/docs/design/kv-cache.md
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
34
|
-
|
|
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 | `
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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)`:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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).
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
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.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
297
|
-
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
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
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
(
|
|
431
|
-
|
|
432
|
-
|
|
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
|
|
449
|
-
- An attempt checks SSD coverage
|
|
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
|
|
452
|
-
|
|
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
|
|
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
|
|
500
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
vLLM
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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.
|