mlx-bun 0.2.0 → 0.3.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 +71 -588
- package/docs/README.md +91 -0
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/design/batching.md +569 -0
- package/docs/design/decode-speed-program.md +3178 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +579 -334
- package/docs/design/kv-cache.md +680 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1627 -0
- package/docs/design/turboquant.md +2102 -0
- package/docs/design/unified-engine-frontier-plan.md +1831 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +34 -1
- package/docs/planning/release-notes-v0.3.0.md +35 -0
- package/docs/planning/video-series.md +2 -2
- package/docs/reference/benchmarks.md +1162 -0
- package/docs/reference/cli.md +394 -159
- package/docs/reference/distribution.md +321 -182
- package/docs/reference/environment.md +348 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +566 -168
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +104 -6
- package/docs/reference/server-api.md +886 -425
- package/docs/reference/server-config.md +638 -330
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +166 -0
- package/package.json +9 -4
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1646 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +47 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/gateway-binding.ts +71 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/memory-client.ts +189 -0
- package/src/backends/mlx/memory-guard.ts +40 -0
- package/src/backends/mlx/methods.ts +90 -0
- package/src/backends/mlx/model-prompt.ts +164 -0
- package/src/backends/mlx/model-serving.ts +79 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/rollback.ts +51 -0
- package/src/backends/mlx/serial-executor.ts +273 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-views.ts +26 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/cli.ts +58 -25
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +49 -0
- package/src/contracts/generation.ts +103 -0
- package/src/contracts/host.ts +7 -0
- package/src/contracts/jobs.ts +66 -0
- package/src/contracts/pi-provider.ts +30 -0
- package/src/contracts/pi.ts +259 -0
- package/src/contracts/resources.ts +18 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/contracts/tsconfig.json +14 -0
- package/src/diffusion/diffusion-generate.ts +218 -153
- package/src/embed.ts +1 -1
- package/src/engine/admission.ts +59 -0
- package/src/engine/cancellation.ts +38 -0
- package/src/engine/engine.ts +56 -0
- package/src/engine/execution-coordinator.ts +70 -0
- package/src/engine/execution-plan.ts +51 -0
- package/src/engine/resources.ts +42 -0
- package/src/engine/scheduler.ts +67 -0
- package/src/engine/session.ts +247 -0
- package/src/eval/README.md +44 -1
- package/src/eval/kl.ts +236 -3
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +8 -96
- package/src/fill/echo-index.ts +188 -0
- package/src/fill/fill-session.ts +452 -0
- package/src/fill/proposal.ts +68 -0
- package/src/fill/schema-rows.ts +506 -0
- package/src/fill/tool-boundary.ts +103 -0
- package/src/fit.ts +1 -1
- package/src/generate.ts +656 -268
- package/src/grammar.ts +12 -7
- package/src/harness-pi.ts +14 -33
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +7 -0
- package/src/inference/denoising.ts +17 -0
- package/src/inference/graph.ts +33 -0
- package/src/inference/prefill.ts +39 -0
- package/src/inference/rollback.ts +7 -0
- package/src/jobs/db.ts +4 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-entry.ts +12 -12
- package/src/jobs/runner.ts +98 -13
- package/src/jobs/task-client.ts +19 -0
- package/src/jobs/types.ts +2 -58
- package/src/kv-store.ts +109 -49
- package/src/lab/README.md +18 -0
- package/src/{assets → lab/curve}/curve-designer.html +2 -2
- package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
- package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
- package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
- package/src/library.ts +40 -0
- package/src/lora.ts +18 -0
- package/src/media-fetch.ts +1 -1
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +30 -169
- package/src/memory/prompts.ts +1 -1
- package/src/memory/rest.ts +1 -1
- package/src/memory/schedule.ts +1 -1
- package/src/memory/stages.ts +4 -1
- package/src/memory/vault.ts +1 -3
- package/src/mlx/array.ts +9 -10
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +58 -11
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +33 -13
- package/src/mlx/quantized-rows.ts +36 -0
- package/src/mlx/token-bitmask.ts +52 -0
- package/src/mlx/turboquant-kv-decode.ts +82 -0
- package/src/mlx/turboquant-kv-inverse.ts +122 -0
- package/src/mlx/turboquant-ops.ts +33 -3
- package/src/mlx/turboquant-tables.ts +2 -2
- package/src/model/batched-mask.ts +8 -2
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +2 -2
- package/src/model/batched-rotating.ts +1 -1
- package/src/model/compiled-decode.ts +1 -1
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +76 -33
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/gemma4-base.ts +26 -9
- package/src/model/gemma4.ts +6 -6
- package/src/model/generated/gemma4-12b.ts +2 -2
- package/src/model/generated/gemma4-26b.ts +2 -2
- package/src/model/generated/gemma4-e4b.ts +2 -2
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +1 -1
- package/src/model/implementation.ts +69 -0
- package/src/model/minicpm5.ts +6 -8
- package/src/model/profile.ts +12 -2
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +140 -56
- package/src/model/support.ts +1 -1
- package/src/model/trellis-balanced-scatter.ts +51 -0
- package/src/model/trellis-linear.ts +672 -0
- package/src/model/trellis-shared-m.ts +151 -0
- package/src/model/trellis-shared-scatter.ts +144 -0
- package/src/model/trellis-splitk-prefill.ts +158 -0
- package/src/model/trellis-tiled-prefill.ts +136 -0
- package/src/model/trellis-vector-expand.ts +62 -0
- package/src/model/trellis-wide-prefill.ts +94 -0
- package/src/model/universal/dense.ts +1 -1
- package/src/model/universal/modules.ts +8 -1
- package/src/native-pack.ts +6 -5
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +8 -315
- package/src/prompt-cache.ts +96 -53
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/config-writer.ts +10 -3
- package/src/quantize/rotate.ts +4 -4
- package/src/quantize/trellis.ts +586 -0
- package/src/responses.ts +28 -1
- package/src/runtime-config.ts +18 -4
- package/src/sampler.ts +4 -4
- package/src/serve/batch-scheduler.ts +8 -1671
- package/src/serve/chat-request.ts +391 -0
- package/src/serve/chat-stage.ts +195 -0
- package/src/serve/checkpoint-identity.ts +64 -0
- package/src/serve/completion-executor.ts +15 -7
- package/src/serve/discovery-routes.ts +12 -13
- package/src/serve/generation-gateway.ts +154 -193
- package/src/serve/http.ts +159 -0
- package/src/serve/inference-request.ts +79 -0
- package/src/serve/isolate.ts +297 -105
- package/src/serve/lab-routes.ts +3 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +7 -5
- package/src/serve/model-binding.ts +43 -0
- package/src/serve/model-host.ts +512 -0
- package/src/serve/openai-wire.ts +103 -0
- package/src/serve/parent-application.ts +56 -0
- package/src/serve/pipeline.ts +33 -0
- package/src/serve/preparation.ts +33 -0
- package/src/serve/request-plan.ts +19 -15
- package/src/serve/request-prep.ts +346 -0
- package/src/serve/response-lease.ts +24 -0
- package/src/serve/responses-client.ts +69 -0
- package/src/serve/session-completion-engine.ts +110 -0
- package/src/serve/session-search.ts +1 -1
- package/src/serve/static-routes.ts +5 -5
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +317 -2649
- package/src/spec/assistant-source.ts +10 -44
- package/src/spec/deepspec-source.ts +6 -6
- package/src/spec/dflash-source.ts +8 -8
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +2 -2
- package/src/spec/dspark/generate-dflash.ts +2 -2
- package/src/spec/dspark/generate.ts +2 -2
- package/src/spec/dspark/loader.ts +1 -1
- package/src/spec/dspark/module-dflash.ts +4 -3
- package/src/spec/dspark/module.ts +3 -3
- package/src/spec/dspark/quantize-drafter.ts +1 -1
- package/src/spec/generate.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +1 -1
- package/src/spec/ngram-source.ts +1 -1
- package/src/spec/prefix-state.ts +45 -0
- package/src/spec/qwen-mtp-source.ts +166 -69
- package/src/spec/serve-loop.ts +154 -156
- package/src/spec/source.ts +42 -10
- package/src/spec/two-model.ts +1 -1
- package/src/ssd-cache.ts +80 -12
- package/src/ssd-durability.ts +25 -13
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +2 -2
- package/src/train/forward.ts +6 -0
- package/src/train/loss.ts +1 -1
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +5 -3
- package/src/train/trainer.ts +6 -6
- package/src/vision/prompt.ts +1 -1
- package/src/vision/qwen3vl-tower.ts +52 -10
- package/src/web/app.html +1 -1
- package/src/web/app.js +5 -3
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +4 -4
- package/src/web/src/composer.ts +4 -4
- package/src/web/src/hub.ts +1 -1
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +1 -1
- package/src/web/src/model-picker.ts +1 -1
- package/src/web/src/palette.ts +1 -1
- package/src/web/src/protocol.ts +3 -39
- package/src/web/src/sessions.ts +1 -1
- package/src/web/src/ui-catalog.ts +4 -18
- package/src/web-assets.ts +47 -0
- package/src/weights.ts +55 -9
- package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/archive/investigations/hlg-ab.html +0 -34
- package/docs/archive/investigations/hlg-explorer.html +0 -238
- package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
- package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/archive/investigations/hlg-report.html +0 -262
- package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/archive/mlx-bun-lab-report.html +0 -275
- package/docs/archive/wwdc-mlx-bun.html +0 -848
- package/docs/design/adapters-end-to-end.md +0 -205
- package/docs/design/batching-perf-path.md +0 -255
- package/docs/design/batching-v2-plan.md +0 -529
- package/docs/design/bucketing-stage.md +0 -197
- package/docs/design/compat-cli-surface-design.md +0 -230
- package/docs/design/docs-reorg-plan.md +0 -165
- package/docs/design/dspark-serving-program.md +0 -371
- package/docs/design/dspark-speculative-decoding.md +0 -318
- package/docs/design/grammar-spec-batching-integration.md +0 -301
- package/docs/design/memory-inference-path.md +0 -422
- package/docs/design/memory-synthesis.md +0 -287
- package/docs/design/memory-system.md +0 -237
- package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
- package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
- package/docs/design/omlx-adoption-map.md +0 -98
- package/docs/design/orpo-dynamic-lambda.md +0 -220
- package/docs/design/orpo-future-enhancements.md +0 -340
- package/docs/design/paged-kv-cache.md +0 -214
- package/docs/design/parallel-slots.md +0 -567
- package/docs/design/parity-tier-dag.md +0 -161
- package/docs/design/repo-cleanup-plan.md +0 -231
- package/docs/design/runtime-isolation.md +0 -263
- package/docs/design/segmented-backward-training.md +0 -543
- package/docs/design/spec-decode-larger-targets.md +0 -229
- package/docs/design/ssd-kv-cold-tier.md +0 -415
- package/docs/design/structured-output.md +0 -484
- package/docs/design/tauri-desktop-app.md +0 -127
- package/docs/design/the-dreaming-master-plan.md +0 -445
- package/docs/design/turboquant-kv.md +0 -235
- package/docs/design/turboquant-weights.md +0 -467
- package/docs/design/w4a16-compute-precision-spike.md +0 -232
- package/docs/design/web-chat-beat-matrix.md +0 -315
- package/docs/design/write-pipeline-entity.md +0 -468
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -101
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -11
- /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
- /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
- /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
- /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
- /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
- /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: landed
|
|
3
|
+
axis: ON
|
|
4
|
+
canonical-for: kv-cache
|
|
5
|
+
plan-anchor: "Phase 18 — Concurrent / batched serving (slots) + parallel load benchmark `[~]` (2026-06-13)"
|
|
6
|
+
last-verified: 2026-08-23
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# KV cache — residency and layout
|
|
10
|
+
|
|
11
|
+
The one doc for where KV lives and how it is arranged: the cache classes
|
|
12
|
+
and their `signature()` capability model, the RAM prompt cache
|
|
13
|
+
(`PromptCache.take()`), the SSD cold tier and its durability boundary, the
|
|
14
|
+
optional paged block allocator, and how KV *schemes* plug into all of it.
|
|
15
|
+
The TurboQuant codec itself is documented in
|
|
16
|
+
[docs/design/turboquant.md](./turboquant.md); this doc only covers how its cache
|
|
17
|
+
class participates in residency.
|
|
18
|
+
|
|
19
|
+
Sources consolidated here: `docs/design/kv-cache.md`, `docs/design/kv-cache.md`, and
|
|
20
|
+
the cache-integration parts of `docs/design/turboquant.md`. Status and changelog
|
|
21
|
+
prose lives in PLAN.md; this file keeps mechanism, invariants, decisions,
|
|
22
|
+
and open items.
|
|
23
|
+
|
|
24
|
+
Code: `src/model/gemma4-base.ts` (Cache interface + the K/V cache family),
|
|
25
|
+
`src/model/batched-*.ts`, `src/lab/paged-kv/paged-kv.ts`, `src/prompt-cache.ts`,
|
|
26
|
+
`src/kv-store.ts`, `src/ssd-cache.ts`, `src/ssd-durability.ts`,
|
|
27
|
+
`src/kv-scheme.ts`, `src/generate.ts` (`maybeQuantizeKv` / `maybePageKv`),
|
|
28
|
+
`src/server.ts` (wiring, `/stats`, `POST /admin/cache/flush`),
|
|
29
|
+
`src/serve/generation-gateway.ts`, `src/serve/batch-scheduler.ts`.
|
|
30
|
+
|
|
31
|
+
## 1. Layering
|
|
32
|
+
|
|
33
|
+
Residency is a three-tier stack, each tier keyed by the exact token prefix
|
|
34
|
+
its KV encodes:
|
|
35
|
+
|
|
36
|
+
| tier | owner | unit | key |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| 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 |
|
|
40
|
+
| 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
|
+
|
|
42
|
+
The prompt cache tiers over the SSD store *inside* `take()` (structural
|
|
43
|
+
`ColdTier` interface), so every consumer (serial lane, batch scheduler
|
|
44
|
+
joiners, `/admin/drain`) gets both tiers through the same `take()`/`put()`
|
|
45
|
+
calls. The default path does no persistence work on the token loop:
|
|
46
|
+
spill/persist happen at request boundaries, evictions, idle sweeps, and
|
|
47
|
+
explicit flushes. The opt-in `--generation-checkpoint N` path deliberately
|
|
48
|
+
pauses at coarse, safe decode boundaries to atomically persist an in-flight
|
|
49
|
+
generation (section 5.7). Paged KV
|
|
50
|
+
(section 6) is a *layout* choice for the live tier and, in v1, bypasses the
|
|
51
|
+
other two.
|
|
52
|
+
|
|
53
|
+
## 2. Cache classes and the signature/capability model
|
|
54
|
+
|
|
55
|
+
### 2.1 The `Cache` contract (`gemma4-base.ts`)
|
|
56
|
+
|
|
57
|
+
Every cache-owning layer holds one `Cache`:
|
|
58
|
+
|
|
59
|
+
- `updateAndFetch(k, v)` appends and returns the attended window;
|
|
60
|
+
`makeMask(N, windowSize)`; `state()` (arrays to eval at a prefill chunk
|
|
61
|
+
boundary); `dispose()`.
|
|
62
|
+
- `isTrimmable()` / `trim(n, bypass?)` — whether the last `n` tokens can be
|
|
63
|
+
dropped. This single predicate drives prefix reuse: an entry longer than
|
|
64
|
+
the matched prefix is usable only if every cache can trim.
|
|
65
|
+
- `signature?()` — stable storage identity ("`kv:plain`",
|
|
66
|
+
"`kv:quant:<bits>:<group>`", "`kv:rotating-plain`",
|
|
67
|
+
"`kv:rotating-quant:<bits>:<group>`", "`kv:turboquant:<k>:<v>`", "`ssm`",
|
|
68
|
+
"`kv:mla:target`", "`kv:mla:target:dsa`", "`kv:mla:mtp`"). Persistence
|
|
69
|
+
codecs (`kv-store.ts` `CACHE_CODECS`) and the scheduler's layer-kind map
|
|
70
|
+
dispatch on it. `cacheSignature(cache)` returns `"unknown"` when the
|
|
71
|
+
method is absent.
|
|
72
|
+
- `bytesPerToken?()` — physical growth per token per row (0 for recurrent
|
|
73
|
+
state); `stateNeedsDispose` (TurboQuant's `state()` returns fresh views
|
|
74
|
+
the caller must dispose); `ropeOffsetArr` (per-row RoPE positions for
|
|
75
|
+
batched/compiled paths); `specRound*` (speculative rollback for
|
|
76
|
+
untrimmable recurrent caches).
|
|
77
|
+
|
|
78
|
+
Capability interfaces sit beside the signature: `RowBatchCache`
|
|
79
|
+
(`batchSize`/`filterRows`/`extractRow`) and `BatchableCache` (caches that
|
|
80
|
+
own their own dynamic-row batching — GLM's MLA/DSA state). Type guards
|
|
81
|
+
(`isPlainKvCache`, `isRotatingPlainCache`, …) are signature-based;
|
|
82
|
+
`isRowBatchCache`/`isBatchableCache` are structural.
|
|
83
|
+
|
|
84
|
+
### 2.2 The family
|
|
85
|
+
|
|
86
|
+
| class | file | signature | trimmable | notes |
|
|
87
|
+
|---|---|---|---|---|
|
|
88
|
+
| `KVCache` | gemma4-base.ts | `kv:plain` | yes | mlx-lm port; grows in `STEP = 256` slabs, in-place `sliceUpdate` |
|
|
89
|
+
| `QuantizedKVCache` | gemma4-base.ts | `kv:quant:b:g` | yes | affine triples (packed/scales/biases), groups along head_dim |
|
|
90
|
+
| `RotatingKVCache` | gemma4-base.ts | `kv:rotating-plain` | **only while `offset < maxSize`** (pre-wrap) | sliding-window ring |
|
|
91
|
+
| `RotatingQuantizedKVCache` | gemma4-base.ts | `kv:rotating-quant:b:g` | pre-wrap | quantized ring |
|
|
92
|
+
| `TurboQuantKVCache` | gemma4-base.ts | `kv:turboquant:k:v` | yes | not a `KVCache` subclass — deliberately fails every `instanceof` gate (monolith fallback, solo-only); see docs/design/turboquant.md |
|
|
93
|
+
| `SSMCache` | qwen3-delta.ts | `ssm` | **no** (recurrent) | `bytesPerToken()` = 0; per-row `offsets` in the batch lane |
|
|
94
|
+
| `Glm52Cache` / `MLACache` | glm52-cache.ts | `kv:mla:*` | yes | `BatchableCache`; compressed latent + rope (+ DSA index) |
|
|
95
|
+
| `PagedKVCache` | paged-kv.ts | **none** | yes | block pool + gather; section 6 |
|
|
96
|
+
| `BatchedRotatingCache` | batched-rotating.ts | `kv:rotating-plain` | pre-wrap | `RowBatchCache`; batched ring, host-side scalar ring state |
|
|
97
|
+
| `BatchedRotatingQuantCache` | batched-rotating-quant.ts | inherited (`extends RotatingQuantizedKVCache`) | pre-wrap | batched quantized ring |
|
|
98
|
+
| `BatchedDecodeMaskCache` | batched-mask.ts | **none** | delegates | per-step wrapper over a B-generic inner (padding mask + per-row RoPE) |
|
|
99
|
+
| `BatchedQuantDecodeMaskCache` | batched-quant.ts | inherited (`extends QuantizedKVCache`) | delegates | quantized twin of the wrapper; subclassing keeps generated forwards' `instanceof` guards happy |
|
|
100
|
+
| compiled-decode `Trace*` | compiled-decode.ts | inherited | — | trace adapters over the four compiled-supported classes |
|
|
101
|
+
| training caches (`TrainingCache`, `TrainingSSMCache`, `BatchedMaskCache`, `PrefixSharedCache`, `Gemma4PrefixSharedCache`, `Gemma4PrefixSharedSegCache`) | src/train/* | **none** | — | never admitted, cloned, or persisted; the interface comment explicitly allows omission here |
|
|
102
|
+
|
|
103
|
+
Invariant: **quantization groups run along head_dim, never the token
|
|
104
|
+
axis**, so token-axis slicing (trim, `cloneKvCaches` live views, batch row
|
|
105
|
+
surgery, persistence slicing to `offset`) is byte-safe for every quantized
|
|
106
|
+
kind. Every cache kind persisted by kv-store has a `restoreState(...)`.
|
|
107
|
+
|
|
108
|
+
### 2.3 Batching gates
|
|
109
|
+
|
|
110
|
+
Two independent checks decide whether a model/scheme can batch
|
|
111
|
+
(`generation-gateway.ts`):
|
|
112
|
+
|
|
113
|
+
- `#modelCachesBatchable()` — every fresh `makeCache()` entry is a
|
|
114
|
+
`KVCache`, `RotatingKVCache`, `BatchableCache`, or (unless
|
|
115
|
+
`MLX_BUN_BATCH_SSM=0`) `SSMCache`. Novel classes (TurboQuant, paged) fail
|
|
116
|
+
this by construction.
|
|
117
|
+
- `#kvBatchable()` — `KvScheme.batchable(config, canConvert)`: only the
|
|
118
|
+
per-layer `affine-config` scheme batches, and only when every configured
|
|
119
|
+
layer is a plain or rotating cache that can convert. Uniform `kvBits`
|
|
120
|
+
and TurboQuant stay serial; a scheme-less gateway must never silently
|
|
121
|
+
drop quantization (`kv-scheme.ts` makes the capability probe mandatory
|
|
122
|
+
for quantized schemes).
|
|
123
|
+
|
|
124
|
+
The scheduler then maps each layer to a kind ("owned-batch" / "rot" /
|
|
125
|
+
"ssm" / "full") from the prototype caches, and joins/filters/extracts by
|
|
126
|
+
that kind.
|
|
127
|
+
|
|
128
|
+
### 2.4 Known design gap: optional `signature()` with an `"unknown"` fallback
|
|
129
|
+
|
|
130
|
+
`Cache.signature` is optional and `cacheSignature()` substitutes
|
|
131
|
+
`"unknown"`. That makes an unsigned class *route* rather than *fail*: any
|
|
132
|
+
signature-based conjunct silently evaluates false. This is exactly how the
|
|
133
|
+
2026-08-22 agg×4 regression happened (PLAN.md "agg×4 regression —
|
|
134
|
+
root-caused and fixed", commit `443f333`; the BatchedRotatingCache fix
|
|
135
|
+
PRs #42/#43): the scheduler's rot-merge required
|
|
136
|
+
`isRowBatchCache(prev) && isRotatingPlainCache(prev)`, `BatchedRotatingCache`
|
|
137
|
+
had no `signature()`, the running batch's ring was dropped from the join,
|
|
138
|
+
and the next full-B decode step crashed in the grow-path concatenate. The
|
|
139
|
+
quantized twin escaped only because it inherits a real signature.
|
|
140
|
+
|
|
141
|
+
What holds today (verified in src, 2026-08-23):
|
|
142
|
+
|
|
143
|
+
- `BatchedRotatingCache` now returns `"kv:rotating-plain"`
|
|
144
|
+
(`batched-rotating.ts`), and the scheduler's rot branch routes by
|
|
145
|
+
capability alone (`isRowBatchCache`). The comment at
|
|
146
|
+
`batch-scheduler.ts` ~line 1007 still says the class has *no* override —
|
|
147
|
+
that comment is stale; the guard order it describes (capability first)
|
|
148
|
+
is still the rule.
|
|
149
|
+
- Still unsigned: `PagedKVCache` and `BatchedDecodeMaskCache` in the
|
|
150
|
+
serving tree, plus the six training-only caches listed above. Neither
|
|
151
|
+
serving class is currently reachable by a signature-dispatched path
|
|
152
|
+
(paged bypasses prompt-cache/kv-store/batching; the mask wrapper is
|
|
153
|
+
rebuilt per step and the scheduler dispatches on the *inner* caches),
|
|
154
|
+
so today the gap is latent, not live.
|
|
155
|
+
- Rule going forward (PLAN.md lesson): **"unknown" is a bug, not a
|
|
156
|
+
route.** Any new `Cache` that can reach the scheduler, `cloneKvCaches`,
|
|
157
|
+
or kv-store must declare a signature; kv-store already throws on an
|
|
158
|
+
unknown signature rather than guessing. Making `signature()` required on
|
|
159
|
+
the interface (with training adapters returning an explicit
|
|
160
|
+
`"training"`) is the open hardening item.
|
|
161
|
+
|
|
162
|
+
## 3. KV schemes — how they plug into the cache (`src/kv-scheme.ts`)
|
|
163
|
+
|
|
164
|
+
`KvScheme` is the immutable declaration of *what bytes a token costs and
|
|
165
|
+
which cache class holds it*: kind `bf16` | `affine-uniform` |
|
|
166
|
+
`affine-config` | `turbo`, resolved once at server start by
|
|
167
|
+
`resolveKvScheme` (TurboQuant wins if set; `--kv-quant off|<bits>|config`
|
|
168
|
+
otherwise; a missing `kv_config.json` under `config` is bf16 or an error
|
|
169
|
+
per caller). Its roles in residency:
|
|
170
|
+
|
|
171
|
+
- **Conversion chokepoint.** `maybeQuantizeKv` (`generate.ts`) converts
|
|
172
|
+
populated plain/rotating caches in place after the first prefill
|
|
173
|
+
(`offset === 0` caches are skipped — the optiq mixed-KV convention; the
|
|
174
|
+
affine start threshold is `quantizedKvStart`, which `resolveKvScheme`
|
|
175
|
+
pins to 0 for every server scheme — the `generate.ts` fallback of 5000
|
|
176
|
+
for uniform bits only reaches library callers who pass `kvBits` without
|
|
177
|
+
a threshold) and evaluates layer by layer so the bf16 source frees
|
|
178
|
+
before the next layer converts. TurboQuant converts
|
|
179
|
+
full-attention `KVCache` layers only; rotating layers stay bf16 with a
|
|
180
|
+
one-time warning.
|
|
181
|
+
- **Byte accounting.** `kvBytesAt`/`kvGeometry` bill per layer type (full
|
|
182
|
+
/ sliding capped at the window / linear state) with
|
|
183
|
+
`bits/8 + 4/groupSize` bytes per affine element. `fitOptions` bills
|
|
184
|
+
TurboQuant as bf16 on purpose (conservative until its packed layout
|
|
185
|
+
exposes a stable projector). The scheduler's KV-budget admission uses
|
|
186
|
+
the same seam, and `BatchableCache.projectedBytes` for owned-batch
|
|
187
|
+
caches.
|
|
188
|
+
- **Cache compatibility key.** `cacheKey` (`bf16` | `kv<bits>` | `config`
|
|
189
|
+
| `turbo-k<k>v<v>`) is folded into the SSD store's fingerprint
|
|
190
|
+
(`${configFingerprint(config)}-${schemeKey}`, `server.ts`), so restored
|
|
191
|
+
files always match what serving would have produced. `configFingerprint`
|
|
192
|
+
itself covers the per-layer `kv_config.json` scheme but not CLI-only
|
|
193
|
+
levers (uniform bits, TurboQuant) — the scheme key is the anti-collision
|
|
194
|
+
mechanism for those.
|
|
195
|
+
- **Batch placement.** `batchable()` as in 2.3.
|
|
196
|
+
|
|
197
|
+
Uniform 4/8-bit affine KV is bit-exact L1 (unfused SDPA == mlx-lm
|
|
198
|
+
`base.py`); the per-layer config scheme is the L2/optiq composition, and
|
|
199
|
+
TurboQuant is a third, oracle-backed codec (vllm-metal). The scheme axis
|
|
200
|
+
(which bits where) is orthogonal to the residency axis (which tier holds
|
|
201
|
+
them); nothing in this doc changes numerics.
|
|
202
|
+
|
|
203
|
+
## 4. RAM tier — `PromptCache` (`src/prompt-cache.ts`)
|
|
204
|
+
|
|
205
|
+
Byte-capped LRU of whole prefix entries (`--prompt-cache <GB>`, default
|
|
206
|
+
8 GB; `0` disables). The mlx-lm lesson that motivated it — a count-capped
|
|
207
|
+
cache of multi-GB entries is an OOM footgun — still stands even though
|
|
208
|
+
upstream is byte-capped now.
|
|
209
|
+
|
|
210
|
+
**`take(prompt, ns)` — prefix sharing.** Finds the entry with the longest
|
|
211
|
+
usable common prefix (usable = common prefix capped at `prompt.length - 1`,
|
|
212
|
+
so at least one token is forwarded; an entry longer than the match must be
|
|
213
|
+
trimmable). Trimmable entries are cloned zero-copy (`cloneKvCaches`: slice
|
|
214
|
+
views of live arrays), trimmed, and returned with a ref-counted `retain`
|
|
215
|
+
share; the donor stays in place, so N agents can share one prefill. An exact
|
|
216
|
+
hit on a Qwen hybrid entry containing recurrent SSM state transfers the donor
|
|
217
|
+
itself to bound resident snapshots; the request puts its extended state back
|
|
218
|
+
on completion. With an SSD tier, `take` persists that exact boundary before
|
|
219
|
+
transferring ownership. An already-durable prefix needs only an index check.
|
|
220
|
+
The extended recurrent state cannot trim back to the consumed boundary, so
|
|
221
|
+
keeping its SSD copy preserves later exact prompt repeats. A failed required
|
|
222
|
+
store leaves the donor and backing owned by the cache and refuses the transfer.
|
|
223
|
+
|
|
224
|
+
**Tier order inside `take()`.** The cold tier is consulted with
|
|
225
|
+
`find(prompt, ns)` (index-only, no I/O) and wins only with a *strictly*
|
|
226
|
+
longer usable prefix; then `restore(handle)` materializes it and the
|
|
227
|
+
divergent tail is trimmed. A cold entry that would need a trim but is
|
|
228
|
+
untrimmable (wrapped ring, SSM) is disposed loudly and the RAM candidate
|
|
229
|
+
(or a fresh prefill) serves. `hits`/`misses` count RAM candidacy only.
|
|
230
|
+
|
|
231
|
+
**`put(tokens, caches, ns, retain)` — supersession + eviction.** Exact
|
|
232
|
+
duplicates (same tokens) are replaced regardless of trimmability. Strict
|
|
233
|
+
prefix-ancestors in the same ns are superseded without spill — but only
|
|
234
|
+
when the *new* entry is fully trimmable; an untrimmable new entry (wrapped
|
|
235
|
+
ring) can only serve exact-length matches, so shorter ancestors (the
|
|
236
|
+
prompt-boundary snapshot) must survive. Then LRU-evict until under cap,
|
|
237
|
+
spilling each evictee to the cold tier first. An entry larger than the cap
|
|
238
|
+
is spilled and disposed, never stored. `onPut` fires after every
|
|
239
|
+
successful put — the server hangs the write-behind scheduler there so both
|
|
240
|
+
lanes' entries persist.
|
|
241
|
+
|
|
242
|
+
**Prompt-boundary snapshot.** Serial lane (`server.ts`) and batch
|
|
243
|
+
scheduler (`snapshotAt`) both clone-and-put a trim-free *prompt-only*
|
|
244
|
+
entry at the template probe's stable boundary during a substantial cold
|
|
245
|
+
prefill. Rationale: the prompt+generation entry put at completion is
|
|
246
|
+
untrimmable past the sliding window (wrapped rings) and under mid-group
|
|
247
|
+
quantization, so any decode→encode drift in the reply the client sends
|
|
248
|
+
back would turn the next turn into a total miss; a prompt-only entry is
|
|
249
|
+
always an exact prefix of the next rendering.
|
|
250
|
+
|
|
251
|
+
**Namespaces.** `ns` = the adapter spec (`adapters.join("+")`, `""` = base).
|
|
252
|
+
KV computed under one adapter never seeds another's prefill.
|
|
253
|
+
|
|
254
|
+
**Bypasses.** Media (vision/audio) requests skip the cache (soft tokens are
|
|
255
|
+
identical placeholder ids → false hits), as do paged-KV requests (v1) and
|
|
256
|
+
the speculative lane (v1). Merged batch rows are not re-put on finish;
|
|
257
|
+
never-merged lone rows put their adopted caches back zero-copy, and
|
|
258
|
+
merged rows are extracted per row (`#extractAndPut`) before `filter()`
|
|
259
|
+
mutates the batched inners.
|
|
260
|
+
|
|
261
|
+
**Idle demotion (`demoteIdle`).** Entries unused past a threshold spill to
|
|
262
|
+
the cold tier and free their GPU arrays; the prefix stays reachable via
|
|
263
|
+
restore. No-op without a cold tier (demotion with nowhere to demote to is
|
|
264
|
+
data loss). `/admin/drain` (unix-socket engine children only) is
|
|
265
|
+
`demoteIdle(0)` under the gateway lock — the model pool's lossless
|
|
266
|
+
pre-eviction step.
|
|
267
|
+
|
|
268
|
+
**Spill contract (`SpillSink`).** Preferred `spillOwned`: the cache hands
|
|
269
|
+
the sink zero-copy clones + copied tokens made *before* the donor is
|
|
270
|
+
disposed, and the sink owns their disposal on every settle path. A sink
|
|
271
|
+
that throws synchronously has not taken ownership; the spill degrades to
|
|
272
|
+
a no-op and eviction never unwinds. `spillSync` (live entry, synchronous)
|
|
273
|
+
remains for tests and simple embedders.
|
|
274
|
+
|
|
275
|
+
Known numerics property (not a residency defect): continuing from a
|
|
276
|
+
restored or reused prefix produces the same output as the RAM tier's own
|
|
277
|
+
cache hit, but a bf16 prefix-reuse continuation can differ from a fresh
|
|
278
|
+
full prefill (GEMV-vs-GEMM reduction order). Both tiers share it.
|
|
279
|
+
|
|
280
|
+
## 5. SSD cold tier
|
|
281
|
+
|
|
282
|
+
### 5.1 Decisions
|
|
283
|
+
|
|
284
|
+
- **D1 — Whole-prefix-entry spill, not paged blocks.** Single-user serving
|
|
285
|
+
with a prefix-entry cache that grows per conversation. Per-block hashing
|
|
286
|
+
and save-queue bookkeeping during decode is where oMLX's measured ~20%
|
|
287
|
+
steady-state tax came from; ours does zero work on the token loop
|
|
288
|
+
(prefix tokens are the key). Partial-prefix reuse works without paging:
|
|
289
|
+
restore + `trim()`. The economics that justify the tier are
|
|
290
|
+
SSD-vs-recompute, not SSD-vs-RAM: at 12B prefill rates a 30k agent
|
|
291
|
+
context is minutes of GPU compute against a ~1 s NVMe restore.
|
|
292
|
+
- **D2 — One store, two tiers.** The cold tier is bound *into*
|
|
293
|
+
`PromptCache` (section 4); restart persistence comes from a debounced
|
|
294
|
+
write-behind snapshot after each put, not only from spill-on-evict.
|
|
295
|
+
- **D3 — Own page-aligned format, not safetensors.** `src/kv-store.ts`
|
|
296
|
+
writes `MLXBUNKV2\n`-magic files whose current layout is the **v3
|
|
297
|
+
streaming writer**: header sized up front with fixed-width tensor
|
|
298
|
+
hashes, tensors materialized/hashed/written one at a time at 16 KiB
|
|
299
|
+
aligned offsets, header patched last (peak host transient = one
|
|
300
|
+
tensor). Header carries `formatVersion`, `modelId`,
|
|
301
|
+
`configFingerprint`, `ns`, `tokenizerHash`, `createdAt`, `tokens`, and
|
|
302
|
+
per-cache `{kind, offset, idx?, maxSize?, groupSize?, bits?, kBits?,
|
|
303
|
+
vBits?, headDim?, GLM geometry, tensors[]}`. Kinds: `kv`, `rotating`,
|
|
304
|
+
`qkv`, `rotating-qkv`, `ssm`, `turboquant`, `mla`, `mla-dsa`, `mtp-mla`.
|
|
305
|
+
Quantized buffers are sliced to `offset` (no 256-step slack persisted).
|
|
306
|
+
Header hash always verified; per-tensor hashes verified only under
|
|
307
|
+
`--ssd-cache-verify` (eager reads roughly double restore cost). Writes
|
|
308
|
+
are atomic (tmp + fsync + rename). Older v1/v2 files read as
|
|
309
|
+
unsupported and are deleted-and-regenerated (machine-local, cheap).
|
|
310
|
+
- **D4 — Files are the database.**
|
|
311
|
+
`<dir>/<configFingerprint>/<nsHash>/<uuid>.mlxkv`. Startup recovery is a
|
|
312
|
+
header-only scan of *our* fingerprint dir into an in-memory
|
|
313
|
+
`SsdIndexEntry[]` (`{path, ns, tokens, bytes, mtimeMs, trimmable}`);
|
|
314
|
+
foreign fingerprint dirs are ignored, never deleted. LRU = file mtime
|
|
315
|
+
(`utimes` on hit); the byte cap is enforced at write time by oldest-mtime
|
|
316
|
+
eviction; corrupt headers and metadata mismatches are unlinked; `.tmp`
|
|
317
|
+
orphans reaped; disk-full is a warn-once soft-fail. Every failure path
|
|
318
|
+
degrades to "no hit" / "not stored" — the tier can never take serving
|
|
319
|
+
down.
|
|
320
|
+
- **Numerical compatibility.** The serving fingerprint also includes a hash
|
|
321
|
+
of `ModelServingBinding.stateCompatibility`. The MLX backend supplies its
|
|
322
|
+
actual runtime version and GPU architecture at binding creation. The same
|
|
323
|
+
artifact produces different cache tensors in the recorded 0.31.2/0.32.2
|
|
324
|
+
RTN4 comparisons, so graph geometry and KV format alone cannot qualify
|
|
325
|
+
automatic reuse. A runtime/architecture change selects another directory;
|
|
326
|
+
previous snapshots remain intact. The binding owns this identity, keeping
|
|
327
|
+
backend details out of request parsing and portable model configuration.
|
|
328
|
+
A replacement-binding restart regression verifies matching reuse, isolation
|
|
329
|
+
and recovery of the original snapshot. Evidence:
|
|
330
|
+
`reports/qwen38-rd/mlx-upgrade-0.32.2/cross-runtime-cache-numerics-review.json`.
|
|
331
|
+
The real RTN4 HTTP gate also passes all eight responses across four
|
|
332
|
+
internal-SSD server starts. Its first requests reuse 0/352/0/352 tokens
|
|
333
|
+
under default/default/alternate/default identities; every second request
|
|
334
|
+
reuses 352. The alternate tag changes metadata only. All response content,
|
|
335
|
+
tool calls and usage counts other than cached tokens match. Exit allocation
|
|
336
|
+
includes a retained prompt cache with different fresh/restored capacity,
|
|
337
|
+
so this is not a post-disposal memory comparison. Evidence:
|
|
338
|
+
`reports/qwen38-rd/runtime-state-http-review.json`.
|
|
339
|
+
- **D5 — Flags and interactions.** `--ssd-cache <dir>` (off unless set),
|
|
340
|
+
`--ssd-cache-max <GB>` (optional; `0` or unset = unlimited), `--ssd-cache-verify`,
|
|
341
|
+
`--ssd-demote-idle <sec>` (default 300 with the tier on; `0` disables).
|
|
342
|
+
Requires the RAM tier (`--prompt-cache 0` + `--ssd-cache` is a startup
|
|
343
|
+
error). Sub-flags without `--ssd-cache` warn and are ignored.
|
|
344
|
+
`--memory-budget` is unchanged (admission already assumes worst-case
|
|
345
|
+
bf16). `--paged-kv` warns that the tier sees nothing (section 6).
|
|
346
|
+
- **D6 — In-flight resume is explicit.** `--generation-checkpoint <tokens>`
|
|
347
|
+
requires `--ssd-cache` and `--batch 1`. It is off by default because each
|
|
348
|
+
checkpoint synchronizes and writes the live KV, trading decode speed and
|
|
349
|
+
SSD writes for bounded lost work. An identical request resumes; a changed
|
|
350
|
+
prompt or sampling policy starts normally.
|
|
351
|
+
|
|
352
|
+
### 5.2 Restore is a streamed copy (supersedes the original zero-copy plan)
|
|
353
|
+
|
|
354
|
+
The first design restored by COW mmap + `fromPointer` and unmapped in the
|
|
355
|
+
entry's `retain`. Two things killed it: the 2026-07-06 rule that mlx
|
|
356
|
+
buffer destructors must never call into JS removed the unmap signal, so
|
|
357
|
+
every restore became a process-lifetime mapping leak; and exactly-sized
|
|
358
|
+
restored buffers made the first post-restore decode step concat-copy the
|
|
359
|
+
whole entry anyway. `loadKvCache` now copies each tensor out of a
|
|
360
|
+
read-only mmap into an mlx-owned leaf (`fromBytesCopy`), drops the clean
|
|
361
|
+
file pages (`MADV_DONTNEED`; offsets are 16 KiB = arm64 page size),
|
|
362
|
+
unmaps before returning, and lands plain-KV tensors in STEP-rounded
|
|
363
|
+
capacity with slack so the first step updates in place. Peak host
|
|
364
|
+
transient = live entry + one tensor; nothing outlives the call. The
|
|
365
|
+
`retain` thunk stays in the entry contract as a no-op so callers' dispose
|
|
366
|
+
ordering is unchanged (the server's `coldTier.restore` returns
|
|
367
|
+
`() => {}`). Comments in `prompt-cache.ts` that still say "zero-copy
|
|
368
|
+
mmap" describe the superseded mechanism.
|
|
369
|
+
|
|
370
|
+
The write side is the mirror: hashing and writing from a zero-copy view
|
|
371
|
+
of the contiguous mlx buffer (`rawBytesView`), no JS-heap copy at any
|
|
372
|
+
point.
|
|
373
|
+
|
|
374
|
+
### 5.3 `find()` returns partial matches, gated on trimmability
|
|
375
|
+
|
|
376
|
+
`SsdCacheStore.find` returns the longest usable prefix (capped at
|
|
377
|
+
`prompt.length - 1`) and *skips* entries that would need a trim but are
|
|
378
|
+
untrimmable (`trimmable` is derived from the header kinds + state at
|
|
379
|
+
store/scan time via `cacheHeadersTrimmable`). Before this gate the big
|
|
380
|
+
untrimmable `[prompt+gen]` file always outranked the usable boundary
|
|
381
|
+
snapshot, got restored, and was thrown away — the 2026-07-06 "restart
|
|
382
|
+
restores 0 tokens" defect and its silent restore-then-re-prefill cost.
|
|
383
|
+
Store-side supersession mirrors `PromptCache.put`: exact duplicates
|
|
384
|
+
replaced; prefix-ancestors superseded only when the new entry is
|
|
385
|
+
trimmable.
|
|
386
|
+
|
|
387
|
+
### 5.4 Write-behind scheduling contract: own each tensor step
|
|
388
|
+
|
|
389
|
+
Every per-tensor flush step is real GPU-stream and JS-thread work
|
|
390
|
+
(`ops.contiguous` on the decode stream, a synchronous eval for
|
|
391
|
+
`rawBytesView`, a synchronous multi-MB `writeSync`). Interleaving those
|
|
392
|
+
between decode tokens taxed decode at long context, so:
|
|
393
|
+
|
|
394
|
+
- `saveKvCacheAsync` / `SsdCacheStore.storeAsync` accept a per-step runner.
|
|
395
|
+
The server passes `gateway.runExclusive`, so the idle decision and the
|
|
396
|
+
blocking MLX readback are one atomic engine turn. The earlier
|
|
397
|
+
`gateway.onIdle()` check had a check/use race: a request could begin after
|
|
398
|
+
the check and overlap the next tensor sync. A request arriving mid-flush
|
|
399
|
+
previously could run between tensor writes. Serial generation now first
|
|
400
|
+
drains the durability coordinator outside the engine lock, so pending
|
|
401
|
+
snapshot clones release their buffers before the next prefill.
|
|
402
|
+
- `GenerationGateway.busy` covers both lanes (serial mutex held/awaited or
|
|
403
|
+
batch rows active/pending).
|
|
404
|
+
- `MLX_BUN_SSD_WRITEBEHIND=0` disables write-behind snapshots entirely
|
|
405
|
+
(kill switch + paired-A/B lever; restart survival then degrades to
|
|
406
|
+
spill-on-evict).
|
|
407
|
+
|
|
408
|
+
Serial requests may wait for outstanding snapshots to finish. The drain
|
|
409
|
+
runs before acquiring the generation lock because the writer needs that
|
|
410
|
+
same lock. Continuous batching retains per-tensor scheduling.
|
|
411
|
+
|
|
412
|
+
The RAM cache's configured byte cap is only an upper bound. Serial serving
|
|
413
|
+
checks MLX's live allocation count against the smaller of admission's usable
|
|
414
|
+
memory and Metal's recommended working set, reserving prefill workspace and
|
|
415
|
+
the prompt's KV footprint for growth/copy-on-write overlap. Pressure relief
|
|
416
|
+
persists LRU entries synchronously before disposing them, avoiding deferred
|
|
417
|
+
spill clones that would keep the memory resident. The selected request cache
|
|
418
|
+
remains owned by the request; older entries remain reusable from SSD. Qwen
|
|
419
|
+
checks between prefill layers, and serial decode checks every 256 tokens.
|
|
420
|
+
If no reclaimable entries remain, the guard raises a request error instead
|
|
421
|
+
of intentionally proceeding over that budget. In-flight checkpoints survive.
|
|
422
|
+
|
|
423
|
+
### 5.5 Bounded retention: `SpillQueue` (`kv-store.ts`)
|
|
424
|
+
|
|
425
|
+
All three producers — eviction spills, idle demotions, write-behind
|
|
426
|
+
snapshots — go through one serial, byte-capped queue. Pending clones pin
|
|
427
|
+
their entries' GPU buffers until the generation-locked flush gets a turn; without
|
|
428
|
+
a cap, sustained traffic (gate starved, evictions ongoing) made resident
|
|
429
|
+
memory = prompt-cache cap + every queued clone. Policy: cap default 2 GiB
|
|
430
|
+
(`MLX_BUN_SSD_SPILL_QUEUE_GB`; `0` keeps only the newest + in-flight
|
|
431
|
+
clone); over cap the *oldest* not-in-flight item drops and its clones are
|
|
432
|
+
disposed immediately; the item just enqueued is never its own victim
|
|
433
|
+
(soft cap). A dropped spill is a future cache miss, never a wrong result.
|
|
434
|
+
Clones are disposed on every settle path (fulfil, reject, drop). The
|
|
435
|
+
snapshot timer never holds batch admission: while rows are active it
|
|
436
|
+
re-arms instead of grabbing the exclusive (entries are immutable; a late
|
|
437
|
+
snapshot is equally valid).
|
|
438
|
+
|
|
439
|
+
### 5.6 Durability boundary (`src/ssd-durability.ts`)
|
|
440
|
+
|
|
441
|
+
The 2026-08-22 finding: a debounce timer (1 s), a busy re-arm (5 s), and a
|
|
442
|
+
serial queue meant a SIGTERM shortly after a long request could outrun
|
|
443
|
+
the write — the server implied durability it did not have.
|
|
444
|
+
`SsdDurabilityCoordinator` turns puts into a state machine:
|
|
445
|
+
|
|
446
|
+
- `schedule(tokens, ns)` records a *dirty* key that includes the exact
|
|
447
|
+
token sequence (two same-ns, same-length conversations cannot cancel
|
|
448
|
+
each other) and arms the debounce.
|
|
449
|
+
- An attempt checks SSD coverage under `gateway.runExclusive` before cloning.
|
|
450
|
+
Otherwise it snapshots the RAM entry (`findExact` + `cloneKvCaches`) and
|
|
451
|
+
enqueues it; a busy gateway re-arms;
|
|
452
|
+
a dropped or failed store leaves the key dirty so it is retryable.
|
|
453
|
+
A snapshot that vanished from RAM is "stored" only if the SSD index
|
|
454
|
+
already covers the prefix (`hasDurablePrefix`), else "missing" and still
|
|
455
|
+
dirty. A second flush cannot clear that failure without durable coverage.
|
|
456
|
+
- `flush()` cancels timers, awaits in-flight attempts and `drain()`, then
|
|
457
|
+
forces each dirty record version once, one at a time (so the queue cap
|
|
458
|
+
cannot drop a boundary snapshot while a large final snapshot is in flight).
|
|
459
|
+
A replacement scheduled during a write receives its own attempt. After
|
|
460
|
+
all writes settle, the coordinator checks missing ancestors against the
|
|
461
|
+
committed SSD index again: a later trimmable descendant may now cover a
|
|
462
|
+
superseded RAM prefix. Uncovered or foreign-namespace prefixes stay dirty;
|
|
463
|
+
this adds no duplicate snapshot write. Result:
|
|
464
|
+
`durable` is true only when nothing is pending, nothing dropped or
|
|
465
|
+
failed during this flush, and no missing prefix remains uncovered.
|
|
466
|
+
|
|
467
|
+
Surfaces: `POST /admin/cache/flush` returns 200 at the boundary (503
|
|
468
|
+
otherwise) with pending/dropped/failed/entry-count/longest-prefix
|
|
469
|
+
evidence; `mlx-bun serve` handles SIGINT/SIGTERM by stopping admission,
|
|
470
|
+
draining active requests, and awaiting the same flush, bounded by
|
|
471
|
+
`MLX_BUN_SHUTDOWN_TIMEOUT_MS` (default 120000). `/stats.ssd_cache` exposes
|
|
472
|
+
`entries`, `bytes`, `max_bytes`, `restores`, `spills`, `restore_ms_last`,
|
|
473
|
+
`demotions`, `pending_snapshots` (includes debounce/re-arm timers),
|
|
474
|
+
`pending_spills`, `pending_spill_bytes`, `dropped_spills`,
|
|
475
|
+
`failed_spills`, `longest_durable_prefix_tokens`. The standard restart
|
|
476
|
+
benchmark calls the endpoint instead of sleeping.
|
|
477
|
+
|
|
478
|
+
### 5.7 In-flight generation checkpoints
|
|
479
|
+
|
|
480
|
+
At every configured token interval, `generate()` reaches a boundary where
|
|
481
|
+
the live caches cover every emitted token and the next token has already been
|
|
482
|
+
sampled but not emitted. The SSD entry stores both pieces atomically:
|
|
483
|
+
|
|
484
|
+
- the normal token list and cache tensors encode the emitted prefix;
|
|
485
|
+
- `generationCheckpoint` in the KV header records the request key, original
|
|
486
|
+
prompt length, emitted-token count, adapter namespace, and pending token.
|
|
487
|
+
|
|
488
|
+
Saving the pending token is what makes the mechanism work for Qwen hybrid
|
|
489
|
+
models: recurrent `SSMCache` state cannot trim one token to recompute logits.
|
|
490
|
+
After restart, an identical request restores the full cache, replays the saved
|
|
491
|
+
tokens through the normal completion sink so the client receives one ordinary
|
|
492
|
+
assistant response, then continues directly from the pending token with the
|
|
493
|
+
original sampler step and history. The request key includes rendered prompt
|
|
494
|
+
IDs and every sampling/KV policy field. Grammar, token-fill, media, paged KV,
|
|
495
|
+
and logprob requests are not checkpointed in v1 because they carry additional
|
|
496
|
+
state or response data that cannot yet be reconstructed by token replay.
|
|
497
|
+
|
|
498
|
+
Only the newest successful checkpoint for a request is retained. The old file
|
|
499
|
+
remains valid until the new file's fsync+rename completes; a crash mid-write
|
|
500
|
+
therefore loses at most one interval. Normal completion removes the checkpoint.
|
|
501
|
+
Client cancellation throws through the serial token callback and preserves the
|
|
502
|
+
last checkpoint, so terminating a long-lived client does not erase its restart
|
|
503
|
+
point.
|
|
504
|
+
|
|
505
|
+
### 5.8 Invalidation
|
|
506
|
+
|
|
507
|
+
Compatibility key = `configFingerprint(config)` + `KvScheme.cacheKey` +
|
|
508
|
+
tokenizer hash (`Bun.hash(tokenizer.json)`) + adapter ns; all enforced on
|
|
509
|
+
scan and load (`KvLoadExpect`). Chat-template drift degrades to a shorter
|
|
510
|
+
matched prefix (safe). GLM compressed caches additionally validate
|
|
511
|
+
geometry (`kvLoraRank`, `ropeHeadDim`, `dsaHeadDim`, `maxTokens`) against
|
|
512
|
+
the model's prototype cache before opening the tensor mmap.
|
|
513
|
+
|
|
514
|
+
### 5.9 Measured (kept with labels)
|
|
515
|
+
|
|
516
|
+
- 2026-07-02, M1 Max, MiniCPM5, 13.7k-token prefix, `kv_config` quant:
|
|
517
|
+
restart TTFT 12,083 ms (full prefill) → 236 ms restored; steady-state
|
|
518
|
+
decode 192.0 vs 192.3 tok/s (flag off/on). Restored + trimmed
|
|
519
|
+
continuation identical to the RAM tier's cache-hit output.
|
|
520
|
+
- 2026-07-07, streamed-copy restore, 512 MB synthetic entry: mlx peak
|
|
521
|
+
during restore + first step = live entry + one tensor (552 vs 520 MB
|
|
522
|
+
active); `vmmap` shows no `.mlxkv` mapping survives; 12B real-model cold
|
|
523
|
+
cache-load → first token 277 ms.
|
|
524
|
+
- 2026-07-07, e4b 9.5k-token entry: repeat decode 37.9 tok/s with the
|
|
525
|
+
flush overlapping vs 47.1 cold; flat 44–46 across three samples after
|
|
526
|
+
the idle gate.
|
|
527
|
+
- 2026-08-22, unified e4b, dirty machine (correctness evidence only):
|
|
528
|
+
4,026-token prompt flushed in 408 ms with `durable=true`, then 4,025
|
|
529
|
+
cached tokens restored after SIGTERM + respawn.
|
|
530
|
+
|
|
531
|
+
### 5.9 Open items
|
|
532
|
+
|
|
533
|
+
- Full clean 16k e4b / 12B / Qwen3.8 restart matrix (batch 8 and
|
|
534
|
+
`--batch 1`, `cached_tokens ≥ prompt_tokens - 1`) is the acceptance run
|
|
535
|
+
for the durability fix; only the dirty-machine smoke has run.
|
|
536
|
+
- P4 hardening extras never closed: kill-during-write crash test; a
|
|
537
|
+
`THIRD_PARTY_LICENSES.md` note if any oMLX-derived code (not just ideas)
|
|
538
|
+
ever lands.
|
|
539
|
+
- Block-granular dedup for multi-user shared prefixes stays out of scope
|
|
540
|
+
(D1); revisit only with the paged follow-ups in 6.4.
|
|
541
|
+
- Stale comments: `prompt-cache.ts` still describes restore as zero-copy
|
|
542
|
+
mmap (5.2).
|
|
543
|
+
|
|
544
|
+
## 6. Optional paged KV (`src/lab/paged-kv/paged-kv.ts`, `--paged-kv`)
|
|
545
|
+
|
|
546
|
+
Default off; with the flag unset no paged code executes and the plain
|
|
547
|
+
`KVCache`/`RotatingKVCache` path is byte-identical.
|
|
548
|
+
|
|
549
|
+
### 6.1 What it buys here, honestly
|
|
550
|
+
|
|
551
|
+
vLLM's PagedAttention solves contiguous worst-case per-sequence
|
|
552
|
+
reservation and allocator fragmentation for a multi-tenant server.
|
|
553
|
+
mlx-bun is one process on unified memory with byte-budget admission
|
|
554
|
+
already shipped, so what transfers is: (a) padded-batch waste removal —
|
|
555
|
+
today's batched buffer width tracks the longest live row
|
|
556
|
+
(`mergeKVRows`/`extendKVRows`), so a short row cohabiting with a long one
|
|
557
|
+
pays the long one's KV; and (b) block-level copy-on-write prefix sharing.
|
|
558
|
+
Neither is in v1. **At batch=1 paging buys nothing** — gather-before-attend
|
|
559
|
+
is bandwidth tax on a bandwidth-bound decode. v1 ships the
|
|
560
|
+
correctness-proven block-manager abstraction at serial scope as
|
|
561
|
+
scaffolding for the follow-ups, not as a speed win.
|
|
562
|
+
|
|
563
|
+
This is neither of the two paging rejections already on record:
|
|
564
|
+
paged KV as a *prompt-cache substitute* (rejected 2026-07-07 — `take()`'s
|
|
565
|
+
zero-copy clones already share physically) and paged *blocks* as the SSD
|
|
566
|
+
spill granularity (D1). It is the rung-3 allocation abstraction from
|
|
567
|
+
docs/design/batching.md.
|
|
568
|
+
|
|
569
|
+
No external oracle exists (mlx-lm's `cache.py` has no paged cache); the
|
|
570
|
+
gate is mlx-bun's own plain `KVCache`, valid because the claim is storage
|
|
571
|
+
equivalence (same bytes, different arrangement), so the bar is bit-exact
|
|
572
|
+
(tol 0): `tests/paged-kv.test.ts` (model-free: block-boundary crossing,
|
|
573
|
+
free-list reuse after trim, typed exhaustion, gathered fetch vs a plain
|
|
574
|
+
reference, dispose) and `tests/paged-kv-parity.test.ts` (weights-gated
|
|
575
|
+
greedy trajectory paged-on vs paged-off).
|
|
576
|
+
|
|
577
|
+
### 6.2 Mechanism
|
|
578
|
+
|
|
579
|
+
- **`BlockPool`** — per-layer arena: K and V pool tensors
|
|
580
|
+
`[numBlocks, H_kv, blockSize, headDim]` (V may differ in head dim),
|
|
581
|
+
`ops.zeros`-allocated so they are mlx-owned end to end (no host-pointer
|
|
582
|
+
alignment/dtor hazards). LIFO free list; `alloc()` throws the typed
|
|
583
|
+
`PagedPoolExhausted`; writes are `ops.sliceUpdate` at
|
|
584
|
+
`[block, 0, within, 0]` — the incoming `[1,H,l,D]` piece matches the
|
|
585
|
+
destination slice directly.
|
|
586
|
+
- **`PagedKVCache implements Cache`** — standalone (not a `KVCache`
|
|
587
|
+
subclass, the TurboQuant reasoning: every `instanceof` gate excludes it
|
|
588
|
+
and routes to the monolith forward). `updateAndFetch` splits the
|
|
589
|
+
incoming `L` along block boundaries, allocates tail blocks as reached,
|
|
590
|
+
then gathers occupied blocks with `ops.takeAxis(pool, blockTable, 0)` →
|
|
591
|
+
transpose `[1,0,2,3]` → reshape `[1,H,nb·bs,D]` → slice to `offset`. Only
|
|
592
|
+
existing bound ops — no `mlx_gather` FFI binding was needed. `makeMask`
|
|
593
|
+
is `KVCache.makeMask`'s logic; `trim(n)` rewinds `offset` and frees
|
|
594
|
+
now-unoccupied tail blocks (stale bytes past `offset` are never read —
|
|
595
|
+
the `KVCache` padding invariant, block-shaped). Block table is a
|
|
596
|
+
host-side `number[]`; all intermediates are bound and disposed.
|
|
597
|
+
- Pool lazily allocates on the first write (head count/dims/dtype from
|
|
598
|
+
the first k/v pair) and is sized once from `capacityTokens` =
|
|
599
|
+
prompt + `max_tokens`, so exhaustion is unreachable absent an
|
|
600
|
+
accounting bug — it exists as a tripwire, never silent truncation.
|
|
601
|
+
- **Wiring** — `maybePageKv(cache, options, capacityTokens)`
|
|
602
|
+
(`generate.ts`) mirrors `maybeQuantizeKv`'s post-construction in-place
|
|
603
|
+
swap but runs *once before prefill*: paging changes layout, not
|
|
604
|
+
arithmetic, so there is no "convert when populated" trigger. Only fresh
|
|
605
|
+
(`offset === 0`) plain `KVCache` entries are replaced; rotating layers
|
|
606
|
+
keep their scheme — mixed paged-full + rotating-sliding is the supported
|
|
607
|
+
shape. Default block size 256 = `KVCache.STEP`, so v1's growth
|
|
608
|
+
granularity is a permutation of today's into reusable slots, not a new
|
|
609
|
+
tuning axis.
|
|
610
|
+
|
|
611
|
+
### 6.3 Gates (explicit refusals, never silent downgrades)
|
|
612
|
+
|
|
613
|
+
- Startup (`server.ts`): `--paged-kv` with `--batch N>1`, any `--kv-quant`
|
|
614
|
+
(affine or TurboQuant), `--draft-model`, or a non-`gemma4*` model exits
|
|
615
|
+
with a clear message; `--paged-kv-block-size` must be a positive
|
|
616
|
+
integer; `--ssd-cache` alongside it warns that the tier sees nothing.
|
|
617
|
+
With no explicit `--batch`, the CLI pins `--batch 1` (default is 8) and
|
|
618
|
+
says so. Env `MLX_BUN_PAGED_KV=1` is equivalent to the flag.
|
|
619
|
+
- Request scope: media (vision/audio) and LoRA-adapter requests strip the
|
|
620
|
+
flag and run the plain cache path (v1 non-goal cells, never a 400). One
|
|
621
|
+
effective `pagedKv` value per request keeps the strip and the
|
|
622
|
+
prompt-cache bypass coherent.
|
|
623
|
+
- Serial lane: paged requests skip `take`/`put` and the boundary
|
|
624
|
+
snapshot; caches are disposed on completion. Spec eligibility also
|
|
625
|
+
excludes `options.pagedKv` (belt on top of the startup refusal).
|
|
626
|
+
- Compiled decode: `CompiledDecode.supports()` excludes `PagedKVCache`
|
|
627
|
+
automatically (not one of the four supported classes) — a
|
|
628
|
+
data-dependent block-list length is the shape shapeless replay already
|
|
629
|
+
broke on.
|
|
630
|
+
- Batch lane: unreachable by construction (startup pins serial); the
|
|
631
|
+
`RequestShape` gate to add when batched paging lands is noted here.
|
|
632
|
+
|
|
633
|
+
### 6.4 Follow-ups (dependency order) and open items
|
|
634
|
+
|
|
635
|
+
1. Batched integration — `PagedKVCache` into the scheduler's inner union;
|
|
636
|
+
block-count admission replacing byte projection; join/filter/extract
|
|
637
|
+
allocating and freeing blocks instead of pad+concat. This is where the
|
|
638
|
+
padded-batch-waste win lives. Tracked under the plan anchor (Phase 18
|
|
639
|
+
S3+).
|
|
640
|
+
2. Block-level CoW prefix sharing — refcounted block table, fork on
|
|
641
|
+
divergent write; extends `PromptCache`'s entry-level ref-counting down
|
|
642
|
+
to blocks.
|
|
643
|
+
3. Quantized paged blocks — dtype-parametric block descriptor sharing one
|
|
644
|
+
block-index space.
|
|
645
|
+
4. Fused paged-attention Metal kernel (optiq's `sdpa_2pass_paged` is the
|
|
646
|
+
port source) — kills the per-step gather copy.
|
|
647
|
+
- Perf disclosure: paged-on vs paged-off decode/prefill on the quiet
|
|
648
|
+
reference machine has not been recorded in `docs/reference/benchmarks.md`
|
|
649
|
+
(expected: a small decode regression at batch=1, to be reported plainly).
|
|
650
|
+
- `PagedKVCache` has no `signature()` (2.4) and no kv-store codec; both
|
|
651
|
+
are prerequisites for any prompt-cache or SSD integration.
|
|
652
|
+
|
|
653
|
+
## History
|
|
654
|
+
|
|
655
|
+
- 2026-06-14 — `BatchedRotatingCache` (sliding-window batching) landed.
|
|
656
|
+
- 2026-07-02 — SSD cold tier P1–P3 landed: kv-store v2 (all cache kinds,
|
|
657
|
+
invalidation metadata, atomic writes), `SsdCacheStore`, tier
|
|
658
|
+
integration, flags.
|
|
659
|
+
- 2026-07-04 — prompt-boundary snapshot (multi-turn agent reuse fix).
|
|
660
|
+
- 2026-07-05 — non-consuming prefix sharing in `take()`; Layer-0 tiering
|
|
661
|
+
moved inside `PromptCache.take()` (both lanes); idle demotion
|
|
662
|
+
(`--ssd-demote-idle`).
|
|
663
|
+
- 2026-07-06 — TurboQuant v1 cache class + kv-store `turboquant` kind;
|
|
664
|
+
RAM cap default 2 GB → 8 GB; non-blocking `spillOwned` spills; `find()`
|
|
665
|
+
trimmability gate (restart-0 defect).
|
|
666
|
+
- 2026-07-07 — streamed-copy restore + zero-copy write view (A7 RSS);
|
|
667
|
+
idle-gated write-behind (`MLX_BUN_SSD_WRITEBEHIND`); bounded
|
|
668
|
+
`SpillQueue` (`MLX_BUN_SSD_SPILL_QUEUE_GB`); admission-safe snapshot
|
|
669
|
+
timer.
|
|
670
|
+
- Phase 21 (closed 2026-08-17) — GLM `mla` / `mla-dsa` / `mtp-mla`
|
|
671
|
+
kv-store kinds.
|
|
672
|
+
- 2026-08-21 — `KvScheme` seam: immutable scheme drives conversion,
|
|
673
|
+
admission billing, SSD key, and batch placement.
|
|
674
|
+
- 2026-08-22 — agg×4 regression root-caused to the unsigned
|
|
675
|
+
`BatchedRotatingCache` (capability-only routing fix, signature added);
|
|
676
|
+
`SsdDurabilityCoordinator`, `POST /admin/cache/flush`, graceful
|
|
677
|
+
SIGINT/SIGTERM flush.
|
|
678
|
+
- Paged KV v1 (serial, Gemma4, bf16, gather-then-SDPA) landed with
|
|
679
|
+
bit-exact gates; flag rows in cli.md / server-config.md (landing date
|
|
680
|
+
not recorded in the source doc, which still reads "in progress").
|