mlx-bun 0.0.13 → 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 -578
- package/bin/mlx-bun.mjs +1 -1
- package/docs/README.md +91 -0
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/colibri-glm52-port.md +1 -1
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/{planning → archive/planning}/chunk-task-roadmap.md +1 -1
- package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +4 -4
- package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +1 -1
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/archive/planning/release-notes-v0.2.0.md +49 -0
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- 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 +421 -0
- package/docs/reference/benchmarks.md +1162 -0
- package/docs/reference/cli.md +395 -149
- package/docs/reference/distribution.md +321 -181
- package/docs/reference/environment.md +348 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +580 -142
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +137 -6
- package/docs/reference/server-api.md +938 -422
- package/docs/reference/server-config.md +652 -320
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +166 -0
- package/package.json +10 -5
- package/src/admission-plan.ts +18 -0
- package/src/anthropic.ts +92 -3
- 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 +189 -61
- 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 +263 -23
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +10 -79
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- 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 +24 -43
- package/src/generate.ts +740 -337
- package/src/grammar.ts +14 -8
- package/src/harness-pi.ts +15 -34
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +27 -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 +15 -14
- 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-scheme.ts +199 -0
- package/src/kv-store.ts +459 -234
- 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} +7 -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 +20 -4
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +32 -170
- 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 +3 -4
- package/src/mlx/array.ts +9 -10
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +66 -22
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +64 -19
- 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 +9 -3
- package/src/model/batched-quant.ts +2 -2
- package/src/model/batched-rotating-quant.ts +45 -99
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +57 -106
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +4 -3
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +87 -49
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/gemma4-base.ts +187 -17
- package/src/model/gemma4.ts +6 -6
- package/src/model/generated/gemma4-12b.ts +52 -148
- package/src/model/generated/gemma4-26b.ts +34 -94
- package/src/model/generated/gemma4-e4b.ts +28 -76
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/implementation.ts +69 -0
- package/src/model/minicpm5.ts +7 -8
- package/src/model/profile.ts +443 -0
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +336 -60
- 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/frame_extract.swift +140 -0
- package/src/native-pack.ts +15 -7
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +14 -320
- package/src/prompt-cache.ts +97 -54
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +16 -3
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/trellis.ts +586 -0
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +130 -3
- package/src/runtime-config.ts +81 -0
- package/src/sampler.ts +276 -4
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +8 -1558
- 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 +354 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +204 -0
- package/src/serve/generation-gateway.ts +211 -243
- 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/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +341 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +155 -0
- 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/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +162 -0
- 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 +101 -0
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +523 -3551
- package/src/spec/assistant-source.ts +10 -44
- package/src/spec/deepspec-source.ts +6 -6
- package/src/spec/dflash-source.ts +9 -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 +5 -4
- 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 +9 -1
- package/src/spec/ngram-source.ts +1 -1
- package/src/spec/prefix-state.ts +45 -0
- package/src/spec/qwen-mtp-source.ts +528 -0
- package/src/spec/serve-loop.ts +179 -213
- package/src/spec/source.ts +42 -10
- package/src/spec/two-model.ts +1 -1
- package/src/ssd-cache.ts +95 -24
- package/src/ssd-durability.ts +207 -0
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +14 -13
- package/src/train/forward.ts +38 -1
- package/src/train/loss.ts +3 -2
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +9 -6
- package/src/train/trainer.ts +9 -8
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +48 -2
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +555 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.html +1 -1
- package/src/web/app.js +15 -6
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +16 -9
- 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 +134 -33
- 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 -268
- 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-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 -189
- 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 -288
- 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/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/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/investigations/hlg-ab.html +0 -34
- package/docs/investigations/hlg-explorer.html +0 -238
- package/docs/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/investigations/hlg-figs/cross.svg +0 -1
- package/docs/investigations/hlg-figs/edges.svg +0 -1
- package/docs/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/investigations/hlg-report.html +0 -262
- package/docs/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -100
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -13
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
- /package/{docs/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
package/src/kv-store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { cleanupFailure, disposeResources } from "./engine/resources";
|
|
1
2
|
// KV-cache persistence: save prompt caches to disk, reload by streamed copy.
|
|
2
|
-
// The serialization core of the SSD cold tier (docs/design/
|
|
3
|
+
// The serialization core of the SSD cold tier (docs/design/kv-cache.md).
|
|
3
4
|
//
|
|
4
5
|
// File layout (every tensor PAGE-ALIGNED — the Phase 1 corollary: files
|
|
5
6
|
// we write can be mmap'd and handed to the GPU without copies):
|
|
@@ -46,7 +47,7 @@ import { MlxArray } from "./mlx/array";
|
|
|
46
47
|
import type { Dtype } from "./mlx/ffi";
|
|
47
48
|
import * as ops from "./mlx/ops";
|
|
48
49
|
import {
|
|
49
|
-
type Cache, KVCache, RotatingKVCache,
|
|
50
|
+
type Cache, cacheSignature, KVCache, RotatingKVCache,
|
|
50
51
|
QuantizedKVCache, RotatingQuantizedKVCache,
|
|
51
52
|
TurboQuantKVCache, type TurboQuantTensor,
|
|
52
53
|
} from "./model/gemma4-base";
|
|
@@ -121,9 +122,26 @@ export interface KvSaveMeta {
|
|
|
121
122
|
ns?: string;
|
|
122
123
|
/** sha256 of tokenizer.json: identical ids must mean identical text. */
|
|
123
124
|
tokenizerHash?: string;
|
|
125
|
+
/** In-flight generation state. The serialized caches cover `tokens`;
|
|
126
|
+
* pendingToken was sampled from that state but has not been emitted. */
|
|
127
|
+
generationCheckpoint?: {
|
|
128
|
+
key: string;
|
|
129
|
+
cacheNs: string;
|
|
130
|
+
originalPromptTokens: number;
|
|
131
|
+
generatedTokens: number;
|
|
132
|
+
pendingToken: number;
|
|
133
|
+
/** Actual sampler seed, including a server-generated default. Required to
|
|
134
|
+
* continue the random stream exactly after a restart. */
|
|
135
|
+
seed: number;
|
|
136
|
+
/** True when `seed` was part of the client request rather than generated
|
|
137
|
+
* by the server. */
|
|
138
|
+
seedWasExplicit: boolean;
|
|
139
|
+
};
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
export interface KvFileHeader extends KvSaveMeta {
|
|
143
|
+
/** Absent in earlier v3 files, which used mlx-cache-v3. */
|
|
144
|
+
codecProvider?: string;
|
|
127
145
|
formatVersion: 3;
|
|
128
146
|
createdAt: number;
|
|
129
147
|
tokens: number[];
|
|
@@ -142,7 +160,366 @@ const alignUp = (n: number) => Math.ceil(n / ALIGN) * ALIGN;
|
|
|
142
160
|
* as-laid-out with ringIdx. Slice handles are lazy graph nodes (no GPU
|
|
143
161
|
* materialization until rawBytes at write time). */
|
|
144
162
|
interface TensorSource { arr: MlxArray; disposeAfter: boolean }
|
|
145
|
-
|
|
163
|
+
|
|
164
|
+
export interface SnapshotContext {
|
|
165
|
+
slots: TensorSlot[];
|
|
166
|
+
push(a: MlxArray, disposeAfter: boolean): void;
|
|
167
|
+
liveSlice(a: MlxArray, upTo: number): MlxArray;
|
|
168
|
+
liveMlaSlice(a: MlxArray, upTo: number): MlxArray;
|
|
169
|
+
pushTriple(t: ops.QuantizedTensor, upTo: number | null): void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface CloneContext {
|
|
173
|
+
view(a: MlxArray): MlxArray;
|
|
174
|
+
liveView(a: MlxArray, upTo: number): MlxArray;
|
|
175
|
+
mlaView(a: MlxArray, upTo: number): MlxArray;
|
|
176
|
+
tripleView(t: ops.QuantizedTensor, upTo: number | null): ops.QuantizedTensor;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface LoadContext {
|
|
180
|
+
path: string;
|
|
181
|
+
arr(slot: TensorSlot): MlxArray;
|
|
182
|
+
grownArr(slot: TensorSlot, offset: number): MlxArray;
|
|
183
|
+
triple(slots: TensorSlot[], at: number): ops.QuantizedTensor;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface CacheCodec {
|
|
187
|
+
matches(cache: Cache): boolean;
|
|
188
|
+
snapshot(cache: Cache, context: SnapshotContext): CacheHeaderEntry;
|
|
189
|
+
clone(cache: Cache, context: CloneContext): Cache;
|
|
190
|
+
load(entry: CacheHeaderEntry, context: LoadContext): Cache;
|
|
191
|
+
headerTrimmable(entry: CacheHeaderEntry): boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const requireMlaState = (cache: Glm52Cache, operation: string): void => {
|
|
195
|
+
if (!cache.latent || !cache.rope || cache.batchSize === null || cache.offset === 0)
|
|
196
|
+
throw new Error(`cannot ${operation} an empty GLM compressed cache`);
|
|
197
|
+
if (cache.dsa && (!cache.dsa.data || cache.dsa.offset !== cache.offset))
|
|
198
|
+
throw new Error(`cannot ${operation} misaligned GLM DSA state`);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const snapshotMla = (
|
|
202
|
+
kind: Extract<CacheKind, "mla" | "mla-dsa" | "mtp-mla">,
|
|
203
|
+
cache: Cache,
|
|
204
|
+
context: SnapshotContext,
|
|
205
|
+
): CacheHeaderEntry => {
|
|
206
|
+
const c = cache as Glm52Cache;
|
|
207
|
+
requireMlaState(c, "persist");
|
|
208
|
+
context.push(context.liveMlaSlice(c.latent!, c.offset), true);
|
|
209
|
+
context.push(context.liveMlaSlice(c.rope!, c.offset), true);
|
|
210
|
+
if (c.dsa) context.push(context.liveMlaSlice(c.dsa.data!, c.offset), true);
|
|
211
|
+
return {
|
|
212
|
+
kind,
|
|
213
|
+
offset: c.offset,
|
|
214
|
+
kvLoraRank: c.kvLoraRank,
|
|
215
|
+
ropeHeadDim: c.ropeHeadDim,
|
|
216
|
+
...(c.dsa ? { dsaHeadDim: c.dsa.headDim } : {}),
|
|
217
|
+
maxTokens: c.maxTokens,
|
|
218
|
+
tensors: context.slots,
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const cloneMla = (cache: Cache, context: CloneContext): Cache => {
|
|
223
|
+
const c = cache as Glm52Cache;
|
|
224
|
+
requireMlaState(c, "clone");
|
|
225
|
+
const clone = new MLACache({
|
|
226
|
+
kvLoraRank: c.kvLoraRank,
|
|
227
|
+
ropeHeadDim: c.ropeHeadDim,
|
|
228
|
+
...(c.dsa ? { dsa: { headDim: c.dsa.headDim } } : {}),
|
|
229
|
+
maxTokens: c.maxTokens,
|
|
230
|
+
role: c.role,
|
|
231
|
+
});
|
|
232
|
+
clone.restoreCompressedState(
|
|
233
|
+
context.mlaView(c.latent!, c.offset),
|
|
234
|
+
context.mlaView(c.rope!, c.offset),
|
|
235
|
+
c.dsa ? context.mlaView(c.dsa.data!, c.offset) : null,
|
|
236
|
+
c.offset,
|
|
237
|
+
);
|
|
238
|
+
return clone;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const loadMla = (entry: CacheHeaderEntry, context: LoadContext): Cache => {
|
|
242
|
+
const hasDsa = entry.kind === "mla-dsa";
|
|
243
|
+
if (entry.kind === "mtp-mla" && entry.dsaHeadDim !== undefined)
|
|
244
|
+
throw new Error(`${context.path}: native MTP cache cannot contain DSA state`);
|
|
245
|
+
const cache = new MLACache({
|
|
246
|
+
kvLoraRank: entry.kvLoraRank!,
|
|
247
|
+
ropeHeadDim: entry.ropeHeadDim!,
|
|
248
|
+
...(hasDsa ? { dsa: { headDim: entry.dsaHeadDim! } } : {}),
|
|
249
|
+
maxTokens: entry.maxTokens,
|
|
250
|
+
role: entry.kind === "mtp-mla" ? "mtp" : "target",
|
|
251
|
+
});
|
|
252
|
+
cache.restoreCompressedState(
|
|
253
|
+
context.arr(entry.tensors[0]!),
|
|
254
|
+
context.arr(entry.tensors[1]!),
|
|
255
|
+
hasDsa ? context.arr(entry.tensors[2]!) : null,
|
|
256
|
+
entry.offset,
|
|
257
|
+
);
|
|
258
|
+
return cache;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/** One authoritative persistence algebra. Adding a cache storage kind must
|
|
262
|
+
* define all four operations here; save/clone/load and SSD eligibility
|
|
263
|
+
* therefore cannot grow independent instanceof/switch chains. */
|
|
264
|
+
const CACHE_CODECS = {
|
|
265
|
+
kv: {
|
|
266
|
+
matches: (cache) => cacheSignature(cache) === "kv:plain",
|
|
267
|
+
snapshot: (cache, context) => {
|
|
268
|
+
const c = cache as KVCache;
|
|
269
|
+
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
270
|
+
context.push(context.liveSlice(c.keys, c.offset), true);
|
|
271
|
+
context.push(context.liveSlice(c.values, c.offset), true);
|
|
272
|
+
return { kind: "kv", offset: c.offset, tensors: context.slots };
|
|
273
|
+
},
|
|
274
|
+
clone: (cache, context) => {
|
|
275
|
+
const c = cache as KVCache;
|
|
276
|
+
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
277
|
+
const clone = new KVCache();
|
|
278
|
+
clone.restoreState(
|
|
279
|
+
context.liveView(c.keys, c.offset),
|
|
280
|
+
context.liveView(c.values, c.offset),
|
|
281
|
+
c.offset,
|
|
282
|
+
);
|
|
283
|
+
return clone;
|
|
284
|
+
},
|
|
285
|
+
load: (entry, context) => {
|
|
286
|
+
const cache = new KVCache();
|
|
287
|
+
cache.restoreState(
|
|
288
|
+
context.grownArr(entry.tensors[0]!, entry.offset),
|
|
289
|
+
context.grownArr(entry.tensors[1]!, entry.offset),
|
|
290
|
+
entry.offset,
|
|
291
|
+
);
|
|
292
|
+
return cache;
|
|
293
|
+
},
|
|
294
|
+
headerTrimmable: () => true,
|
|
295
|
+
},
|
|
296
|
+
rotating: {
|
|
297
|
+
matches: (cache) => cacheSignature(cache) === "kv:rotating-plain",
|
|
298
|
+
snapshot: (cache, context) => {
|
|
299
|
+
const c = cache as RotatingKVCache;
|
|
300
|
+
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
301
|
+
context.push(c.keys, false);
|
|
302
|
+
context.push(c.values, false);
|
|
303
|
+
return { kind: "rotating", offset: c.offset, idx: c.ringIdx,
|
|
304
|
+
maxSize: c.maxSize, tensors: context.slots };
|
|
305
|
+
},
|
|
306
|
+
clone: (cache, context) => {
|
|
307
|
+
const c = cache as RotatingKVCache;
|
|
308
|
+
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
309
|
+
const clone = new RotatingKVCache(c.maxSize);
|
|
310
|
+
clone.restoreState(context.view(c.keys), context.view(c.values), c.offset, c.ringIdx);
|
|
311
|
+
return clone;
|
|
312
|
+
},
|
|
313
|
+
load: (entry, context) => {
|
|
314
|
+
const cache = new RotatingKVCache(entry.maxSize!);
|
|
315
|
+
cache.restoreState(
|
|
316
|
+
context.arr(entry.tensors[0]!), context.arr(entry.tensors[1]!),
|
|
317
|
+
entry.offset, entry.idx!,
|
|
318
|
+
);
|
|
319
|
+
return cache;
|
|
320
|
+
},
|
|
321
|
+
headerTrimmable: (entry) => entry.offset < (entry.maxSize ?? 0),
|
|
322
|
+
},
|
|
323
|
+
qkv: {
|
|
324
|
+
matches: (cache) => cacheSignature(cache).startsWith("kv:quant:"),
|
|
325
|
+
snapshot: (cache, context) => {
|
|
326
|
+
const c = cache as QuantizedKVCache;
|
|
327
|
+
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
328
|
+
context.pushTriple(c.keys, c.offset);
|
|
329
|
+
context.pushTriple(c.values, c.offset);
|
|
330
|
+
return { kind: "qkv", offset: c.offset, groupSize: c.groupSize,
|
|
331
|
+
bits: c.bits, tensors: context.slots };
|
|
332
|
+
},
|
|
333
|
+
clone: (cache, context) => {
|
|
334
|
+
const c = cache as QuantizedKVCache;
|
|
335
|
+
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
336
|
+
const clone = new QuantizedKVCache(c.groupSize, c.bits);
|
|
337
|
+
clone.restoreState(
|
|
338
|
+
context.tripleView(c.keys, c.offset),
|
|
339
|
+
context.tripleView(c.values, c.offset),
|
|
340
|
+
c.offset,
|
|
341
|
+
);
|
|
342
|
+
return clone;
|
|
343
|
+
},
|
|
344
|
+
load: (entry, context) => {
|
|
345
|
+
const cache = new QuantizedKVCache(entry.groupSize!, entry.bits!);
|
|
346
|
+
cache.restoreState(
|
|
347
|
+
context.triple(entry.tensors, 0), context.triple(entry.tensors, 3), entry.offset,
|
|
348
|
+
);
|
|
349
|
+
return cache;
|
|
350
|
+
},
|
|
351
|
+
headerTrimmable: () => true,
|
|
352
|
+
},
|
|
353
|
+
"rotating-qkv": {
|
|
354
|
+
matches: (cache) => cacheSignature(cache).startsWith("kv:rotating-quant:"),
|
|
355
|
+
snapshot: (cache, context) => {
|
|
356
|
+
const c = cache as RotatingQuantizedKVCache;
|
|
357
|
+
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
358
|
+
context.pushTriple(c.keys, null);
|
|
359
|
+
context.pushTriple(c.values, null);
|
|
360
|
+
return { kind: "rotating-qkv", offset: c.offset, idx: c.ringIdx,
|
|
361
|
+
maxSize: c.maxSize, groupSize: c.groupSize, bits: c.bits,
|
|
362
|
+
tensors: context.slots };
|
|
363
|
+
},
|
|
364
|
+
clone: (cache, context) => {
|
|
365
|
+
const c = cache as RotatingQuantizedKVCache;
|
|
366
|
+
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
367
|
+
const clone = new RotatingQuantizedKVCache(c.maxSize, c.groupSize, c.bits);
|
|
368
|
+
clone.restoreState(
|
|
369
|
+
context.tripleView(c.keys, null), context.tripleView(c.values, null),
|
|
370
|
+
c.offset, c.ringIdx,
|
|
371
|
+
);
|
|
372
|
+
return clone;
|
|
373
|
+
},
|
|
374
|
+
load: (entry, context) => {
|
|
375
|
+
const cache = new RotatingQuantizedKVCache(
|
|
376
|
+
entry.maxSize!, entry.groupSize!, entry.bits!,
|
|
377
|
+
);
|
|
378
|
+
cache.restoreState(
|
|
379
|
+
context.triple(entry.tensors, 0), context.triple(entry.tensors, 3),
|
|
380
|
+
entry.offset, entry.idx!,
|
|
381
|
+
);
|
|
382
|
+
return cache;
|
|
383
|
+
},
|
|
384
|
+
headerTrimmable: (entry) => entry.offset < (entry.maxSize ?? 0),
|
|
385
|
+
},
|
|
386
|
+
ssm: {
|
|
387
|
+
matches: (cache) => cacheSignature(cache) === "ssm",
|
|
388
|
+
snapshot: (cache, context) => {
|
|
389
|
+
const c = cache as SSMCache;
|
|
390
|
+
if (!c.conv || !c.recurrent) throw new Error("cannot persist an empty cache");
|
|
391
|
+
context.push(c.conv, false);
|
|
392
|
+
context.push(c.recurrent, false);
|
|
393
|
+
return { kind: "ssm", offset: c.offset, tensors: context.slots };
|
|
394
|
+
},
|
|
395
|
+
clone: (cache, context) => {
|
|
396
|
+
const c = cache as SSMCache;
|
|
397
|
+
if (!c.conv || !c.recurrent) throw new Error("cannot clone an empty cache");
|
|
398
|
+
const clone = new SSMCache();
|
|
399
|
+
clone.conv = context.view(c.conv);
|
|
400
|
+
clone.recurrent = context.view(c.recurrent);
|
|
401
|
+
clone.offset = c.offset;
|
|
402
|
+
return clone;
|
|
403
|
+
},
|
|
404
|
+
load: (entry, context) => {
|
|
405
|
+
const cache = new SSMCache();
|
|
406
|
+
cache.conv = context.arr(entry.tensors[0]!);
|
|
407
|
+
cache.recurrent = context.arr(entry.tensors[1]!);
|
|
408
|
+
cache.offset = entry.offset;
|
|
409
|
+
return cache;
|
|
410
|
+
},
|
|
411
|
+
headerTrimmable: () => false,
|
|
412
|
+
},
|
|
413
|
+
turboquant: {
|
|
414
|
+
matches: (cache) => cacheSignature(cache).startsWith("kv:turboquant:"),
|
|
415
|
+
snapshot: (cache, context) => {
|
|
416
|
+
const c = cache as TurboQuantKVCache;
|
|
417
|
+
const state = c.state();
|
|
418
|
+
if (state.length === 0) throw new Error("cannot persist an empty cache");
|
|
419
|
+
for (const array of state) context.push(array, true);
|
|
420
|
+
return { kind: "turboquant", offset: c.offset, kBits: c.kBits,
|
|
421
|
+
vBits: c.vBits, headDim: c.headDim ?? 0, tensors: context.slots };
|
|
422
|
+
},
|
|
423
|
+
clone: (cache, context) => {
|
|
424
|
+
const c = cache as TurboQuantKVCache;
|
|
425
|
+
const state = c.state();
|
|
426
|
+
if (state.length === 0) throw new Error("cannot clone an empty cache");
|
|
427
|
+
try {
|
|
428
|
+
const [kIdx, kScales, kZeros, vPacked, vScales] = state;
|
|
429
|
+
const clone = new TurboQuantKVCache(c.kBits, c.vBits);
|
|
430
|
+
clone.restoreState({
|
|
431
|
+
kIdx: context.view(kIdx!),
|
|
432
|
+
kScales: context.view(kScales!),
|
|
433
|
+
kZeros: context.view(kZeros!),
|
|
434
|
+
vPacked: context.view(vPacked!),
|
|
435
|
+
vScales: context.view(vScales!),
|
|
436
|
+
}, c.offset, c.headDim!);
|
|
437
|
+
return clone;
|
|
438
|
+
} finally {
|
|
439
|
+
for (const array of state) array.dispose();
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
load: (entry, context) => {
|
|
443
|
+
const cache = new TurboQuantKVCache(entry.kBits!, entry.vBits!);
|
|
444
|
+
const tensors: TurboQuantTensor = {
|
|
445
|
+
kIdx: context.arr(entry.tensors[0]!),
|
|
446
|
+
kScales: context.arr(entry.tensors[1]!),
|
|
447
|
+
kZeros: context.arr(entry.tensors[2]!),
|
|
448
|
+
vPacked: context.arr(entry.tensors[3]!),
|
|
449
|
+
vScales: context.arr(entry.tensors[4]!),
|
|
450
|
+
};
|
|
451
|
+
cache.restoreState(tensors, entry.offset, entry.headDim!);
|
|
452
|
+
return cache;
|
|
453
|
+
},
|
|
454
|
+
headerTrimmable: () => true,
|
|
455
|
+
},
|
|
456
|
+
mla: {
|
|
457
|
+
matches: (cache) => cacheSignature(cache) === "kv:mla:target",
|
|
458
|
+
snapshot: (cache, context) => snapshotMla("mla", cache, context),
|
|
459
|
+
clone: cloneMla,
|
|
460
|
+
load: loadMla,
|
|
461
|
+
headerTrimmable: () => true,
|
|
462
|
+
},
|
|
463
|
+
"mla-dsa": {
|
|
464
|
+
matches: (cache) => cacheSignature(cache) === "kv:mla:target:dsa",
|
|
465
|
+
snapshot: (cache, context) => snapshotMla("mla-dsa", cache, context),
|
|
466
|
+
clone: cloneMla,
|
|
467
|
+
load: loadMla,
|
|
468
|
+
headerTrimmable: () => true,
|
|
469
|
+
},
|
|
470
|
+
"mtp-mla": {
|
|
471
|
+
matches: (cache) => cacheSignature(cache) === "kv:mla:mtp",
|
|
472
|
+
snapshot: (cache, context) => snapshotMla("mtp-mla", cache, context),
|
|
473
|
+
clone: cloneMla,
|
|
474
|
+
load: loadMla,
|
|
475
|
+
headerTrimmable: () => true,
|
|
476
|
+
},
|
|
477
|
+
} satisfies Record<CacheKind, CacheCodec>;
|
|
478
|
+
|
|
479
|
+
/** Codecs belong to the loaded backend binding. The persisted ID is checked
|
|
480
|
+
* before allocating state; codecs are engine code, never loaded from a file. */
|
|
481
|
+
export interface CacheCodecProvider {
|
|
482
|
+
readonly id: string;
|
|
483
|
+
forCache(cache: Cache): CacheCodec;
|
|
484
|
+
forHeader(entry: CacheHeaderEntry): CacheCodec;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function createCacheCodecProvider(
|
|
488
|
+
id: string, entries: Readonly<Partial<Record<CacheKind, CacheCodec>>>,
|
|
489
|
+
): CacheCodecProvider {
|
|
490
|
+
if (!id.trim()) throw new Error("cache codec provider needs an identity");
|
|
491
|
+
const table = new Map(Object.entries(entries).map(([kind, codec]) => [kind, Object.freeze({
|
|
492
|
+
matches: codec.matches.bind(codec), snapshot: codec.snapshot.bind(codec), clone: codec.clone.bind(codec),
|
|
493
|
+
load: codec.load.bind(codec), headerTrimmable: codec.headerTrimmable.bind(codec),
|
|
494
|
+
})]));
|
|
495
|
+
const candidates = [...table.values()];
|
|
496
|
+
const selected = new WeakMap<Cache, CacheCodec>();
|
|
497
|
+
return Object.freeze({
|
|
498
|
+
id,
|
|
499
|
+
forCache(cache: Cache) {
|
|
500
|
+
const bound = selected.get(cache);
|
|
501
|
+
if (bound) return bound;
|
|
502
|
+
const matches = candidates.filter((codec) => codec.matches(cache));
|
|
503
|
+
if (matches.length !== 1)
|
|
504
|
+
throw new Error(`cache codec provider ${id}: ${matches.length} matches for ${cacheSignature(cache)}`);
|
|
505
|
+
selected.set(cache, matches[0]!);
|
|
506
|
+
return matches[0]!;
|
|
507
|
+
},
|
|
508
|
+
forHeader(entry: CacheHeaderEntry) {
|
|
509
|
+
const codec = table.get(entry.kind);
|
|
510
|
+
if (!codec) throw new Error(`cache codec provider ${id}: unsupported cache kind ${entry.kind}`);
|
|
511
|
+
return codec;
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export const legacyCacheCodecs = createCacheCodecProvider("mlx-cache-v3", CACHE_CODECS);
|
|
517
|
+
|
|
518
|
+
export const cacheHeadersTrimmable = (
|
|
519
|
+
caches: readonly CacheHeaderEntry[], codecs: CacheCodecProvider = legacyCacheCodecs,
|
|
520
|
+
): boolean => caches.every((entry) => codecs.forHeader(entry).headerTrimmable(entry));
|
|
521
|
+
|
|
522
|
+
function snapshotCache(c: Cache, dataOffset: number, codecs: CacheCodecProvider): { entry: CacheHeaderEntry; sources: TensorSource[]; next: number } {
|
|
146
523
|
const slots: TensorSlot[] = [];
|
|
147
524
|
const sources: TensorSource[] = [];
|
|
148
525
|
let cursor = dataOffset;
|
|
@@ -168,65 +545,13 @@ function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry;
|
|
|
168
545
|
}
|
|
169
546
|
};
|
|
170
547
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (c.dsa) {
|
|
178
|
-
if (!c.dsa.data || c.dsa.offset !== c.offset)
|
|
179
|
-
throw new Error("cannot persist misaligned GLM DSA state");
|
|
180
|
-
push(liveMlaSlice(c.dsa.data, c.offset), true);
|
|
181
|
-
}
|
|
182
|
-
entry = {
|
|
183
|
-
kind: c.role === "mtp" ? "mtp-mla" : c.dsa ? "mla-dsa" : "mla",
|
|
184
|
-
offset: c.offset,
|
|
185
|
-
kvLoraRank: c.kvLoraRank,
|
|
186
|
-
ropeHeadDim: c.ropeHeadDim,
|
|
187
|
-
...(c.dsa ? { dsaHeadDim: c.dsa.headDim } : {}),
|
|
188
|
-
maxTokens: c.maxTokens,
|
|
189
|
-
tensors: slots,
|
|
190
|
-
};
|
|
191
|
-
} else if (c instanceof TurboQuantKVCache) {
|
|
192
|
-
const t = c.state();
|
|
193
|
-
if (t.length === 0) throw new Error("cannot persist an empty cache");
|
|
194
|
-
const [kIdx, kScales, kZeros, vPacked, vScales] = t as [MlxArray, MlxArray, MlxArray, MlxArray, MlxArray];
|
|
195
|
-
// state() already trims to offset — dispose these lazy slice views
|
|
196
|
-
// once written (same disposeAfter contract as liveSlice elsewhere).
|
|
197
|
-
for (const a of [kIdx, kScales, kZeros, vPacked, vScales]) push(a, true);
|
|
198
|
-
entry = { kind: "turboquant", offset: c.offset, kBits: c.kBits, vBits: c.vBits,
|
|
199
|
-
headDim: c.headDim ?? 0, tensors: slots };
|
|
200
|
-
} else if (c instanceof RotatingQuantizedKVCache) {
|
|
201
|
-
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
202
|
-
pushTriple(c.keys, null);
|
|
203
|
-
pushTriple(c.values, null);
|
|
204
|
-
entry = { kind: "rotating-qkv", offset: c.offset, idx: c.ringIdx, maxSize: c.maxSize,
|
|
205
|
-
groupSize: c.groupSize, bits: c.bits, tensors: slots };
|
|
206
|
-
} else if (c instanceof QuantizedKVCache) {
|
|
207
|
-
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
208
|
-
pushTriple(c.keys, c.offset);
|
|
209
|
-
pushTriple(c.values, c.offset);
|
|
210
|
-
entry = { kind: "qkv", offset: c.offset, groupSize: c.groupSize, bits: c.bits, tensors: slots };
|
|
211
|
-
} else if (c instanceof RotatingKVCache) {
|
|
212
|
-
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
213
|
-
push(c.keys, false);
|
|
214
|
-
push(c.values, false);
|
|
215
|
-
entry = { kind: "rotating", offset: c.offset, idx: c.ringIdx, maxSize: c.maxSize, tensors: slots };
|
|
216
|
-
} else if (c instanceof SSMCache) {
|
|
217
|
-
if (!c.conv || !c.recurrent) throw new Error("cannot persist an empty cache");
|
|
218
|
-
push(c.conv, false);
|
|
219
|
-
push(c.recurrent, false);
|
|
220
|
-
entry = { kind: "ssm", offset: c.offset, tensors: slots };
|
|
221
|
-
} else if (c instanceof KVCache) {
|
|
222
|
-
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
223
|
-
push(liveSlice(c.keys, c.offset), true);
|
|
224
|
-
push(liveSlice(c.values, c.offset), true);
|
|
225
|
-
entry = { kind: "kv", offset: c.offset, tensors: slots };
|
|
226
|
-
} else {
|
|
227
|
-
throw new Error("unknown cache type");
|
|
548
|
+
try {
|
|
549
|
+
const entry = codecs.forCache(c).snapshot(c, { slots, push, liveSlice, liveMlaSlice, pushTriple });
|
|
550
|
+
return { entry, sources, next: cursor };
|
|
551
|
+
} catch (error) {
|
|
552
|
+
for (const source of sources) if (source.disposeAfter) source.arr.dispose();
|
|
553
|
+
throw error;
|
|
228
554
|
}
|
|
229
|
-
return { entry, sources, next: cursor };
|
|
230
555
|
}
|
|
231
556
|
|
|
232
557
|
/** Persist `caches` (+ the exact token prefix they encode) to `path`.
|
|
@@ -243,15 +568,17 @@ function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry;
|
|
|
243
568
|
* called while the caches hold EXACTLY the state to snapshot (post-wrap
|
|
244
569
|
* rings cannot be rewound later). Same per-kind dispatch as
|
|
245
570
|
* snapshotCache/loadKvCache. */
|
|
246
|
-
export function cloneKvCaches(caches: Cache[]): Cache[] {
|
|
247
|
-
const
|
|
571
|
+
export function cloneKvCaches(caches: Cache[], codecs: CacheCodecProvider = legacyCacheCodecs): Cache[] {
|
|
572
|
+
const pending: MlxArray[] = [];
|
|
573
|
+
const hold = (array: MlxArray): MlxArray => { pending.push(array); return array; };
|
|
574
|
+
const view = (a: MlxArray): MlxArray => hold(a.slice(a.shape.map(() => 0), [...a.shape]));
|
|
248
575
|
const liveView = (a: MlxArray, upTo: number): MlxArray => {
|
|
249
576
|
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
250
|
-
return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
|
|
577
|
+
return hold(a.slice([0, 0, 0, 0], [B, H, upTo, D]));
|
|
251
578
|
};
|
|
252
579
|
const mlaView = (a: MlxArray, upTo: number): MlxArray => {
|
|
253
580
|
const [B, , D] = a.shape as [number, number, number];
|
|
254
|
-
return a.slice([0, 0, 0], [B, upTo, D]);
|
|
581
|
+
return hold(a.slice([0, 0, 0], [B, upTo, D]));
|
|
255
582
|
};
|
|
256
583
|
const tripleView = (t: ops.QuantizedTensor, upTo: number | null): ops.QuantizedTensor => ({
|
|
257
584
|
packed: upTo === null ? view(t.packed) : liveView(t.packed, upTo),
|
|
@@ -261,69 +588,11 @@ export function cloneKvCaches(caches: Cache[]): Cache[] {
|
|
|
261
588
|
const out: Cache[] = [];
|
|
262
589
|
try {
|
|
263
590
|
for (const c of caches) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
throw new Error("cannot clone an empty GLM compressed cache");
|
|
267
|
-
const n = new MLACache({
|
|
268
|
-
kvLoraRank: c.kvLoraRank,
|
|
269
|
-
ropeHeadDim: c.ropeHeadDim,
|
|
270
|
-
...(c.dsa ? { dsa: { headDim: c.dsa.headDim } } : {}),
|
|
271
|
-
maxTokens: c.maxTokens,
|
|
272
|
-
role: c.role,
|
|
273
|
-
});
|
|
274
|
-
n.restoreCompressedState(
|
|
275
|
-
mlaView(c.latent, c.offset),
|
|
276
|
-
mlaView(c.rope, c.offset),
|
|
277
|
-
c.dsa ? mlaView(c.dsa.data!, c.offset) : null,
|
|
278
|
-
c.offset,
|
|
279
|
-
);
|
|
280
|
-
out.push(n);
|
|
281
|
-
} else if (c instanceof TurboQuantKVCache) {
|
|
282
|
-
const state = c.state();
|
|
283
|
-
if (state.length === 0) throw new Error("cannot clone an empty cache");
|
|
284
|
-
const [kIdx, kScales, kZeros, vPacked, vScales] = state;
|
|
285
|
-
const n = new TurboQuantKVCache(c.kBits, c.vBits);
|
|
286
|
-
n.restoreState(
|
|
287
|
-
{ kIdx: view(kIdx!), kScales: view(kScales!), kZeros: view(kZeros!),
|
|
288
|
-
vPacked: view(vPacked!), vScales: view(vScales!) },
|
|
289
|
-
c.offset, c.headDim!,
|
|
290
|
-
);
|
|
291
|
-
for (const a of state) a.dispose();
|
|
292
|
-
out.push(n);
|
|
293
|
-
} else if (c instanceof RotatingQuantizedKVCache) {
|
|
294
|
-
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
295
|
-
const n = new RotatingQuantizedKVCache(c.maxSize, c.groupSize, c.bits);
|
|
296
|
-
n.restoreState(tripleView(c.keys, null), tripleView(c.values, null), c.offset, c.ringIdx);
|
|
297
|
-
out.push(n);
|
|
298
|
-
} else if (c instanceof QuantizedKVCache) {
|
|
299
|
-
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
300
|
-
const n = new QuantizedKVCache(c.groupSize, c.bits);
|
|
301
|
-
n.restoreState(tripleView(c.keys, c.offset), tripleView(c.values, c.offset), c.offset);
|
|
302
|
-
out.push(n);
|
|
303
|
-
} else if (c instanceof RotatingKVCache) {
|
|
304
|
-
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
305
|
-
const n = new RotatingKVCache(c.maxSize);
|
|
306
|
-
n.restoreState(view(c.keys), view(c.values), c.offset, c.ringIdx);
|
|
307
|
-
out.push(n);
|
|
308
|
-
} else if (c instanceof SSMCache) {
|
|
309
|
-
if (!c.conv || !c.recurrent) throw new Error("cannot clone an empty cache");
|
|
310
|
-
const n = new SSMCache();
|
|
311
|
-
n.conv = view(c.conv);
|
|
312
|
-
n.recurrent = view(c.recurrent);
|
|
313
|
-
n.offset = c.offset;
|
|
314
|
-
out.push(n);
|
|
315
|
-
} else if (c instanceof KVCache) {
|
|
316
|
-
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
317
|
-
const n = new KVCache();
|
|
318
|
-
n.restoreState(liveView(c.keys, c.offset), liveView(c.values, c.offset), c.offset);
|
|
319
|
-
out.push(n);
|
|
320
|
-
} else {
|
|
321
|
-
throw new Error("cloneKvCaches: unknown cache type");
|
|
322
|
-
}
|
|
591
|
+
out.push(codecs.forCache(c).clone(c, { view, liveView, mlaView, tripleView }));
|
|
592
|
+
pending.length = 0; // transferred to the returned cache
|
|
323
593
|
}
|
|
324
594
|
} catch (err) {
|
|
325
|
-
|
|
326
|
-
throw err;
|
|
595
|
+
cleanupFailure(err, () => disposeResources([...pending, ...out]));
|
|
327
596
|
}
|
|
328
597
|
return out;
|
|
329
598
|
}
|
|
@@ -333,14 +602,14 @@ export function cloneKvCaches(caches: Cache[]): Cache[] {
|
|
|
333
602
|
* between yields so the EVENT LOOP KEEPS SERVING while a multi-hundred-MB
|
|
334
603
|
* entry flushes (the write-behind persistence contract: durability never
|
|
335
604
|
* blocks a request). try/finally still runs on early generator close. */
|
|
336
|
-
function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta): Generator<void, void, void> {
|
|
605
|
+
function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta, codecs: CacheCodecProvider): Generator<void, void, void> {
|
|
337
606
|
// Plan pass: header entries + lazy tensor sources, NO bytes materialized.
|
|
338
607
|
const entries: CacheHeaderEntry[] = [];
|
|
339
608
|
const sources: TensorSource[] = [];
|
|
340
609
|
let dataOffset = 0; // relative; rebased after header is sized
|
|
341
610
|
try {
|
|
342
611
|
for (const c of caches) {
|
|
343
|
-
const s = snapshotCache(c, dataOffset);
|
|
612
|
+
const s = snapshotCache(c, dataOffset, codecs);
|
|
344
613
|
entries.push(s.entry);
|
|
345
614
|
sources.push(...s.sources);
|
|
346
615
|
dataOffset = s.next;
|
|
@@ -349,7 +618,7 @@ function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta
|
|
|
349
618
|
// Header size is final NOW: hashes are fixed-width placeholders that get
|
|
350
619
|
// patched in place (same byte length) after the data pass computes them.
|
|
351
620
|
const header: KvFileHeader = {
|
|
352
|
-
formatVersion: 3, createdAt: Date.now(), ...meta, tokens, caches: entries,
|
|
621
|
+
formatVersion: 3, createdAt: Date.now(), ...meta, codecProvider: codecs.id, tokens, caches: entries,
|
|
353
622
|
};
|
|
354
623
|
const headerLen = new TextEncoder().encode(JSON.stringify(header)).length;
|
|
355
624
|
const dataStart = alignUp(PREFIX_LEN + headerLen);
|
|
@@ -414,32 +683,34 @@ function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta
|
|
|
414
683
|
}
|
|
415
684
|
}
|
|
416
685
|
|
|
417
|
-
export function saveKvCache(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {}): void {
|
|
418
|
-
for (const _ of saveKvCacheSteps(path, tokens, caches, meta)) { /* drain */ }
|
|
686
|
+
export function saveKvCache(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {}, codecs: CacheCodecProvider = legacyCacheCodecs): void {
|
|
687
|
+
for (const _ of saveKvCacheSteps(path, tokens, caches, meta, codecs)) { /* drain */ }
|
|
419
688
|
}
|
|
420
689
|
|
|
421
690
|
/** Non-blocking variant: yields the event loop after every tensor write so
|
|
422
691
|
* serving interleaves with the flush. Caller owns `caches` lifetime for
|
|
423
692
|
* the duration (pass zero-copy clones, dispose after).
|
|
424
693
|
*
|
|
425
|
-
* `
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
* setImmediate pacing interleaved those slices exactly between decode
|
|
430
|
-
* tokens — the 2026-07-07 decode@ctx contamination. A request arriving
|
|
431
|
-
* MID-flush pauses the remaining tensors until idle again). Gate failures
|
|
432
|
-
* are swallowed: scheduling advice must never corrupt the write path
|
|
433
|
-
* (an early generator close inside the fd-open section would leak the fd). */
|
|
694
|
+
* `runStep` (optional) runs each blocking tensor step inside the caller's
|
|
695
|
+
* exclusion domain. Waiting for an idle observation is not sufficient: a
|
|
696
|
+
* request can acquire the engine between that check and `steps.next()`,
|
|
697
|
+
* putting rawBytesView's GPU sync on the decode stream during prefill. */
|
|
434
698
|
export async function saveKvCacheAsync(
|
|
435
699
|
path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {},
|
|
436
|
-
|
|
700
|
+
runStep?: <T>(step: () => T) => Promise<T>, codecs: CacheCodecProvider = legacyCacheCodecs,
|
|
437
701
|
): Promise<void> {
|
|
438
|
-
const steps = saveKvCacheSteps(path, tokens, caches, meta);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
702
|
+
const steps = saveKvCacheSteps(path, tokens, caches, meta, codecs);
|
|
703
|
+
try {
|
|
704
|
+
while (true) {
|
|
705
|
+
const next = runStep ? await runStep(() => steps.next()) : steps.next();
|
|
706
|
+
if (next.done) break;
|
|
707
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
708
|
+
}
|
|
709
|
+
} catch (error) {
|
|
710
|
+
// A rejected execution lease can leave the writer suspended with an
|
|
711
|
+
// open fd. Throw through it so its existing failure cleanup runs.
|
|
712
|
+
steps.throw(error);
|
|
713
|
+
throw error;
|
|
443
714
|
}
|
|
444
715
|
}
|
|
445
716
|
|
|
@@ -454,7 +725,7 @@ export interface SpillItem {
|
|
|
454
725
|
/** Bounded write-behind queue (2026-07-07 post-merge review fix).
|
|
455
726
|
*
|
|
456
727
|
* Pending spill/snapshot clones pin their entries' GPU buffers until the
|
|
457
|
-
*
|
|
728
|
+
* generation-locked flush gets a turn; the old bare promise chain queued them
|
|
458
729
|
* WITHOUT BOUND, so under sustained traffic (gate starved, evictions
|
|
459
730
|
* ongoing) resident memory = prompt-cache cap + every queued clone —
|
|
460
731
|
* allocator pressure exactly under the load that caused the evictions.
|
|
@@ -468,12 +739,17 @@ export interface SpillItem {
|
|
|
468
739
|
* Items run strictly serially in enqueue order via an internal chain;
|
|
469
740
|
* store failures are swallowed (cold tier is best-effort) but clones
|
|
470
741
|
* are disposed on every settle path. */
|
|
471
|
-
type SpillRec = SpillItem & {
|
|
742
|
+
type SpillRec = SpillItem & {
|
|
743
|
+
bytes: number;
|
|
744
|
+
dropped: boolean;
|
|
745
|
+
settle: (stored: boolean) => void;
|
|
746
|
+
};
|
|
472
747
|
|
|
473
748
|
export class SpillQueue {
|
|
474
749
|
#queue: SpillRec[] = [];
|
|
475
750
|
#bytes = 0;
|
|
476
751
|
#dropped = 0;
|
|
752
|
+
#failed = 0;
|
|
477
753
|
#inFlight: object | null = null;
|
|
478
754
|
#chain: Promise<void> = Promise.resolve();
|
|
479
755
|
|
|
@@ -492,9 +768,18 @@ export class SpillQueue {
|
|
|
492
768
|
get pendingCount(): number { return this.#queue.length; }
|
|
493
769
|
/** Spills dropped by the cap since start (each = one future cache miss). */
|
|
494
770
|
get droppedCount(): number { return this.#dropped; }
|
|
771
|
+
/** Store calls that threw or returned false. */
|
|
772
|
+
get failedCount(): number { return this.#failed; }
|
|
495
773
|
|
|
496
|
-
enqueue(item: SpillItem):
|
|
497
|
-
|
|
774
|
+
enqueue(item: SpillItem): Promise<boolean> {
|
|
775
|
+
let settle!: (stored: boolean) => void;
|
|
776
|
+
const result = new Promise<boolean>((resolve) => { settle = resolve; });
|
|
777
|
+
const rec: SpillRec = {
|
|
778
|
+
...item,
|
|
779
|
+
bytes: this.bytesOf(item.caches),
|
|
780
|
+
dropped: false,
|
|
781
|
+
settle,
|
|
782
|
+
};
|
|
498
783
|
this.#queue.push(rec);
|
|
499
784
|
this.#bytes += rec.bytes;
|
|
500
785
|
while (this.#bytes > this.capBytes) {
|
|
@@ -508,24 +793,30 @@ export class SpillQueue {
|
|
|
508
793
|
if (rec.dropped) return; // clones already disposed at drop time
|
|
509
794
|
this.#inFlight = rec;
|
|
510
795
|
try {
|
|
511
|
-
await this.store(rec);
|
|
796
|
+
const stored = await this.store(rec);
|
|
797
|
+
if (stored === false) this.#failed++;
|
|
798
|
+
rec.settle(stored !== false);
|
|
512
799
|
} catch {
|
|
513
800
|
// best-effort tier — the entry simply won't restore
|
|
801
|
+
this.#failed++;
|
|
802
|
+
rec.settle(false);
|
|
514
803
|
} finally {
|
|
515
804
|
this.#inFlight = null;
|
|
516
805
|
this.#remove(rec);
|
|
517
806
|
this.disposeClones(rec.caches);
|
|
518
807
|
}
|
|
519
808
|
});
|
|
809
|
+
return result;
|
|
520
810
|
}
|
|
521
811
|
|
|
522
812
|
/** Settles when everything enqueued so far has flushed or dropped
|
|
523
|
-
* (tests
|
|
813
|
+
* (tests and graceful shutdown). */
|
|
524
814
|
drain(): Promise<void> { return this.#chain; }
|
|
525
815
|
|
|
526
816
|
#drop(rec: SpillRec): void {
|
|
527
817
|
rec.dropped = true;
|
|
528
818
|
this.#dropped++;
|
|
819
|
+
rec.settle(false);
|
|
529
820
|
this.#remove(rec);
|
|
530
821
|
this.disposeClones(rec.caches);
|
|
531
822
|
}
|
|
@@ -592,24 +883,26 @@ function validateGlm52Prototype(
|
|
|
592
883
|
): void {
|
|
593
884
|
const glmKind = entry.kind === "mla" || entry.kind === "mla-dsa" ||
|
|
594
885
|
entry.kind === "mtp-mla";
|
|
595
|
-
|
|
886
|
+
const prototypeSignature = cacheSignature(prototype);
|
|
887
|
+
if (!prototypeSignature.startsWith("kv:mla:")) {
|
|
596
888
|
if (glmKind)
|
|
597
889
|
throw new Error(`${path}: GLM cache kind ${entry.kind} does not match model cache`);
|
|
598
890
|
return;
|
|
599
891
|
}
|
|
600
|
-
const
|
|
892
|
+
const glmPrototype = prototype as Glm52Cache;
|
|
893
|
+
const expectedKind: CacheKind = glmPrototype.role === "mtp"
|
|
601
894
|
? "mtp-mla"
|
|
602
|
-
:
|
|
895
|
+
: glmPrototype.dsa ? "mla-dsa" : "mla";
|
|
603
896
|
if (entry.kind !== expectedKind) {
|
|
604
897
|
throw new Error(
|
|
605
898
|
`${path}: cache kind ${entry.kind} != model cache kind ${expectedKind}`,
|
|
606
899
|
);
|
|
607
900
|
}
|
|
608
901
|
for (const [field, actual, expected] of [
|
|
609
|
-
["kvLoraRank", entry.kvLoraRank,
|
|
610
|
-
["ropeHeadDim", entry.ropeHeadDim,
|
|
611
|
-
["dsaHeadDim", entry.dsaHeadDim,
|
|
612
|
-
["maxTokens", entry.maxTokens,
|
|
902
|
+
["kvLoraRank", entry.kvLoraRank, glmPrototype.kvLoraRank],
|
|
903
|
+
["ropeHeadDim", entry.ropeHeadDim, glmPrototype.ropeHeadDim],
|
|
904
|
+
["dsaHeadDim", entry.dsaHeadDim, glmPrototype.dsa?.headDim],
|
|
905
|
+
["maxTokens", entry.maxTokens, glmPrototype.maxTokens],
|
|
613
906
|
] as const) {
|
|
614
907
|
if (actual !== expected) {
|
|
615
908
|
throw new Error(
|
|
@@ -617,7 +910,7 @@ function validateGlm52Prototype(
|
|
|
617
910
|
);
|
|
618
911
|
}
|
|
619
912
|
}
|
|
620
|
-
const expectedTensors =
|
|
913
|
+
const expectedTensors = glmPrototype.dsa ? 3 : 2;
|
|
621
914
|
if (entry.tensors.length !== expectedTensors) {
|
|
622
915
|
throw new Error(
|
|
623
916
|
`${path}: ${entry.kind} has ${entry.tensors.length} tensors, ` +
|
|
@@ -670,9 +963,12 @@ function withStepCapacity(a: MlxArray, offset: number): MlxArray {
|
|
|
670
963
|
export function loadKvCache(
|
|
671
964
|
path: string,
|
|
672
965
|
model: { makeCache(): Cache[] },
|
|
673
|
-
expect: KvLoadExpect = {},
|
|
966
|
+
expect: KvLoadExpect = {}, codecs: CacheCodecProvider = legacyCacheCodecs,
|
|
674
967
|
): LoadedKvCache {
|
|
675
968
|
const header = readKvHeader(path);
|
|
969
|
+
if ((header.codecProvider ?? legacyCacheCodecs.id) !== codecs.id)
|
|
970
|
+
throw new Error(`${path}: cache codec provider mismatch`);
|
|
971
|
+
for (const entry of header.caches) codecs.forHeader(entry);
|
|
676
972
|
for (const key of [
|
|
677
973
|
"modelId",
|
|
678
974
|
"configFingerprint",
|
|
@@ -732,79 +1028,8 @@ export function loadKvCache(
|
|
|
732
1028
|
const caches: Cache[] = [];
|
|
733
1029
|
try {
|
|
734
1030
|
for (const e of header.caches) {
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
case "kv": {
|
|
738
|
-
const c = new KVCache();
|
|
739
|
-
c.restoreState(
|
|
740
|
-
grownArr(t[0]!, e.offset),
|
|
741
|
-
grownArr(t[1]!, e.offset),
|
|
742
|
-
e.offset,
|
|
743
|
-
);
|
|
744
|
-
caches.push(c);
|
|
745
|
-
break;
|
|
746
|
-
}
|
|
747
|
-
case "rotating": {
|
|
748
|
-
const c = new RotatingKVCache(e.maxSize!);
|
|
749
|
-
c.restoreState(arr(t[0]!), arr(t[1]!), e.offset, e.idx!);
|
|
750
|
-
caches.push(c);
|
|
751
|
-
break;
|
|
752
|
-
}
|
|
753
|
-
case "qkv": {
|
|
754
|
-
const c = new QuantizedKVCache(e.groupSize!, e.bits!);
|
|
755
|
-
c.restoreState(triple(t, 0), triple(t, 3), e.offset);
|
|
756
|
-
caches.push(c);
|
|
757
|
-
break;
|
|
758
|
-
}
|
|
759
|
-
case "rotating-qkv": {
|
|
760
|
-
const c = new RotatingQuantizedKVCache(e.maxSize!, e.groupSize!, e.bits!);
|
|
761
|
-
c.restoreState(triple(t, 0), triple(t, 3), e.offset, e.idx!);
|
|
762
|
-
caches.push(c);
|
|
763
|
-
break;
|
|
764
|
-
}
|
|
765
|
-
case "ssm": {
|
|
766
|
-
const c = new SSMCache();
|
|
767
|
-
c.conv = arr(t[0]!);
|
|
768
|
-
c.recurrent = arr(t[1]!);
|
|
769
|
-
c.offset = e.offset;
|
|
770
|
-
caches.push(c);
|
|
771
|
-
break;
|
|
772
|
-
}
|
|
773
|
-
case "turboquant": {
|
|
774
|
-
const c = new TurboQuantKVCache(e.kBits!, e.vBits!);
|
|
775
|
-
const kv: TurboQuantTensor = {
|
|
776
|
-
kIdx: arr(t[0]!), kScales: arr(t[1]!), kZeros: arr(t[2]!),
|
|
777
|
-
vPacked: arr(t[3]!), vScales: arr(t[4]!),
|
|
778
|
-
};
|
|
779
|
-
c.restoreState(kv, e.offset, e.headDim!);
|
|
780
|
-
caches.push(c);
|
|
781
|
-
break;
|
|
782
|
-
}
|
|
783
|
-
case "mla":
|
|
784
|
-
case "mla-dsa":
|
|
785
|
-
case "mtp-mla": {
|
|
786
|
-
const hasDsa = e.kind === "mla-dsa";
|
|
787
|
-
if (e.kind === "mtp-mla" && e.dsaHeadDim !== undefined)
|
|
788
|
-
throw new Error(`${path}: native MTP cache cannot contain DSA state`);
|
|
789
|
-
const c = new MLACache({
|
|
790
|
-
kvLoraRank: e.kvLoraRank!,
|
|
791
|
-
ropeHeadDim: e.ropeHeadDim!,
|
|
792
|
-
...(hasDsa ? { dsa: { headDim: e.dsaHeadDim! } } : {}),
|
|
793
|
-
maxTokens: e.maxTokens,
|
|
794
|
-
role: e.kind === "mtp-mla" ? "mtp" : "target",
|
|
795
|
-
});
|
|
796
|
-
c.restoreCompressedState(
|
|
797
|
-
arr(t[0]!),
|
|
798
|
-
arr(t[1]!),
|
|
799
|
-
hasDsa ? arr(t[2]!) : null,
|
|
800
|
-
e.offset,
|
|
801
|
-
);
|
|
802
|
-
caches.push(c);
|
|
803
|
-
break;
|
|
804
|
-
}
|
|
805
|
-
default:
|
|
806
|
-
throw new Error(`${path}: unknown cache kind ${(e as { kind: string }).kind}`);
|
|
807
|
-
}
|
|
1031
|
+
const codec = codecs.forHeader(e);
|
|
1032
|
+
caches.push(codec.load(e, { path, arr, grownArr, triple }));
|
|
808
1033
|
// This entry's tensors are now owned by its cache — stop tracking them
|
|
809
1034
|
// (the catch must not double-free through both pending AND caches).
|
|
810
1035
|
pending.length = 0;
|