mlx-bun 0.2.0 → 0.4.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 +94 -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/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/archive/planning/release-notes-v0.3.0.md +35 -0
- package/docs/design/batching.md +1841 -0
- package/docs/design/decode-speed-program.md +2346 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +655 -334
- package/docs/design/kv-cache.md +824 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1990 -0
- package/docs/design/turboquant.md +2212 -0
- package/docs/design/unified-engine-frontier-plan.md +1833 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +50 -1
- package/docs/planning/release-notes-v0.4.0.md +45 -0
- package/docs/planning/video-series.md +2 -2
- package/docs/reference/benchmarks.md +3613 -0
- package/docs/reference/cli.md +434 -160
- package/docs/reference/distribution.md +328 -182
- package/docs/reference/environment.md +398 -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 +113 -6
- package/docs/reference/server-api.md +944 -429
- package/docs/reference/server-config.md +740 -338
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +158 -0
- package/package.json +9 -4
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1790 -0
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +106 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +32 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +130 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +133 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/memory-client.ts +189 -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-cohort.ts +89 -0
- package/src/backends/mlx/prefill-rows.ts +200 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/request-state-policy.ts +41 -0
- package/src/backends/mlx/rollback.ts +89 -0
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +242 -0
- package/src/backends/mlx/speculative-group.ts +359 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +46 -0
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
- package/src/cli.ts +66 -58
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +60 -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/prefix-cache.ts +23 -0
- package/src/contracts/resources.ts +25 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +45 -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 +53 -0
- package/src/engine/resources.ts +53 -0
- package/src/engine/scheduler.ts +72 -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 +30 -16
- package/src/generate.ts +709 -417
- 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/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -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-scheme.ts +70 -12
- package/src/kv-store.ts +177 -63
- 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/lab/paged-kv/paged-kv-rows.ts +83 -0
- package/src/{model → lab/paged-kv}/paged-kv.ts +32 -15
- 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 +11 -11
- 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/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +20 -6
- package/src/model/batched-quant.ts +9 -3
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +78 -17
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +65 -23
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +93 -0
- package/src/model/compiled-decode.ts +8 -2
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +55 -0
- 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/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +179 -80
- package/src/model/gemma4.ts +40 -32
- package/src/model/generated/gemma4-12b.ts +6 -3
- package/src/model/generated/gemma4-26b.ts +6 -3
- package/src/model/generated/gemma4-e4b.ts +6 -3
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +1 -1
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/implementation.ts +69 -0
- package/src/model/kv-attention-view.ts +67 -0
- package/src/model/minicpm5.ts +17 -15
- 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-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +202 -78
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +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/turboquant-codec.ts +83 -0
- package/src/model/universal/dense.ts +15 -14
- 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/preflight.ts +31 -15
- package/src/prompt-cache.ts +110 -69
- 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 +40 -29
- 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 +183 -213
- 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 +511 -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 +23 -18
- package/src/serve/request-prep.ts +344 -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 +335 -2711
- package/src/spec/assistant-rows.ts +92 -0
- package/src/spec/assistant-source.ts +31 -55
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +16 -14
- package/src/spec/dflash-source.ts +36 -12
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +97 -122
- 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 +129 -172
- 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-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -137
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +93 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +219 -0
- package/src/spec/qwen-mtp-source.ts +147 -195
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +201 -201
- package/src/spec/source.ts +110 -13
- package/src/spec/two-model-rows.ts +61 -0
- package/src/spec/two-model.ts +44 -102
- package/src/ssd-cache.ts +99 -19
- package/src/ssd-durability.ts +36 -20
- 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 +62 -60
- 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
package/src/kv-store.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type CheckpointAttachment, attachmentBytes, disposeAttachments } from "./backends/mlx/checkpoint-state";
|
|
2
|
+
import { cleanupFailure, disposeResources } from "./engine/resources";
|
|
1
3
|
// KV-cache persistence: save prompt caches to disk, reload by streamed copy.
|
|
2
|
-
// The serialization core of the SSD cold tier (docs/design/
|
|
4
|
+
// The serialization core of the SSD cold tier (docs/design/kv-cache.md).
|
|
3
5
|
//
|
|
4
6
|
// File layout (every tensor PAGE-ALIGNED — the Phase 1 corollary: files
|
|
5
7
|
// we write can be mmap'd and handed to the GPU without copies):
|
|
@@ -85,6 +87,7 @@ interface TensorSlot {
|
|
|
85
87
|
export interface CacheHeaderEntry {
|
|
86
88
|
kind: CacheKind;
|
|
87
89
|
offset: number;
|
|
90
|
+
minimumReusableOffset?: number;
|
|
88
91
|
/** rotating variants: ring write index */
|
|
89
92
|
idx?: number;
|
|
90
93
|
/** rotating variants: window size */
|
|
@@ -113,6 +116,7 @@ export interface CacheHeaderEntry {
|
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
export interface KvSaveMeta {
|
|
119
|
+
attachments?: CheckpointAttachment[];
|
|
116
120
|
modelId?: string;
|
|
117
121
|
/** configFingerprint(config) — covers every graph-shaping field incl.
|
|
118
122
|
* the kv-quant scheme, so a scheme flip invalidates naturally. */
|
|
@@ -121,10 +125,28 @@ export interface KvSaveMeta {
|
|
|
121
125
|
ns?: string;
|
|
122
126
|
/** sha256 of tokenizer.json: identical ids must mean identical text. */
|
|
123
127
|
tokenizerHash?: string;
|
|
128
|
+
/** In-flight generation state. The serialized caches cover `tokens`;
|
|
129
|
+
* pendingToken was sampled from that state but has not been emitted. */
|
|
130
|
+
generationCheckpoint?: {
|
|
131
|
+
key: string;
|
|
132
|
+
cacheNs: string;
|
|
133
|
+
originalPromptTokens: number;
|
|
134
|
+
generatedTokens: number;
|
|
135
|
+
pendingToken: number;
|
|
136
|
+
/** Actual sampler seed, including a server-generated default. Required to
|
|
137
|
+
* continue the random stream exactly after a restart. */
|
|
138
|
+
seed: number;
|
|
139
|
+
/** True when `seed` was part of the client request rather than generated
|
|
140
|
+
* by the server. */
|
|
141
|
+
seedWasExplicit: boolean;
|
|
142
|
+
};
|
|
124
143
|
}
|
|
125
144
|
|
|
126
|
-
export interface KvFileHeader extends KvSaveMeta {
|
|
127
|
-
|
|
145
|
+
export interface KvFileHeader extends Omit<KvSaveMeta, "attachments"> {
|
|
146
|
+
attachments?: Array<Omit<CheckpointAttachment, "tensors"> & { tensors: TensorSlot[] }>;
|
|
147
|
+
/** Absent in earlier v3 files, which used mlx-cache-v3. */
|
|
148
|
+
codecProvider?: string;
|
|
149
|
+
formatVersion: 3 | 4;
|
|
128
150
|
createdAt: number;
|
|
129
151
|
tokens: number[];
|
|
130
152
|
caches: CacheHeaderEntry[];
|
|
@@ -143,7 +165,7 @@ const alignUp = (n: number) => Math.ceil(n / ALIGN) * ALIGN;
|
|
|
143
165
|
* materialization until rawBytes at write time). */
|
|
144
166
|
interface TensorSource { arr: MlxArray; disposeAfter: boolean }
|
|
145
167
|
|
|
146
|
-
interface SnapshotContext {
|
|
168
|
+
export interface SnapshotContext {
|
|
147
169
|
slots: TensorSlot[];
|
|
148
170
|
push(a: MlxArray, disposeAfter: boolean): void;
|
|
149
171
|
liveSlice(a: MlxArray, upTo: number): MlxArray;
|
|
@@ -151,21 +173,21 @@ interface SnapshotContext {
|
|
|
151
173
|
pushTriple(t: ops.QuantizedTensor, upTo: number | null): void;
|
|
152
174
|
}
|
|
153
175
|
|
|
154
|
-
interface CloneContext {
|
|
176
|
+
export interface CloneContext {
|
|
155
177
|
view(a: MlxArray): MlxArray;
|
|
156
178
|
liveView(a: MlxArray, upTo: number): MlxArray;
|
|
157
179
|
mlaView(a: MlxArray, upTo: number): MlxArray;
|
|
158
180
|
tripleView(t: ops.QuantizedTensor, upTo: number | null): ops.QuantizedTensor;
|
|
159
181
|
}
|
|
160
182
|
|
|
161
|
-
interface LoadContext {
|
|
183
|
+
export interface LoadContext {
|
|
162
184
|
path: string;
|
|
163
185
|
arr(slot: TensorSlot): MlxArray;
|
|
164
186
|
grownArr(slot: TensorSlot, offset: number): MlxArray;
|
|
165
187
|
triple(slots: TensorSlot[], at: number): ops.QuantizedTensor;
|
|
166
188
|
}
|
|
167
189
|
|
|
168
|
-
interface CacheCodec {
|
|
190
|
+
export interface CacheCodec {
|
|
169
191
|
matches(cache: Cache): boolean;
|
|
170
192
|
snapshot(cache: Cache, context: SnapshotContext): CacheHeaderEntry;
|
|
171
193
|
clone(cache: Cache, context: CloneContext): Cache;
|
|
@@ -255,8 +277,8 @@ const CACHE_CODECS = {
|
|
|
255
277
|
},
|
|
256
278
|
clone: (cache, context) => {
|
|
257
279
|
const c = cache as KVCache;
|
|
258
|
-
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
259
280
|
const clone = new KVCache();
|
|
281
|
+
if (!c.keys || !c.values) return clone;
|
|
260
282
|
clone.restoreState(
|
|
261
283
|
context.liveView(c.keys, c.offset),
|
|
262
284
|
context.liveView(c.values, c.offset),
|
|
@@ -287,8 +309,8 @@ const CACHE_CODECS = {
|
|
|
287
309
|
},
|
|
288
310
|
clone: (cache, context) => {
|
|
289
311
|
const c = cache as RotatingKVCache;
|
|
290
|
-
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
291
312
|
const clone = new RotatingKVCache(c.maxSize);
|
|
313
|
+
if (!c.keys || !c.values) return clone;
|
|
292
314
|
clone.restoreState(context.view(c.keys), context.view(c.values), c.offset, c.ringIdx);
|
|
293
315
|
return clone;
|
|
294
316
|
},
|
|
@@ -314,8 +336,8 @@ const CACHE_CODECS = {
|
|
|
314
336
|
},
|
|
315
337
|
clone: (cache, context) => {
|
|
316
338
|
const c = cache as QuantizedKVCache;
|
|
317
|
-
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
318
339
|
const clone = new QuantizedKVCache(c.groupSize, c.bits);
|
|
340
|
+
if (!c.keys || !c.values) return clone;
|
|
319
341
|
clone.restoreState(
|
|
320
342
|
context.tripleView(c.keys, c.offset),
|
|
321
343
|
context.tripleView(c.values, c.offset),
|
|
@@ -345,8 +367,8 @@ const CACHE_CODECS = {
|
|
|
345
367
|
},
|
|
346
368
|
clone: (cache, context) => {
|
|
347
369
|
const c = cache as RotatingQuantizedKVCache;
|
|
348
|
-
if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
|
|
349
370
|
const clone = new RotatingQuantizedKVCache(c.maxSize, c.groupSize, c.bits);
|
|
371
|
+
if (!c.keys || !c.values) return clone;
|
|
350
372
|
clone.restoreState(
|
|
351
373
|
context.tripleView(c.keys, null), context.tripleView(c.values, null),
|
|
352
374
|
c.offset, c.ringIdx,
|
|
@@ -458,16 +480,61 @@ const CACHE_CODECS = {
|
|
|
458
480
|
},
|
|
459
481
|
} satisfies Record<CacheKind, CacheCodec>;
|
|
460
482
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
483
|
+
/** Codecs belong to the loaded backend binding. The persisted ID is checked
|
|
484
|
+
* before allocating state; codecs are engine code, never loaded from a file. */
|
|
485
|
+
export interface CacheCodecProvider {
|
|
486
|
+
readonly id: string;
|
|
487
|
+
forCache(cache: Cache): CacheCodec;
|
|
488
|
+
forHeader(entry: CacheHeaderEntry): CacheCodec;
|
|
489
|
+
}
|
|
466
490
|
|
|
467
|
-
export
|
|
468
|
-
|
|
491
|
+
export function createCacheCodecProvider(
|
|
492
|
+
id: string, entries: Readonly<Partial<Record<CacheKind, CacheCodec>>>,
|
|
493
|
+
): CacheCodecProvider {
|
|
494
|
+
if (!id.trim()) throw new Error("cache codec provider needs an identity");
|
|
495
|
+
const table = new Map(Object.entries(entries).map(([kind, codec]) => [kind, Object.freeze({
|
|
496
|
+
matches: codec.matches.bind(codec), snapshot: codec.snapshot.bind(codec), clone: codec.clone.bind(codec),
|
|
497
|
+
load: codec.load.bind(codec), headerTrimmable: codec.headerTrimmable.bind(codec),
|
|
498
|
+
})]));
|
|
499
|
+
const candidates = [...table.values()];
|
|
500
|
+
const selected = new WeakMap<Cache, CacheCodec>();
|
|
501
|
+
return Object.freeze({
|
|
502
|
+
id,
|
|
503
|
+
forCache(cache: Cache) {
|
|
504
|
+
const bound = selected.get(cache);
|
|
505
|
+
if (bound) return bound;
|
|
506
|
+
const matches = candidates.filter((codec) => codec.matches(cache));
|
|
507
|
+
if (matches.length !== 1)
|
|
508
|
+
throw new Error(`cache codec provider ${id}: ${matches.length} matches for ${cacheSignature(cache)}`);
|
|
509
|
+
selected.set(cache, matches[0]!);
|
|
510
|
+
return matches[0]!;
|
|
511
|
+
},
|
|
512
|
+
forHeader(entry: CacheHeaderEntry) {
|
|
513
|
+
const codec = table.get(entry.kind);
|
|
514
|
+
if (!codec) throw new Error(`cache codec provider ${id}: unsupported cache kind ${entry.kind}`);
|
|
515
|
+
return codec;
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
}
|
|
469
519
|
|
|
470
|
-
|
|
520
|
+
export const legacyCacheCodecs = createCacheCodecProvider("mlx-cache-v3", CACHE_CODECS);
|
|
521
|
+
|
|
522
|
+
/** Old TQ files lack the conversion boundary. Keep them reusable at their
|
|
523
|
+
* stored offset, but never infer that an earlier precision state is valid. */
|
|
524
|
+
export function cacheHeaderMinimumReusableOffset(entry: CacheHeaderEntry): number {
|
|
525
|
+
const offset = entry.minimumReusableOffset ?? (entry.kind === "turboquant" ? entry.offset : 0);
|
|
526
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > entry.offset)
|
|
527
|
+
throw new Error("invalid cache minimum reusable offset");
|
|
528
|
+
return offset;
|
|
529
|
+
}
|
|
530
|
+
export const cacheHeadersMinimumReusableOffset = (caches: readonly CacheHeaderEntry[]): number =>
|
|
531
|
+
Math.max(0, ...caches.map(cacheHeaderMinimumReusableOffset));
|
|
532
|
+
|
|
533
|
+
export const cacheHeadersTrimmable = (
|
|
534
|
+
caches: readonly CacheHeaderEntry[], codecs: CacheCodecProvider = legacyCacheCodecs,
|
|
535
|
+
): boolean => caches.every((entry) => codecs.forHeader(entry).headerTrimmable(entry));
|
|
536
|
+
|
|
537
|
+
function snapshotCache(c: Cache, dataOffset: number, codecs: CacheCodecProvider): { entry: CacheHeaderEntry; sources: TensorSource[]; next: number } {
|
|
471
538
|
const slots: TensorSlot[] = [];
|
|
472
539
|
const sources: TensorSource[] = [];
|
|
473
540
|
let cursor = dataOffset;
|
|
@@ -493,10 +560,14 @@ function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry;
|
|
|
493
560
|
}
|
|
494
561
|
};
|
|
495
562
|
|
|
496
|
-
|
|
497
|
-
slots, push, liveSlice, liveMlaSlice, pushTriple
|
|
498
|
-
|
|
499
|
-
|
|
563
|
+
try {
|
|
564
|
+
const entry = codecs.forCache(c).snapshot(c, { slots, push, liveSlice, liveMlaSlice, pushTriple });
|
|
565
|
+
if (c.minimumReusableOffset !== undefined) entry.minimumReusableOffset = c.minimumReusableOffset;
|
|
566
|
+
return { entry, sources, next: cursor };
|
|
567
|
+
} catch (error) {
|
|
568
|
+
for (const source of sources) if (source.disposeAfter) source.arr.dispose();
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
500
571
|
}
|
|
501
572
|
|
|
502
573
|
/** Persist `caches` (+ the exact token prefix they encode) to `path`.
|
|
@@ -513,15 +584,17 @@ function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry;
|
|
|
513
584
|
* called while the caches hold EXACTLY the state to snapshot (post-wrap
|
|
514
585
|
* rings cannot be rewound later). Same per-kind dispatch as
|
|
515
586
|
* snapshotCache/loadKvCache. */
|
|
516
|
-
export function cloneKvCaches(caches: Cache[]): Cache[] {
|
|
517
|
-
const
|
|
587
|
+
export function cloneKvCaches(caches: Cache[], codecs: CacheCodecProvider = legacyCacheCodecs): Cache[] {
|
|
588
|
+
const pending: MlxArray[] = [];
|
|
589
|
+
const hold = (array: MlxArray): MlxArray => { pending.push(array); return array; };
|
|
590
|
+
const view = (a: MlxArray): MlxArray => hold(a.slice(a.shape.map(() => 0), [...a.shape]));
|
|
518
591
|
const liveView = (a: MlxArray, upTo: number): MlxArray => {
|
|
519
592
|
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
520
|
-
return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
|
|
593
|
+
return hold(a.slice([0, 0, 0, 0], [B, H, upTo, D]));
|
|
521
594
|
};
|
|
522
595
|
const mlaView = (a: MlxArray, upTo: number): MlxArray => {
|
|
523
596
|
const [B, , D] = a.shape as [number, number, number];
|
|
524
|
-
return a.slice([0, 0, 0], [B, upTo, D]);
|
|
597
|
+
return hold(a.slice([0, 0, 0], [B, upTo, D]));
|
|
525
598
|
};
|
|
526
599
|
const tripleView = (t: ops.QuantizedTensor, upTo: number | null): ops.QuantizedTensor => ({
|
|
527
600
|
packed: upTo === null ? view(t.packed) : liveView(t.packed, upTo),
|
|
@@ -531,11 +604,13 @@ export function cloneKvCaches(caches: Cache[]): Cache[] {
|
|
|
531
604
|
const out: Cache[] = [];
|
|
532
605
|
try {
|
|
533
606
|
for (const c of caches) {
|
|
534
|
-
|
|
607
|
+
const clone = codecs.forCache(c).clone(c, { view, liveView, mlaView, tripleView });
|
|
608
|
+
if (c.minimumReusableOffset !== undefined) clone.minimumReusableOffset = c.minimumReusableOffset;
|
|
609
|
+
out.push(clone);
|
|
610
|
+
pending.length = 0; // transferred to the returned cache
|
|
535
611
|
}
|
|
536
612
|
} catch (err) {
|
|
537
|
-
|
|
538
|
-
throw err;
|
|
613
|
+
cleanupFailure(err, () => disposeResources([...pending, ...out]));
|
|
539
614
|
}
|
|
540
615
|
return out;
|
|
541
616
|
}
|
|
@@ -545,23 +620,37 @@ export function cloneKvCaches(caches: Cache[]): Cache[] {
|
|
|
545
620
|
* between yields so the EVENT LOOP KEEPS SERVING while a multi-hundred-MB
|
|
546
621
|
* entry flushes (the write-behind persistence contract: durability never
|
|
547
622
|
* blocks a request). try/finally still runs on early generator close. */
|
|
548
|
-
function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta): Generator<void, void, void> {
|
|
623
|
+
function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta, codecs: CacheCodecProvider): Generator<void, void, void> {
|
|
549
624
|
// Plan pass: header entries + lazy tensor sources, NO bytes materialized.
|
|
550
625
|
const entries: CacheHeaderEntry[] = [];
|
|
551
626
|
const sources: TensorSource[] = [];
|
|
552
627
|
let dataOffset = 0; // relative; rebased after header is sized
|
|
553
628
|
try {
|
|
554
629
|
for (const c of caches) {
|
|
555
|
-
const s = snapshotCache(c, dataOffset);
|
|
630
|
+
const s = snapshotCache(c, dataOffset, codecs);
|
|
556
631
|
entries.push(s.entry);
|
|
557
632
|
sources.push(...s.sources);
|
|
558
633
|
dataOffset = s.next;
|
|
559
634
|
}
|
|
560
635
|
|
|
636
|
+
const { attachments: liveAttachments, ...identity } = meta;
|
|
637
|
+
const attachments = liveAttachments?.map((attachment) => ({
|
|
638
|
+
schema: attachment.schema,
|
|
639
|
+
metadata: { ...attachment.metadata },
|
|
640
|
+
tensors: attachment.tensors.map((arr) => {
|
|
641
|
+
const slot: TensorSlot = { off: dataOffset, bytes: arr.nbytes,
|
|
642
|
+
shape: [...arr.shape], dtype: arr.dtype, hash: "0000000000000000" };
|
|
643
|
+
dataOffset = alignUp(dataOffset + slot.bytes);
|
|
644
|
+
sources.push({ arr, disposeAfter: false });
|
|
645
|
+
return slot;
|
|
646
|
+
}),
|
|
647
|
+
}));
|
|
648
|
+
|
|
561
649
|
// Header size is final NOW: hashes are fixed-width placeholders that get
|
|
562
650
|
// patched in place (same byte length) after the data pass computes them.
|
|
563
651
|
const header: KvFileHeader = {
|
|
564
|
-
formatVersion: 3, createdAt: Date.now(), ...
|
|
652
|
+
formatVersion: attachments?.length ? 4 : 3, createdAt: Date.now(), ...identity,
|
|
653
|
+
...(attachments?.length ? { attachments } : {}), codecProvider: codecs.id, tokens, caches: entries,
|
|
565
654
|
};
|
|
566
655
|
const headerLen = new TextEncoder().encode(JSON.stringify(header)).length;
|
|
567
656
|
const dataStart = alignUp(PREFIX_LEN + headerLen);
|
|
@@ -582,7 +671,7 @@ function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta
|
|
|
582
671
|
// pages, which makes them VISIBLE to ps RSS — accounting, not an
|
|
583
672
|
// allocation (see bench-serve.ts' per-leg RSS note).
|
|
584
673
|
let srcIdx = 0;
|
|
585
|
-
for (const e of entries) {
|
|
674
|
+
for (const e of [...entries, ...(attachments ?? [])]) {
|
|
586
675
|
for (const slot of e.tensors) {
|
|
587
676
|
const src = sources[srcIdx++]!;
|
|
588
677
|
const c = ops.contiguous(src.arr);
|
|
@@ -626,38 +715,41 @@ function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta
|
|
|
626
715
|
}
|
|
627
716
|
}
|
|
628
717
|
|
|
629
|
-
export function saveKvCache(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {}): void {
|
|
630
|
-
for (const _ of saveKvCacheSteps(path, tokens, caches, meta)) { /* drain */ }
|
|
718
|
+
export function saveKvCache(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {}, codecs: CacheCodecProvider = legacyCacheCodecs): void {
|
|
719
|
+
for (const _ of saveKvCacheSteps(path, tokens, caches, meta, codecs)) { /* drain */ }
|
|
631
720
|
}
|
|
632
721
|
|
|
633
722
|
/** Non-blocking variant: yields the event loop after every tensor write so
|
|
634
723
|
* serving interleaves with the flush. Caller owns `caches` lifetime for
|
|
635
724
|
* the duration (pass zero-copy clones, dispose after).
|
|
636
725
|
*
|
|
637
|
-
* `
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
* setImmediate pacing interleaved those slices exactly between decode
|
|
642
|
-
* tokens — the 2026-07-07 decode@ctx contamination. A request arriving
|
|
643
|
-
* MID-flush pauses the remaining tensors until idle again). Gate failures
|
|
644
|
-
* are swallowed: scheduling advice must never corrupt the write path
|
|
645
|
-
* (an early generator close inside the fd-open section would leak the fd). */
|
|
726
|
+
* `runStep` (optional) runs each blocking tensor step inside the caller's
|
|
727
|
+
* exclusion domain. Waiting for an idle observation is not sufficient: a
|
|
728
|
+
* request can acquire the engine between that check and `steps.next()`,
|
|
729
|
+
* putting rawBytesView's GPU sync on the decode stream during prefill. */
|
|
646
730
|
export async function saveKvCacheAsync(
|
|
647
731
|
path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {},
|
|
648
|
-
|
|
732
|
+
runStep?: <T>(step: () => T) => Promise<T>, codecs: CacheCodecProvider = legacyCacheCodecs,
|
|
649
733
|
): Promise<void> {
|
|
650
|
-
const steps = saveKvCacheSteps(path, tokens, caches, meta);
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
734
|
+
const steps = saveKvCacheSteps(path, tokens, caches, meta, codecs);
|
|
735
|
+
try {
|
|
736
|
+
while (true) {
|
|
737
|
+
const next = runStep ? await runStep(() => steps.next()) : steps.next();
|
|
738
|
+
if (next.done) break;
|
|
739
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
740
|
+
}
|
|
741
|
+
} catch (error) {
|
|
742
|
+
// A rejected execution lease can leave the writer suspended with an
|
|
743
|
+
// open fd. Throw through it so its existing failure cleanup runs.
|
|
744
|
+
steps.throw(error);
|
|
745
|
+
throw error;
|
|
655
746
|
}
|
|
656
747
|
}
|
|
657
748
|
|
|
658
749
|
/** One pending write-behind item: an entry's copied tokens + OWNED
|
|
659
750
|
* zero-copy cache clones (the queue disposes them on every exit path). */
|
|
660
751
|
export interface SpillItem {
|
|
752
|
+
attachments?: CheckpointAttachment[];
|
|
661
753
|
tokens: number[];
|
|
662
754
|
caches: Cache[];
|
|
663
755
|
ns: string;
|
|
@@ -666,7 +758,7 @@ export interface SpillItem {
|
|
|
666
758
|
/** Bounded write-behind queue (2026-07-07 post-merge review fix).
|
|
667
759
|
*
|
|
668
760
|
* Pending spill/snapshot clones pin their entries' GPU buffers until the
|
|
669
|
-
*
|
|
761
|
+
* generation-locked flush gets a turn; the old bare promise chain queued them
|
|
670
762
|
* WITHOUT BOUND, so under sustained traffic (gate starved, evictions
|
|
671
763
|
* ongoing) resident memory = prompt-cache cap + every queued clone —
|
|
672
764
|
* allocator pressure exactly under the load that caused the evictions.
|
|
@@ -702,6 +794,8 @@ export class SpillQueue {
|
|
|
702
794
|
readonly store: (item: SpillItem) => Promise<unknown>,
|
|
703
795
|
/** Clone disposal — frees the pinned GPU memory. */
|
|
704
796
|
readonly disposeClones: (caches: Cache[]) => void,
|
|
797
|
+
/** Zero-byte control barriers may need to survive capacity shedding. */
|
|
798
|
+
readonly canDrop: (item: SpillItem) => boolean = () => true,
|
|
705
799
|
) {}
|
|
706
800
|
|
|
707
801
|
/** Bytes pinned by queued (not yet flushed) clones. */
|
|
@@ -717,7 +811,7 @@ export class SpillQueue {
|
|
|
717
811
|
const result = new Promise<boolean>((resolve) => { settle = resolve; });
|
|
718
812
|
const rec: SpillRec = {
|
|
719
813
|
...item,
|
|
720
|
-
bytes: this.bytesOf(item.caches),
|
|
814
|
+
bytes: this.bytesOf(item.caches) + attachmentBytes(item.attachments),
|
|
721
815
|
dropped: false,
|
|
722
816
|
settle,
|
|
723
817
|
};
|
|
@@ -725,7 +819,7 @@ export class SpillQueue {
|
|
|
725
819
|
this.#bytes += rec.bytes;
|
|
726
820
|
while (this.#bytes > this.capBytes) {
|
|
727
821
|
const victim = this.#queue.find(
|
|
728
|
-
(p) => !p.dropped && p !== this.#inFlight && p !== rec,
|
|
822
|
+
(p) => !p.dropped && p !== this.#inFlight && p !== rec && this.canDrop(p),
|
|
729
823
|
);
|
|
730
824
|
if (!victim) break; // only the new item (or in-flight) left — soft cap
|
|
731
825
|
this.#drop(victim);
|
|
@@ -744,12 +838,19 @@ export class SpillQueue {
|
|
|
744
838
|
} finally {
|
|
745
839
|
this.#inFlight = null;
|
|
746
840
|
this.#remove(rec);
|
|
747
|
-
this.disposeClones(rec.caches)
|
|
841
|
+
disposeResources([{ dispose: () => this.disposeClones(rec.caches) },
|
|
842
|
+
{ dispose: () => disposeAttachments(rec.attachments) }]);
|
|
748
843
|
}
|
|
749
844
|
});
|
|
750
845
|
return result;
|
|
751
846
|
}
|
|
752
847
|
|
|
848
|
+
/** Cancel queued ownership, excluding the write already in flight. */
|
|
849
|
+
cancelWhere(matches: (item: SpillItem) => boolean): void {
|
|
850
|
+
for (const rec of [...this.#queue])
|
|
851
|
+
if (!rec.dropped && rec !== this.#inFlight && matches(rec)) this.#drop(rec);
|
|
852
|
+
}
|
|
853
|
+
|
|
753
854
|
/** Settles when everything enqueued so far has flushed or dropped
|
|
754
855
|
* (tests and graceful shutdown). */
|
|
755
856
|
drain(): Promise<void> { return this.#chain; }
|
|
@@ -759,7 +860,8 @@ export class SpillQueue {
|
|
|
759
860
|
this.#dropped++;
|
|
760
861
|
rec.settle(false);
|
|
761
862
|
this.#remove(rec);
|
|
762
|
-
this.disposeClones(rec.caches)
|
|
863
|
+
disposeResources([{ dispose: () => this.disposeClones(rec.caches) },
|
|
864
|
+
{ dispose: () => disposeAttachments(rec.attachments) }]);
|
|
763
865
|
}
|
|
764
866
|
|
|
765
867
|
#remove(rec: SpillRec): void {
|
|
@@ -789,7 +891,7 @@ export function readKvHeader(path: string): KvFileHeader & { dataStart: number }
|
|
|
789
891
|
if (BigInt(Bun.hash(body)) !== expectHash)
|
|
790
892
|
throw new Error(`${path}: header hash mismatch (truncated or corrupt)`);
|
|
791
893
|
const header = JSON.parse(new TextDecoder().decode(body)) as KvFileHeader;
|
|
792
|
-
if (header.formatVersion !== 3)
|
|
894
|
+
if (header.formatVersion !== 3 && header.formatVersion !== 4)
|
|
793
895
|
throw new Error(`${path}: unsupported formatVersion ${header.formatVersion}`);
|
|
794
896
|
return { ...header, dataStart };
|
|
795
897
|
} finally {
|
|
@@ -798,6 +900,7 @@ export function readKvHeader(path: string): KvFileHeader & { dataStart: number }
|
|
|
798
900
|
}
|
|
799
901
|
|
|
800
902
|
export interface LoadedKvCache {
|
|
903
|
+
attachments?: CheckpointAttachment[];
|
|
801
904
|
tokens: number[];
|
|
802
905
|
header: KvFileHeader;
|
|
803
906
|
caches: Cache[];
|
|
@@ -904,9 +1007,12 @@ function withStepCapacity(a: MlxArray, offset: number): MlxArray {
|
|
|
904
1007
|
export function loadKvCache(
|
|
905
1008
|
path: string,
|
|
906
1009
|
model: { makeCache(): Cache[] },
|
|
907
|
-
expect: KvLoadExpect = {},
|
|
1010
|
+
expect: KvLoadExpect = {}, codecs: CacheCodecProvider = legacyCacheCodecs,
|
|
908
1011
|
): LoadedKvCache {
|
|
909
1012
|
const header = readKvHeader(path);
|
|
1013
|
+
if ((header.codecProvider ?? legacyCacheCodecs.id) !== codecs.id)
|
|
1014
|
+
throw new Error(`${path}: cache codec provider mismatch`);
|
|
1015
|
+
for (const entry of header.caches) codecs.forHeader(entry);
|
|
910
1016
|
for (const key of [
|
|
911
1017
|
"modelId",
|
|
912
1018
|
"configFingerprint",
|
|
@@ -964,22 +1070,30 @@ export function loadKvCache(
|
|
|
964
1070
|
({ packed: arr(slots[at]!), scales: arr(slots[at + 1]!), biases: arr(slots[at + 2]!) });
|
|
965
1071
|
|
|
966
1072
|
const caches: Cache[] = [];
|
|
1073
|
+
const attachments: CheckpointAttachment[] = [];
|
|
967
1074
|
try {
|
|
968
1075
|
for (const e of header.caches) {
|
|
969
|
-
const codec =
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1076
|
+
const codec = codecs.forHeader(e);
|
|
1077
|
+
const minimumOffset = cacheHeaderMinimumReusableOffset(e);
|
|
1078
|
+
const cache = codec.load(e, { path, arr, grownArr, triple });
|
|
1079
|
+
cache.minimumReusableOffset = minimumOffset;
|
|
1080
|
+
caches.push(cache);
|
|
973
1081
|
// This entry's tensors are now owned by its cache — stop tracking them
|
|
974
1082
|
// (the catch must not double-free through both pending AND caches).
|
|
975
1083
|
pending.length = 0;
|
|
976
1084
|
}
|
|
1085
|
+
for (const attachment of header.attachments ?? []) {
|
|
1086
|
+
attachments.push({ schema: attachment.schema, metadata: { ...attachment.metadata },
|
|
1087
|
+
tensors: attachment.tensors.map(arr) });
|
|
1088
|
+
pending.length = 0;
|
|
1089
|
+
}
|
|
977
1090
|
} catch (err) {
|
|
1091
|
+
disposeAttachments(attachments);
|
|
978
1092
|
for (const a of pending) a.dispose(); // the mid-entry orphans
|
|
979
1093
|
for (const c of caches) c.dispose();
|
|
980
1094
|
throw err;
|
|
981
1095
|
} finally {
|
|
982
1096
|
mmap.unmap(); // every tensor was copied out — nothing aliases the file
|
|
983
1097
|
}
|
|
984
|
-
return { tokens: header.tokens, header, caches };
|
|
1098
|
+
return { tokens: header.tokens, header, caches, ...(attachments.length ? { attachments } : {}) };
|
|
985
1099
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# src/lab — quarantined, no-oracle code
|
|
2
|
+
|
|
3
|
+
Everything here is a Lab-tier module (docs/design/unified-engine-frontier-plan.md):
|
|
4
|
+
no external oracle, off by default, kept only while a measurement decides its
|
|
5
|
+
fate (PLAN.md "Maintainability program", D6 darlings). Rules, gate-enforced by
|
|
6
|
+
`scripts/check-hygiene.ts` (check 11):
|
|
7
|
+
|
|
8
|
+
- nothing outside `src/lab/` may import from `src/lab/` except the edges listed
|
|
9
|
+
in `LAB_IMPORT_ALLOWLIST` — those are recorded debt, not permission;
|
|
10
|
+
- a new production dependency on a Lab module is a decision, not a default:
|
|
11
|
+
either promote the module out of the lab with an oracle/measurement, or
|
|
12
|
+
route it through an option seam.
|
|
13
|
+
|
|
14
|
+
| module | flag / entry | oracle |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `curve/` (curve sampler + designer page) | `--curve-*`, `/curves` | none (Lab) |
|
|
17
|
+
| `paged-kv/` | `--paged-kv` (serial-only) | parity vs plain KVCache (bit-exact), no measured win |
|
|
18
|
+
| `expert-trace/` | `MLX_BUN_EXPERT_TRACE` hook in gemma4 | closed study (expert-offload) |
|
|
@@ -88,7 +88,7 @@ button.ghost{background:var(--card);border:1px solid var(--bd);border-radius:8px
|
|
|
88
88
|
|
|
89
89
|
<script>
|
|
90
90
|
const ln=Math.log;
|
|
91
|
-
const UMIN=ln(1e-6); // bottom-left / plot floor (matches src/curve-sampler.ts CURVE_UMIN)
|
|
91
|
+
const UMIN=ln(1e-6); // bottom-left / plot floor (matches src/lab/curve/curve-sampler.ts CURVE_UMIN)
|
|
92
92
|
const ENGINE_BASE = location.protocol.startsWith("http") ? location.origin : "http://localhost:8080";
|
|
93
93
|
// ---- curve state: 5 movable control points, sorted ascending in p_in ----
|
|
94
94
|
function identityPoints(){ return [{x_pct:1e-4,y_pct:1e-4},{x_pct:0.1,y_pct:0.1},{x_pct:1,y_pct:1},{x_pct:9,y_pct:9},{x_pct:100,y_pct:100}]; }
|
|
@@ -102,7 +102,7 @@ const PROMPTS=[
|
|
|
102
102
|
"Describe the feeling of a city at 3am in one vivid sentence.",
|
|
103
103
|
];
|
|
104
104
|
|
|
105
|
-
// ---- math: monotone cubic (PCHIP), IDENTICAL to src/curve-sampler.ts ----
|
|
105
|
+
// ---- math: monotone cubic (PCHIP), IDENTICAL to src/lab/curve/curve-sampler.ts ----
|
|
106
106
|
const Lp=pt=>[ln(Math.max(pt.x_pct,1e-9)/100),ln(Math.max(pt.y_pct,1e-9)/100)];
|
|
107
107
|
function knotArrays(c){ const k=c.points.map(Lp).sort((a,b)=>a[0]-b[0]); return {X:k.map(q=>q[0]),Y:k.map(q=>q[1])}; }
|
|
108
108
|
function secants(c){ const {X,Y}=knotArrays(c),s=[]; for(let i=X.length-1;i>0;i--)s.push((Y[i]-Y[i-1])/(X[i]-X[i-1])); return s; }
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// BELOW lowers them (renorm makes the literal number an intent); the SLOPE across
|
|
17
17
|
// a region sets how evenly the tokens inside it spread (flat<1 even, steep>1 separated).
|
|
18
18
|
|
|
19
|
-
import { MlxArray } from "
|
|
20
|
-
import * as ops from "
|
|
19
|
+
import { MlxArray } from "../../mlx/array";
|
|
20
|
+
import * as ops from "../../mlx/ops";
|
|
21
21
|
|
|
22
22
|
export interface CurvePoint { x_pct: number; y_pct: number; }
|
|
23
23
|
export interface CurveParams {
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Records every MoE router decision as JSONL — one record per router call:
|
|
4
4
|
// { c: callSeq, l: layerIdx, s: [B, L, k], i: [flattened uint32 expert ids] }
|
|
5
|
-
// Analyse with
|
|
5
|
+
// Analyse with analyze-expert-trace.ts (deleted 2026-08-23; git history) (coverage curve, working-set
|
|
6
6
|
// size, within-task stability, cross-task specialisation).
|
|
7
7
|
//
|
|
8
8
|
// Two ways to drive it:
|
|
9
9
|
// - env: `MLX_BUN_EXPERT_TRACE=<path> mlx-bun serve ...` (auto-starts here)
|
|
10
10
|
// - programmatic: beginExpertTrace(path) / endExpertTrace() per domain
|
|
11
|
-
// (
|
|
11
|
+
// (run-expert-trace.ts (deleted 2026-08-23; git history) uses this to trace several domains from
|
|
12
12
|
// one model load).
|
|
13
13
|
//
|
|
14
14
|
// Recording adds a per-call GPU->host sync (it reads the routed indices back),
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
// Router.forward is a single branch (zero overhead) — inert by default, same
|
|
18
18
|
// discipline as the slots flag.
|
|
19
19
|
|
|
20
|
-
import { Dtype } from "
|
|
21
|
-
import type { MlxArray } from "
|
|
22
|
-
import { runtimeValue } from "
|
|
20
|
+
import { Dtype } from "../../mlx/ffi";
|
|
21
|
+
import type { MlxArray } from "../../mlx/array";
|
|
22
|
+
import { runtimeValue } from "../../runtime-config";
|
|
23
23
|
|
|
24
24
|
let sink: Bun.FileSink | null = null;
|
|
25
25
|
let seq = 0;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { BatchableCache, Cache, Mask } from "../../model/gemma4-base";
|
|
2
|
+
import { buildBatchedDecodeMask, mergeKVRows } from "../../model/batched-mask";
|
|
3
|
+
import { MlxArray } from "../../mlx/array";
|
|
4
|
+
import { disposeResources } from "../../engine/resources";
|
|
5
|
+
import { PagedKVCache, poolBlocksFor } from "./paged-kv";
|
|
6
|
+
|
|
7
|
+
/** Block storage owns row membership; attention consumes the same gathered
|
|
8
|
+
* K/V representation as the existing paged cache. No scheduler policy here. */
|
|
9
|
+
export class PagedKvRows implements BatchableCache {
|
|
10
|
+
#rows: PagedKVCache[] = [];
|
|
11
|
+
#rope?: MlxArray;
|
|
12
|
+
#ropeOffset = -1;
|
|
13
|
+
|
|
14
|
+
constructor(readonly capacityTokens: number, readonly blockSize: number) {}
|
|
15
|
+
signature(): string { return "kv:paged-rows"; }
|
|
16
|
+
get batchSize(): number { return this.#rows.length; }
|
|
17
|
+
get offset(): number { return Math.max(0, ...this.#rows.map(row => row.offset)); }
|
|
18
|
+
get rowOffsets(): number[] { return this.#rows.map(row => row.offset); }
|
|
19
|
+
get leftPad(): number[] { const width = this.offset; return this.#rows.map(row => width - row.offset); }
|
|
20
|
+
get ropeOffsetArr(): MlxArray | undefined {
|
|
21
|
+
if (this.#rows.length < 2) return undefined;
|
|
22
|
+
if (!this.#rope || this.#ropeOffset !== this.offset) {
|
|
23
|
+
this.releaseRopeArr();
|
|
24
|
+
this.#rope = MlxArray.fromInt32(Int32Array.from(this.rowOffsets), [this.batchSize]);
|
|
25
|
+
this.#ropeOffset = this.offset;
|
|
26
|
+
}
|
|
27
|
+
return this.#rope;
|
|
28
|
+
}
|
|
29
|
+
releaseRopeArr(): void { this.#rope?.dispose(); this.#rope = undefined; this.#ropeOffset = -1; }
|
|
30
|
+
makeEmptyBatch(): PagedKvRows { return new PagedKvRows(this.capacityTokens, this.blockSize); }
|
|
31
|
+
|
|
32
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
33
|
+
const next: PagedKVCache[] = [];
|
|
34
|
+
try {
|
|
35
|
+
for (const source of rows) {
|
|
36
|
+
if (source instanceof PagedKvRows) {
|
|
37
|
+
for (const row of source.#rows) next.push(row.clone());
|
|
38
|
+
} else if (source instanceof PagedKVCache) next.push(source.clone());
|
|
39
|
+
else throw new Error(`Paged rows cannot merge ${source.signature()}`);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) { disposeResources(next); throw error; }
|
|
42
|
+
const previous = this.#rows; this.#rows = next;
|
|
43
|
+
this.releaseRopeArr(); disposeResources(previous);
|
|
44
|
+
}
|
|
45
|
+
extractRow(row: number): PagedKVCache { return this.#rows[row]!.clone(); }
|
|
46
|
+
filterRows(keep: readonly number[]): void {
|
|
47
|
+
const previous = this.#rows;
|
|
48
|
+
this.#rows = keep.map(row => previous[row]!);
|
|
49
|
+
const retained = new Set(keep);
|
|
50
|
+
this.releaseRopeArr();
|
|
51
|
+
disposeResources(previous.filter((_, row) => !retained.has(row)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
updateAndFetch(keys: MlxArray, values: MlxArray): [MlxArray, MlxArray] {
|
|
55
|
+
if (this.#rows.length === 1) return this.#rows[0]!.updateAndFetch(keys, values);
|
|
56
|
+
const fetched: Array<{ keys: MlxArray; values: MlxArray }> = [];
|
|
57
|
+
try {
|
|
58
|
+
for (const [row, storage] of this.#rows.entries()) {
|
|
59
|
+
using k = keys.slice([row, 0, 0, 0], [row + 1, ...keys.shape.slice(1)]);
|
|
60
|
+
using v = values.slice([row, 0, 0, 0], [row + 1, ...values.shape.slice(1)]);
|
|
61
|
+
const [nextK, nextV] = storage.updateAndFetch(k, v);
|
|
62
|
+
fetched.push({ keys: nextK, values: nextV });
|
|
63
|
+
}
|
|
64
|
+
const result = mergeKVRows(fetched);
|
|
65
|
+
return [result.keys, result.values];
|
|
66
|
+
} finally { disposeResources(fetched.flatMap(row => [row.keys, row.values])); }
|
|
67
|
+
}
|
|
68
|
+
makeMask(tokens: number, window: number | null): Mask {
|
|
69
|
+
if (this.#rows.length === 1) return this.#rows[0]!.makeMask(tokens, window);
|
|
70
|
+
return { mode: "array", arr: buildBatchedDecodeMask(this.batchSize, tokens,
|
|
71
|
+
this.offset + tokens, this.leftPad, window) };
|
|
72
|
+
}
|
|
73
|
+
state(): MlxArray[] { return this.#rows.flatMap(row => row.state()); }
|
|
74
|
+
isTrimmable(): boolean { return true; }
|
|
75
|
+
trim(tokens: number): void { for (const row of this.#rows) row.trim(tokens); this.releaseRopeArr(); }
|
|
76
|
+
projectedBytes(tokens: number): number {
|
|
77
|
+
const pool = this.#rows.find(row => row.pool)?.pool;
|
|
78
|
+
if (!pool) return 0;
|
|
79
|
+
const bytesPerBlock = (pool.keys.nbytes + pool.values.nbytes) / pool.numBlocks;
|
|
80
|
+
return poolBlocksFor(tokens, this.blockSize) * bytesPerBlock;
|
|
81
|
+
}
|
|
82
|
+
dispose(): void { const rows = this.#rows; this.#rows = []; this.releaseRopeArr(); disposeResources(rows); }
|
|
83
|
+
}
|