@mlx-bun/inference 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +1475 -2
- package/THIRD_PARTY_NOTICES.md +3404 -0
- package/dist/native/libmlx_bun_ane.dylib +0 -0
- package/dist/native/libmlx_bun_expert_io.dylib +0 -0
- package/dist/native/mlx-bun-frame-extract +0 -0
- package/examples/fit-model.ts +25 -0
- package/examples/qwen3-forward.ts +42 -0
- package/examples/qwen3-generate.ts +28 -0
- package/examples/trellis-expand.ts +23 -0
- package/package.json +129 -4
- package/src/adapters/available.ts +110 -0
- package/src/adapters/index.ts +3 -0
- package/src/adapters/manager.ts +316 -0
- package/src/artifacts/auxiliary-files.ts +45 -0
- package/src/artifacts/config-dialects.ts +94 -0
- package/src/artifacts/config.ts +341 -0
- package/src/artifacts/draft-vocabulary.ts +30 -0
- package/src/artifacts/expert-offload-build.ts +75 -0
- package/src/artifacts/expert-offload.ts +89 -0
- package/src/artifacts/experts/index.ts +3 -0
- package/src/artifacts/experts/io.ts +386 -0
- package/src/artifacts/experts/residency.ts +1134 -0
- package/src/artifacts/experts/usage.ts +415 -0
- package/src/artifacts/fingerprint.ts +44 -0
- package/src/artifacts/glm52/container.ts +305 -0
- package/src/artifacts/glm52/expert-layout.ts +249 -0
- package/src/artifacts/glm52/index.ts +7 -0
- package/src/artifacts/glm52/kernel-layout.ts +124 -0
- package/src/artifacts/glm52/layout.ts +152 -0
- package/src/artifacts/glm52/memory.ts +500 -0
- package/src/artifacts/glm52/resident-weights.ts +135 -0
- package/src/artifacts/glm52/weights.ts +503 -0
- package/src/artifacts/glm52-config.ts +262 -0
- package/src/artifacts/identity.ts +94 -0
- package/src/artifacts/index.ts +15 -0
- package/src/artifacts/mmap.ts +90 -0
- package/src/artifacts/safetensors-writer.ts +249 -0
- package/src/artifacts/safetensors.ts +164 -0
- package/src/artifacts/turboquant-fold.ts +42 -0
- package/src/artifacts/weight-audit.ts +43 -0
- package/src/artifacts/weights.ts +255 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/mlx/attention.ts +8 -0
- package/src/contracts/mlx/cache.ts +510 -0
- package/src/contracts/mlx/checkpoint.ts +10 -0
- package/src/contracts/mlx/draft-checkpoint.ts +14 -0
- package/src/contracts/mlx/draft-projection.ts +27 -0
- package/src/contracts/mlx/draft-target.ts +84 -0
- package/src/contracts/mlx/drafter.ts +176 -0
- package/src/contracts/mlx/forward-work.ts +23 -0
- package/src/contracts/mlx/glm52-weights.ts +25 -0
- package/src/contracts/mlx/graph.ts +309 -0
- package/src/contracts/mlx/head.ts +6 -0
- package/src/contracts/mlx/index.ts +15 -0
- package/src/contracts/mlx/kv-codec.ts +9 -0
- package/src/contracts/mlx/media.ts +113 -0
- package/src/contracts/mlx/positions.ts +16 -0
- package/src/contracts/mlx/row-table.ts +2 -0
- package/src/contracts/mlx/trainable.ts +103 -0
- package/src/contracts/mlx/whisper.ts +96 -0
- package/src/contracts/portable/composition.ts +106 -0
- package/src/contracts/portable/denoising.ts +17 -0
- package/src/contracts/portable/execution.ts +79 -0
- package/src/contracts/portable/generation.ts +103 -0
- package/src/contracts/portable/graph.ts +80 -0
- package/src/contracts/portable/index.ts +15 -0
- package/src/contracts/portable/memory-plan.ts +26 -0
- package/src/contracts/portable/object-cache.ts +8 -0
- package/src/contracts/portable/prefill.ts +63 -0
- package/src/contracts/portable/prefix-cache.ts +33 -0
- package/src/contracts/portable/prompt.ts +2 -0
- package/src/contracts/portable/resources.ts +25 -0
- package/src/contracts/portable/rollback.ts +7 -0
- package/src/contracts/portable/sampling.ts +44 -0
- package/src/contracts/portable/scheduling.ts +54 -0
- package/src/contracts/portable/token-delivery.ts +4 -0
- package/src/contracts/portable/tsconfig.json +14 -0
- package/src/embeddings/index.ts +1 -0
- package/src/embeddings/text.ts +85 -0
- package/src/execution/admission.ts +59 -0
- package/src/execution/batch-group.ts +1240 -0
- package/src/execution/batch-types.ts +225 -0
- package/src/execution/cancellation.ts +38 -0
- package/src/execution/continuation-request.ts +45 -0
- package/src/execution/continuation-types.ts +10 -0
- package/src/execution/coordinator.ts +70 -0
- package/src/execution/denoising-group.ts +180 -0
- package/src/execution/engine.ts +56 -0
- package/src/execution/fill-group.ts +505 -0
- package/src/execution/fit.ts +184 -0
- package/src/execution/gateway-binding.ts +178 -0
- package/src/execution/grammar-group.ts +224 -0
- package/src/execution/index.ts +18 -0
- package/src/execution/methods.ts +75 -0
- package/src/execution/mixed-iteration.ts +96 -0
- package/src/execution/plan.ts +65 -0
- package/src/execution/prefill-cohort.ts +100 -0
- package/src/execution/prefill-rows.ts +263 -0
- package/src/execution/request-policy.ts +19 -0
- package/src/execution/row-sampling.ts +49 -0
- package/src/execution/scheduler.ts +92 -0
- package/src/execution/session.ts +247 -0
- package/src/execution/speculative-group.ts +797 -0
- package/src/execution/tasks.ts +44 -0
- package/src/generation/autoregressive.ts +840 -0
- package/src/generation/bindings/autoregressive.ts +104 -0
- package/src/generation/bindings/denoising.ts +60 -0
- package/src/generation/bindings/prefill-policy.ts +36 -0
- package/src/generation/checkpoint-identity.ts +60 -0
- package/src/generation/denoising.ts +85 -0
- package/src/generation/diffusion.ts +583 -0
- package/src/generation/fill/echo-index.ts +188 -0
- package/src/generation/fill/index.ts +3 -0
- package/src/generation/fill/proposal.ts +69 -0
- package/src/generation/fill/schema-rows.ts +506 -0
- package/src/generation/fill/session.ts +464 -0
- package/src/generation/fill/tool-boundary.ts +103 -0
- package/src/generation/fill-append.ts +58 -0
- package/src/generation/generate.ts +44 -0
- package/src/generation/grammar-step.ts +39 -0
- package/src/generation/index.ts +14 -0
- package/src/generation/phase-dispatch.ts +115 -0
- package/src/generation/prefill.ts +33 -0
- package/src/generation/result.ts +49 -0
- package/src/generation/scopes.ts +157 -0
- package/src/generation/speculative/acceptance.ts +41 -0
- package/src/generation/speculative/adaptive-draft-gate.ts +166 -0
- package/src/generation/speculative/bindings/assistant-rows.ts +102 -0
- package/src/generation/speculative/bindings/autoregressive-rows.ts +124 -0
- package/src/generation/speculative/bindings/binding.ts +106 -0
- package/src/generation/speculative/bindings/deepspec-rows.ts +33 -0
- package/src/generation/speculative/bindings/draft-target.ts +10 -0
- package/src/generation/speculative/bindings/glm52-mtp-rows.ts +128 -0
- package/src/generation/speculative/bindings/projected-context-rows.ts +78 -0
- package/src/generation/speculative/bindings/projected-draft-rows.ts +95 -0
- package/src/generation/speculative/bindings/qwen-mtp-rows.ts +336 -0
- package/src/generation/speculative/bindings/two-model-rows.ts +63 -0
- package/src/generation/speculative/cache-identity.ts +16 -0
- package/src/generation/speculative/draft-kind.ts +149 -0
- package/src/generation/speculative/draft-policy.ts +41 -0
- package/src/generation/speculative/draft-registry.ts +118 -0
- package/src/generation/speculative/draft-vocab.ts +110 -0
- package/src/generation/speculative/index.ts +17 -0
- package/src/generation/speculative/output.ts +49 -0
- package/src/generation/speculative/round.ts +307 -0
- package/src/generation/speculative/source.ts +144 -0
- package/src/generation/speculative/sources/assistant-source.ts +64 -0
- package/src/generation/speculative/sources/deepspec-source.ts +70 -0
- package/src/generation/speculative/sources/dflash-source.ts +84 -0
- package/src/generation/speculative/sources/dflash2-source.ts +86 -0
- package/src/generation/speculative/sources/glm52-mtp-source.ts +66 -0
- package/src/generation/speculative/sources/ngram-source.ts +165 -0
- package/src/generation/speculative/sources/qwen-mtp-source.ts +154 -0
- package/src/generation/speculative/sources/two-model.ts +75 -0
- package/src/generation/types.ts +194 -0
- package/src/index.ts +20 -0
- package/src/input/audio/audiotoolbox.ts +112 -0
- package/src/input/audio/decode.ts +133 -0
- package/src/input/audio/features.ts +202 -0
- package/src/input/audio/index.ts +6 -0
- package/src/input/audio/transcode.ts +67 -0
- package/src/input/audio/whisper-mel.ts +185 -0
- package/src/input/audio/whisper-tokenizer.ts +238 -0
- package/src/input/chat-template.ts +389 -0
- package/src/input/index.ts +6 -0
- package/src/input/media-fetch.ts +439 -0
- package/src/input/packed-mixed.ts +43 -0
- package/src/input/special-tokens.ts +75 -0
- package/src/input/token-groups.ts +49 -0
- package/src/input/tokenizer.ts +336 -0
- package/src/input/tool-call.ts +689 -0
- package/src/input/vision/index.ts +3 -0
- package/src/input/vision/preprocess.ts +247 -0
- package/src/input/vision/prompt.ts +398 -0
- package/src/input/vision/qwen3vl-preprocess.ts +409 -0
- package/src/input/vision/qwen3vl-prompt.ts +237 -0
- package/src/input/vision/video-frames.ts +100 -0
- package/src/kernels/ane/fill.ts +101 -0
- package/src/kernels/ane/linear.ts +416 -0
- package/src/kernels/attention/flash.ts +694 -0
- package/src/kernels/attention/kvq4-decode.ts +155 -0
- package/src/kernels/attention/masks.ts +40 -0
- package/src/kernels/attention/multi-query.ts +193 -0
- package/src/kernels/attention/paged.ts +130 -0
- package/src/kernels/delta/gated.ts +306 -0
- package/src/kernels/delta/index.ts +3 -0
- package/src/kernels/delta/silu-rms-heads.ts +69 -0
- package/src/kernels/delta/state.ts +74 -0
- package/src/kernels/gated-rms-norm.ts +64 -0
- package/src/kernels/glm52/layout.ts +37 -0
- package/src/kernels/glm52/streamed-experts.ts +360 -0
- package/src/kernels/logits.ts +38 -0
- package/src/kernels/quantization/affine-row.ts +154 -0
- package/src/kernels/quantization/affine-rows.ts +36 -0
- package/src/kernels/quantization/affine-split-mma.ts +144 -0
- package/src/kernels/quantization/affine3-mma.ts +117 -0
- package/src/kernels/quantization/affine3-rows.ts +103 -0
- package/src/kernels/quantization/affine4-mma.ts +113 -0
- package/src/kernels/quantization/affine4-rows.ts +102 -0
- package/src/kernels/residual-rms-norm.ts +65 -0
- package/src/kernels/sampling/index.ts +2 -0
- package/src/kernels/sampling/normalized-argmax.ts +116 -0
- package/src/kernels/sampling/token-bitmask.ts +52 -0
- package/src/kernels/softcap.ts +35 -0
- package/src/kernels/trellis/codebook.ts +123 -0
- package/src/kernels/trellis/down-codebook.ts +184 -0
- package/src/kernels/trellis/down-factored.ts +180 -0
- package/src/kernels/trellis/down-k3-interleaved-factored.ts +146 -0
- package/src/kernels/trellis/down-mma-wide.ts +195 -0
- package/src/kernels/trellis/down-mma.ts +145 -0
- package/src/kernels/trellis/expand.ts +42 -0
- package/src/kernels/trellis/gate-up-codebook-mma.ts +141 -0
- package/src/kernels/trellis/gate-up-codebook.ts +137 -0
- package/src/kernels/trellis/gate-up-factored.ts +197 -0
- package/src/kernels/trellis/gate-up-mma-wide.ts +147 -0
- package/src/kernels/trellis/gate-up-mma.ts +148 -0
- package/src/kernels/trellis/gate-up.ts +248 -0
- package/src/kernels/trellis/geometry.ts +22 -0
- package/src/kernels/trellis/index.ts +103 -0
- package/src/kernels/trellis/launch.ts +12 -0
- package/src/kernels/trellis/mixed-gate-up-factored.ts +119 -0
- package/src/kernels/trellis/mixed-gate-up.ts +146 -0
- package/src/kernels/trellis/qualified.ts +34 -0
- package/src/kernels/trellis/reduce.ts +150 -0
- package/src/kernels/trellis/scatter.ts +407 -0
- package/src/kernels/trellis/splitk-prefill.ts +152 -0
- package/src/kernels/trellis/tiled-prefill.ts +131 -0
- package/src/kernels/trellis/vector-expand.ts +58 -0
- package/src/kernels/trellis/wide-prefill.ts +89 -0
- package/src/kernels/turboquant/decode.ts +82 -0
- package/src/kernels/turboquant/index.ts +5 -0
- package/src/kernels/turboquant/inverse.ts +122 -0
- package/src/kernels/turboquant/ops.ts +860 -0
- package/src/kernels/turboquant/signs.ts +130 -0
- package/src/kernels/turboquant/tables.ts +124 -0
- package/src/layers/affine-verify-linear.ts +75 -0
- package/src/layers/ane-prefill-split.ts +283 -0
- package/src/layers/compiled-normalization.ts +41 -0
- package/src/layers/delta-heads.ts +23 -0
- package/src/layers/dense-embedding.ts +29 -0
- package/src/layers/dense-linear.ts +32 -0
- package/src/layers/dequantized-linear.ts +72 -0
- package/src/layers/folded-attention.ts +42 -0
- package/src/layers/geglu.ts +130 -0
- package/src/layers/helpers.ts +7 -0
- package/src/layers/index.ts +36 -0
- package/src/layers/kv4-decode-attention.ts +15 -0
- package/src/layers/layer-norm.ts +24 -0
- package/src/layers/loading.ts +92 -0
- package/src/layers/lora.ts +150 -0
- package/src/layers/normalization.ts +9 -0
- package/src/layers/prefix-layout.ts +33 -0
- package/src/layers/quantized-append-attention.ts +32 -0
- package/src/layers/quantized-attention.ts +353 -0
- package/src/layers/quantized-embedding.ts +39 -0
- package/src/layers/quantized-linear.ts +137 -0
- package/src/layers/quantized-switch-linear.ts +37 -0
- package/src/layers/qwen-append.ts +12 -0
- package/src/layers/qwen-mrope.ts +238 -0
- package/src/layers/rope-2d.ts +110 -0
- package/src/layers/rope.ts +389 -0
- package/src/layers/swiglu.ts +23 -0
- package/src/layers/trellis-codebook-down.ts +52 -0
- package/src/layers/trellis-codebook-gate-up.ts +42 -0
- package/src/layers/trellis-down.ts +117 -0
- package/src/layers/trellis-gate-up.ts +71 -0
- package/src/layers/trellis-linear.ts +211 -0
- package/src/layers/trellis-prompt.ts +102 -0
- package/src/layers/trellis-reduce-gate-up.ts +73 -0
- package/src/layers/trellis-scatter-down.ts +98 -0
- package/src/models/ane-install.ts +74 -0
- package/src/models/audio/conformer.ts +656 -0
- package/src/models/cache-composition.ts +44 -0
- package/src/models/capabilities.ts +89 -0
- package/src/models/chat-template.ts +10 -0
- package/src/models/composition.ts +107 -0
- package/src/models/diffusion-gemma/experts.ts +59 -0
- package/src/models/diffusion-gemma/family.ts +21 -0
- package/src/models/diffusion-gemma/model.ts +900 -0
- package/src/models/diffusion-gemma/vision.ts +549 -0
- package/src/models/drafter-loaders.ts +49 -0
- package/src/models/drafters.ts +39 -0
- package/src/models/factory.ts +351 -0
- package/src/models/families.ts +70 -0
- package/src/models/family.ts +125 -0
- package/src/models/gemma4/assistant-target.ts +46 -0
- package/src/models/gemma4/assistant.ts +327 -0
- package/src/models/gemma4/blocks.ts +755 -0
- package/src/models/gemma4/compiled-decode.ts +452 -0
- package/src/models/gemma4/family.ts +39 -0
- package/src/models/gemma4/gemma4-12b.ts +677 -0
- package/src/models/gemma4/gemma4-26b.ts +544 -0
- package/src/models/gemma4/gemma4-e4b.ts +599 -0
- package/src/models/gemma4/graphs.ts +147 -0
- package/src/models/gemma4/media.ts +88 -0
- package/src/models/gemma4/model.ts +860 -0
- package/src/models/gemma4/serving-blocks.ts +72 -0
- package/src/models/gemma4/trainable.ts +186 -0
- package/src/models/gemma4/written-blocks.ts +232 -0
- package/src/models/gemma4/written-forward.ts +159 -0
- package/src/models/gemma4/written-input.ts +29 -0
- package/src/models/glm52/atlas.ts +507 -0
- package/src/models/glm52/coupling.ts +388 -0
- package/src/models/glm52/dsa.ts +601 -0
- package/src/models/glm52/family.ts +20 -0
- package/src/models/glm52/mla.ts +812 -0
- package/src/models/glm52/model.ts +1127 -0
- package/src/models/glm52/moe.ts +223 -0
- package/src/models/glm52/mtp.ts +60 -0
- package/src/models/glm52/pilot.ts +537 -0
- package/src/models/glm52/reference.ts +445 -0
- package/src/models/glm52/residency.ts +557 -0
- package/src/models/glm52/streamed-experts.ts +447 -0
- package/src/models/graph.ts +59 -0
- package/src/models/implementation.ts +230 -0
- package/src/models/index.ts +15 -0
- package/src/models/media-input.ts +15 -0
- package/src/models/memory-plan.ts +39 -0
- package/src/models/minicpm5/family.ts +29 -0
- package/src/models/minicpm5/model.ts +394 -0
- package/src/models/minicpm5/trainable.ts +90 -0
- package/src/models/profile.ts +337 -0
- package/src/models/qwen/blocks.ts +658 -0
- package/src/models/qwen/checkpoint.ts +190 -0
- package/src/models/qwen/families.ts +57 -0
- package/src/models/qwen/media-input.ts +161 -0
- package/src/models/qwen/mtp.ts +101 -0
- package/src/models/qwen/qwen3-moe.ts +355 -0
- package/src/models/qwen/qwen3.ts +323 -0
- package/src/models/qwen/qwen38-27b-trellis-m1max.ts +1532 -0
- package/src/models/qwen/qwen38-27b-trellis-m4pro.ts +1092 -0
- package/src/models/qwen/qwen3_5.ts +433 -0
- package/src/models/qwen/written-layers.ts +285 -0
- package/src/models/row-table.ts +1 -0
- package/src/models/runtime.ts +73 -0
- package/src/models/speculative/deepspec-artifact.ts +10 -0
- package/src/models/speculative/deepspec.ts +744 -0
- package/src/models/speculative/dflash.ts +553 -0
- package/src/models/speculative/dflash2-artifact.ts +9 -0
- package/src/models/speculative/dflash2-operations.ts +1 -0
- package/src/models/speculative/dflash2.ts +576 -0
- package/src/models/speculative/loader.ts +29 -0
- package/src/models/support.ts +79 -0
- package/src/models/universal/archs.ts +365 -0
- package/src/models/universal/blocks.ts +479 -0
- package/src/models/universal/dense.ts +255 -0
- package/src/models/universal/family.ts +17 -0
- package/src/models/vision/qwen3vl.ts +566 -0
- package/src/models/vision/siglip.ts +520 -0
- package/src/models/vision/unified.ts +176 -0
- package/src/models/whisper/family.ts +19 -0
- package/src/models/whisper/fast.ts +734 -0
- package/src/models/whisper/model.ts +430 -0
- package/src/runtime/compiled-trace.ts +16 -0
- package/src/runtime/config.ts +77 -0
- package/src/runtime/expert-trace.ts +71 -0
- package/src/runtime/graph-forward.ts +65 -0
- package/src/runtime/integers.ts +6 -0
- package/src/runtime/native.ts +16 -0
- package/src/runtime/resources.ts +53 -0
- package/src/runtime/row-operations.ts +42 -0
- package/src/runtime/trace.ts +161 -0
- package/src/sampling/curve.ts +122 -0
- package/src/sampling/draft.ts +55 -0
- package/src/sampling/extras.ts +45 -0
- package/src/sampling/filters.ts +134 -0
- package/src/sampling/grammar.ts +630 -0
- package/src/sampling/hlg.ts +290 -0
- package/src/sampling/index.ts +9 -0
- package/src/sampling/processors.ts +142 -0
- package/src/sampling/sampler.ts +120 -0
- package/src/sampling/step.ts +343 -0
- package/src/sampling/types.ts +161 -0
- package/src/sampling/window-filters.ts +46 -0
- package/src/scoring/decisions.ts +96 -0
- package/src/scoring/full-sequence.ts +103 -0
- package/src/scoring/index.ts +9 -0
- package/src/scoring/kl.ts +29 -0
- package/src/scoring/perplexity.ts +165 -0
- package/src/scoring/response-logps.ts +65 -0
- package/src/state/affine-attention.ts +180 -0
- package/src/state/attachment.ts +68 -0
- package/src/state/attention-read.ts +215 -0
- package/src/state/batched-kv.ts +294 -0
- package/src/state/batched-mask.ts +445 -0
- package/src/state/batched-quant.ts +389 -0
- package/src/state/batched-quantized-kv.ts +209 -0
- package/src/state/batched-rotating-quant.ts +457 -0
- package/src/state/batched-rotating-state.ts +149 -0
- package/src/state/batched-rotating.ts +430 -0
- package/src/state/batched-row-storage.ts +179 -0
- package/src/state/batched-ssm.ts +54 -0
- package/src/state/batched-turboquant-kv.ts +144 -0
- package/src/state/bf16-first-kv.ts +663 -0
- package/src/state/cache-service.ts +592 -0
- package/src/state/capabilities.ts +65 -0
- package/src/state/checkpoint.ts +46 -0
- package/src/state/continuation-persistence.ts +126 -0
- package/src/state/decode-step-inputs.ts +31 -0
- package/src/state/decoded-kv-donor.ts +12 -0
- package/src/state/delayed-quantized-kv.ts +141 -0
- package/src/state/delayed-rotating-quantized-kv.ts +178 -0
- package/src/state/delayed-turboquant-kv.ts +123 -0
- package/src/state/durability.ts +214 -0
- package/src/state/encoder-cache.ts +26 -0
- package/src/state/full-kv-row-append.ts +33 -0
- package/src/state/full-kv-row-donor.ts +54 -0
- package/src/state/full-prefill-padding.ts +56 -0
- package/src/state/full-prefill-row.ts +112 -0
- package/src/state/full-transitioning-kv-rows.ts +70 -0
- package/src/state/glm52-cache.ts +783 -0
- package/src/state/grouped-heads-kv4-cache.ts +26 -0
- package/src/state/index.ts +38 -0
- package/src/state/kv-attention-view.ts +77 -0
- package/src/state/kv-budget.ts +14 -0
- package/src/state/kv-scheme.ts +315 -0
- package/src/state/kv-storage.ts +57 -0
- package/src/state/kv-tensor-rows.ts +212 -0
- package/src/state/kv.ts +324 -0
- package/src/state/kv4-head256-cache.ts +46 -0
- package/src/state/latent-legos.ts +895 -0
- package/src/state/layout.ts +277 -0
- package/src/state/leases.ts +26 -0
- package/src/state/paged/cache.ts +689 -0
- package/src/state/paged/codec.ts +36 -0
- package/src/state/paged/rows.ts +223 -0
- package/src/state/persistence-types.ts +173 -0
- package/src/state/persistence-worker.ts +83 -0
- package/src/state/persistence.ts +1036 -0
- package/src/state/persistence.worker.d.ts +3 -0
- package/src/state/persistence.worker.js +264 -0
- package/src/state/prefix-cache.ts +685 -0
- package/src/state/quantized-kv.ts +369 -0
- package/src/state/quantized-tensor.ts +26 -0
- package/src/state/request-policy.ts +48 -0
- package/src/state/retention-policy.ts +37 -0
- package/src/state/rollback.ts +131 -0
- package/src/state/rotating-kv-donor.ts +35 -0
- package/src/state/rotating-kv-layout.ts +341 -0
- package/src/state/rotating-kv.ts +529 -0
- package/src/state/rotating-quantized-kv.ts +616 -0
- package/src/state/rotating-row-transaction.ts +35 -0
- package/src/state/rows.ts +104 -0
- package/src/state/softcap-attention.ts +118 -0
- package/src/state/speculative/glm52-mtp-state.ts +38 -0
- package/src/state/speculative/qwen-mtp-state.ts +34 -0
- package/src/state/speculative-rotating-kv.ts +113 -0
- package/src/state/ssd-cache.ts +434 -0
- package/src/state/ssm-prefill-padding.ts +39 -0
- package/src/state/ssm.ts +422 -0
- package/src/state/target-layout.ts +7 -0
- package/src/state/tiered-prefix-cache.ts +45 -0
- package/src/state/training-cache.ts +300 -0
- package/src/state/transitioning-kv-rows.ts +267 -0
- package/src/state/turboquant-attention.ts +97 -0
- package/src/state/turboquant-codec.ts +90 -0
- package/src/state/turboquant-kv.ts +442 -0
- package/src/state/views.ts +26 -0
- package/src/transcription/index.ts +3 -0
- package/src/transcription/silero-vad.ts +379 -0
- package/src/transcription/whisper/decode.ts +773 -0
- package/src/transcription/whisper/format.ts +74 -0
- package/src/transcription/whisper/index.ts +4 -0
- package/src/transcription/whisper/timing.ts +274 -0
- package/src/transcription/whisper/transcribe.ts +394 -0
- package/src/transcription/whisper/types.ts +22 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import type { CheckpointAttachment } from "../contracts/mlx/checkpoint";
|
|
2
|
+
// SSD cold tier for the prompt/KV cache (`02d723a:docs/design/kv-cache.md` §5).
|
|
3
|
+
//
|
|
4
|
+
// Files ARE the database: no sidecar index. Layout
|
|
5
|
+
// <dir>/<configFingerprint>/<nsHash>/<uuid>.mlxkv
|
|
6
|
+
// where the caller's configFingerprint includes the model identity, so two
|
|
7
|
+
// models (or two kv-quant schemes, or two downloaded snapshot revisions)
|
|
8
|
+
// share a directory without ever colliding, and a foreign fingerprint dir is
|
|
9
|
+
// simply ignored (never deleted — it may belong to the user's other model).
|
|
10
|
+
// Startup recovery = a header-only scan of OUR fingerprint dir: header hash
|
|
11
|
+
// verified (cheap), corrupt files unlinked, files whose header names another
|
|
12
|
+
// identity SKIPPED (never deleted), `.tmp` write orphans reaped. LRU state = file mtime (bumped on hit); the byte
|
|
13
|
+
// cap is enforced at write time by oldest-mtime eviction. A cache is always
|
|
14
|
+
// droppable: every failure path degrades to "no hit" or "not stored" — the
|
|
15
|
+
// tier must never take serving down (oMLX paged_ssd_cache lesson, Apache-2.0
|
|
16
|
+
// idea port; ours spills whole prefix entries, not content-hashed blocks —
|
|
17
|
+
// see the design doc's D1 for why).
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
existsSync, mkdirSync, readdirSync, rmSync, statSync, utimesSync,
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import { kvWriter, type KvWriteRequest } from "./persistence-worker";
|
|
23
|
+
import { join, dirname } from "node:path";
|
|
24
|
+
import { randomUUID } from "node:crypto";
|
|
25
|
+
import { commonPrefixLength } from "./prefix-cache";
|
|
26
|
+
import { saveKvCache, saveKvCacheAsync, loadKvCache, loadKvCacheAsync, readKvHeader, cacheHeadersTrimmable, cacheHeadersMinimumReusableOffset, legacyCacheCodecs } from "./persistence";
|
|
27
|
+
import { type CacheCodecProvider, type KvSaveMeta, type KvLoadExpect, type LoadedKvCache } from "./persistence-types";
|
|
28
|
+
import type { Cache } from "../contracts/mlx/cache";
|
|
29
|
+
import { minimumReusableOffset } from "./views";
|
|
30
|
+
|
|
31
|
+
export interface SsdIndexEntry {
|
|
32
|
+
blocks?: Array<{ hash: string; bytes: number }>;
|
|
33
|
+
path: string;
|
|
34
|
+
ns: string;
|
|
35
|
+
tokens: number[];
|
|
36
|
+
bytes: number;
|
|
37
|
+
mtimeMs: number;
|
|
38
|
+
/** Whether every serialized cache can trim (derived from kinds+state at
|
|
39
|
+
* store/scan time). An untrimmable entry (wrapped ring, SSM) can only
|
|
40
|
+
* seed prompts it matches WITHOUT a trim — find() must not prefer it
|
|
41
|
+
* over a shorter usable one (the 2026-07-06 restart-0 defect: the
|
|
42
|
+
* [prompt+gen] file always won, always got rejected after restore). */
|
|
43
|
+
trimmable: boolean;
|
|
44
|
+
minimumReusableOffset?: number;
|
|
45
|
+
generationCheckpoint?: NonNullable<KvSaveMeta["generationCheckpoint"]>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SsdStoreOptions {
|
|
49
|
+
storage?: Pick<KvWriteRequest, "layout" | "blockBytes" | "segmented">;
|
|
50
|
+
codecs?: CacheCodecProvider;
|
|
51
|
+
dir: string;
|
|
52
|
+
maxBytes: number;
|
|
53
|
+
/** A live byte cap that replaces `maxBytes` when set: a caller sharing one
|
|
54
|
+
* budget across several stores answers with what this store may hold now. */
|
|
55
|
+
limit?: () => number;
|
|
56
|
+
/** Server identity — written into every file, enforced on scan/load. */
|
|
57
|
+
configFingerprint: string;
|
|
58
|
+
tokenizerHash: string;
|
|
59
|
+
modelId: string;
|
|
60
|
+
/** Verify every tensor hash on load (--ssd-cache-verify). */
|
|
61
|
+
verify?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const nsHash = (ns: string): string =>
|
|
65
|
+
ns === "" ? "base" : Bun.hash(ns).toString(16);
|
|
66
|
+
|
|
67
|
+
export class SsdCacheStore {
|
|
68
|
+
readonly #opts: SsdStoreOptions;
|
|
69
|
+
readonly #codecs: CacheCodecProvider;
|
|
70
|
+
readonly #root: string; // <dir>/<configFingerprint>
|
|
71
|
+
#index: SsdIndexEntry[] = [];
|
|
72
|
+
#exact = new Map<string, SsdIndexEntry>();
|
|
73
|
+
#tokenKeys = new WeakMap<number[], string>();
|
|
74
|
+
#key(tokens: number[], ns: string): string {
|
|
75
|
+
let hash = this.#tokenKeys.get(tokens);
|
|
76
|
+
if (!hash) {
|
|
77
|
+
hash = Bun.hash(new Uint32Array(tokens)).toString(16);
|
|
78
|
+
this.#tokenKeys.set(tokens, hash);
|
|
79
|
+
}
|
|
80
|
+
return JSON.stringify([ns, tokens.length, hash]);
|
|
81
|
+
}
|
|
82
|
+
#addIndex(entry: SsdIndexEntry): void {
|
|
83
|
+
this.#index.push(entry);
|
|
84
|
+
this.#exact.set(this.#key(entry.tokens, entry.ns), entry);
|
|
85
|
+
}
|
|
86
|
+
/** Exact checkpoint selection for a known session, independent of residency. */
|
|
87
|
+
findExact(tokens: number[], ns = ""): { entry: SsdIndexEntry; prefixLen: number } | null {
|
|
88
|
+
const entry = this.#exact.get(this.#key(tokens, ns));
|
|
89
|
+
return entry && commonPrefixLength(entry.tokens, tokens) === tokens.length
|
|
90
|
+
? { entry, prefixLen: tokens.length } : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#warnedWriteFailure = false;
|
|
94
|
+
stats = { restores: 0, spills: 0, restoreMsLast: 0 };
|
|
95
|
+
|
|
96
|
+
constructor(opts: SsdStoreOptions) {
|
|
97
|
+
this.#opts = opts;
|
|
98
|
+
this.#codecs = opts.codecs ?? legacyCacheCodecs;
|
|
99
|
+
this.#root = join(opts.dir, opts.configFingerprint);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get entries(): number {
|
|
103
|
+
return this.#index.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Header-only provenance for the serving tokenizer; no tensor restore. */
|
|
107
|
+
*tokenPrefixes(): Iterable<readonly number[]> {
|
|
108
|
+
for (const entry of this.#index) yield entry.tokens;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get totalBytes(): number {
|
|
112
|
+
const blocks = new Map<string, number>();
|
|
113
|
+
for (const entry of this.#index) for (const block of entry.blocks ?? [])
|
|
114
|
+
blocks.set(`${dirname(entry.path)}/${block.hash}`, block.bytes);
|
|
115
|
+
return this.#index.reduce((a, e) => a + e.bytes, 0) + [...blocks.values()].reduce((a, b) => a + b, 0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get maxBytes(): number {
|
|
119
|
+
return this.#opts.limit?.() ?? this.#opts.maxBytes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Longest token prefix whose atomic cache file is present in the index. */
|
|
123
|
+
get longestDurablePrefixTokens(): number {
|
|
124
|
+
return this.#index.reduce((best, entry) => Math.max(best, entry.tokens.length), 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** True when the SSD index can seed every token in this prefix. A longer
|
|
128
|
+
* trimmable descendant is equivalent to an exact boundary snapshot. */
|
|
129
|
+
hasDurablePrefix(tokens: number[], ns = ""): boolean {
|
|
130
|
+
return this.#index.some((entry) => {
|
|
131
|
+
if (entry.ns !== ns || entry.tokens.length < tokens.length || tokens.length < (entry.minimumReusableOffset ?? 0)) return false;
|
|
132
|
+
if (commonPrefixLength(entry.tokens, tokens) !== tokens.length) return false;
|
|
133
|
+
return entry.tokens.length === tokens.length || entry.trimmable;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Startup recovery: rebuild the in-memory index from disk. Corrupt
|
|
138
|
+
* headers are unlinked; `.tmp` orphans reaped; a file whose header names
|
|
139
|
+
* another model/tokenizer/fingerprint is left in place and not indexed.
|
|
140
|
+
* Only OUR fingerprint dir is touched. Returns entries indexed. */
|
|
141
|
+
scan(): number {
|
|
142
|
+
this.#index = []; this.#exact.clear();
|
|
143
|
+
if (!existsSync(this.#root)) return 0;
|
|
144
|
+
for (const nsDir of readdirSync(this.#root)) {
|
|
145
|
+
const nsPath = join(this.#root, nsDir);
|
|
146
|
+
let files: string[];
|
|
147
|
+
try { files = readdirSync(nsPath); } catch { continue; }
|
|
148
|
+
for (const f of files) {
|
|
149
|
+
const path = join(nsPath, f);
|
|
150
|
+
if (f.endsWith(".tmp")) {
|
|
151
|
+
// A crash mid-write: the rename never happened. Ours by
|
|
152
|
+
// construction (uuid names), safe to reap.
|
|
153
|
+
try { rmSync(path, { force: true }); } catch {}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!f.endsWith(".mlxkv")) continue;
|
|
157
|
+
try {
|
|
158
|
+
const h = readKvHeader(path); // verifies the header hash
|
|
159
|
+
// Another identity's entry: never ours to delete or serve.
|
|
160
|
+
if (
|
|
161
|
+
h.modelId !== this.#opts.modelId ||
|
|
162
|
+
h.configFingerprint !== this.#opts.configFingerprint ||
|
|
163
|
+
h.tokenizerHash !== this.#opts.tokenizerHash
|
|
164
|
+
) continue;
|
|
165
|
+
if ((h.codecProvider ?? legacyCacheCodecs.id) !== this.#codecs.id) continue;
|
|
166
|
+
const st = statSync(path);
|
|
167
|
+
this.#addIndex({
|
|
168
|
+
blocks: [...h.caches, ...(h.attachments ?? [])].flatMap(e => e.tensors.flatMap(t => t.blocks ?? [])),
|
|
169
|
+
path, ns: h.ns ?? "", tokens: h.tokens, bytes: st.size, mtimeMs: st.mtimeMs,
|
|
170
|
+
trimmable: !h.attachments?.length && cacheHeadersTrimmable(h.caches, this.#codecs),
|
|
171
|
+
minimumReusableOffset: cacheHeadersMinimumReusableOffset(h.caches),
|
|
172
|
+
...(h.generationCheckpoint
|
|
173
|
+
? { generationCheckpoint: h.generationCheckpoint }
|
|
174
|
+
: {}),
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
try { rmSync(path, { force: true }); } catch {}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
void kvWriter.collect(this.#root).catch(() => {});
|
|
182
|
+
return this.#index.length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Best USABLE stored entry for `prompt` in `ns`: the longest common
|
|
186
|
+
* prefix (capped at prompt.length−1 — at least one token must forward).
|
|
187
|
+
* An entry LONGER than the matched prefix is returned only when it can
|
|
188
|
+
* actually seed the prompt: the divergent tail must be trimmable after
|
|
189
|
+
* restore. Untrimmable entries (ring post-wrap, SSM) that would need a
|
|
190
|
+
* trim are SKIPPED here — before this gate, the big [prompt+gen] file
|
|
191
|
+
* always outranked the usable boundary snapshot, got restored, and was
|
|
192
|
+
* thrown away by take()'s backstop (the 2026-07-06 restart-0 + wasted
|
|
193
|
+
* 84 s restore-then-re-prefill path). Pure index lookup; no I/O. */
|
|
194
|
+
find(prompt: number[], ns = ""): { entry: SsdIndexEntry; prefixLen: number } | null {
|
|
195
|
+
let best: SsdIndexEntry | null = null;
|
|
196
|
+
let bestLen = 0;
|
|
197
|
+
for (const e of this.#index) {
|
|
198
|
+
if (e.ns !== ns) continue;
|
|
199
|
+
const p = Math.min(commonPrefixLength(e.tokens, prompt), prompt.length - 1);
|
|
200
|
+
if (p === 0 || p <= bestLen || p < (e.minimumReusableOffset ?? 0)) continue;
|
|
201
|
+
if (e.tokens.length - p > 0 && !e.trimmable) continue;
|
|
202
|
+
bestLen = p;
|
|
203
|
+
best = e;
|
|
204
|
+
}
|
|
205
|
+
return best ? { entry: best, prefixLen: bestLen } : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Restore an entry via packages/inference/src/state/persistence.ts's STREAMED COPY (bounded host
|
|
209
|
+
* transient — live entry + one tensor; no mapping survives the call,
|
|
210
|
+
* the caches own their bytes). Bumps LRU mtime. On ANY failure the
|
|
211
|
+
* file is dropped and null returned. */
|
|
212
|
+
restore(entry: SsdIndexEntry, model: { makeCache(): Cache[] }): LoadedKvCache | null {
|
|
213
|
+
const t0 = performance.now();
|
|
214
|
+
try {
|
|
215
|
+
const expect: KvLoadExpect = {
|
|
216
|
+
modelId: this.#opts.modelId,
|
|
217
|
+
configFingerprint: this.#opts.configFingerprint,
|
|
218
|
+
tokenizerHash: this.#opts.tokenizerHash,
|
|
219
|
+
ns: entry.ns,
|
|
220
|
+
verify: this.#opts.verify,
|
|
221
|
+
};
|
|
222
|
+
const loaded = loadKvCache(entry.path, model, expect, this.#codecs);
|
|
223
|
+
const now = new Date();
|
|
224
|
+
try { utimesSync(entry.path, now, now); entry.mtimeMs = now.getTime(); } catch {}
|
|
225
|
+
this.stats.restores++;
|
|
226
|
+
this.stats.restoreMsLast = performance.now() - t0;
|
|
227
|
+
return loaded;
|
|
228
|
+
} catch (err) {
|
|
229
|
+
// Loud: a failed restore silently degraded to a full re-prefill for
|
|
230
|
+
// a month before the 2026-07-06 bench made it observable.
|
|
231
|
+
console.warn(`[ssd-cache] restore failed, dropping ${entry.path}: ${(err as Error).message}`);
|
|
232
|
+
this.remove(entry.path);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Read on the dedicated CPU queue, then let the model codec adopt state. */
|
|
238
|
+
async restoreAsync(entry: SsdIndexEntry, model: { makeCache(): Cache[] }): Promise<LoadedKvCache | null> {
|
|
239
|
+
const t0 = performance.now();
|
|
240
|
+
try {
|
|
241
|
+
const loaded = await loadKvCacheAsync(entry.path, model, {
|
|
242
|
+
...this.#meta(entry.ns), verify: this.#opts.verify,
|
|
243
|
+
}, this.#codecs);
|
|
244
|
+
const now = new Date();
|
|
245
|
+
try { utimesSync(entry.path, now, now); entry.mtimeMs = now.getTime(); } catch {}
|
|
246
|
+
this.stats.restores++; this.stats.restoreMsLast = performance.now() - t0;
|
|
247
|
+
return loaded;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.warn(`[ssd-cache] restore failed, dropping ${entry.path}: ${error}`);
|
|
250
|
+
this.remove(entry.path); return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Persist an entry. Synchronous (the tier calls it on the idle serial
|
|
255
|
+
* lane between requests); atomic via
|
|
256
|
+
* packages/inference/src/state/persistence.ts's tmp+fsync+rename. An
|
|
257
|
+
* entry bigger than the cap is refused; disk/write failure is a warn-once
|
|
258
|
+
* soft-fail — serving never stalls on the cold tier. Existing entries in
|
|
259
|
+
* the same ns whose tokens are a PREFIX of the new entry are superseded
|
|
260
|
+
* (the agent-conversation pattern: one growing entry, not N generations
|
|
261
|
+
* of it). Returns true if stored. */
|
|
262
|
+
store(tokens: number[], caches: Cache[], ns = "", attachments?: CheckpointAttachment[]): boolean {
|
|
263
|
+
const dir = join(this.#root, nsHash(ns));
|
|
264
|
+
const path = join(dir, `${randomUUID()}.mlxkv`);
|
|
265
|
+
try {
|
|
266
|
+
mkdirSync(dir, { recursive: true });
|
|
267
|
+
saveKvCache(path, tokens, caches, { ...this.#meta(ns), attachments }, this.#codecs);
|
|
268
|
+
return this.#indexStored(path, tokens, caches, ns, undefined, attachments);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return this.#storeFailed(path, err);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Non-blocking store (the write-behind persistence path): same file,
|
|
275
|
+
* index, and supersede semantics as store(), but the tensor flush
|
|
276
|
+
* yields the event loop between tensors so serving interleaves.
|
|
277
|
+
* Caller passes zero-copy CLONES it owns (a consistent snapshot no
|
|
278
|
+
* matter what the live entry does meanwhile) and disposes them after.
|
|
279
|
+
* `runStep` optionally wraps owner-thread snapshot preparation. Hashing,
|
|
280
|
+
* packing and disk writes execute on the CPU worker. */
|
|
281
|
+
async storeAsync(
|
|
282
|
+
tokens: number[], caches: Cache[], ns = "",
|
|
283
|
+
runStep?: <T>(step: () => T) => Promise<T>,
|
|
284
|
+
attachments?: CheckpointAttachment[],
|
|
285
|
+
): Promise<boolean> {
|
|
286
|
+
const dir = join(this.#root, nsHash(ns));
|
|
287
|
+
const path = join(dir, `${randomUUID()}.mlxkv`);
|
|
288
|
+
try {
|
|
289
|
+
await saveKvCacheAsync(path, tokens, caches, { ...this.#meta(ns), attachments }, runStep, this.#codecs, this.#opts.storage);
|
|
290
|
+
return this.#indexStored(path, tokens, caches, ns, undefined, attachments);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
return this.#storeFailed(path, err);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Longest durable in-flight generation for an identical request. The
|
|
297
|
+
* checkpoint namespace is separate from the ordinary prompt-cache tier,
|
|
298
|
+
* so incomplete assistant output can never seed an unrelated prefill. */
|
|
299
|
+
findGenerationCheckpoint(
|
|
300
|
+
prompt: number[], key: string, cacheNs = "",
|
|
301
|
+
): SsdIndexEntry | null {
|
|
302
|
+
let best: SsdIndexEntry | null = null;
|
|
303
|
+
for (const entry of this.#index) {
|
|
304
|
+
const checkpoint = entry.generationCheckpoint;
|
|
305
|
+
if (!checkpoint || checkpoint.key !== key || checkpoint.cacheNs !== cacheNs)
|
|
306
|
+
continue;
|
|
307
|
+
if (checkpoint.originalPromptTokens !== prompt.length) continue;
|
|
308
|
+
if (entry.tokens.length <= prompt.length) continue;
|
|
309
|
+
if (commonPrefixLength(entry.tokens, prompt) !== prompt.length) continue;
|
|
310
|
+
if (!best || entry.tokens.length > best.tokens.length) best = entry;
|
|
311
|
+
}
|
|
312
|
+
return best;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Persist one in-flight generation atomically. A completed newer
|
|
316
|
+
* checkpoint supersedes older checkpoints for the same request only after
|
|
317
|
+
* its rename succeeds, so a crash during a write leaves the prior point. */
|
|
318
|
+
async storeGenerationCheckpoint(
|
|
319
|
+
tokens: number[], caches: Cache[], checkpoint: NonNullable<KvSaveMeta["generationCheckpoint"]>,
|
|
320
|
+
runStep?: <T>(step: () => T) => Promise<T>,
|
|
321
|
+
): Promise<boolean> {
|
|
322
|
+
const ns = `__generation_checkpoint__:${checkpoint.key}`;
|
|
323
|
+
const dir = join(this.#root, nsHash(ns));
|
|
324
|
+
const path = join(dir, `${randomUUID()}.mlxkv`);
|
|
325
|
+
try {
|
|
326
|
+
if (runStep) await runStep(() => mkdirSync(dir, { recursive: true }));
|
|
327
|
+
else mkdirSync(dir, { recursive: true });
|
|
328
|
+
await saveKvCacheAsync(path, tokens, caches, {
|
|
329
|
+
...this.#meta(ns), generationCheckpoint: checkpoint,
|
|
330
|
+
}, runStep, this.#codecs, this.#opts.storage);
|
|
331
|
+
const index = () => {
|
|
332
|
+
const stored = this.#indexStored(path, tokens, caches, ns, checkpoint);
|
|
333
|
+
if (stored) {
|
|
334
|
+
for (const entry of [...this.#index]) {
|
|
335
|
+
if (entry.path === path) continue;
|
|
336
|
+
if (entry.generationCheckpoint?.key === checkpoint.key) this.remove(entry.path);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return stored;
|
|
340
|
+
};
|
|
341
|
+
return runStep ? await runStep(index) : index();
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return runStep ? await runStep(() => this.#storeFailed(path, err)) : this.#storeFailed(path, err);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
removeGenerationCheckpoints(key: string): void {
|
|
348
|
+
for (const entry of [...this.#index])
|
|
349
|
+
if (entry.generationCheckpoint?.key === key) this.remove(entry.path);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#meta(ns: string): KvSaveMeta {
|
|
353
|
+
return {
|
|
354
|
+
modelId: this.#opts.modelId,
|
|
355
|
+
configFingerprint: this.#opts.configFingerprint,
|
|
356
|
+
tokenizerHash: this.#opts.tokenizerHash,
|
|
357
|
+
ns,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#indexStored(
|
|
362
|
+
path: string, tokens: number[], caches: Cache[], ns: string,
|
|
363
|
+
generationCheckpoint?: NonNullable<KvSaveMeta["generationCheckpoint"]>,
|
|
364
|
+
attachments?: CheckpointAttachment[],
|
|
365
|
+
): boolean {
|
|
366
|
+
const st = statSync(path);
|
|
367
|
+
const header = readKvHeader(path);
|
|
368
|
+
const blocks = [...header.caches, ...(header.attachments ?? [])].flatMap(e => e.tensors.flatMap(t => t.blocks ?? []));
|
|
369
|
+
const unique = new Map(blocks.map(block => [block.hash, block.bytes]));
|
|
370
|
+
const entryBytes = st.size + [...unique.values()].reduce((a, b) => a + b, 0);
|
|
371
|
+
if (entryBytes > this.maxBytes) {
|
|
372
|
+
this.remove(path);
|
|
373
|
+
console.warn(`[ssd-cache] entry not stored: ${entryBytes} bytes exceeds the ${this.maxBytes}-byte cap`);
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
const trimmable = !attachments?.length && caches.every((c) => c.isTrimmable());
|
|
377
|
+
// Exact duplicates (same tokens) are replaced regardless of
|
|
378
|
+
// trimmability — the new file serves exactly the old one's matches.
|
|
379
|
+
for (const e of [...this.#index]) {
|
|
380
|
+
if (e.ns !== ns || e.tokens.length !== tokens.length) continue;
|
|
381
|
+
if (commonPrefixLength(e.tokens, tokens) === tokens.length) this.remove(e.path);
|
|
382
|
+
}
|
|
383
|
+
// Supersede shadowed ancestors of this prefix (same ns) — ONLY when
|
|
384
|
+
// the new entry can serve their prefixes (all caches trimmable; the
|
|
385
|
+
// same guard as PromptCache.put()). An untrimmable [prompt+gen]
|
|
386
|
+
// entry deleting the boundary snapshot was the gemma restart-0
|
|
387
|
+
// defect (2026-07-06): the only restorable file died the moment the
|
|
388
|
+
// unrestorable one landed.
|
|
389
|
+
if (trimmable) {
|
|
390
|
+
for (const e of [...this.#index]) {
|
|
391
|
+
if (e.ns !== ns || e.tokens.length >= tokens.length || e.tokens.length < minimumReusableOffset(caches)) continue;
|
|
392
|
+
if (commonPrefixLength(e.tokens, tokens) === e.tokens.length) this.remove(e.path);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
this.#addIndex({
|
|
396
|
+
blocks, path, ns, tokens, bytes: st.size, mtimeMs: Date.now(), trimmable,
|
|
397
|
+
minimumReusableOffset: minimumReusableOffset(caches),
|
|
398
|
+
...(generationCheckpoint ? { generationCheckpoint } : {}),
|
|
399
|
+
});
|
|
400
|
+
this.stats.spills++;
|
|
401
|
+
this.evictToCap();
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#storeFailed(path: string, err: unknown): boolean {
|
|
406
|
+
this.remove(path);
|
|
407
|
+
try { rmSync(`${path}.tmp`, { force: true }); } catch {}
|
|
408
|
+
if (!this.#warnedWriteFailure) {
|
|
409
|
+
this.#warnedWriteFailure = true;
|
|
410
|
+
console.warn(`[ssd-cache] store failed (disk full or unwritable?) — cold tier disabled for this entry: ${(err as Error).message}`);
|
|
411
|
+
}
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Enforce the byte cap: unlink oldest-mtime entries until under it. */
|
|
416
|
+
evictToCap(): void {
|
|
417
|
+
while (this.totalBytes > this.maxBytes && this.#index.length > 0) {
|
|
418
|
+
let oldest = 0;
|
|
419
|
+
for (let i = 1; i < this.#index.length; i++)
|
|
420
|
+
if (this.#index[i]!.mtimeMs < this.#index[oldest]!.mtimeMs) oldest = i;
|
|
421
|
+
this.remove(this.#index[oldest]!.path);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
remove(path: string): void {
|
|
426
|
+
try { rmSync(path, { force: true }); } catch {}
|
|
427
|
+
for (const entry of this.#index) if (entry.path === path) {
|
|
428
|
+
const key = this.#key(entry.tokens, entry.ns);
|
|
429
|
+
if (this.#exact.get(key) === entry) this.#exact.delete(key);
|
|
430
|
+
}
|
|
431
|
+
this.#index = this.#index.filter((e) => e.path !== path);
|
|
432
|
+
void kvWriter.collect(this.#root).catch(() => {});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PrefillPadding } from "../contracts/mlx/cache";
|
|
2
|
+
import type { MlxArray } from "@mlx-bun/mlx/array";
|
|
3
|
+
import { Dtype } from "@mlx-bun/mlx/ffi";
|
|
4
|
+
import * as ops from "@mlx-bun/mlx/ops";
|
|
5
|
+
|
|
6
|
+
/** ArraysCache's remaining lengths and padding from mlx-lm (MIT).
|
|
7
|
+
* Recurrent state has no token axis to roll: masked tokens must not update it. */
|
|
8
|
+
export class SsmPrefillPadding {
|
|
9
|
+
#left: number[];
|
|
10
|
+
#lengths: number[];
|
|
11
|
+
constructor(padding: PrefillPadding) {
|
|
12
|
+
this.#left = padding.leftPadding ? [...padding.leftPadding] : padding.lengths.map(() => 0);
|
|
13
|
+
this.#lengths = [...padding.lengths];
|
|
14
|
+
}
|
|
15
|
+
convolutionLengths(tokens: number): number[] {
|
|
16
|
+
return this.#lengths.map(length => Math.max(0, Math.min(tokens, length)));
|
|
17
|
+
}
|
|
18
|
+
makeMask(tokens: number): MlxArray {
|
|
19
|
+
const B = this.#lengths.length;
|
|
20
|
+
using arange = ops.arange(0, tokens, 1, Dtype.int32);
|
|
21
|
+
using positions = ops.reshape(arange, [1, tokens]);
|
|
22
|
+
using left = ops.fromInt32(this.#left, [B, 1]);
|
|
23
|
+
using ends = ops.fromInt32(this.#lengths, [B, 1]);
|
|
24
|
+
using after = ops.greaterEqual(positions, left);
|
|
25
|
+
using before = ops.less(positions, ends);
|
|
26
|
+
return ops.logicalAnd(after, before);
|
|
27
|
+
}
|
|
28
|
+
advance(tokens: number): number[] {
|
|
29
|
+
const counts = this.#lengths.map((length, row) => Math.max(0,
|
|
30
|
+
Math.min(tokens, length) - Math.max(0, Math.min(tokens, this.#left[row]!))));
|
|
31
|
+
this.#lengths = this.#lengths.map(length => length - tokens);
|
|
32
|
+
this.#left = this.#left.map(left => left - tokens);
|
|
33
|
+
return counts;
|
|
34
|
+
}
|
|
35
|
+
filter(keep: readonly number[]): void {
|
|
36
|
+
this.#left = keep.map(row => this.#left[row]!);
|
|
37
|
+
this.#lengths = keep.map(row => this.#lengths[row]!);
|
|
38
|
+
}
|
|
39
|
+
}
|