mlx-bun 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -588
- package/docs/README.md +94 -0
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/archive/planning/release-notes-v0.3.0.md +35 -0
- package/docs/design/batching.md +1841 -0
- package/docs/design/decode-speed-program.md +2346 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +655 -334
- package/docs/design/kv-cache.md +824 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1990 -0
- package/docs/design/turboquant.md +2212 -0
- package/docs/design/unified-engine-frontier-plan.md +1833 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +50 -1
- package/docs/planning/release-notes-v0.4.0.md +45 -0
- package/docs/planning/video-series.md +2 -2
- package/docs/reference/benchmarks.md +3613 -0
- package/docs/reference/cli.md +434 -160
- package/docs/reference/distribution.md +328 -182
- package/docs/reference/environment.md +398 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +566 -168
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +113 -6
- package/docs/reference/server-api.md +944 -429
- package/docs/reference/server-config.md +740 -338
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +158 -0
- package/package.json +9 -4
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1790 -0
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +106 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +32 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +130 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +133 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/memory-client.ts +189 -0
- package/src/backends/mlx/methods.ts +90 -0
- package/src/backends/mlx/model-prompt.ts +164 -0
- package/src/backends/mlx/model-serving.ts +79 -0
- package/src/backends/mlx/prefill-cohort.ts +89 -0
- package/src/backends/mlx/prefill-rows.ts +200 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/request-state-policy.ts +41 -0
- package/src/backends/mlx/rollback.ts +89 -0
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +242 -0
- package/src/backends/mlx/speculative-group.ts +359 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +46 -0
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
- package/src/cli.ts +66 -58
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +60 -0
- package/src/contracts/generation.ts +103 -0
- package/src/contracts/host.ts +7 -0
- package/src/contracts/jobs.ts +66 -0
- package/src/contracts/pi-provider.ts +30 -0
- package/src/contracts/pi.ts +259 -0
- package/src/contracts/prefix-cache.ts +23 -0
- package/src/contracts/resources.ts +25 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +45 -0
- package/src/contracts/tsconfig.json +14 -0
- package/src/diffusion/diffusion-generate.ts +218 -153
- package/src/embed.ts +1 -1
- package/src/engine/admission.ts +59 -0
- package/src/engine/cancellation.ts +38 -0
- package/src/engine/engine.ts +56 -0
- package/src/engine/execution-coordinator.ts +70 -0
- package/src/engine/execution-plan.ts +53 -0
- package/src/engine/resources.ts +53 -0
- package/src/engine/scheduler.ts +72 -0
- package/src/engine/session.ts +247 -0
- package/src/eval/README.md +44 -1
- package/src/eval/kl.ts +236 -3
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +8 -96
- package/src/fill/echo-index.ts +188 -0
- package/src/fill/fill-session.ts +452 -0
- package/src/fill/proposal.ts +68 -0
- package/src/fill/schema-rows.ts +506 -0
- package/src/fill/tool-boundary.ts +103 -0
- package/src/fit.ts +30 -16
- package/src/generate.ts +709 -417
- package/src/grammar.ts +12 -7
- package/src/harness-pi.ts +14 -33
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +7 -0
- package/src/inference/denoising.ts +17 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/graph.ts +33 -0
- package/src/inference/prefill.ts +39 -0
- package/src/inference/rollback.ts +7 -0
- package/src/jobs/db.ts +4 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-entry.ts +12 -12
- package/src/jobs/runner.ts +98 -13
- package/src/jobs/task-client.ts +19 -0
- package/src/jobs/types.ts +2 -58
- package/src/kv-scheme.ts +70 -12
- package/src/kv-store.ts +177 -63
- package/src/lab/README.md +18 -0
- package/src/{assets → lab/curve}/curve-designer.html +2 -2
- package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
- package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
- package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
- package/src/{model → lab/paged-kv}/paged-kv.ts +32 -15
- package/src/library.ts +40 -0
- package/src/lora.ts +18 -0
- package/src/media-fetch.ts +1 -1
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +30 -169
- package/src/memory/prompts.ts +1 -1
- package/src/memory/rest.ts +1 -1
- package/src/memory/schedule.ts +1 -1
- package/src/memory/stages.ts +4 -1
- package/src/memory/vault.ts +1 -3
- package/src/mlx/array.ts +11 -11
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +58 -11
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +33 -13
- package/src/mlx/quantized-rows.ts +36 -0
- package/src/mlx/token-bitmask.ts +52 -0
- package/src/mlx/turboquant-kv-decode.ts +82 -0
- package/src/mlx/turboquant-kv-inverse.ts +122 -0
- package/src/mlx/turboquant-ops.ts +33 -3
- package/src/mlx/turboquant-tables.ts +2 -2
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +20 -6
- package/src/model/batched-quant.ts +9 -3
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +78 -17
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +65 -23
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +93 -0
- package/src/model/compiled-decode.ts +8 -2
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +55 -0
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +76 -33
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +179 -80
- package/src/model/gemma4.ts +40 -32
- package/src/model/generated/gemma4-12b.ts +6 -3
- package/src/model/generated/gemma4-26b.ts +6 -3
- package/src/model/generated/gemma4-e4b.ts +6 -3
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +1 -1
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/implementation.ts +69 -0
- package/src/model/kv-attention-view.ts +67 -0
- package/src/model/minicpm5.ts +17 -15
- package/src/model/profile.ts +12 -2
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +202 -78
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +1 -1
- package/src/model/trellis-balanced-scatter.ts +51 -0
- package/src/model/trellis-linear.ts +672 -0
- package/src/model/trellis-shared-m.ts +151 -0
- package/src/model/trellis-shared-scatter.ts +144 -0
- package/src/model/trellis-splitk-prefill.ts +158 -0
- package/src/model/trellis-tiled-prefill.ts +136 -0
- package/src/model/trellis-vector-expand.ts +62 -0
- package/src/model/trellis-wide-prefill.ts +94 -0
- package/src/model/turboquant-codec.ts +83 -0
- package/src/model/universal/dense.ts +15 -14
- package/src/model/universal/modules.ts +8 -1
- package/src/native-pack.ts +6 -5
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +8 -315
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +110 -69
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/config-writer.ts +10 -3
- package/src/quantize/rotate.ts +4 -4
- package/src/quantize/trellis.ts +586 -0
- package/src/responses.ts +28 -1
- package/src/runtime-config.ts +18 -4
- package/src/sampler.ts +40 -29
- package/src/serve/batch-scheduler.ts +8 -1671
- package/src/serve/chat-request.ts +391 -0
- package/src/serve/chat-stage.ts +195 -0
- package/src/serve/checkpoint-identity.ts +64 -0
- package/src/serve/completion-executor.ts +15 -7
- package/src/serve/discovery-routes.ts +12 -13
- package/src/serve/generation-gateway.ts +183 -213
- package/src/serve/http.ts +159 -0
- package/src/serve/inference-request.ts +79 -0
- package/src/serve/isolate.ts +297 -105
- package/src/serve/lab-routes.ts +3 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +7 -5
- package/src/serve/model-binding.ts +43 -0
- package/src/serve/model-host.ts +511 -0
- package/src/serve/openai-wire.ts +103 -0
- package/src/serve/parent-application.ts +56 -0
- package/src/serve/pipeline.ts +33 -0
- package/src/serve/preparation.ts +33 -0
- package/src/serve/request-plan.ts +23 -18
- package/src/serve/request-prep.ts +344 -0
- package/src/serve/response-lease.ts +24 -0
- package/src/serve/responses-client.ts +69 -0
- package/src/serve/session-completion-engine.ts +110 -0
- package/src/serve/session-search.ts +1 -1
- package/src/serve/static-routes.ts +5 -5
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +335 -2711
- package/src/spec/assistant-rows.ts +92 -0
- package/src/spec/assistant-source.ts +31 -55
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +16 -14
- package/src/spec/dflash-source.ts +36 -12
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +97 -122
- package/src/spec/dspark/generate-dflash.ts +2 -2
- package/src/spec/dspark/generate.ts +2 -2
- package/src/spec/dspark/loader.ts +1 -1
- package/src/spec/dspark/module-dflash.ts +129 -172
- package/src/spec/dspark/module.ts +3 -3
- package/src/spec/dspark/quantize-drafter.ts +1 -1
- package/src/spec/generate.ts +1 -1
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -137
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +93 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +219 -0
- package/src/spec/qwen-mtp-source.ts +147 -195
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +201 -201
- package/src/spec/source.ts +110 -13
- package/src/spec/two-model-rows.ts +61 -0
- package/src/spec/two-model.ts +44 -102
- package/src/ssd-cache.ts +99 -19
- package/src/ssd-durability.ts +36 -20
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +2 -2
- package/src/train/forward.ts +6 -0
- package/src/train/loss.ts +1 -1
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +5 -3
- package/src/train/trainer.ts +6 -6
- package/src/vision/prompt.ts +1 -1
- package/src/vision/qwen3vl-tower.ts +52 -10
- package/src/web/app.html +1 -1
- package/src/web/app.js +62 -60
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +4 -4
- package/src/web/src/composer.ts +4 -4
- package/src/web/src/hub.ts +1 -1
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +1 -1
- package/src/web/src/model-picker.ts +1 -1
- package/src/web/src/palette.ts +1 -1
- package/src/web/src/protocol.ts +3 -39
- package/src/web/src/sessions.ts +1 -1
- package/src/web/src/ui-catalog.ts +4 -18
- package/src/web-assets.ts +47 -0
- package/src/weights.ts +55 -9
- package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/archive/investigations/hlg-ab.html +0 -34
- package/docs/archive/investigations/hlg-explorer.html +0 -238
- package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
- package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/archive/investigations/hlg-report.html +0 -262
- package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/archive/mlx-bun-lab-report.html +0 -275
- package/docs/archive/wwdc-mlx-bun.html +0 -848
- package/docs/design/adapters-end-to-end.md +0 -205
- package/docs/design/batching-perf-path.md +0 -255
- package/docs/design/batching-v2-plan.md +0 -529
- package/docs/design/bucketing-stage.md +0 -197
- package/docs/design/compat-cli-surface-design.md +0 -230
- package/docs/design/docs-reorg-plan.md +0 -165
- package/docs/design/dspark-serving-program.md +0 -371
- package/docs/design/dspark-speculative-decoding.md +0 -318
- package/docs/design/grammar-spec-batching-integration.md +0 -301
- package/docs/design/memory-inference-path.md +0 -422
- package/docs/design/memory-synthesis.md +0 -287
- package/docs/design/memory-system.md +0 -237
- package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
- package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
- package/docs/design/omlx-adoption-map.md +0 -98
- package/docs/design/orpo-dynamic-lambda.md +0 -220
- package/docs/design/orpo-future-enhancements.md +0 -340
- package/docs/design/paged-kv-cache.md +0 -214
- package/docs/design/parallel-slots.md +0 -567
- package/docs/design/parity-tier-dag.md +0 -161
- package/docs/design/repo-cleanup-plan.md +0 -231
- package/docs/design/runtime-isolation.md +0 -263
- package/docs/design/segmented-backward-training.md +0 -543
- package/docs/design/spec-decode-larger-targets.md +0 -229
- package/docs/design/ssd-kv-cold-tier.md +0 -415
- package/docs/design/structured-output.md +0 -484
- package/docs/design/tauri-desktop-app.md +0 -127
- package/docs/design/the-dreaming-master-plan.md +0 -445
- package/docs/design/turboquant-kv.md +0 -235
- package/docs/design/turboquant-weights.md +0 -467
- package/docs/design/w4a16-compute-precision-spike.md +0 -232
- package/docs/design/web-chat-beat-matrix.md +0 -315
- package/docs/design/write-pipeline-entity.md +0 -468
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -101
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -11
- /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
- /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
- /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
- /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
- /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
- /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
2
|
+
import type { MlxArray } from "../../mlx/array";
|
|
3
|
+
import * as ops from "../../mlx/ops";
|
|
4
|
+
import { legacyCacheCodecs, type CacheCodecProvider, type CacheHeaderEntry,
|
|
5
|
+
type SnapshotContext } from "../../kv-store";
|
|
6
|
+
import { disposeResources } from "../../engine/resources";
|
|
7
|
+
import type { CheckpointAttachment } from "./checkpoint-state";
|
|
8
|
+
type TensorSlot = SnapshotContext["slots"][number];
|
|
9
|
+
|
|
10
|
+
/** Reuse the backend cache codecs for method-owned companion state. Tensor
|
|
11
|
+
* slots address attachment indices here; the persistence tier supplies the
|
|
12
|
+
* outer file offsets and checksums. No host serialization of tensors occurs. */
|
|
13
|
+
export function captureCacheAttachment(schema: string, caches: readonly Cache[],
|
|
14
|
+
metadata: CheckpointAttachment["metadata"], codecs: CacheCodecProvider = legacyCacheCodecs): CheckpointAttachment {
|
|
15
|
+
const tensors: MlxArray[] = [], headers: Array<CacheHeaderEntry | null> = [];
|
|
16
|
+
try {
|
|
17
|
+
for (const cache of caches) {
|
|
18
|
+
if (!cache.offset) { headers.push(null); continue; }
|
|
19
|
+
const slots: TensorSlot[] = [];
|
|
20
|
+
const liveSlice = (array: MlxArray, length: number, axis = 2) => {
|
|
21
|
+
const end = [...array.shape]; end[axis] = length;
|
|
22
|
+
return array.slice(array.shape.map(() => 0), end);
|
|
23
|
+
};
|
|
24
|
+
const push = (array: MlxArray, owned: boolean) => {
|
|
25
|
+
const value = owned ? array : ops.copyOf(array);
|
|
26
|
+
const off = tensors.length; tensors.push(value);
|
|
27
|
+
slots.push({ off, bytes: value.nbytes, shape: [...value.shape], dtype: value.dtype, hash: "" });
|
|
28
|
+
};
|
|
29
|
+
const context: SnapshotContext = { slots, push, liveSlice,
|
|
30
|
+
liveMlaSlice: (array, length) => liveSlice(array, length, 1),
|
|
31
|
+
pushTriple(triple, length) {
|
|
32
|
+
for (const array of [triple.packed, triple.scales, triple.biases])
|
|
33
|
+
push(length === null ? array : liveSlice(array, length), length !== null);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const entry = codecs.forCache(cache).snapshot(cache, context);
|
|
37
|
+
if (cache.minimumReusableOffset !== undefined) entry.minimumReusableOffset = cache.minimumReusableOffset;
|
|
38
|
+
headers.push(entry);
|
|
39
|
+
}
|
|
40
|
+
return { schema, metadata: { ...metadata, cacheCodecs: codecs.id, cacheHeaders: JSON.stringify(headers) }, tensors };
|
|
41
|
+
} catch (error) { disposeResources(tensors); throw error; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Returned caches own immutable aliases. The caller keeps an external SSD
|
|
45
|
+
* lease until its normal state materialization boundary has completed. */
|
|
46
|
+
export function restoreCacheAttachment(attachment: CheckpointAttachment, makeCache: () => Cache[],
|
|
47
|
+
codecs: CacheCodecProvider = legacyCacheCodecs): Cache[] {
|
|
48
|
+
if (attachment.metadata.cacheCodecs !== codecs.id) throw new Error("companion cache codec mismatch");
|
|
49
|
+
const headers = JSON.parse(attachment.metadata.cacheHeaders as string) as Array<CacheHeaderEntry | null>;
|
|
50
|
+
const caches = makeCache(), pending: MlxArray[] = [];
|
|
51
|
+
const arr = (slot: TensorSlot) => {
|
|
52
|
+
const value = ops.copyOf(attachment.tensors[slot.off]!); pending.push(value); return value;
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
for (const [index, entry] of headers.entries()) {
|
|
56
|
+
if (!entry) continue;
|
|
57
|
+
const cache = codecs.forHeader(entry).load(entry, { path: attachment.schema, arr, grownArr: arr,
|
|
58
|
+
triple: (slots, at) => ({ packed: arr(slots[at]!), scales: arr(slots[at + 1]!), biases: arr(slots[at + 2]!) }),
|
|
59
|
+
});
|
|
60
|
+
pending.length = 0;
|
|
61
|
+
cache.minimumReusableOffset = entry.minimumReusableOffset ?? 0;
|
|
62
|
+
const previous = caches[index]; caches[index] = cache;
|
|
63
|
+
previous?.dispose();
|
|
64
|
+
}
|
|
65
|
+
return caches;
|
|
66
|
+
} catch (error) { disposeResources([...pending, ...caches]); throw error; }
|
|
67
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TurboQuantKVCache, RotatingKVCache, RotatingQuantizedKVCache, isBatchableCache, type BatchableCache, type Cache } from "../../model/gemma4-base";
|
|
2
|
+
import { DelayedRotatingQuantizedKVCache } from "../../model/delayed-rotating-quantized-kv";
|
|
3
|
+
import { RotatingAffineLayout } from "../../model/rotating-kv-layout";
|
|
4
|
+
import { BatchedTurboQuantKVCache } from "../../model/batched-turboquant-kv";
|
|
5
|
+
import { targetRowLayoutFactory, type TargetRowLayout } from "./target-layout-capability";
|
|
6
|
+
import { PagedKVCache } from "../../lab/paged-kv/paged-kv";
|
|
7
|
+
import { PagedKvRows } from "../../lab/paged-kv/paged-kv-rows";
|
|
8
|
+
|
|
9
|
+
/** Bind encoded state to its row layout. Execution owners only move rows. */
|
|
10
|
+
export function ownedCacheLayout(cache: Cache): BatchableCache | undefined {
|
|
11
|
+
if (cache instanceof PagedKVCache) return new PagedKvRows(cache.capacityTokens, cache.blockSize);
|
|
12
|
+
if (isBatchableCache(cache)) return cache.makeEmptyBatch();
|
|
13
|
+
if (cache instanceof TurboQuantKVCache) return new BatchedTurboQuantKVCache(cache.kBits, cache.vBits);
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Prefill reuses decode's layouts; shape selection belongs to storage. */
|
|
18
|
+
export function prefillCacheLayout(cache: Cache): BatchableCache {
|
|
19
|
+
if (cache instanceof PagedKVCache) return new PagedKvRows(cache.capacityTokens, cache.blockSize);
|
|
20
|
+
if (isBatchableCache(cache)) return cache.makeEmptyBatch();
|
|
21
|
+
if (cache instanceof RotatingKVCache) return new DelayedRotatingQuantizedKVCache(cache.maxSize, 64, 4, Infinity, () => {});
|
|
22
|
+
if (cache instanceof RotatingQuantizedKVCache) return new RotatingAffineLayout(cache.maxSize, cache.groupSize, cache.bits);
|
|
23
|
+
return targetCacheLayout(cache);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Target transaction layouts retain each source cache's numerical codec. */
|
|
27
|
+
export function targetCacheLayout(cache: Cache): TargetRowLayout {
|
|
28
|
+
const make = targetRowLayoutFactory(cache);
|
|
29
|
+
if (!make) throw new Error(`No target row layout for ${cache.signature()}`);
|
|
30
|
+
return make();
|
|
31
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
2
|
+
import * as ops from "../../mlx/ops";
|
|
3
|
+
import { leaseCacheStates } from "./state-views";
|
|
4
|
+
import { withResource } from "../../engine/resources";
|
|
5
|
+
import type { MlxArray } from "../../mlx/array";
|
|
6
|
+
import { disposeResources, cleanupFailure } from "../../engine/resources";
|
|
7
|
+
|
|
8
|
+
/** Generation-method state accompanying the target cache. The method owns
|
|
9
|
+
* its schema and alignment; storage owns these immutable tensor snapshots.
|
|
10
|
+
* Attachments require an exact prefix boundary and are never trimmed. */
|
|
11
|
+
export interface CheckpointAttachment {
|
|
12
|
+
schema: string;
|
|
13
|
+
metadata: Record<string, string | number | boolean>;
|
|
14
|
+
tensors: MlxArray[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function attachmentBytes(attachments: readonly CheckpointAttachment[] = []): number {
|
|
18
|
+
return attachments.reduce((sum, entry) =>
|
|
19
|
+
sum + entry.tensors.reduce((bytes, tensor) => bytes + tensor.nbytes, 0), 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function disposeAttachments(attachments: readonly CheckpointAttachment[] = []): void {
|
|
23
|
+
disposeResources(attachments.flatMap((entry) => entry.tensors));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cloneAttachments(attachments: readonly CheckpointAttachment[] = []): CheckpointAttachment[] {
|
|
27
|
+
const held: MlxArray[] = [];
|
|
28
|
+
try {
|
|
29
|
+
return attachments.map((entry) => ({
|
|
30
|
+
schema: entry.schema, metadata: { ...entry.metadata },
|
|
31
|
+
tensors: entry.tensors.map((tensor) => {
|
|
32
|
+
const copy = tensor.slice(tensor.shape.map(() => 0), [...tensor.shape]);
|
|
33
|
+
held.push(copy);
|
|
34
|
+
return copy;
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(held)); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Shared cache port for MLX execution methods and their companion state. */
|
|
41
|
+
export type MlxPrefixCache = import("../../contracts/prefix-cache").PrefixCache<
|
|
42
|
+
import("../../model/gemma4").Cache[], CheckpointAttachment[]
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
/** Resolve snapshot copies before storage retains them. A deferred compact
|
|
46
|
+
* row still owns its full parent batch; persistence may wait until idle.
|
|
47
|
+
* This only settles GPU state and performs no serialization or disk I/O. */
|
|
48
|
+
export function materializeCheckpoint(caches: Cache[], attachments: readonly CheckpointAttachment[] = []): void {
|
|
49
|
+
withResource(leaseCacheStates(caches), state => {
|
|
50
|
+
const tensors = [...state, ...attachments.flatMap(attachment => attachment.tensors)];
|
|
51
|
+
if (tensors.length) ops.evalAll(tensors);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { SpillQueue, type SpillItem } from "../../kv-store";
|
|
2
|
+
import { cacheBytes } from "../../prompt-cache";
|
|
3
|
+
import { disposeResources } from "../../engine/resources";
|
|
4
|
+
import type { ContinuationStore, OrdinaryContinuationState } from "./continuation";
|
|
5
|
+
|
|
6
|
+
type Metadata = Parameters<ContinuationStore["storeGenerationCheckpoint"]>[2];
|
|
7
|
+
interface Attempt { key: string; completed: boolean; pending: number; failed: boolean; version: number; retired: boolean }
|
|
8
|
+
interface Job { attempt: Attempt; metadata?: Metadata; cleanup?: number }
|
|
9
|
+
|
|
10
|
+
/** One coordinator per checkpoint store. Queue owns snapshots through disk
|
|
11
|
+
* completion; per-key attempts order cleanup before subsequent writes. */
|
|
12
|
+
export class ContinuationPersistence {
|
|
13
|
+
readonly #attempts = new Map<string, Attempt>();
|
|
14
|
+
readonly #jobs = new WeakMap<SpillItem["caches"], Job>();
|
|
15
|
+
readonly #queue: SpillQueue;
|
|
16
|
+
readonly #cleanupFailures = new Map<string, Set<number>>();
|
|
17
|
+
readonly #cleanupPending = new Map<string, Set<number>>();
|
|
18
|
+
readonly #cleanupSucceeded = new Map<string, number>();
|
|
19
|
+
#nextBarrier = 0;
|
|
20
|
+
constructor(readonly store: ContinuationStore, options: {
|
|
21
|
+
maxBytes: number;
|
|
22
|
+
/** All blocking tensor serialization runs through the owner's idle gate. */
|
|
23
|
+
runStep: <T>(step: () => T) => Promise<T>;
|
|
24
|
+
}) {
|
|
25
|
+
this.#queue = new SpillQueue(options.maxBytes, cacheBytes, async item => {
|
|
26
|
+
const job = this.#jobs.get(item.caches)!;
|
|
27
|
+
if (job.cleanup) {
|
|
28
|
+
await options.runStep(() => store.removeGenerationCheckpoints(job.attempt.key));
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
const current = () => this.#attempts.get(job.attempt.key) === job.attempt && !job.attempt.completed;
|
|
32
|
+
if (!current()) return true;
|
|
33
|
+
const stored = await store.storeGenerationCheckpoint(item.tokens, item.caches, job.metadata!, options.runStep);
|
|
34
|
+
// No newer queued write can run until this callback returns. An old
|
|
35
|
+
// in-flight rename can therefore be removed without deleting new work.
|
|
36
|
+
if (!current()) await options.runStep(() => store.removeGenerationCheckpoints(job.attempt.key));
|
|
37
|
+
return stored;
|
|
38
|
+
}, disposeResources, item => !this.#jobs.get(item.caches)?.cleanup);
|
|
39
|
+
}
|
|
40
|
+
/** A completed response is ineligible immediately, before queued unlink. */
|
|
41
|
+
canRestore(key: string): boolean { return !this.#cleanupPending.has(key) && !this.#cleanupFailures.has(key); }
|
|
42
|
+
begin(key: string): Attempt {
|
|
43
|
+
const attempt = { key, completed: false, pending: 0, failed: false, version: 0, retired: false };
|
|
44
|
+
this.#attempts.set(key, attempt);
|
|
45
|
+
this.#queue.cancelWhere(item => !this.#jobs.get(item.caches)?.cleanup && this.#jobs.get(item.caches)?.attempt.key === key);
|
|
46
|
+
return attempt;
|
|
47
|
+
}
|
|
48
|
+
/** Takes ownership even when a completed/superseded attempt declines it. */
|
|
49
|
+
enqueue(attempt: Attempt, state: Omit<OrdinaryContinuationState, "seed">, metadata: Metadata): void {
|
|
50
|
+
if (attempt.completed || this.#attempts.get(attempt.key) !== attempt) {
|
|
51
|
+
disposeResources(state.caches); return;
|
|
52
|
+
}
|
|
53
|
+
this.#jobs.set(state.caches, { attempt, metadata: { ...metadata } });
|
|
54
|
+
attempt.pending++;
|
|
55
|
+
const version = ++attempt.version;
|
|
56
|
+
let completion: Promise<boolean>;
|
|
57
|
+
try { completion = this.#queue.enqueue({ caches: state.caches, tokens: [...state.cacheTokens], ns: attempt.key }); }
|
|
58
|
+
catch (error) { attempt.pending--; disposeResources(state.caches); throw error; }
|
|
59
|
+
void completion.then(stored => {
|
|
60
|
+
attempt.pending--;
|
|
61
|
+
if (version === attempt.version && !attempt.completed) attempt.failed = !stored;
|
|
62
|
+
this.#releaseSettled(attempt);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
complete(attempt: Attempt): void {
|
|
66
|
+
attempt.completed = true;
|
|
67
|
+
this.#queue.cancelWhere(item => !this.#jobs.get(item.caches)?.cleanup && this.#jobs.get(item.caches)?.attempt === attempt);
|
|
68
|
+
if (this.#attempts.get(attempt.key) === attempt) {
|
|
69
|
+
this.#attempts.delete(attempt.key);
|
|
70
|
+
const barrier = ++this.#nextBarrier;
|
|
71
|
+
const pending = this.#cleanupPending.get(attempt.key) ?? new Set<number>();
|
|
72
|
+
pending.add(barrier); this.#cleanupPending.set(attempt.key, pending);
|
|
73
|
+
const caches: SpillItem["caches"] = [];
|
|
74
|
+
this.#jobs.set(caches, { attempt, cleanup: barrier });
|
|
75
|
+
void this.#queue.enqueue({ caches, tokens: [], ns: attempt.key }).then(stored => {
|
|
76
|
+
const key = attempt.key;
|
|
77
|
+
const outstanding = this.#cleanupPending.get(key)!;
|
|
78
|
+
outstanding.delete(barrier);
|
|
79
|
+
if (!outstanding.size) this.#cleanupPending.delete(key);
|
|
80
|
+
const failures = this.#cleanupFailures.get(key) ?? new Set<number>();
|
|
81
|
+
if (stored) {
|
|
82
|
+
this.#cleanupSucceeded.set(key, Math.max(barrier, this.#cleanupSucceeded.get(key) ?? 0));
|
|
83
|
+
// A successful unlink covers older failures, never a newer barrier.
|
|
84
|
+
for (const failed of failures) if (failed <= barrier) failures.delete(failed);
|
|
85
|
+
} else if (barrier > (this.#cleanupSucceeded.get(key) ?? 0)) failures.add(barrier);
|
|
86
|
+
if (failures.size) this.#cleanupFailures.set(key, failures);
|
|
87
|
+
else this.#cleanupFailures.delete(key);
|
|
88
|
+
if (!outstanding.size) this.#cleanupSucceeded.delete(key);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Request cancellation/disposal preserves queued and durable state. */
|
|
93
|
+
release(attempt: Attempt): void { attempt.retired = true; this.#releaseSettled(attempt); }
|
|
94
|
+
#releaseSettled(attempt: Attempt): void {
|
|
95
|
+
if (attempt.retired && attempt.pending === 0 && !attempt.failed && this.#attempts.get(attempt.key) === attempt)
|
|
96
|
+
this.#attempts.delete(attempt.key);
|
|
97
|
+
}
|
|
98
|
+
get stats() { return { pendingCount: this.#queue.pendingCount, pendingBytes: this.#queue.pendingBytes,
|
|
99
|
+
dropped: this.#queue.droppedCount, failed: this.#queue.failedCount }; }
|
|
100
|
+
async flush(): Promise<{ durable: boolean; pendingBytes: number; failed: number }> {
|
|
101
|
+
await this.#queue.drain();
|
|
102
|
+
const active = [...this.#attempts.values()].filter(attempt => !attempt.completed);
|
|
103
|
+
return { durable: this.#cleanupFailures.size === 0 && active.every(attempt => attempt.pending === 0 && !attempt.failed),
|
|
104
|
+
pendingBytes: this.#queue.pendingBytes, failed: this.#cleanupFailures.size + active.filter(attempt => attempt.failed).length };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { snapshotGenerationPolicy } from "./request-policy";
|
|
2
|
+
import type { GenerateOptions } from "../../generate";
|
|
3
|
+
import type { ResolvedExecution } from "../../contracts/execution";
|
|
4
|
+
import { makeStepSampler } from "../../sampler";
|
|
5
|
+
import { createRowSampling } from "./row-sampling";
|
|
6
|
+
import { bindContinuationPolicy, type OrdinaryContinuation } from "./continuation";
|
|
7
|
+
|
|
8
|
+
/** Optional policy and sampler recovery for the existing ordinary driver.
|
|
9
|
+
* KV layout and delayed conversion remain owned by cache maintenance and persistence. */
|
|
10
|
+
export function createOrdinaryContinuationRequest(input: Omit<Parameters<typeof bindContinuationPolicy>[0], "namespace"> & {
|
|
11
|
+
interval: number;
|
|
12
|
+
persistence: import("./continuation-persistence").ContinuationPersistence;
|
|
13
|
+
onToken: Parameters<typeof createRowSampling>[1];
|
|
14
|
+
}) {
|
|
15
|
+
const options: GenerateOptions = snapshotGenerationPolicy(input.options);
|
|
16
|
+
if (!Number.isSafeInteger(input.interval) || input.interval < 1) throw new Error("continuation interval must be positive");
|
|
17
|
+
if (options.grammar || options.logprobs || options.topLogprobs || options.fill || options.pagedKv ||
|
|
18
|
+
input.execution.method !== "autoregressive")
|
|
19
|
+
throw new Error("ordinary continuation requires replayable ordinary sampling");
|
|
20
|
+
const make = (seed = options.seed, history: readonly number[] = input.prompt) => createRowSampling(makeStepSampler(
|
|
21
|
+
{ ...options, seed }, { tokenRepresentation: "device", grammarWait: "external", historyUpdate: "after-sample",
|
|
22
|
+
initialHistory: [...history] }), input.onToken);
|
|
23
|
+
let sampling = make();
|
|
24
|
+
let policy: ReturnType<typeof bindContinuationPolicy> | undefined;
|
|
25
|
+
const continuation: OrdinaryContinuation = {
|
|
26
|
+
interval: input.interval,
|
|
27
|
+
restore(namespace) { policy = bindContinuationPolicy({ ...input, options, namespace }); return policy.restore(); },
|
|
28
|
+
resumeSampling(state) {
|
|
29
|
+
const next = make(state.seed, [...state.cacheTokens, state.pendingToken]);
|
|
30
|
+
const previous = sampling; sampling = next; previous.dispose();
|
|
31
|
+
},
|
|
32
|
+
captureOwned: state => policy!.captureOwned(state),
|
|
33
|
+
complete: () => policy?.complete(),
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
continuation,
|
|
37
|
+
sample: ((...args) => sampling.sample(...args)) as ReturnType<typeof createRowSampling>["sample"],
|
|
38
|
+
// History-free greedy sampling needs no recovery work in the fast path.
|
|
39
|
+
plainGreedy: sampling.plainGreedy,
|
|
40
|
+
onToken: input.onToken,
|
|
41
|
+
dispose: () => { policy?.release(); sampling.dispose(); },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ContinuationPersistence } from "./continuation-persistence";
|
|
2
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
3
|
+
import type { GenerateOptions } from "../../generate";
|
|
4
|
+
import type { ResolvedExecution } from "../../contracts/execution";
|
|
5
|
+
import type { SsdCacheStore } from "../../ssd-cache";
|
|
6
|
+
import { generationCheckpointKey } from "../../serve/checkpoint-identity";
|
|
7
|
+
import { disposeResources } from "../../engine/resources";
|
|
8
|
+
|
|
9
|
+
export interface OrdinaryContinuationState {
|
|
10
|
+
caches: Cache[];
|
|
11
|
+
cacheTokens: number[];
|
|
12
|
+
generatedTokens: number;
|
|
13
|
+
pendingToken: number;
|
|
14
|
+
seed: number;
|
|
15
|
+
}
|
|
16
|
+
/** Request-owned policy. Restore and captureOwned transfer cache ownership.
|
|
17
|
+
* No store or key knowledge enters the driver. */
|
|
18
|
+
export interface OrdinaryContinuation {
|
|
19
|
+
readonly interval: number;
|
|
20
|
+
restore(namespace: string): OrdinaryContinuationState | null;
|
|
21
|
+
resumeSampling(state: Omit<OrdinaryContinuationState, "caches">): void;
|
|
22
|
+
captureOwned(state: Omit<OrdinaryContinuationState, "seed">): void;
|
|
23
|
+
complete(): void;
|
|
24
|
+
}
|
|
25
|
+
export type ContinuationStore = Pick<SsdCacheStore, "findGenerationCheckpoint" | "restore" |
|
|
26
|
+
"storeGenerationCheckpoint" | "removeGenerationCheckpoints">;
|
|
27
|
+
|
|
28
|
+
/** Shared persistence policy for compatibility and grouped ordinary drivers. */
|
|
29
|
+
export function bindContinuationPolicy(input: {
|
|
30
|
+
store: ContinuationStore;
|
|
31
|
+
restore: (entry: NonNullable<ReturnType<ContinuationStore["findGenerationCheckpoint"]>>) => ReturnType<ContinuationStore["restore"]>;
|
|
32
|
+
prompt: number[]; options: GenerateOptions; execution: ResolvedExecution;
|
|
33
|
+
identity: unknown; namespace: string;
|
|
34
|
+
persistence?: ContinuationPersistence;
|
|
35
|
+
}) {
|
|
36
|
+
const { store, options, prompt, namespace } = input;
|
|
37
|
+
const key = generationCheckpointKey(prompt, options, namespace, input.execution, input.identity);
|
|
38
|
+
let seed = options.seed ?? 0;
|
|
39
|
+
const attempt = input.persistence?.begin(key);
|
|
40
|
+
return {
|
|
41
|
+
key,
|
|
42
|
+
restore(): OrdinaryContinuationState | null {
|
|
43
|
+
if (input.persistence && !input.persistence.canRestore(key)) return null;
|
|
44
|
+
const entry = store.findGenerationCheckpoint(prompt, key, namespace);
|
|
45
|
+
const restored = entry ? input.restore(entry) : null;
|
|
46
|
+
if (!restored) return null;
|
|
47
|
+
const metadata = restored.header.generationCheckpoint;
|
|
48
|
+
if (!metadata || metadata.originalPromptTokens !== prompt.length ||
|
|
49
|
+
metadata.generatedTokens !== restored.tokens.length - prompt.length) {
|
|
50
|
+
disposeResources(restored.caches);
|
|
51
|
+
throw new Error("generation checkpoint has inconsistent continuation metadata");
|
|
52
|
+
}
|
|
53
|
+
seed = metadata.seed;
|
|
54
|
+
return { caches: restored.caches, cacheTokens: restored.tokens,
|
|
55
|
+
generatedTokens: metadata.generatedTokens, pendingToken: metadata.pendingToken, seed };
|
|
56
|
+
},
|
|
57
|
+
async capture(state: Omit<OrdinaryContinuationState, "seed">): Promise<void> {
|
|
58
|
+
const stored = await store.storeGenerationCheckpoint(state.cacheTokens, state.caches, {
|
|
59
|
+
key, cacheNs: namespace, originalPromptTokens: prompt.length,
|
|
60
|
+
generatedTokens: state.generatedTokens, pendingToken: state.pendingToken,
|
|
61
|
+
seed, seedWasExplicit: options.seedWasExplicit === true,
|
|
62
|
+
});
|
|
63
|
+
if (stored) console.log(`[generation-checkpoint] saved ${state.generatedTokens} emitted tokens`);
|
|
64
|
+
},
|
|
65
|
+
captureOwned(state: Omit<OrdinaryContinuationState, "seed">): void {
|
|
66
|
+
if (!input.persistence || !attempt) { disposeResources(state.caches); throw new Error("owned continuation requires persistence coordinator"); }
|
|
67
|
+
input.persistence.enqueue(attempt, state, { key, cacheNs: namespace, originalPromptTokens: prompt.length,
|
|
68
|
+
generatedTokens: state.generatedTokens, pendingToken: state.pendingToken,
|
|
69
|
+
seed, seedWasExplicit: options.seedWasExplicit === true });
|
|
70
|
+
},
|
|
71
|
+
release() { if (input.persistence && attempt) input.persistence.release(attempt); },
|
|
72
|
+
complete() {
|
|
73
|
+
if (input.persistence && attempt) input.persistence.complete(attempt);
|
|
74
|
+
else store.removeGenerationCheckpoints(key);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DenoisingGraph } from "../../inference/denoising";
|
|
2
|
+
import type { MlxArray } from "../../mlx/array";
|
|
3
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
4
|
+
import type { DiffusionGemmaModel } from "../../model/diffusion-gemma";
|
|
5
|
+
import type { MlxModelMemory } from "./autoregressive";
|
|
6
|
+
import { disposeResources } from "../../engine/resources";
|
|
7
|
+
import { runtimeConfig, type RuntimeConfig } from "../../runtime-config";
|
|
8
|
+
|
|
9
|
+
export interface MlxDenoisingBinding<State = Cache[]> {
|
|
10
|
+
readonly runtime?: RuntimeConfig;
|
|
11
|
+
readonly graph: DenoisingGraph<MlxArray, State>;
|
|
12
|
+
readonly memory: MlxModelMemory;
|
|
13
|
+
readonly adapters?: { active: string[] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function bindLegacyDenoisingModel(model: DiffusionGemmaModel): MlxDenoisingBinding {
|
|
17
|
+
return {
|
|
18
|
+
runtime: runtimeConfig(),
|
|
19
|
+
memory: model, adapters: model.loraState,
|
|
20
|
+
graph: {
|
|
21
|
+
descriptor: Object.freeze({ id: "legacy-diffusion-gemma", backend: "mlx",
|
|
22
|
+
graphAbi: "mlx-denoising-v1", stateAbi: "legacy-cache-array-v1", artifact: "legacy-resident-model" }),
|
|
23
|
+
vocabSize: model.config.text.vocabSize, canvasLength: model.canvasLength, embedScale: model.embedScale,
|
|
24
|
+
prefill: (ids, vision) => vision ? model.prefillVision(ids, vision) : model.prefill(ids),
|
|
25
|
+
extendPrefill: model.extendPrefill.bind(model),
|
|
26
|
+
decoderLogits: model.decoderLogits.bind(model),
|
|
27
|
+
dequantEmbedWeight: model.dequantEmbedWeight.bind(model),
|
|
28
|
+
softEmbeddings: model.softEmbeddings.bind(model),
|
|
29
|
+
closeState: disposeResources,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function assertMlxDenoisingGraph<State>(graph: DenoisingGraph<MlxArray, State>): void {
|
|
35
|
+
const descriptor = graph.descriptor;
|
|
36
|
+
if (descriptor.backend !== "mlx" || descriptor.graphAbi !== "mlx-denoising-v1" ||
|
|
37
|
+
!descriptor.stateAbi)
|
|
38
|
+
throw new Error(`denoising graph ${descriptor.id} has an incompatible backend, graph, or state ABI`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readAssistantDonors } from "./assistant-target";
|
|
2
|
+
import type { RuntimeModel } from "../../model/factory";
|
|
3
|
+
import { Gemma4Model, type Cache } from "../../model/gemma4";
|
|
4
|
+
import { Qwen35Model } from "../../model/qwen3_5";
|
|
5
|
+
import type { MlxArray } from "../../mlx/array";
|
|
6
|
+
import * as ops from "../../mlx/ops";
|
|
7
|
+
import type { TargetView } from "../../spec/source";
|
|
8
|
+
|
|
9
|
+
/** Class checks and live cache layout stay at the compatibility boundary.
|
|
10
|
+
* A replacement graph supplies the same ports without becoming these classes. */
|
|
11
|
+
export function bindLegacyDraftTarget(model: RuntimeModel, caches: Cache[]): TargetView {
|
|
12
|
+
if (model instanceof Gemma4Model) {
|
|
13
|
+
let sliding = -1, full = -1;
|
|
14
|
+
for (let i = 0; i < model.numDonors; i++) {
|
|
15
|
+
if (model.layers[i]!.layerType === "sliding_attention") sliding = i;
|
|
16
|
+
else full = i;
|
|
17
|
+
}
|
|
18
|
+
return Object.freeze({ identity: model, assistantRows: sliding >= 0 && full >= 0 ? {
|
|
19
|
+
hiddenSize: model.config.text.hiddenSize,
|
|
20
|
+
embed(ids: MlxArray) { using embedded = model.embed.encode(ids); return ops.mulScalar(embedded, model.embedScale); },
|
|
21
|
+
readDonors: () => readAssistantDonors(caches[sliding]!, caches[full]!),
|
|
22
|
+
} : undefined, gemmaTaps: Object.freeze({
|
|
23
|
+
layerCount: model.layers.length,
|
|
24
|
+
projection: Object.freeze({ embed: model.embed, logitsFromHidden: model.logitsFromHidden.bind(model) }),
|
|
25
|
+
}) });
|
|
26
|
+
}
|
|
27
|
+
if (model instanceof Qwen35Model) return Object.freeze({ identity: model, qwenMtp: Object.freeze({
|
|
28
|
+
hiddenSize: model.config.text.hiddenSize, layerCount: model.config.text.numHiddenLayers,
|
|
29
|
+
embed: model.embed.encode.bind(model.embed), logitsFromHidden: model.logitsFromHidden.bind(model),
|
|
30
|
+
}) });
|
|
31
|
+
return Object.freeze({ identity: model });
|
|
32
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { TaskModel, GenOpts } from "../../eval/runner";
|
|
2
|
+
import type { CompletionClient } from "../../contracts/completion";
|
|
3
|
+
import { generate } from "../../generate";
|
|
4
|
+
import { clearCache } from "../../mlx/ffi";
|
|
5
|
+
import * as ops from "../../mlx/ops";
|
|
6
|
+
import { runtimeValue } from "../../runtime-config";
|
|
7
|
+
import { DiffusionGemmaModel } from "../../model/diffusion-gemma";
|
|
8
|
+
|
|
9
|
+
/** Bit-exact greedy decode: raw `model.forward` + argmax loop, which matches mlx-lm
|
|
10
|
+
* token-for-token (gen8k: 8000/8000). The product `generate()` decode wrapper
|
|
11
|
+
* (forwardHidden/logitsFromHidden + pipelined sampler) diverges from the raw forward
|
|
12
|
+
* on near-ties past ~32 tokens, so the EVAL decodes bit-exactly itself rather than
|
|
13
|
+
* alter the serving path. Greedy + full-precision KV only. */
|
|
14
|
+
export function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: number): string {
|
|
15
|
+
if (tm.activeAdapters) tm.model.loraState.active = tm.activeAdapters; // apply the mounted adapter
|
|
16
|
+
const cache = tm.model.makeCache();
|
|
17
|
+
try {
|
|
18
|
+
// Chunked, LM-head-free prefill. The old single-shot `forward(prompt)`
|
|
19
|
+
// materialized [1, L, vocab] logits just to discard them — at a 248k
|
|
20
|
+
// vocab that's ~1 GB per 1k prompt tokens, and with no allocator-cache
|
|
21
|
+
// clears the Metal buffer cache retained every giant freed tensor
|
|
22
|
+
// (the 27B capability-suite 73 GB swap-thrash). Prefill only needs the
|
|
23
|
+
// cache state, which forwardHidden advances identically (the LM head
|
|
24
|
+
// never touches the cache) — bit-exact by construction. Chunk size and
|
|
25
|
+
// clear cadence mirror mlx-lm generate_step / our generate().
|
|
26
|
+
const PREFILL_CHUNK = 2048;
|
|
27
|
+
const prompt = ids.slice(0, -1);
|
|
28
|
+
for (let s = 0; s < prompt.length; s += PREFILL_CHUNK) {
|
|
29
|
+
const chunk = prompt.slice(s, s + PREFILL_CHUNK);
|
|
30
|
+
const cIds = ops.fromInt32(chunk, [1, chunk.length]);
|
|
31
|
+
tm.model.forwardHidden(cIds, cache).dispose();
|
|
32
|
+
cIds.dispose();
|
|
33
|
+
clearCache();
|
|
34
|
+
}
|
|
35
|
+
let last = ids[ids.length - 1]!;
|
|
36
|
+
const eos = new Set(tm.config.eosTokenIds);
|
|
37
|
+
const out: number[] = [];
|
|
38
|
+
for (let i = 0; i < maxTokens; i++) {
|
|
39
|
+
const lg = tm.model.forward([last], cache);
|
|
40
|
+
const am = ops.argmaxAxis(lg, -1); // GPU argmax (mlx-lm greedy: lowest-index tie-break)
|
|
41
|
+
lg.dispose();
|
|
42
|
+
const best = ops.itemUint32(am);
|
|
43
|
+
am.dispose();
|
|
44
|
+
if (i % 256 === 0) clearCache(); // mlx-lm decode cadence
|
|
45
|
+
if (eos.has(best)) break; // mlx-lm halts on EOS and excludes it from the output
|
|
46
|
+
out.push(best);
|
|
47
|
+
last = best;
|
|
48
|
+
}
|
|
49
|
+
return tm.tokenizer.decode(out, true);
|
|
50
|
+
} finally {
|
|
51
|
+
for (const c of cache) c.dispose();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Complete `body` via the model's real quantized-KV path. Greedy by default;
|
|
56
|
+
* pass `opts.sampler` to drive temperature/HLG/etc. */
|
|
57
|
+
async function generateTextNative(tm: TaskModel, body: string, opts: GenOpts = {}): Promise<string> {
|
|
58
|
+
const maxTokens = opts.maxTokens ?? 256;
|
|
59
|
+
const enableThinking = opts.enableThinking ?? runtimeValue("MLX_BUN_EVAL_THINK") === "1";
|
|
60
|
+
const templated = opts.useChat !== false && tm.template !== null;
|
|
61
|
+
const text = templated
|
|
62
|
+
? tm.template!.render([{ role: "user", content: body }], { addGenerationPrompt: true, enableThinking })
|
|
63
|
+
: body;
|
|
64
|
+
// The chat template ALREADY emits the BOS; encoding it with add_special_tokens on
|
|
65
|
+
// would prepend a SECOND BOS (<s><s>…), corrupting generation. Raw bodies (no
|
|
66
|
+
// template) get the BOS added as normal.
|
|
67
|
+
const ids = tm.tokenizer.encode(text, /* addSpecialTokens */ !templated);
|
|
68
|
+
|
|
69
|
+
// optiq's PUBLISHED eval generates with a FULL-PRECISION KV cache — its eval calls
|
|
70
|
+
// plain mlx_lm.generate (kv-quant lives in the serving runtime, not the eval). So
|
|
71
|
+
// for like-for-like parity we default the eval to UNQUANTIZED KV; MMLU's argmax
|
|
72
|
+
// path was already unquantized, which is why it matched while generation didn't.
|
|
73
|
+
// MLX_BUN_EVAL_KV_QUANT=1 generates through the model's quantized KV (serving
|
|
74
|
+
// config); an explicit opts.kvScheme (cli generate's resolved tier route) wins.
|
|
75
|
+
const envKv: GenOpts["kvScheme"] | undefined =
|
|
76
|
+
(runtimeValue("MLX_BUN_EVAL_KV_QUANT") === "1" && tm.config.kvQuant?.length)
|
|
77
|
+
? { kvConfig: tm.config.kvQuant, quantizedKvStart: 0 } : undefined;
|
|
78
|
+
const kvScheme = opts.kvScheme ?? envKv;
|
|
79
|
+
const kvActive = !!(kvScheme && (kvScheme.kvBits || kvScheme.kvConfig?.length || kvScheme.turboQuant));
|
|
80
|
+
|
|
81
|
+
// Parity default — greedy + full-precision KV + no sampler arm: decode bit-exactly
|
|
82
|
+
// via the raw forward (matches mlx-lm token-for-token). Sampler arms / kv-quant fall
|
|
83
|
+
// through to the product generate(). DiffusionGemma is non-autoregressive (no AR
|
|
84
|
+
// forward) — always go through generate(), which routes it to the denoising engine.
|
|
85
|
+
const isDiffusion = tm.model instanceof DiffusionGemmaModel;
|
|
86
|
+
if (!isDiffusion && !kvActive && !opts.sampler && !tm.samplerOverride) {
|
|
87
|
+
return greedyDecodeBitExact(tm, ids, maxTokens);
|
|
88
|
+
}
|
|
89
|
+
const gen = generate(tm.model, ids, {
|
|
90
|
+
maxTokens,
|
|
91
|
+
temperature: 0, // greedy default — deterministic head-to-head arms
|
|
92
|
+
adapters: tm.activeAdapters ?? [], // per-request activation of the mounted eval-adapter
|
|
93
|
+
...(kvActive ? kvScheme : {}),
|
|
94
|
+
...(opts.sampler ?? {}), // overrides temperature when provided
|
|
95
|
+
...(tm.samplerOverride ?? {}), // arm override wins over the task's own sampler
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const out: number[] = [];
|
|
99
|
+
for await (const { token } of gen) out.push(token);
|
|
100
|
+
return tm.tokenizer.decode(out, true);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createEvalCompletionClient(tm: TaskModel): CompletionClient<{ body: string; options?: GenOpts }, string> {
|
|
104
|
+
return { complete: ({ body, options }) => generateTextNative(tm, body, options) };
|
|
105
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { MlxArray } from "../../mlx/array";
|
|
2
|
+
import * as ops from "../../mlx/ops";
|
|
3
|
+
import type { Cache } from "../../model/gemma4";
|
|
4
|
+
import { withResource } from "../../engine/resources";
|
|
5
|
+
import { leaseCacheStates } from "./state-views";
|
|
6
|
+
|
|
7
|
+
/** Append an already accepted span without intermediate vocabulary heads.
|
|
8
|
+
* Chunking changes the graph shape, so callers must qualify its numerics for
|
|
9
|
+
* their model. A verify round must use chunkSize=0: recurrent rollback records
|
|
10
|
+
* one forward per round. The complete append precedes emission/cancellation
|
|
11
|
+
* boundaries, preserving the cache-covered prefix on a consumer break.
|
|
12
|
+
*/
|
|
13
|
+
export async function appendFillHidden(
|
|
14
|
+
forward: (ids: MlxArray, cache: Cache[]) => MlxArray | Promise<MlxArray>,
|
|
15
|
+
cache: Cache[],
|
|
16
|
+
ids: number[],
|
|
17
|
+
chunkSize: (cache: readonly Cache[]) => number,
|
|
18
|
+
): Promise<MlxArray> {
|
|
19
|
+
const input = ops.fromInt32(ids, [1, ids.length]);
|
|
20
|
+
const hidden: MlxArray[] = [];
|
|
21
|
+
try {
|
|
22
|
+
for (let start = 0; start < ids.length;) {
|
|
23
|
+
const limit = chunkSize(cache);
|
|
24
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
25
|
+
throw new Error("Model append chunk limit must be a positive integer");
|
|
26
|
+
const end = Math.min(ids.length, start + limit);
|
|
27
|
+
if (start === 0 && end === ids.length) return await forward(input, cache);
|
|
28
|
+
const part = input.slice([0, start], [1, end]);
|
|
29
|
+
try {
|
|
30
|
+
const h = await forward(part, cache);
|
|
31
|
+
hidden.push(h);
|
|
32
|
+
// Bound transient graphs while retaining all positions for an optional
|
|
33
|
+
// diagnostic trace. Normal generation projects only the last position.
|
|
34
|
+
withResource(leaseCacheStates(cache), (state) => ops.evalAll([h, ...state]));
|
|
35
|
+
} finally {
|
|
36
|
+
part.dispose();
|
|
37
|
+
}
|
|
38
|
+
start = end;
|
|
39
|
+
}
|
|
40
|
+
return ops.concatAxis(hidden, 1);
|
|
41
|
+
} finally {
|
|
42
|
+
for (const h of hidden) h.dispose();
|
|
43
|
+
input.dispose();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MlxArray } from "../../mlx/array";
|
|
2
|
+
import * as ops from "../../mlx/ops";
|
|
3
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
4
|
+
import { fullRowPadding } from "../../model/full-prefill-row";
|
|
5
|
+
import { disposeResources } from "../../engine/resources";
|
|
6
|
+
|
|
7
|
+
/** Assemble full-attention tensors at the model's B while storage retains
|
|
8
|
+
* per-row formats and positions. Returned arrays own their captured state. */
|
|
9
|
+
export function appendFullKvRows(rows: readonly Cache[], leftPad: readonly number[], width: number,
|
|
10
|
+
k: MlxArray, v: MlxArray, deferredValues = false): [MlxArray, MlxArray] {
|
|
11
|
+
const parts: [MlxArray, MlxArray][] = [], padded: MlxArray[] = [];
|
|
12
|
+
try {
|
|
13
|
+
for (const [index, row] of rows.entries()) {
|
|
14
|
+
using kr = k.slice([index, 0, 0, 0], [index + 1, k.shape[1]!, k.shape[2]!, k.shape[3]!]);
|
|
15
|
+
using vr = v.slice([index, 0, 0, 0], [index + 1, v.shape[1]!, v.shape[2]!, v.shape[3]!]);
|
|
16
|
+
parts.push(deferredValues && row.rotatedValueAttention
|
|
17
|
+
? row.rotatedValueAttention.updateAndFetchDeferredV(kr, vr) : row.updateAndFetch(kr, vr));
|
|
18
|
+
}
|
|
19
|
+
const combine = (field: 0 | 1) => {
|
|
20
|
+
const views = parts.map((part, row) => {
|
|
21
|
+
const a = part[field], before = leftPad[row]! - fullRowPadding(rows[row]!), after = width - before - a.shape[2]!;
|
|
22
|
+
if (!before && !after) return a;
|
|
23
|
+
using prefix = ops.zeros([1, a.shape[1]!, before, a.shape[3]!], a.dtype);
|
|
24
|
+
using suffix = ops.zeros([1, a.shape[1]!, after, a.shape[3]!], a.dtype);
|
|
25
|
+
const result = ops.concatAxis([prefix, a, suffix], 2); padded.push(result); return result;
|
|
26
|
+
});
|
|
27
|
+
return views.length === 1 ? ops.contiguous(views[0]!) : ops.concatAxis(views, 0);
|
|
28
|
+
};
|
|
29
|
+
const keys = combine(0);
|
|
30
|
+
try { return [keys, combine(1)]; } catch (error) { keys.dispose(); throw error; }
|
|
31
|
+
} finally { disposeResources([...parts.flat(), ...padded]); }
|
|
32
|
+
}
|