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/ssd-cache.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SSD cold tier for the prompt/KV cache (docs/design/
|
|
1
|
+
// SSD cold tier for the prompt/KV cache (docs/design/kv-cache.md).
|
|
2
2
|
//
|
|
3
3
|
// Files ARE the database: no sidecar index. Layout
|
|
4
4
|
// <dir>/<configFingerprint>/<nsHash>/<uuid>.mlxkv
|
|
@@ -22,6 +22,7 @@ import { randomUUID } from "node:crypto";
|
|
|
22
22
|
import { commonPrefixLength } from "./prompt-cache";
|
|
23
23
|
import {
|
|
24
24
|
saveKvCache, saveKvCacheAsync, loadKvCache, readKvHeader,
|
|
25
|
+
cacheHeadersTrimmable, legacyCacheCodecs, type CacheCodecProvider,
|
|
25
26
|
type KvSaveMeta, type KvLoadExpect, type LoadedKvCache,
|
|
26
27
|
} from "./kv-store";
|
|
27
28
|
import type { Cache } from "./model/gemma4-base";
|
|
@@ -38,22 +39,11 @@ export interface SsdIndexEntry {
|
|
|
38
39
|
* over a shorter usable one (the 2026-07-06 restart-0 defect: the
|
|
39
40
|
* [prompt+gen] file always won, always got rejected after restore). */
|
|
40
41
|
trimmable: boolean;
|
|
42
|
+
generationCheckpoint?: NonNullable<KvSaveMeta["generationCheckpoint"]>;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
/** Trimmability from a file header alone (mirror of the live caches'
|
|
44
|
-
* isTrimmable(): kv/qkv/turboquant always trim; rotating kinds only pre-wrap
|
|
45
|
-
* (offset < maxSize); GLM compressed kinds always trim; ssm never). Keep in
|
|
46
|
-
* sync with gemma4-base/qwen3-delta/glm52-cache. */
|
|
47
|
-
const headerTrimmable = (caches: { kind: string; offset: number; maxSize?: number }[]): boolean =>
|
|
48
|
-
caches.every((c) =>
|
|
49
|
-
c.kind === "kv" || c.kind === "qkv" || c.kind === "turboquant" ||
|
|
50
|
-
c.kind === "mla" || c.kind === "mla-dsa" || c.kind === "mtp-mla"
|
|
51
|
-
? true
|
|
52
|
-
: c.kind === "rotating" || c.kind === "rotating-qkv"
|
|
53
|
-
? c.offset < (c.maxSize ?? 0)
|
|
54
|
-
: false);
|
|
55
|
-
|
|
56
45
|
export interface SsdStoreOptions {
|
|
46
|
+
codecs?: CacheCodecProvider;
|
|
57
47
|
dir: string;
|
|
58
48
|
maxBytes: number;
|
|
59
49
|
/** Server identity — written into every file, enforced on scan/load. */
|
|
@@ -69,6 +59,7 @@ const nsHash = (ns: string): string =>
|
|
|
69
59
|
|
|
70
60
|
export class SsdCacheStore {
|
|
71
61
|
readonly #opts: SsdStoreOptions;
|
|
62
|
+
readonly #codecs: CacheCodecProvider;
|
|
72
63
|
readonly #root: string; // <dir>/<configFingerprint>
|
|
73
64
|
#index: SsdIndexEntry[] = [];
|
|
74
65
|
#warnedWriteFailure = false;
|
|
@@ -76,6 +67,7 @@ export class SsdCacheStore {
|
|
|
76
67
|
|
|
77
68
|
constructor(opts: SsdStoreOptions) {
|
|
78
69
|
this.#opts = opts;
|
|
70
|
+
this.#codecs = opts.codecs ?? legacyCacheCodecs;
|
|
79
71
|
this.#root = join(opts.dir, opts.configFingerprint);
|
|
80
72
|
}
|
|
81
73
|
|
|
@@ -91,6 +83,21 @@ export class SsdCacheStore {
|
|
|
91
83
|
return this.#opts.maxBytes;
|
|
92
84
|
}
|
|
93
85
|
|
|
86
|
+
/** Longest token prefix whose atomic cache file is present in the index. */
|
|
87
|
+
get longestDurablePrefixTokens(): number {
|
|
88
|
+
return this.#index.reduce((best, entry) => Math.max(best, entry.tokens.length), 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** True when the SSD index can seed every token in this prefix. A longer
|
|
92
|
+
* trimmable descendant is equivalent to an exact boundary snapshot. */
|
|
93
|
+
hasDurablePrefix(tokens: number[], ns = ""): boolean {
|
|
94
|
+
return this.#index.some((entry) => {
|
|
95
|
+
if (entry.ns !== ns || entry.tokens.length < tokens.length) return false;
|
|
96
|
+
if (commonPrefixLength(entry.tokens, tokens) !== tokens.length) return false;
|
|
97
|
+
return entry.tokens.length === tokens.length || entry.trimmable;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
/** Startup recovery: rebuild the in-memory index from disk. Corrupt
|
|
95
102
|
* headers and metadata mismatches are unlinked; `.tmp` orphans reaped.
|
|
96
103
|
* Only OUR fingerprint dir is touched. Returns entries indexed. */
|
|
@@ -118,10 +125,14 @@ export class SsdCacheStore {
|
|
|
118
125
|
h.tokenizerHash !== this.#opts.tokenizerHash
|
|
119
126
|
)
|
|
120
127
|
throw new Error("metadata mismatch");
|
|
128
|
+
if ((h.codecProvider ?? legacyCacheCodecs.id) !== this.#codecs.id) continue;
|
|
121
129
|
const st = statSync(path);
|
|
122
130
|
this.#index.push({
|
|
123
131
|
path, ns: h.ns ?? "", tokens: h.tokens, bytes: st.size, mtimeMs: st.mtimeMs,
|
|
124
|
-
trimmable:
|
|
132
|
+
trimmable: cacheHeadersTrimmable(h.caches, this.#codecs),
|
|
133
|
+
...(h.generationCheckpoint
|
|
134
|
+
? { generationCheckpoint: h.generationCheckpoint }
|
|
135
|
+
: {}),
|
|
125
136
|
});
|
|
126
137
|
} catch {
|
|
127
138
|
try { rmSync(path, { force: true }); } catch {}
|
|
@@ -168,7 +179,7 @@ export class SsdCacheStore {
|
|
|
168
179
|
ns: entry.ns,
|
|
169
180
|
verify: this.#opts.verify,
|
|
170
181
|
};
|
|
171
|
-
const loaded = loadKvCache(entry.path, model, expect);
|
|
182
|
+
const loaded = loadKvCache(entry.path, model, expect, this.#codecs);
|
|
172
183
|
const now = new Date();
|
|
173
184
|
try { utimesSync(entry.path, now, now); entry.mtimeMs = now.getTime(); } catch {}
|
|
174
185
|
this.stats.restores++;
|
|
@@ -195,7 +206,7 @@ export class SsdCacheStore {
|
|
|
195
206
|
const path = join(dir, `${randomUUID()}.mlxkv`);
|
|
196
207
|
try {
|
|
197
208
|
mkdirSync(dir, { recursive: true });
|
|
198
|
-
saveKvCache(path, tokens, caches, this.#meta(ns));
|
|
209
|
+
saveKvCache(path, tokens, caches, this.#meta(ns), this.#codecs);
|
|
199
210
|
return this.#indexStored(path, tokens, caches, ns);
|
|
200
211
|
} catch (err) {
|
|
201
212
|
return this.#storeFailed(path, err);
|
|
@@ -207,21 +218,75 @@ export class SsdCacheStore {
|
|
|
207
218
|
* yields the event loop between tensors so serving interleaves.
|
|
208
219
|
* Caller passes zero-copy CLONES it owns (a consistent snapshot no
|
|
209
220
|
* matter what the live entry does meanwhile) and disposes them after.
|
|
210
|
-
* `
|
|
211
|
-
* server passes the gateway's
|
|
212
|
-
*
|
|
213
|
-
async storeAsync(
|
|
221
|
+
* `runStep` owns every per-tensor step (see saveKvCacheAsync) — the
|
|
222
|
+
* server passes the gateway's exclusive runner so a request cannot start
|
|
223
|
+
* between an idle check and the blocking MLX readback. */
|
|
224
|
+
async storeAsync(
|
|
225
|
+
tokens: number[], caches: Cache[], ns = "",
|
|
226
|
+
runStep?: <T>(step: () => T) => Promise<T>,
|
|
227
|
+
): Promise<boolean> {
|
|
214
228
|
const dir = join(this.#root, nsHash(ns));
|
|
215
229
|
const path = join(dir, `${randomUUID()}.mlxkv`);
|
|
216
230
|
try {
|
|
217
231
|
mkdirSync(dir, { recursive: true });
|
|
218
|
-
await saveKvCacheAsync(path, tokens, caches, this.#meta(ns),
|
|
232
|
+
await saveKvCacheAsync(path, tokens, caches, this.#meta(ns), runStep, this.#codecs);
|
|
219
233
|
return this.#indexStored(path, tokens, caches, ns);
|
|
220
234
|
} catch (err) {
|
|
221
235
|
return this.#storeFailed(path, err);
|
|
222
236
|
}
|
|
223
237
|
}
|
|
224
238
|
|
|
239
|
+
/** Longest durable in-flight generation for an identical request. The
|
|
240
|
+
* checkpoint namespace is separate from the ordinary prompt-cache tier,
|
|
241
|
+
* so incomplete assistant output can never seed an unrelated prefill. */
|
|
242
|
+
findGenerationCheckpoint(
|
|
243
|
+
prompt: number[], key: string, cacheNs = "",
|
|
244
|
+
): SsdIndexEntry | null {
|
|
245
|
+
let best: SsdIndexEntry | null = null;
|
|
246
|
+
for (const entry of this.#index) {
|
|
247
|
+
const checkpoint = entry.generationCheckpoint;
|
|
248
|
+
if (!checkpoint || checkpoint.key !== key || checkpoint.cacheNs !== cacheNs)
|
|
249
|
+
continue;
|
|
250
|
+
if (checkpoint.originalPromptTokens !== prompt.length) continue;
|
|
251
|
+
if (entry.tokens.length <= prompt.length) continue;
|
|
252
|
+
if (commonPrefixLength(entry.tokens, prompt) !== prompt.length) continue;
|
|
253
|
+
if (!best || entry.tokens.length > best.tokens.length) best = entry;
|
|
254
|
+
}
|
|
255
|
+
return best;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Persist one in-flight generation atomically. A completed newer
|
|
259
|
+
* checkpoint supersedes older checkpoints for the same request only after
|
|
260
|
+
* its rename succeeds, so a crash during a write leaves the prior point. */
|
|
261
|
+
async storeGenerationCheckpoint(
|
|
262
|
+
tokens: number[], caches: Cache[], checkpoint: NonNullable<KvSaveMeta["generationCheckpoint"]>,
|
|
263
|
+
): Promise<boolean> {
|
|
264
|
+
const ns = `__generation_checkpoint__:${checkpoint.key}`;
|
|
265
|
+
const dir = join(this.#root, nsHash(ns));
|
|
266
|
+
const path = join(dir, `${randomUUID()}.mlxkv`);
|
|
267
|
+
try {
|
|
268
|
+
mkdirSync(dir, { recursive: true });
|
|
269
|
+
await saveKvCacheAsync(path, tokens, caches, {
|
|
270
|
+
...this.#meta(ns), generationCheckpoint: checkpoint,
|
|
271
|
+
}, undefined, this.#codecs);
|
|
272
|
+
const stored = this.#indexStored(path, tokens, caches, ns, checkpoint);
|
|
273
|
+
if (stored) {
|
|
274
|
+
for (const entry of [...this.#index]) {
|
|
275
|
+
if (entry.path === path) continue;
|
|
276
|
+
if (entry.generationCheckpoint?.key === checkpoint.key) this.remove(entry.path);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return stored;
|
|
280
|
+
} catch (err) {
|
|
281
|
+
return this.#storeFailed(path, err);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
removeGenerationCheckpoints(key: string): void {
|
|
286
|
+
for (const entry of [...this.#index])
|
|
287
|
+
if (entry.generationCheckpoint?.key === key) this.remove(entry.path);
|
|
288
|
+
}
|
|
289
|
+
|
|
225
290
|
#meta(ns: string): KvSaveMeta {
|
|
226
291
|
return {
|
|
227
292
|
modelId: this.#opts.modelId,
|
|
@@ -231,7 +296,10 @@ export class SsdCacheStore {
|
|
|
231
296
|
};
|
|
232
297
|
}
|
|
233
298
|
|
|
234
|
-
#indexStored(
|
|
299
|
+
#indexStored(
|
|
300
|
+
path: string, tokens: number[], caches: Cache[], ns: string,
|
|
301
|
+
generationCheckpoint?: NonNullable<KvSaveMeta["generationCheckpoint"]>,
|
|
302
|
+
): boolean {
|
|
235
303
|
const st = statSync(path);
|
|
236
304
|
if (st.size > this.#opts.maxBytes) {
|
|
237
305
|
rmSync(path, { force: true });
|
|
@@ -257,7 +325,10 @@ export class SsdCacheStore {
|
|
|
257
325
|
if (commonPrefixLength(e.tokens, tokens) === e.tokens.length) this.remove(e.path);
|
|
258
326
|
}
|
|
259
327
|
}
|
|
260
|
-
this.#index.push({
|
|
328
|
+
this.#index.push({
|
|
329
|
+
path, ns, tokens, bytes: st.size, mtimeMs: Date.now(), trimmable,
|
|
330
|
+
...(generationCheckpoint ? { generationCheckpoint } : {}),
|
|
331
|
+
});
|
|
261
332
|
this.stats.spills++;
|
|
262
333
|
this.evictToCap();
|
|
263
334
|
return true;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { Cache } from "./model/gemma4";
|
|
2
|
+
import type { PromptCacheEntry } from "./prompt-cache";
|
|
3
|
+
import type { SpillItem, SpillQueue } from "./kv-store";
|
|
4
|
+
|
|
5
|
+
export interface DurabilityGateway {
|
|
6
|
+
readonly busy: boolean;
|
|
7
|
+
runExclusive<T>(fn: () => Promise<T>): Promise<T>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DurabilityPromptCache {
|
|
11
|
+
findExact(tokens: number[], ns?: string): PromptCacheEntry | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DurabilitySnapshotStats {
|
|
15
|
+
pendingSnapshots: number;
|
|
16
|
+
pendingSpills: number;
|
|
17
|
+
pendingSpillBytes: number;
|
|
18
|
+
droppedSpills: number;
|
|
19
|
+
failedSpills: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DurabilityFlushResult extends DurabilitySnapshotStats {
|
|
23
|
+
durable: boolean;
|
|
24
|
+
flushedSnapshots: number;
|
|
25
|
+
missingSnapshots: number;
|
|
26
|
+
elapsedMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface DirtySnapshot {
|
|
30
|
+
key: string;
|
|
31
|
+
tokens: number[];
|
|
32
|
+
ns: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type StoreOutcome = "stored" | "missing" | "failed";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Turns debounced prompt-cache puts into a durability boundary.
|
|
39
|
+
*
|
|
40
|
+
* A dirty record stays present until SpillQueue reports that its atomic store
|
|
41
|
+
* completed. Queue drops and write failures therefore remain retryable during
|
|
42
|
+
* an explicit flush or graceful shutdown.
|
|
43
|
+
*/
|
|
44
|
+
export class SsdDurabilityCoordinator {
|
|
45
|
+
readonly #dirty = new Map<string, DirtySnapshot>();
|
|
46
|
+
readonly #timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
47
|
+
readonly #attempts = new Map<string, Promise<StoreOutcome>>();
|
|
48
|
+
#flush: Promise<DurabilityFlushResult> | null = null;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
readonly gateway: DurabilityGateway,
|
|
52
|
+
readonly promptCache: DurabilityPromptCache,
|
|
53
|
+
readonly spillQueue: SpillQueue,
|
|
54
|
+
readonly cloneCaches: (caches: Cache[]) => Cache[],
|
|
55
|
+
readonly isAlreadyDurable: (tokens: number[], ns: string) => boolean = () => false,
|
|
56
|
+
readonly debounceMs = 1_000,
|
|
57
|
+
readonly busyRetryMs = 5_000,
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
schedule(tokens: number[], ns = ""): void {
|
|
61
|
+
if (tokens.length === 0) return;
|
|
62
|
+
// PromptCache can hold unrelated entries with the same namespace and
|
|
63
|
+
// length. Include the tokens so one conversation cannot cancel another
|
|
64
|
+
// conversation's pending durability record.
|
|
65
|
+
const key = `${ns.length}:${ns}:${tokens.join(",")}`;
|
|
66
|
+
const rec: DirtySnapshot = { key, tokens: [...tokens], ns };
|
|
67
|
+
this.#dirty.set(key, rec);
|
|
68
|
+
this.#arm(key, this.debounceMs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get stats(): DurabilitySnapshotStats {
|
|
72
|
+
return {
|
|
73
|
+
pendingSnapshots: this.#dirty.size,
|
|
74
|
+
pendingSpills: this.spillQueue.pendingCount,
|
|
75
|
+
pendingSpillBytes: this.spillQueue.pendingBytes,
|
|
76
|
+
droppedSpills: this.spillQueue.droppedCount,
|
|
77
|
+
failedSpills: this.spillQueue.failedCount,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Force every dirty RAM snapshot through the queue and wait for storage. */
|
|
82
|
+
flush(): Promise<DurabilityFlushResult> {
|
|
83
|
+
if (this.#flush) return this.#flush;
|
|
84
|
+
const started = performance.now();
|
|
85
|
+
this.#flush = this.#flushInner(started).finally(() => { this.#flush = null; });
|
|
86
|
+
return this.#flush;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#arm(key: string, delayMs: number): void {
|
|
90
|
+
const old = this.#timers.get(key);
|
|
91
|
+
if (old) clearTimeout(old);
|
|
92
|
+
const timer = setTimeout(() => {
|
|
93
|
+
this.#timers.delete(key);
|
|
94
|
+
void this.#attempt(key, false);
|
|
95
|
+
}, delayMs);
|
|
96
|
+
timer.unref?.();
|
|
97
|
+
this.#timers.set(key, timer);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async #attempt(key: string, force: boolean): Promise<StoreOutcome> {
|
|
101
|
+
const running = this.#attempts.get(key);
|
|
102
|
+
if (running) {
|
|
103
|
+
const outcome = await running;
|
|
104
|
+
if (!force || !this.#dirty.has(key)) return outcome;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const rec = this.#dirty.get(key);
|
|
108
|
+
if (!rec) return "stored";
|
|
109
|
+
if (!force && this.gateway.busy) {
|
|
110
|
+
this.#arm(key, this.busyRetryMs);
|
|
111
|
+
return "failed";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const task = this.#store(rec);
|
|
115
|
+
this.#attempts.set(key, task);
|
|
116
|
+
try {
|
|
117
|
+
const outcome = await task;
|
|
118
|
+
if (outcome === "stored" && this.#dirty.get(key) === rec)
|
|
119
|
+
this.#dirty.delete(key);
|
|
120
|
+
if (outcome !== "stored" && !force && this.#dirty.get(key) === rec)
|
|
121
|
+
this.#arm(key, this.busyRetryMs);
|
|
122
|
+
return outcome;
|
|
123
|
+
} finally {
|
|
124
|
+
if (this.#attempts.get(key) === task) this.#attempts.delete(key);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async #store(rec: DirtySnapshot): Promise<StoreOutcome> {
|
|
129
|
+
let snap: SpillItem | null = null;
|
|
130
|
+
try {
|
|
131
|
+
snap = await this.gateway.runExclusive(async () => {
|
|
132
|
+
if (this.isAlreadyDurable(rec.tokens, rec.ns)) return null;
|
|
133
|
+
const entry = this.promptCache.findExact(rec.tokens, rec.ns);
|
|
134
|
+
if (!entry) return null;
|
|
135
|
+
return {
|
|
136
|
+
tokens: [...entry.tokens],
|
|
137
|
+
caches: this.cloneCaches(entry.caches),
|
|
138
|
+
ns: rec.ns,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
} catch {
|
|
142
|
+
return "failed";
|
|
143
|
+
}
|
|
144
|
+
if (!snap)
|
|
145
|
+
return this.isAlreadyDurable(rec.tokens, rec.ns) ? "stored" : "missing";
|
|
146
|
+
return await this.spillQueue.enqueue(snap) ? "stored" : "failed";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async #flushInner(started: number): Promise<DurabilityFlushResult> {
|
|
150
|
+
for (const timer of this.#timers.values()) clearTimeout(timer);
|
|
151
|
+
this.#timers.clear();
|
|
152
|
+
|
|
153
|
+
let flushedSnapshots = 0;
|
|
154
|
+
const missing = new Map<string, DirtySnapshot>();
|
|
155
|
+
const droppedBefore = this.spillQueue.droppedCount;
|
|
156
|
+
const failedBefore = this.spillQueue.failedCount;
|
|
157
|
+
|
|
158
|
+
if (this.#attempts.size > 0)
|
|
159
|
+
await Promise.allSettled([...this.#attempts.values()]);
|
|
160
|
+
await this.spillQueue.drain();
|
|
161
|
+
|
|
162
|
+
// Flush one entry at a time. This prevents the queue cap from dropping a
|
|
163
|
+
// boundary snapshot while a large final snapshot is already in flight.
|
|
164
|
+
// Attempt each record version once. Missing state stays dirty, so a
|
|
165
|
+
// second flush cannot report success without a real durable snapshot.
|
|
166
|
+
const attempted = new Set<DirtySnapshot>();
|
|
167
|
+
while (this.#dirty.size > 0) {
|
|
168
|
+
const records = [...this.#dirty.entries()].filter(([, rec]) => !attempted.has(rec));
|
|
169
|
+
if (records.length === 0) break;
|
|
170
|
+
for (const [key, rec] of records) {
|
|
171
|
+
if (this.#dirty.get(key) !== rec) continue;
|
|
172
|
+
attempted.add(rec);
|
|
173
|
+
const outcome = await this.#attempt(key, true);
|
|
174
|
+
await this.spillQueue.drain();
|
|
175
|
+
if (outcome === "stored") {
|
|
176
|
+
flushedSnapshots++;
|
|
177
|
+
} else if (outcome === "missing") {
|
|
178
|
+
missing.set(key, rec);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// A later trimmable snapshot may now cover a superseded RAM ancestor.
|
|
184
|
+
// Reconcile against committed SSD coverage after all writes settle;
|
|
185
|
+
// never clear a missing prefix merely because another write succeeded.
|
|
186
|
+
for (const [key, rec] of missing) {
|
|
187
|
+
if (!this.isAlreadyDurable(rec.tokens, rec.ns)) continue;
|
|
188
|
+
if (this.#dirty.get(key) === rec) this.#dirty.delete(key);
|
|
189
|
+
missing.delete(key);
|
|
190
|
+
flushedSnapshots++;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const stats = this.stats;
|
|
194
|
+
return {
|
|
195
|
+
...stats,
|
|
196
|
+
durable:
|
|
197
|
+
stats.pendingSnapshots === 0 &&
|
|
198
|
+
stats.pendingSpills === 0 &&
|
|
199
|
+
stats.droppedSpills === droppedBefore &&
|
|
200
|
+
stats.failedSpills === failedBefore &&
|
|
201
|
+
missing.size === 0,
|
|
202
|
+
flushedSnapshots,
|
|
203
|
+
missingSnapshots: missing.size,
|
|
204
|
+
elapsedMs: performance.now() - started,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
package/src/tool-call.ts
CHANGED
|
@@ -161,7 +161,7 @@ export function parseToolCalls(text: string): ParsedToolCall[] {
|
|
|
161
161
|
|
|
162
162
|
const TOOL_CALL_BLOCK_RE = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/gi;
|
|
163
163
|
const XML_FUNCTION_EQUALS_RE = /^\s*<function=([^>\s]+)>\s*([\s\S]*?)\s*<\/function>\s*$/i;
|
|
164
|
-
const XML_PARAMETER_EQUALS_RE = /<parameter=([^>\s]+)
|
|
164
|
+
const XML_PARAMETER_EQUALS_RE = /<parameter=([^>\s]+)>([\s\S]*?)<\/parameter>/gi;
|
|
165
165
|
const GLM52_ARG_KEY_RE = /<arg_key>/i;
|
|
166
166
|
const GLM52_ARG_PAIR_RE = /<arg_key>\s*([\s\S]*?)\s*<\/arg_key>\s*<arg_value>\s*([\s\S]*?)\s*<\/arg_value>/gi;
|
|
167
167
|
const XML_FUNCTION_ATTR_RE = /<function\s+name=["']([^"']+)["']\s*>\s*([\s\S]*?)\s*<\/function>/gi;
|
|
@@ -208,11 +208,9 @@ function toolParameterSchema(
|
|
|
208
208
|
return null;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
/**
|
|
212
|
-
*
|
|
213
|
-
|
|
214
|
-
function decodeToolValue(value: string, schema: Record<string, unknown> | null): unknown {
|
|
215
|
-
const text = decodeXml(value);
|
|
211
|
+
/** Decode a value after its caller removes protocol-specific delimiters.
|
|
212
|
+
* String-typed params stay raw; everything else is JSON-decoded when possible. */
|
|
213
|
+
function decodeToolValue(text: string, schema: Record<string, unknown> | null): unknown {
|
|
216
214
|
if (!text) return "";
|
|
217
215
|
const types = schemaTypeNames(schema);
|
|
218
216
|
if (types.size && [...types].every((t) => t === "string" || t === "null")) return text;
|
|
@@ -546,23 +544,59 @@ function parseJsonToolCall(block: string): ParsedToolCall | null {
|
|
|
546
544
|
return repairs.length ? { ...call, repaired: true, repairs } : call;
|
|
547
545
|
}
|
|
548
546
|
|
|
549
|
-
function parseXmlEqualsToolCall(block: string, tools: ToolSpec[]): ParsedToolCall | null {
|
|
547
|
+
function parseXmlEqualsToolCall(block: string, tools: ToolSpec[], strict = false): ParsedToolCall | null {
|
|
550
548
|
const match = XML_FUNCTION_EQUALS_RE.exec(block);
|
|
551
549
|
XML_FUNCTION_EQUALS_RE.lastIndex = 0;
|
|
552
550
|
if (!match) return null;
|
|
551
|
+
if (strict && (match.index !== 0 || match[0].length !== block.length)) return null;
|
|
553
552
|
const name = match[1]!.trim();
|
|
554
553
|
const body = match[2]!;
|
|
555
554
|
const args: Record<string, unknown> = {};
|
|
555
|
+
let cursor = 0;
|
|
556
556
|
for (const param of body.matchAll(XML_PARAMETER_EQUALS_RE)) {
|
|
557
|
+
if (strict && body.slice(cursor, param.index).trim()) return null;
|
|
557
558
|
const key = param[1]!.trim();
|
|
558
559
|
if (!key) throw new Error(`tool '${name}' contains an empty parameter name`);
|
|
559
|
-
|
|
560
|
+
if (strict && Object.hasOwn(args, key)) return null;
|
|
561
|
+
// Qwen's format carries raw text, not XML entity-encoded text. Match
|
|
562
|
+
// mlx_lm.tool_parsers.qwen3_coder: remove one framing newline at each
|
|
563
|
+
// end, preserving indentation, literal entities and JSON string escapes.
|
|
564
|
+
const value = param[2]!.replace(/^\n|\n$/g, "");
|
|
565
|
+
args[key] = decodeToolValue(value, toolParameterSchema(tools, name, key));
|
|
566
|
+
cursor = param.index + param[0].length;
|
|
560
567
|
}
|
|
561
568
|
XML_PARAMETER_EQUALS_RE.lastIndex = 0;
|
|
569
|
+
if (strict && body.slice(cursor).trim()) return null;
|
|
562
570
|
return { name, arguments: args };
|
|
563
571
|
}
|
|
564
572
|
|
|
565
|
-
|
|
573
|
+
/** One complete envelope, with full consumption and no syntax repair. Used
|
|
574
|
+
* to establish structural fill boundaries, never to verify model predictions. */
|
|
575
|
+
export function parseStrictToolCall(text: string, tools: ToolSpec[]): ParsedToolCall | null {
|
|
576
|
+
const open = "<tool_call>", close = "</tool_call>";
|
|
577
|
+
if (!text.startsWith(open) || !text.endsWith(close)) return null;
|
|
578
|
+
const body = text.slice(open.length, -close.length).trim();
|
|
579
|
+
if (body.includes(open) || body.includes(close)) return null;
|
|
580
|
+
try {
|
|
581
|
+
let call: ParsedToolCall | null;
|
|
582
|
+
if (body.startsWith("{")) {
|
|
583
|
+
// JSON.parse must succeed before the existing payload interpretation.
|
|
584
|
+
JSON.parse(body);
|
|
585
|
+
call = parseJsonToolCall(body);
|
|
586
|
+
} else {
|
|
587
|
+
call = parseXmlEqualsToolCall(body, tools, true) ?? parseGlm52ToolCall(body, tools, true);
|
|
588
|
+
}
|
|
589
|
+
if (!call || call.repaired || !tools.some(tool => toolSpecName(tool) === call.name)) return null;
|
|
590
|
+
return call;
|
|
591
|
+
} catch { return null; }
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** A textual argument delimiter is complete only after its closing bracket. */
|
|
595
|
+
export function hasCompleteToolValueDelimiter(text: string): boolean {
|
|
596
|
+
return /^(?:<\/parameter>|<\/arg_value>)/.test(text.trimStart());
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function parseGlm52ToolCall(block: string, tools: ToolSpec[], strict = false): ParsedToolCall | null {
|
|
566
600
|
const firstArg = block.search(GLM52_ARG_KEY_RE);
|
|
567
601
|
const name = (firstArg < 0 ? block : block.slice(0, firstArg)).trim();
|
|
568
602
|
if (!name || !/^[^\s<>]+$/.test(name)) return null;
|
|
@@ -576,7 +610,8 @@ function parseGlm52ToolCall(block: string, tools: ToolSpec[]): ParsedToolCall |
|
|
|
576
610
|
if (body.slice(cursor, pair.index).trim()) return null;
|
|
577
611
|
const key = pair[1]!.trim();
|
|
578
612
|
if (!key) throw new Error(`tool '${name}' contains an empty argument name`);
|
|
579
|
-
|
|
613
|
+
if (strict && Object.hasOwn(args, key)) return null;
|
|
614
|
+
args[key] = decodeToolValue(decodeXml(pair[2]!), toolParameterSchema(tools, name, key));
|
|
580
615
|
cursor = pair.index + pair[0].length;
|
|
581
616
|
matched = true;
|
|
582
617
|
}
|
|
@@ -594,7 +629,7 @@ function parseXmlAttrToolCalls(text: string, tools: ToolSpec[]): ParsedToolCall[
|
|
|
594
629
|
for (const param of body.matchAll(XML_PARAM_ATTR_RE)) {
|
|
595
630
|
const key = param[1]!.trim();
|
|
596
631
|
if (!key) throw new Error(`tool '${name}' contains an empty parameter name`);
|
|
597
|
-
args[key] = decodeToolValue(param[2]
|
|
632
|
+
args[key] = decodeToolValue(decodeXml(param[2]!), toolParameterSchema(tools, name, key));
|
|
598
633
|
}
|
|
599
634
|
XML_PARAM_ATTR_RE.lastIndex = 0;
|
|
600
635
|
calls.push({ name, arguments: args });
|
|
@@ -616,9 +651,11 @@ export function parseGeneratedToolCalls(text: string, tools: ToolSpec[]): Parsed
|
|
|
616
651
|
const calls: ParsedToolCall[] = [];
|
|
617
652
|
for (const block of text.matchAll(TOOL_CALL_BLOCK_RE)) {
|
|
618
653
|
const body = block[1]!.trim();
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
654
|
+
// A valid Qwen parameter may itself contain JSON. Dispatch by the outer
|
|
655
|
+
// format before JSON repair can mistake that value for the tool envelope.
|
|
656
|
+
const parsed = /^<function=/i.test(body)
|
|
657
|
+
? parseXmlEqualsToolCall(body, tools)
|
|
658
|
+
: parseJsonToolCall(body) ?? parseGlm52ToolCall(body, tools);
|
|
622
659
|
if (!parsed) throw new Error("unsupported tool_call payload format");
|
|
623
660
|
calls.push(parsed);
|
|
624
661
|
}
|
package/src/train/flash-cce.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { MlxArray } from "../mlx/array";
|
|
|
37
37
|
import { Dtype } from "../mlx/ffi";
|
|
38
38
|
import * as ops from "../mlx/ops";
|
|
39
39
|
import { STEEL_QMM_HEADER } from "./steel-qmm-header";
|
|
40
|
+
import { runtimeValue } from "../runtime-config";
|
|
40
41
|
|
|
41
42
|
/** The quantized head pieces the kernel needs (packed weight + per-group affine
|
|
42
43
|
* scale/bias + bits/group_size). Mirrors HeadQuant in loss.ts. */
|
|
@@ -392,7 +393,7 @@ function fwdSgKernel(): MetalKernel {
|
|
|
392
393
|
inputNames: ["h", "wp", "scales", "biases", "targets", "shape", "capv"],
|
|
393
394
|
outputNames: ["blockMax_out", "blockSum_out", "blockTgt_out"],
|
|
394
395
|
source: FWD_SG_SOURCE,
|
|
395
|
-
header: `#include <metal_simdgroup_matrix>\n#define TG ${TG}\n#define BB ${SG_BB}\n#define BV ${SG_BV}\n#define BD ${SG_BD}\n#define BDX ${SG_BD + 4}\n#define BVX ${SG_BV + 4}\n#define CCE_SG_SKIPDQ ${
|
|
396
|
+
header: `#include <metal_simdgroup_matrix>\n#define TG ${TG}\n#define BB ${SG_BB}\n#define BV ${SG_BV}\n#define BD ${SG_BD}\n#define BDX ${SG_BD + 4}\n#define BVX ${SG_BV + 4}\n#define CCE_SG_SKIPDQ ${runtimeValue("MLX_BUN_CCE_SG_SKIPDQ") === "1" ? 1 : 0}\n`,
|
|
396
397
|
ensureRowContiguous: true,
|
|
397
398
|
});
|
|
398
399
|
}
|
|
@@ -404,7 +405,7 @@ function fwdSgKernel(): MetalKernel {
|
|
|
404
405
|
// compute-bound regime. Removing the staging redundancy (one thread per packed
|
|
405
406
|
// word; scale/bias once per group) cut it 1629→848 ms, beating the scalar
|
|
406
407
|
// kernel's 1097 ms. Fall back to scalar (MLX_BUN_CCE_SCALAR=1) when H % SG_BD != 0.
|
|
407
|
-
const USE_SIMDGROUP_FWD =
|
|
408
|
+
const USE_SIMDGROUP_FWD = runtimeValue("MLX_BUN_CCE_SCALAR") !== "1" && runtimeValue("MLX_BUN_CCE_LANE") !== "1";
|
|
408
409
|
|
|
409
410
|
// Backward: dh[t,d] = Σ_v coeff_v · dequant(W_v)[d], coeff_v = g_t·(onehot_v −
|
|
410
411
|
// softmax_v)·sech²_v, softmax_v = exp(softcap(logit_v) − lse_t). Two-phase per
|
|
@@ -549,7 +550,7 @@ function bwdKernel(daccN: number): MetalKernel {
|
|
|
549
550
|
inputNames: ["h", "wp", "scales", "biases", "targets", "lse", "gv", "shape", "capv"],
|
|
550
551
|
outputNames: ["dh"],
|
|
551
552
|
source: BWD_SOURCE,
|
|
552
|
-
header: `#define TG ${TG}\n#define BLOCK_B ${BLOCK_B_BWD}\n#define DACC_N ${daccN}\n#define CCE_BWD_SKIP_P2DQ ${
|
|
553
|
+
header: `#define TG ${TG}\n#define BLOCK_B ${BLOCK_B_BWD}\n#define DACC_N ${daccN}\n#define CCE_BWD_SKIP_P2DQ ${runtimeValue("MLX_BUN_CCE_BWD_SKIP_P2DQ") === "1" ? 1 : 0}\n#define CCE_BWD_SKIP_P2 ${runtimeValue("MLX_BUN_CCE_BWD_SKIP_P2") === "1" ? 1 : 0}\n`,
|
|
553
554
|
ensureRowContiguous: true,
|
|
554
555
|
atomicOutputs: true, // dh is summed across the NBLK vocab-block programs
|
|
555
556
|
});
|
|
@@ -727,7 +728,7 @@ const BWD_SG_SOURCE = String.raw`
|
|
|
727
728
|
// (flipped 2026-07-02 — the kernel-perf-review backlog #1 flip). The old
|
|
728
729
|
// 0.66%→2.7% dh-accuracy scare was on RANDOM targets (flat softmax, the filter's
|
|
729
730
|
// worst case — see PLAN.md "filter-on-real-data"); the real-data gate
|
|
730
|
-
// (
|
|
731
|
+
// (flash-cce-filter-realdata.ts (deleted 2026-08-23; git history)) measured, on REAL chunk-ORPO
|
|
731
732
|
// data (M1 Max, 2026-07-02): CPM5 eps 1e-5 = 0.343% dh for 1.41×; e4b = 0.158% dh
|
|
732
733
|
// for 1.70× — and the teacher-forced full-logits fidelity shows the filter's added
|
|
733
734
|
// error is far below the flash-vs-full fp-reassociation scatter that already
|
|
@@ -735,7 +736,7 @@ const BWD_SG_SOURCE = String.raw`
|
|
|
735
736
|
// (The ~3.5× figure floating around older notes was the retired SG kernel, not
|
|
736
737
|
// steel.) MLX_BUN_CCE_BWD_FILTER_EPS=0 restores exact gradients — eps "0"
|
|
737
738
|
// compiles the filter OUT (#if CCE_BWD_FILTER 0), the identical pre-flip kernel.
|
|
738
|
-
const BWD_FILTER_EPS =
|
|
739
|
+
const BWD_FILTER_EPS = runtimeValue("MLX_BUN_CCE_BWD_FILTER_EPS") ?? "1e-5";
|
|
739
740
|
// blockMax vocab-block early-exit eps (skip whole cold (token-block,vocab-block) programs
|
|
740
741
|
// — attacks BOTH phases). DEFAULT ON at 1e-5 (flipped 2026-07-02). It is ~LOSSLESS when
|
|
741
742
|
// it fires (only skips blocks whose max prob < eps AND which lack the target → ~0
|
|
@@ -744,7 +745,7 @@ const BWD_FILTER_EPS = process.env.MLX_BUN_CCE_BWD_FILTER_EPS ?? "1e-5";
|
|
|
744
745
|
// at ≤0.004% dh; COMBINED with the coeff filter at 1e-5/1e-5 the backward is 1.71×
|
|
745
746
|
// (CPM5) / 3.16× (e4b) vs exact. Fires only when the caller supplies blockMax (the
|
|
746
747
|
// production head does); MLX_BUN_CCE_BWD_BLOCK_EPS=0 disables (compiled out).
|
|
747
|
-
const BWD_BLOCK_EPS =
|
|
748
|
+
const BWD_BLOCK_EPS = runtimeValue("MLX_BUN_CCE_BWD_BLOCK_EPS") ?? "1e-5";
|
|
748
749
|
const _bwdSgKernels = new Map<string, MetalKernel>();
|
|
749
750
|
function bwdSgKernel(dimTiles: number, filterEps: string, blockEps: string): MetalKernel {
|
|
750
751
|
const filterOn = Number.parseFloat(filterEps) > 0 ? 1 : 0;
|
|
@@ -780,7 +781,7 @@ function bwdSgKernel(dimTiles: number, filterEps: string, blockEps: string): Met
|
|
|
780
781
|
// accumulated into D's frags LANE-LOCALLY (frag += frag — no get_coord math, the
|
|
781
782
|
// per-lane element mapping is identical between temp and D). dh atomic-added
|
|
782
783
|
// across the NBLK vocab-block programs. Validated dh ~1e-4 rel vs autograd.
|
|
783
|
-
// (Design + parity:
|
|
784
|
+
// (Design + parity: steel-bwd-htile-test.ts (deleted 2026-08-23; git history).)
|
|
784
785
|
// PHASE-2 DEQUANT: the Wd[32,HTw] staging uses MLX's vectorized+fused
|
|
785
786
|
// QuantizedBlockLoader (the SAME loader the forward uses), NOT a manual scalar
|
|
786
787
|
// dequant. The wrinkle: phase-2's GEMM contracts over vocab (K=32) and outputs H
|
|
@@ -1061,9 +1062,9 @@ export function flashCceForward(
|
|
|
1061
1062
|
// tiling is clean (H,V,blockV all % 32). MLX_BUN_CCE_NOSTEEL=1 falls back to the
|
|
1062
1063
|
// old kernels (simdgroup / scalar / lane).
|
|
1063
1064
|
const blockV0 = Math.ceil(V / NBLK);
|
|
1064
|
-
const useSteel =
|
|
1065
|
+
const useSteel = runtimeValue("MLX_BUN_CCE_NOSTEEL") !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0;
|
|
1065
1066
|
const useSimd = !useSteel && USE_SIMDGROUP_FWD && H % SG_BD === 0;
|
|
1066
|
-
if (!useSteel &&
|
|
1067
|
+
if (!useSteel && runtimeValue("MLX_BUN_CCE_NOSTEEL") !== "1") {
|
|
1067
1068
|
// NOTE blockV0 = ceil(V/NBLK) can violate %32 even when V%32==0 (e.g. V=50304)
|
|
1068
1069
|
const fails = [
|
|
1069
1070
|
H % 32 !== 0 ? `H%32!=0 (H=${H})` : null,
|
|
@@ -1072,7 +1073,7 @@ export function flashCceForward(
|
|
|
1072
1073
|
].filter(Boolean).join(", ");
|
|
1073
1074
|
warnSteelFallback("forward", fails, useSimd ? "simdgroup kernel" : "scalar/lane kernel");
|
|
1074
1075
|
}
|
|
1075
|
-
const useLane = !useSteel && !useSimd &&
|
|
1076
|
+
const useLane = !useSteel && !useSimd && runtimeValue("MLX_BUN_CCE_LANE") === "1" && WPR % 32 === 0;
|
|
1076
1077
|
const kernel = useSteel ? fwdSteelKernel(GS, head.bits) : useSimd ? fwdSgKernel() : useLane ? fwdLaneKernel() : fwdKernel();
|
|
1077
1078
|
const tokTile = useSteel ? STEEL_BM : useSimd ? SG_BB : BLOCK_B;
|
|
1078
1079
|
// steel + simdgroup read h as [M, H] (M on axis 0); lane + scalar read h TRANSPOSED
|
|
@@ -1178,11 +1179,11 @@ export function flashCceBackward(
|
|
|
1178
1179
|
const blockV0 = Math.ceil(V / NBLK);
|
|
1179
1180
|
// HTw == group_size for the phase-2 QuantizedBlockLoader (one group per H-tile); needs
|
|
1180
1181
|
// H % GS == 0 (always, GR*GS=H) and GS % 32 == 0 (TNv = GS/32 frags per H-tile integral).
|
|
1181
|
-
const useSteel =
|
|
1182
|
+
const useSteel = runtimeValue("MLX_BUN_CCE_BWD_NOSTEEL") !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0 && H % GS === 0 && GS % 32 === 0;
|
|
1182
1183
|
// simdgroup_matrix phase-2 GEMM is the next tier (5512→3913 ms, 1.41×) when the H
|
|
1183
1184
|
// tiling is clean; MLX_BUN_CCE_BWD_SCALAR=1 forces the float4 scalar kernel.
|
|
1184
|
-
const useSg = !useSteel &&
|
|
1185
|
-
if (!useSteel &&
|
|
1185
|
+
const useSg = !useSteel && runtimeValue("MLX_BUN_CCE_BWD_SCALAR") !== "1" && H % (SGN * 8) === 0 && H % SG_BD === 0;
|
|
1186
|
+
if (!useSteel && runtimeValue("MLX_BUN_CCE_BWD_NOSTEEL") !== "1") {
|
|
1186
1187
|
// NOTE blockV0 = ceil(V/NBLK) can violate %32 even when V%32==0 (e.g. V=50304)
|
|
1187
1188
|
const fails = [
|
|
1188
1189
|
H % 32 !== 0 ? `H%32!=0 (H=${H})` : null,
|