mlx-bun 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -588
- package/docs/README.md +91 -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/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/design/batching.md +569 -0
- package/docs/design/decode-speed-program.md +3178 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +579 -334
- package/docs/design/kv-cache.md +680 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1627 -0
- package/docs/design/turboquant.md +2102 -0
- package/docs/design/unified-engine-frontier-plan.md +1831 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +34 -1
- package/docs/planning/release-notes-v0.3.0.md +35 -0
- package/docs/planning/video-series.md +2 -2
- package/docs/reference/benchmarks.md +1162 -0
- package/docs/reference/cli.md +394 -159
- package/docs/reference/distribution.md +321 -182
- package/docs/reference/environment.md +348 -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 +104 -6
- package/docs/reference/server-api.md +886 -425
- package/docs/reference/server-config.md +638 -330
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +166 -0
- package/package.json +9 -4
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1646 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +47 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/gateway-binding.ts +71 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/memory-client.ts +189 -0
- package/src/backends/mlx/memory-guard.ts +40 -0
- package/src/backends/mlx/methods.ts +90 -0
- package/src/backends/mlx/model-prompt.ts +164 -0
- package/src/backends/mlx/model-serving.ts +79 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/rollback.ts +51 -0
- package/src/backends/mlx/serial-executor.ts +273 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-views.ts +26 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/cli.ts +58 -25
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +49 -0
- package/src/contracts/generation.ts +103 -0
- package/src/contracts/host.ts +7 -0
- package/src/contracts/jobs.ts +66 -0
- package/src/contracts/pi-provider.ts +30 -0
- package/src/contracts/pi.ts +259 -0
- package/src/contracts/resources.ts +18 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/contracts/tsconfig.json +14 -0
- package/src/diffusion/diffusion-generate.ts +218 -153
- package/src/embed.ts +1 -1
- package/src/engine/admission.ts +59 -0
- package/src/engine/cancellation.ts +38 -0
- package/src/engine/engine.ts +56 -0
- package/src/engine/execution-coordinator.ts +70 -0
- package/src/engine/execution-plan.ts +51 -0
- package/src/engine/resources.ts +42 -0
- package/src/engine/scheduler.ts +67 -0
- package/src/engine/session.ts +247 -0
- package/src/eval/README.md +44 -1
- package/src/eval/kl.ts +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 +1 -1
- package/src/generate.ts +656 -268
- 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/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-store.ts +109 -49
- 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/{model → lab/paged-kv}/paged-kv.ts +9 -6
- package/src/library.ts +40 -0
- package/src/lora.ts +18 -0
- package/src/media-fetch.ts +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 +9 -10
- 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/batched-mask.ts +8 -2
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +2 -2
- package/src/model/batched-rotating.ts +1 -1
- package/src/model/compiled-decode.ts +1 -1
- 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/gemma4-base.ts +26 -9
- package/src/model/gemma4.ts +6 -6
- package/src/model/generated/gemma4-12b.ts +2 -2
- package/src/model/generated/gemma4-26b.ts +2 -2
- package/src/model/generated/gemma4-e4b.ts +2 -2
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +1 -1
- package/src/model/implementation.ts +69 -0
- package/src/model/minicpm5.ts +6 -8
- 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_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +140 -56
- package/src/model/support.ts +1 -1
- package/src/model/trellis-balanced-scatter.ts +51 -0
- package/src/model/trellis-linear.ts +672 -0
- package/src/model/trellis-shared-m.ts +151 -0
- package/src/model/trellis-shared-scatter.ts +144 -0
- package/src/model/trellis-splitk-prefill.ts +158 -0
- package/src/model/trellis-tiled-prefill.ts +136 -0
- package/src/model/trellis-vector-expand.ts +62 -0
- package/src/model/trellis-wide-prefill.ts +94 -0
- package/src/model/universal/dense.ts +1 -1
- package/src/model/universal/modules.ts +8 -1
- package/src/native-pack.ts +6 -5
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +8 -315
- package/src/prompt-cache.ts +96 -53
- 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 +4 -4
- 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 +154 -193
- 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 +512 -0
- package/src/serve/openai-wire.ts +103 -0
- package/src/serve/parent-application.ts +56 -0
- package/src/serve/pipeline.ts +33 -0
- package/src/serve/preparation.ts +33 -0
- package/src/serve/request-plan.ts +19 -15
- package/src/serve/request-prep.ts +346 -0
- package/src/serve/response-lease.ts +24 -0
- package/src/serve/responses-client.ts +69 -0
- package/src/serve/session-completion-engine.ts +110 -0
- package/src/serve/session-search.ts +1 -1
- package/src/serve/static-routes.ts +5 -5
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +317 -2649
- package/src/spec/assistant-source.ts +10 -44
- package/src/spec/deepspec-source.ts +6 -6
- package/src/spec/dflash-source.ts +8 -8
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +2 -2
- package/src/spec/dspark/generate-dflash.ts +2 -2
- package/src/spec/dspark/generate.ts +2 -2
- package/src/spec/dspark/loader.ts +1 -1
- package/src/spec/dspark/module-dflash.ts +4 -3
- package/src/spec/dspark/module.ts +3 -3
- package/src/spec/dspark/quantize-drafter.ts +1 -1
- package/src/spec/generate.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +1 -1
- package/src/spec/ngram-source.ts +1 -1
- package/src/spec/prefix-state.ts +45 -0
- package/src/spec/qwen-mtp-source.ts +166 -69
- package/src/spec/serve-loop.ts +154 -156
- package/src/spec/source.ts +42 -10
- package/src/spec/two-model.ts +1 -1
- package/src/ssd-cache.ts +80 -12
- package/src/ssd-durability.ts +25 -13
- 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 +5 -3
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +4 -4
- package/src/web/src/composer.ts +4 -4
- package/src/web/src/hub.ts +1 -1
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +1 -1
- package/src/web/src/model-picker.ts +1 -1
- package/src/web/src/palette.ts +1 -1
- package/src/web/src/protocol.ts +3 -39
- package/src/web/src/sessions.ts +1 -1
- package/src/web/src/ui-catalog.ts +4 -18
- package/src/web-assets.ts +47 -0
- package/src/weights.ts +55 -9
- package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/archive/investigations/hlg-ab.html +0 -34
- package/docs/archive/investigations/hlg-explorer.html +0 -238
- package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
- package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/archive/investigations/hlg-report.html +0 -262
- package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/archive/mlx-bun-lab-report.html +0 -275
- package/docs/archive/wwdc-mlx-bun.html +0 -848
- package/docs/design/adapters-end-to-end.md +0 -205
- package/docs/design/batching-perf-path.md +0 -255
- package/docs/design/batching-v2-plan.md +0 -529
- package/docs/design/bucketing-stage.md +0 -197
- package/docs/design/compat-cli-surface-design.md +0 -230
- package/docs/design/docs-reorg-plan.md +0 -165
- package/docs/design/dspark-serving-program.md +0 -371
- package/docs/design/dspark-speculative-decoding.md +0 -318
- package/docs/design/grammar-spec-batching-integration.md +0 -301
- package/docs/design/memory-inference-path.md +0 -422
- package/docs/design/memory-synthesis.md +0 -287
- package/docs/design/memory-system.md +0 -237
- package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
- package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
- package/docs/design/omlx-adoption-map.md +0 -98
- package/docs/design/orpo-dynamic-lambda.md +0 -220
- package/docs/design/orpo-future-enhancements.md +0 -340
- package/docs/design/paged-kv-cache.md +0 -214
- package/docs/design/parallel-slots.md +0 -567
- package/docs/design/parity-tier-dag.md +0 -161
- package/docs/design/repo-cleanup-plan.md +0 -231
- package/docs/design/runtime-isolation.md +0 -263
- package/docs/design/segmented-backward-training.md +0 -543
- package/docs/design/spec-decode-larger-targets.md +0 -229
- package/docs/design/ssd-kv-cold-tier.md +0 -415
- package/docs/design/structured-output.md +0 -484
- package/docs/design/tauri-desktop-app.md +0 -127
- package/docs/design/the-dreaming-master-plan.md +0 -445
- package/docs/design/turboquant-kv.md +0 -235
- package/docs/design/turboquant-weights.md +0 -467
- package/docs/design/w4a16-compute-precision-spike.md +0 -232
- package/docs/design/web-chat-beat-matrix.md +0 -315
- package/docs/design/write-pipeline-entity.md +0 -468
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -101
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -11
- /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
- /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
- /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
- /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
- /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
- /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
package/src/jobs/runner.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CancellationSource } from "../engine/cancellation";
|
|
1
2
|
// Job runner: turns a registered `JobRunner` into a tracked, logged job.
|
|
2
3
|
// Two execution modes — both create a row, both stream NDJSON events to the
|
|
3
4
|
// log, both own terminal status:
|
|
@@ -14,6 +15,8 @@
|
|
|
14
15
|
// Port of optiq lab jobs.py `submit` + `_job_main`, split across this file
|
|
15
16
|
// and job-entry.ts.
|
|
16
17
|
|
|
18
|
+
import type { DisposableResource } from "../contracts/resources";
|
|
19
|
+
import { createJobTaskClient } from "./task-client";
|
|
17
20
|
import { appendFileSync } from "node:fs";
|
|
18
21
|
import { fileURLToPath } from "node:url";
|
|
19
22
|
import type { Emit, JobEvent, JobRunner } from "./types";
|
|
@@ -104,18 +107,28 @@ export interface SubmitResult {
|
|
|
104
107
|
* on success sets 'done'/progress 1/ended_at + emits 'done', on throw sets
|
|
105
108
|
* 'failed'/error/ended_at + emits 'failed'. Returns once the task is
|
|
106
109
|
* scheduled (does not await completion — poll the row / tail the log). */
|
|
110
|
+
interface InProcessWork { cancellation: CancellationSource; done: Promise<void>; }
|
|
111
|
+
const inProcess = new WeakMap<JobStore, Set<InProcessWork>>();
|
|
112
|
+
|
|
107
113
|
export function submitInProcess(
|
|
108
114
|
store: JobStore,
|
|
109
115
|
kind: string,
|
|
110
116
|
config: Record<string, unknown>,
|
|
111
117
|
outputPath?: string,
|
|
112
118
|
): SubmitResult {
|
|
119
|
+
if (closedStores.has(store)) throw new Error("job host is closed");
|
|
113
120
|
const row = store.create(kind, config, outputPath);
|
|
114
121
|
const emit = makeEmit(store, row.id, row.log_path);
|
|
115
122
|
store.setStatus(row.id, "running");
|
|
116
123
|
emit({ type: "started", ts: Date.now() });
|
|
117
124
|
|
|
118
125
|
const runner = getRunner(kind);
|
|
126
|
+
const cancellation = new CancellationSource();
|
|
127
|
+
const running = inProcess.get(store) ?? new Set<InProcessWork>();
|
|
128
|
+
inProcess.set(store, running);
|
|
129
|
+
let finish!: () => void;
|
|
130
|
+
const work = { cancellation, done: new Promise<void>((resolve) => { finish = resolve; }) };
|
|
131
|
+
running.add(work);
|
|
119
132
|
void (async () => {
|
|
120
133
|
if (!runner) {
|
|
121
134
|
const error = `no runner registered for kind "${kind}"`;
|
|
@@ -124,7 +137,7 @@ export function submitInProcess(
|
|
|
124
137
|
return;
|
|
125
138
|
}
|
|
126
139
|
try {
|
|
127
|
-
const result = await runner(
|
|
140
|
+
const result = await createJobTaskClient(runner).run(JSON.parse(row.config_json), emit, cancellation);
|
|
128
141
|
const out = result?.outputPath;
|
|
129
142
|
if (out) store.setOutputPath(row.id, out);
|
|
130
143
|
store.setProgress(row.id, 1);
|
|
@@ -135,7 +148,7 @@ export function submitInProcess(
|
|
|
135
148
|
store.setStatus(row.id, "failed", { error, endedAt: nowIso() });
|
|
136
149
|
emit({ type: "failed", error, ts: Date.now() });
|
|
137
150
|
}
|
|
138
|
-
})();
|
|
151
|
+
})().finally(() => { running.delete(work); finish(); });
|
|
139
152
|
|
|
140
153
|
return { jobId: row.id, outputPath };
|
|
141
154
|
}
|
|
@@ -145,16 +158,25 @@ export function submitInProcess(
|
|
|
145
158
|
// ---------------------------------------------------------------------------
|
|
146
159
|
|
|
147
160
|
interface QueuedSpawn {
|
|
161
|
+
acquire?: (signal: AbortSignal) => Promise<DisposableResource>;
|
|
162
|
+
lease?: DisposableResource;
|
|
163
|
+
abort: AbortController;
|
|
164
|
+
proc?: Bun.Subprocess;
|
|
165
|
+
finished: Promise<void>;
|
|
166
|
+
finish(): void;
|
|
148
167
|
store: JobStore;
|
|
149
168
|
jobId: string;
|
|
150
169
|
entry: string;
|
|
151
170
|
bin: string;
|
|
171
|
+
compiled: boolean;
|
|
152
172
|
spawn: typeof Bun.spawn;
|
|
153
173
|
onComplete?: (jobId: string, code: number) => void;
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
let gpuLeaseHolder: string | null = null;
|
|
157
177
|
const spawnQueue: QueuedSpawn[] = [];
|
|
178
|
+
let activeSpawn: QueuedSpawn | undefined;
|
|
179
|
+
const closedStores = new WeakSet<JobStore>();
|
|
158
180
|
|
|
159
181
|
/** True while a GPU (subprocess) job is running — the server gates token
|
|
160
182
|
* generation on this so quantize/finetune don't fight live inference. */
|
|
@@ -168,6 +190,8 @@ export function currentGpuJob(): string | null {
|
|
|
168
190
|
}
|
|
169
191
|
|
|
170
192
|
export interface SubprocessOpts {
|
|
193
|
+
/** Host execution lease, held until child exit (including crashes). */
|
|
194
|
+
acquire?: (signal: AbortSignal) => Promise<DisposableResource>;
|
|
171
195
|
/** Override the entry script (tests). Defaults to JOB_ENTRY_PATH. */
|
|
172
196
|
entry?: string;
|
|
173
197
|
/** Override the runtime binary (tests). Defaults to "bun". */
|
|
@@ -189,14 +213,20 @@ export function submitSubprocess(
|
|
|
189
213
|
outputPath?: string,
|
|
190
214
|
opts: SubprocessOpts = {},
|
|
191
215
|
): SubmitResult {
|
|
216
|
+
if (closedStores.has(store)) throw new Error("job host is closed");
|
|
192
217
|
const row = store.create(kind, config, outputPath);
|
|
218
|
+
let finish!: () => void;
|
|
219
|
+
const finished = new Promise<void>((resolve) => { finish = resolve; });
|
|
193
220
|
const item: QueuedSpawn = {
|
|
221
|
+
abort: new AbortController(), finished, finish,
|
|
194
222
|
store,
|
|
195
223
|
jobId: row.id,
|
|
196
224
|
entry: opts.entry ?? JOB_ENTRY_PATH,
|
|
225
|
+
compiled: opts.entry === undefined && opts.bin === undefined && JOB_ENTRY_PATH.includes("$bunfs"),
|
|
197
226
|
bin: opts.bin ?? "bun",
|
|
198
227
|
spawn: opts.spawn ?? Bun.spawn,
|
|
199
228
|
onComplete: opts.onComplete,
|
|
229
|
+
acquire: opts.acquire,
|
|
200
230
|
};
|
|
201
231
|
spawnQueue.push(item);
|
|
202
232
|
drainQueue();
|
|
@@ -215,7 +245,7 @@ function spawnNow(item: QueuedSpawn): void {
|
|
|
215
245
|
// a file-backed DB.
|
|
216
246
|
let proc: Bun.Subprocess<"ignore", "pipe", "pipe">;
|
|
217
247
|
try {
|
|
218
|
-
proc = spawn([bin, entry, jobId], {
|
|
248
|
+
proc = spawn(item.compiled ? [process.execPath, "__job", jobId] : [bin, entry, jobId], {
|
|
219
249
|
stdout: "pipe",
|
|
220
250
|
stderr: "pipe",
|
|
221
251
|
env: {
|
|
@@ -230,14 +260,16 @@ function spawnNow(item: QueuedSpawn): void {
|
|
|
230
260
|
error: errString(e),
|
|
231
261
|
endedAt: nowIso(),
|
|
232
262
|
});
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(`[jobs] failed to record spawn failure: ${errString(error)}`);
|
|
233
265
|
} finally {
|
|
234
|
-
releaseLease(
|
|
266
|
+
releaseLease(item);
|
|
235
267
|
}
|
|
236
268
|
return;
|
|
237
269
|
}
|
|
238
270
|
|
|
239
|
-
|
|
240
|
-
const logPath =
|
|
271
|
+
item.proc = proc;
|
|
272
|
+
const logPath = store.get(jobId)?.log_path;
|
|
241
273
|
const logLine = (line: string) => {
|
|
242
274
|
if (!line || !logPath) return;
|
|
243
275
|
const ev: JobEvent = { type: "log", line };
|
|
@@ -252,7 +284,7 @@ function spawnNow(item: QueuedSpawn): void {
|
|
|
252
284
|
// code 0 ⇒ trust the child's terminal status (it set done/failed itself).
|
|
253
285
|
// non-zero ⇒ if the row never reached terminal (crash before the wrapper
|
|
254
286
|
// could write), force it failed.
|
|
255
|
-
if (code !== 0) {
|
|
287
|
+
try { if (code !== 0) {
|
|
256
288
|
const cur = store.get(jobId);
|
|
257
289
|
if (cur && (cur.status === "queued" || cur.status === "running")) {
|
|
258
290
|
store.setStatus(jobId, "failed", {
|
|
@@ -260,15 +292,25 @@ function spawnNow(item: QueuedSpawn): void {
|
|
|
260
292
|
endedAt: nowIso(),
|
|
261
293
|
});
|
|
262
294
|
}
|
|
295
|
+
} } catch (error) {
|
|
296
|
+
console.error(`[jobs] failed to record child exit: ${errString(error)}`);
|
|
297
|
+
} finally {
|
|
298
|
+
releaseLease(item);
|
|
299
|
+
try { item.onComplete?.(jobId, code); } catch {}
|
|
300
|
+
drainQueue();
|
|
263
301
|
}
|
|
264
|
-
releaseLease(jobId);
|
|
265
|
-
try { item.onComplete?.(jobId, code); } catch {}
|
|
266
|
-
drainQueue();
|
|
267
302
|
})();
|
|
268
303
|
}
|
|
269
304
|
|
|
270
|
-
function releaseLease(
|
|
271
|
-
|
|
305
|
+
function releaseLease(item: QueuedSpawn): void {
|
|
306
|
+
try { item.lease?.dispose(); }
|
|
307
|
+
catch (error) { console.error(`[jobs] execution lease cleanup failed: ${errString(error)}`); }
|
|
308
|
+
finally {
|
|
309
|
+
item.lease = undefined;
|
|
310
|
+
if (gpuLeaseHolder === item.jobId) gpuLeaseHolder = null;
|
|
311
|
+
if (activeSpawn === item) activeSpawn = undefined;
|
|
312
|
+
item.finish();
|
|
313
|
+
}
|
|
272
314
|
}
|
|
273
315
|
|
|
274
316
|
/** Run the next queued subprocess job if the lease is free. */
|
|
@@ -279,8 +321,51 @@ export function drainQueue(): void {
|
|
|
279
321
|
// A synchronous spawn failure releases the lease. Keep draining
|
|
280
322
|
// iteratively so a run of bad queue entries cannot recurse or wedge the
|
|
281
323
|
// first startable job behind them.
|
|
282
|
-
|
|
324
|
+
activeSpawn = next;
|
|
325
|
+
if (next.acquire) {
|
|
326
|
+
gpuLeaseHolder = next.jobId; // reserve FIFO order while inference drains
|
|
327
|
+
void Promise.resolve().then(() => next.acquire!(next.abort.signal)).then((lease) => {
|
|
328
|
+
next.lease = lease;
|
|
329
|
+
if (next.abort.signal.aborted) throw next.abort.signal.reason;
|
|
330
|
+
spawnNow(next);
|
|
331
|
+
}).catch((error) => {
|
|
332
|
+
try {
|
|
333
|
+
next.store.setStatus(next.jobId, "failed", { error: errString(error), endedAt: nowIso() });
|
|
334
|
+
} catch (recordError) {
|
|
335
|
+
console.error(`[jobs] failed to record admission failure: ${errString(recordError)}`);
|
|
336
|
+
} finally { releaseLease(next); }
|
|
337
|
+
}).finally(drainQueue);
|
|
338
|
+
} else spawnNow(next);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Dataset work stays in-process, but belongs to the application host too. */
|
|
343
|
+
export async function closeInProcessJobs(store: JobStore): Promise<void> {
|
|
344
|
+
closedStores.add(store);
|
|
345
|
+
const active = [...inProcess.get(store) ?? []];
|
|
346
|
+
for (const work of active) work.cancellation.cancel("engine_closed");
|
|
347
|
+
await Promise.all(active.map((work) => work.done));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Stop only this host's managed GPU jobs. Await process death before releasing
|
|
351
|
+
* the execution lease; queued and admission-waiting jobs never spawn. */
|
|
352
|
+
export async function closeSubprocessJobs(store: JobStore): Promise<void> {
|
|
353
|
+
closedStores.add(store);
|
|
354
|
+
for (let i = spawnQueue.length - 1; i >= 0; i--) {
|
|
355
|
+
const item = spawnQueue[i]!;
|
|
356
|
+
if (item.store !== store) continue;
|
|
357
|
+
spawnQueue.splice(i, 1);
|
|
358
|
+
store.setStatus(item.jobId, "failed", { error: "job host closed", endedAt: nowIso() });
|
|
359
|
+
item.finish();
|
|
283
360
|
}
|
|
361
|
+
const active = activeSpawn;
|
|
362
|
+
if (!active || active.store !== store) return;
|
|
363
|
+
active.abort.abort(new Error("job host closed"));
|
|
364
|
+
const proc = active.proc;
|
|
365
|
+
if (proc) proc.kill("SIGTERM");
|
|
366
|
+
const force = proc ? setTimeout(() => { if (proc.exitCode === null) proc.kill("SIGKILL"); }, 3000) : undefined;
|
|
367
|
+
try { await active.finished; }
|
|
368
|
+
finally { if (force) clearTimeout(force); }
|
|
284
369
|
}
|
|
285
370
|
|
|
286
371
|
/** Read a child stream line-by-line, buffering partial trailing lines, and
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TaskClient } from "../contracts/completion";
|
|
2
|
+
import { throwIfCancelled } from "../engine/cancellation";
|
|
3
|
+
import type { JobEvent, JobRunner } from "./types";
|
|
4
|
+
|
|
5
|
+
/** Numerical runners retain their artifact policy; the client owns delivery
|
|
6
|
+
* and cooperative cancellation at progress boundaries. No event buffering. */
|
|
7
|
+
export function createJobTaskClient(runner: JobRunner): TaskClient<
|
|
8
|
+
Record<string, unknown>, JobEvent, { outputPath?: string } | void
|
|
9
|
+
> {
|
|
10
|
+
return {
|
|
11
|
+
async run(request, report, cancellation) {
|
|
12
|
+
const check = () => { if (cancellation) throwIfCancelled(cancellation); };
|
|
13
|
+
check();
|
|
14
|
+
const result = await runner((event) => { check(); report(event); check(); }, request);
|
|
15
|
+
check();
|
|
16
|
+
return result;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
package/src/jobs/types.ts
CHANGED
|
@@ -1,58 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// vocabulary. Mirrors the optiq lab SSE event grammar so the web wizards
|
|
4
|
-
// port over mechanically (started → log/stage/metric → done|failed).
|
|
5
|
-
|
|
6
|
-
/** A single streamed job event, serialized one-per-line into the job's
|
|
7
|
-
* NDJSON log and re-emitted to the browser as SSE `data:` frames. */
|
|
8
|
-
export type JobEvent =
|
|
9
|
-
| { type: "started"; ts: number }
|
|
10
|
-
| { type: "log"; line: string }
|
|
11
|
-
| {
|
|
12
|
-
type: "stage";
|
|
13
|
-
stage: string;
|
|
14
|
-
progress?: number; // 0..1
|
|
15
|
-
message?: string;
|
|
16
|
-
[k: string]: unknown;
|
|
17
|
-
}
|
|
18
|
-
| {
|
|
19
|
-
type: "metric";
|
|
20
|
-
kind: "train" | "val";
|
|
21
|
-
step: number;
|
|
22
|
-
loss: number;
|
|
23
|
-
[k: string]: unknown; // grad_norm, learning_rate, tokens_per_sec, accuracy, margin, progress, message
|
|
24
|
-
}
|
|
25
|
-
| { type: "done"; ts: number; output_dir?: string; summary?: unknown; [k: string]: unknown }
|
|
26
|
-
| { type: "failed"; error: string; ts: number };
|
|
27
|
-
|
|
28
|
-
/** Sink a job calls to report progress. Implementations append to the log
|
|
29
|
-
* file and (for stage/metric events carrying `progress`/`message`) update
|
|
30
|
-
* the SQLite row. Never throws — a logging failure must not kill a job. */
|
|
31
|
-
export type Emit = (e: JobEvent) => void;
|
|
32
|
-
|
|
33
|
-
/** The unit of work. Returns an optional output path recorded on the row.
|
|
34
|
-
* Receives the parsed `config` from the submit request. */
|
|
35
|
-
export type JobRunner = (
|
|
36
|
-
emit: Emit,
|
|
37
|
-
config: Record<string, unknown>,
|
|
38
|
-
) => Promise<{ outputPath?: string } | void>;
|
|
39
|
-
|
|
40
|
-
/** Job kinds in the system. */
|
|
41
|
-
export type JobKind = "quantize" | "finetune" | "dataset" | "noop" | "crash";
|
|
42
|
-
|
|
43
|
-
export type JobStatus = "queued" | "running" | "done" | "failed" | "zombie";
|
|
44
|
-
|
|
45
|
-
/** A persisted job row (shape returned by the HTTP `GET /api/jobs/:id`). */
|
|
46
|
-
export interface JobRow {
|
|
47
|
-
id: string;
|
|
48
|
-
kind: string;
|
|
49
|
-
status: JobStatus;
|
|
50
|
-
config_json: string;
|
|
51
|
-
progress: number;
|
|
52
|
-
message: string | null;
|
|
53
|
-
log_path: string;
|
|
54
|
-
output_path: string | null;
|
|
55
|
-
error: string | null;
|
|
56
|
-
started_at: string;
|
|
57
|
-
ended_at: string | null;
|
|
58
|
-
}
|
|
1
|
+
/** Compatibility exports; producer and browser share the portable wire contract. */
|
|
2
|
+
export type * from "../contracts/jobs";
|
package/src/kv-store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { cleanupFailure, disposeResources } from "./engine/resources";
|
|
1
2
|
// KV-cache persistence: save prompt caches to disk, reload by streamed copy.
|
|
2
|
-
// The serialization core of the SSD cold tier (docs/design/
|
|
3
|
+
// The serialization core of the SSD cold tier (docs/design/kv-cache.md).
|
|
3
4
|
//
|
|
4
5
|
// File layout (every tensor PAGE-ALIGNED — the Phase 1 corollary: files
|
|
5
6
|
// we write can be mmap'd and handed to the GPU without copies):
|
|
@@ -121,9 +122,26 @@ export interface KvSaveMeta {
|
|
|
121
122
|
ns?: string;
|
|
122
123
|
/** sha256 of tokenizer.json: identical ids must mean identical text. */
|
|
123
124
|
tokenizerHash?: string;
|
|
125
|
+
/** In-flight generation state. The serialized caches cover `tokens`;
|
|
126
|
+
* pendingToken was sampled from that state but has not been emitted. */
|
|
127
|
+
generationCheckpoint?: {
|
|
128
|
+
key: string;
|
|
129
|
+
cacheNs: string;
|
|
130
|
+
originalPromptTokens: number;
|
|
131
|
+
generatedTokens: number;
|
|
132
|
+
pendingToken: number;
|
|
133
|
+
/** Actual sampler seed, including a server-generated default. Required to
|
|
134
|
+
* continue the random stream exactly after a restart. */
|
|
135
|
+
seed: number;
|
|
136
|
+
/** True when `seed` was part of the client request rather than generated
|
|
137
|
+
* by the server. */
|
|
138
|
+
seedWasExplicit: boolean;
|
|
139
|
+
};
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
export interface KvFileHeader extends KvSaveMeta {
|
|
143
|
+
/** Absent in earlier v3 files, which used mlx-cache-v3. */
|
|
144
|
+
codecProvider?: string;
|
|
127
145
|
formatVersion: 3;
|
|
128
146
|
createdAt: number;
|
|
129
147
|
tokens: number[];
|
|
@@ -143,7 +161,7 @@ const alignUp = (n: number) => Math.ceil(n / ALIGN) * ALIGN;
|
|
|
143
161
|
* materialization until rawBytes at write time). */
|
|
144
162
|
interface TensorSource { arr: MlxArray; disposeAfter: boolean }
|
|
145
163
|
|
|
146
|
-
interface SnapshotContext {
|
|
164
|
+
export interface SnapshotContext {
|
|
147
165
|
slots: TensorSlot[];
|
|
148
166
|
push(a: MlxArray, disposeAfter: boolean): void;
|
|
149
167
|
liveSlice(a: MlxArray, upTo: number): MlxArray;
|
|
@@ -151,21 +169,21 @@ interface SnapshotContext {
|
|
|
151
169
|
pushTriple(t: ops.QuantizedTensor, upTo: number | null): void;
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
interface CloneContext {
|
|
172
|
+
export interface CloneContext {
|
|
155
173
|
view(a: MlxArray): MlxArray;
|
|
156
174
|
liveView(a: MlxArray, upTo: number): MlxArray;
|
|
157
175
|
mlaView(a: MlxArray, upTo: number): MlxArray;
|
|
158
176
|
tripleView(t: ops.QuantizedTensor, upTo: number | null): ops.QuantizedTensor;
|
|
159
177
|
}
|
|
160
178
|
|
|
161
|
-
interface LoadContext {
|
|
179
|
+
export interface LoadContext {
|
|
162
180
|
path: string;
|
|
163
181
|
arr(slot: TensorSlot): MlxArray;
|
|
164
182
|
grownArr(slot: TensorSlot, offset: number): MlxArray;
|
|
165
183
|
triple(slots: TensorSlot[], at: number): ops.QuantizedTensor;
|
|
166
184
|
}
|
|
167
185
|
|
|
168
|
-
interface CacheCodec {
|
|
186
|
+
export interface CacheCodec {
|
|
169
187
|
matches(cache: Cache): boolean;
|
|
170
188
|
snapshot(cache: Cache, context: SnapshotContext): CacheHeaderEntry;
|
|
171
189
|
clone(cache: Cache, context: CloneContext): Cache;
|
|
@@ -458,16 +476,50 @@ const CACHE_CODECS = {
|
|
|
458
476
|
},
|
|
459
477
|
} satisfies Record<CacheKind, CacheCodec>;
|
|
460
478
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
479
|
+
/** Codecs belong to the loaded backend binding. The persisted ID is checked
|
|
480
|
+
* before allocating state; codecs are engine code, never loaded from a file. */
|
|
481
|
+
export interface CacheCodecProvider {
|
|
482
|
+
readonly id: string;
|
|
483
|
+
forCache(cache: Cache): CacheCodec;
|
|
484
|
+
forHeader(entry: CacheHeaderEntry): CacheCodec;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function createCacheCodecProvider(
|
|
488
|
+
id: string, entries: Readonly<Partial<Record<CacheKind, CacheCodec>>>,
|
|
489
|
+
): CacheCodecProvider {
|
|
490
|
+
if (!id.trim()) throw new Error("cache codec provider needs an identity");
|
|
491
|
+
const table = new Map(Object.entries(entries).map(([kind, codec]) => [kind, Object.freeze({
|
|
492
|
+
matches: codec.matches.bind(codec), snapshot: codec.snapshot.bind(codec), clone: codec.clone.bind(codec),
|
|
493
|
+
load: codec.load.bind(codec), headerTrimmable: codec.headerTrimmable.bind(codec),
|
|
494
|
+
})]));
|
|
495
|
+
const candidates = [...table.values()];
|
|
496
|
+
const selected = new WeakMap<Cache, CacheCodec>();
|
|
497
|
+
return Object.freeze({
|
|
498
|
+
id,
|
|
499
|
+
forCache(cache: Cache) {
|
|
500
|
+
const bound = selected.get(cache);
|
|
501
|
+
if (bound) return bound;
|
|
502
|
+
const matches = candidates.filter((codec) => codec.matches(cache));
|
|
503
|
+
if (matches.length !== 1)
|
|
504
|
+
throw new Error(`cache codec provider ${id}: ${matches.length} matches for ${cacheSignature(cache)}`);
|
|
505
|
+
selected.set(cache, matches[0]!);
|
|
506
|
+
return matches[0]!;
|
|
507
|
+
},
|
|
508
|
+
forHeader(entry: CacheHeaderEntry) {
|
|
509
|
+
const codec = table.get(entry.kind);
|
|
510
|
+
if (!codec) throw new Error(`cache codec provider ${id}: unsupported cache kind ${entry.kind}`);
|
|
511
|
+
return codec;
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export const legacyCacheCodecs = createCacheCodecProvider("mlx-cache-v3", CACHE_CODECS);
|
|
466
517
|
|
|
467
|
-
export const cacheHeadersTrimmable = (
|
|
468
|
-
caches
|
|
518
|
+
export const cacheHeadersTrimmable = (
|
|
519
|
+
caches: readonly CacheHeaderEntry[], codecs: CacheCodecProvider = legacyCacheCodecs,
|
|
520
|
+
): boolean => caches.every((entry) => codecs.forHeader(entry).headerTrimmable(entry));
|
|
469
521
|
|
|
470
|
-
function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry; sources: TensorSource[]; next: number } {
|
|
522
|
+
function snapshotCache(c: Cache, dataOffset: number, codecs: CacheCodecProvider): { entry: CacheHeaderEntry; sources: TensorSource[]; next: number } {
|
|
471
523
|
const slots: TensorSlot[] = [];
|
|
472
524
|
const sources: TensorSource[] = [];
|
|
473
525
|
let cursor = dataOffset;
|
|
@@ -493,10 +545,13 @@ function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry;
|
|
|
493
545
|
}
|
|
494
546
|
};
|
|
495
547
|
|
|
496
|
-
|
|
497
|
-
slots, push, liveSlice, liveMlaSlice, pushTriple
|
|
498
|
-
|
|
499
|
-
|
|
548
|
+
try {
|
|
549
|
+
const entry = codecs.forCache(c).snapshot(c, { slots, push, liveSlice, liveMlaSlice, pushTriple });
|
|
550
|
+
return { entry, sources, next: cursor };
|
|
551
|
+
} catch (error) {
|
|
552
|
+
for (const source of sources) if (source.disposeAfter) source.arr.dispose();
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
500
555
|
}
|
|
501
556
|
|
|
502
557
|
/** Persist `caches` (+ the exact token prefix they encode) to `path`.
|
|
@@ -513,15 +568,17 @@ function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry;
|
|
|
513
568
|
* called while the caches hold EXACTLY the state to snapshot (post-wrap
|
|
514
569
|
* rings cannot be rewound later). Same per-kind dispatch as
|
|
515
570
|
* snapshotCache/loadKvCache. */
|
|
516
|
-
export function cloneKvCaches(caches: Cache[]): Cache[] {
|
|
517
|
-
const
|
|
571
|
+
export function cloneKvCaches(caches: Cache[], codecs: CacheCodecProvider = legacyCacheCodecs): Cache[] {
|
|
572
|
+
const pending: MlxArray[] = [];
|
|
573
|
+
const hold = (array: MlxArray): MlxArray => { pending.push(array); return array; };
|
|
574
|
+
const view = (a: MlxArray): MlxArray => hold(a.slice(a.shape.map(() => 0), [...a.shape]));
|
|
518
575
|
const liveView = (a: MlxArray, upTo: number): MlxArray => {
|
|
519
576
|
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
520
|
-
return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
|
|
577
|
+
return hold(a.slice([0, 0, 0, 0], [B, H, upTo, D]));
|
|
521
578
|
};
|
|
522
579
|
const mlaView = (a: MlxArray, upTo: number): MlxArray => {
|
|
523
580
|
const [B, , D] = a.shape as [number, number, number];
|
|
524
|
-
return a.slice([0, 0, 0], [B, upTo, D]);
|
|
581
|
+
return hold(a.slice([0, 0, 0], [B, upTo, D]));
|
|
525
582
|
};
|
|
526
583
|
const tripleView = (t: ops.QuantizedTensor, upTo: number | null): ops.QuantizedTensor => ({
|
|
527
584
|
packed: upTo === null ? view(t.packed) : liveView(t.packed, upTo),
|
|
@@ -531,11 +588,11 @@ export function cloneKvCaches(caches: Cache[]): Cache[] {
|
|
|
531
588
|
const out: Cache[] = [];
|
|
532
589
|
try {
|
|
533
590
|
for (const c of caches) {
|
|
534
|
-
out.push(
|
|
591
|
+
out.push(codecs.forCache(c).clone(c, { view, liveView, mlaView, tripleView }));
|
|
592
|
+
pending.length = 0; // transferred to the returned cache
|
|
535
593
|
}
|
|
536
594
|
} catch (err) {
|
|
537
|
-
|
|
538
|
-
throw err;
|
|
595
|
+
cleanupFailure(err, () => disposeResources([...pending, ...out]));
|
|
539
596
|
}
|
|
540
597
|
return out;
|
|
541
598
|
}
|
|
@@ -545,14 +602,14 @@ export function cloneKvCaches(caches: Cache[]): Cache[] {
|
|
|
545
602
|
* between yields so the EVENT LOOP KEEPS SERVING while a multi-hundred-MB
|
|
546
603
|
* entry flushes (the write-behind persistence contract: durability never
|
|
547
604
|
* blocks a request). try/finally still runs on early generator close. */
|
|
548
|
-
function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta): Generator<void, void, void> {
|
|
605
|
+
function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta, codecs: CacheCodecProvider): Generator<void, void, void> {
|
|
549
606
|
// Plan pass: header entries + lazy tensor sources, NO bytes materialized.
|
|
550
607
|
const entries: CacheHeaderEntry[] = [];
|
|
551
608
|
const sources: TensorSource[] = [];
|
|
552
609
|
let dataOffset = 0; // relative; rebased after header is sized
|
|
553
610
|
try {
|
|
554
611
|
for (const c of caches) {
|
|
555
|
-
const s = snapshotCache(c, dataOffset);
|
|
612
|
+
const s = snapshotCache(c, dataOffset, codecs);
|
|
556
613
|
entries.push(s.entry);
|
|
557
614
|
sources.push(...s.sources);
|
|
558
615
|
dataOffset = s.next;
|
|
@@ -561,7 +618,7 @@ function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta
|
|
|
561
618
|
// Header size is final NOW: hashes are fixed-width placeholders that get
|
|
562
619
|
// patched in place (same byte length) after the data pass computes them.
|
|
563
620
|
const header: KvFileHeader = {
|
|
564
|
-
formatVersion: 3, createdAt: Date.now(), ...meta, tokens, caches: entries,
|
|
621
|
+
formatVersion: 3, createdAt: Date.now(), ...meta, codecProvider: codecs.id, tokens, caches: entries,
|
|
565
622
|
};
|
|
566
623
|
const headerLen = new TextEncoder().encode(JSON.stringify(header)).length;
|
|
567
624
|
const dataStart = alignUp(PREFIX_LEN + headerLen);
|
|
@@ -626,32 +683,34 @@ function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta
|
|
|
626
683
|
}
|
|
627
684
|
}
|
|
628
685
|
|
|
629
|
-
export function saveKvCache(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {}): void {
|
|
630
|
-
for (const _ of saveKvCacheSteps(path, tokens, caches, meta)) { /* drain */ }
|
|
686
|
+
export function saveKvCache(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {}, codecs: CacheCodecProvider = legacyCacheCodecs): void {
|
|
687
|
+
for (const _ of saveKvCacheSteps(path, tokens, caches, meta, codecs)) { /* drain */ }
|
|
631
688
|
}
|
|
632
689
|
|
|
633
690
|
/** Non-blocking variant: yields the event loop after every tensor write so
|
|
634
691
|
* serving interleaves with the flush. Caller owns `caches` lifetime for
|
|
635
692
|
* the duration (pass zero-copy clones, dispose after).
|
|
636
693
|
*
|
|
637
|
-
* `
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
* setImmediate pacing interleaved those slices exactly between decode
|
|
642
|
-
* tokens — the 2026-07-07 decode@ctx contamination. A request arriving
|
|
643
|
-
* MID-flush pauses the remaining tensors until idle again). Gate failures
|
|
644
|
-
* are swallowed: scheduling advice must never corrupt the write path
|
|
645
|
-
* (an early generator close inside the fd-open section would leak the fd). */
|
|
694
|
+
* `runStep` (optional) runs each blocking tensor step inside the caller's
|
|
695
|
+
* exclusion domain. Waiting for an idle observation is not sufficient: a
|
|
696
|
+
* request can acquire the engine between that check and `steps.next()`,
|
|
697
|
+
* putting rawBytesView's GPU sync on the decode stream during prefill. */
|
|
646
698
|
export async function saveKvCacheAsync(
|
|
647
699
|
path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {},
|
|
648
|
-
|
|
700
|
+
runStep?: <T>(step: () => T) => Promise<T>, codecs: CacheCodecProvider = legacyCacheCodecs,
|
|
649
701
|
): Promise<void> {
|
|
650
|
-
const steps = saveKvCacheSteps(path, tokens, caches, meta);
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
702
|
+
const steps = saveKvCacheSteps(path, tokens, caches, meta, codecs);
|
|
703
|
+
try {
|
|
704
|
+
while (true) {
|
|
705
|
+
const next = runStep ? await runStep(() => steps.next()) : steps.next();
|
|
706
|
+
if (next.done) break;
|
|
707
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
708
|
+
}
|
|
709
|
+
} catch (error) {
|
|
710
|
+
// A rejected execution lease can leave the writer suspended with an
|
|
711
|
+
// open fd. Throw through it so its existing failure cleanup runs.
|
|
712
|
+
steps.throw(error);
|
|
713
|
+
throw error;
|
|
655
714
|
}
|
|
656
715
|
}
|
|
657
716
|
|
|
@@ -666,7 +725,7 @@ export interface SpillItem {
|
|
|
666
725
|
/** Bounded write-behind queue (2026-07-07 post-merge review fix).
|
|
667
726
|
*
|
|
668
727
|
* Pending spill/snapshot clones pin their entries' GPU buffers until the
|
|
669
|
-
*
|
|
728
|
+
* generation-locked flush gets a turn; the old bare promise chain queued them
|
|
670
729
|
* WITHOUT BOUND, so under sustained traffic (gate starved, evictions
|
|
671
730
|
* ongoing) resident memory = prompt-cache cap + every queued clone —
|
|
672
731
|
* allocator pressure exactly under the load that caused the evictions.
|
|
@@ -904,9 +963,12 @@ function withStepCapacity(a: MlxArray, offset: number): MlxArray {
|
|
|
904
963
|
export function loadKvCache(
|
|
905
964
|
path: string,
|
|
906
965
|
model: { makeCache(): Cache[] },
|
|
907
|
-
expect: KvLoadExpect = {},
|
|
966
|
+
expect: KvLoadExpect = {}, codecs: CacheCodecProvider = legacyCacheCodecs,
|
|
908
967
|
): LoadedKvCache {
|
|
909
968
|
const header = readKvHeader(path);
|
|
969
|
+
if ((header.codecProvider ?? legacyCacheCodecs.id) !== codecs.id)
|
|
970
|
+
throw new Error(`${path}: cache codec provider mismatch`);
|
|
971
|
+
for (const entry of header.caches) codecs.forHeader(entry);
|
|
910
972
|
for (const key of [
|
|
911
973
|
"modelId",
|
|
912
974
|
"configFingerprint",
|
|
@@ -966,9 +1028,7 @@ export function loadKvCache(
|
|
|
966
1028
|
const caches: Cache[] = [];
|
|
967
1029
|
try {
|
|
968
1030
|
for (const e of header.caches) {
|
|
969
|
-
const codec =
|
|
970
|
-
if (!codec)
|
|
971
|
-
throw new Error(`${path}: unknown cache kind ${(e as { kind: string }).kind}`);
|
|
1031
|
+
const codec = codecs.forHeader(e);
|
|
972
1032
|
caches.push(codec.load(e, { path, arr, grownArr, triple }));
|
|
973
1033
|
// This entry's tensors are now owned by its cache — stop tracking them
|
|
974
1034
|
// (the catch must not double-free through both pending AND caches).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# src/lab — quarantined, no-oracle code
|
|
2
|
+
|
|
3
|
+
Everything here is a Lab-tier module (docs/design/unified-engine-frontier-plan.md):
|
|
4
|
+
no external oracle, off by default, kept only while a measurement decides its
|
|
5
|
+
fate (PLAN.md "Maintainability program", D6 darlings). Rules, gate-enforced by
|
|
6
|
+
`scripts/check-hygiene.ts` (check 11):
|
|
7
|
+
|
|
8
|
+
- nothing outside `src/lab/` may import from `src/lab/` except the edges listed
|
|
9
|
+
in `LAB_IMPORT_ALLOWLIST` — those are recorded debt, not permission;
|
|
10
|
+
- a new production dependency on a Lab module is a decision, not a default:
|
|
11
|
+
either promote the module out of the lab with an oracle/measurement, or
|
|
12
|
+
route it through an option seam.
|
|
13
|
+
|
|
14
|
+
| module | flag / entry | oracle |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `curve/` (curve sampler + designer page) | `--curve-*`, `/curves` | none (Lab) |
|
|
17
|
+
| `paged-kv/` | `--paged-kv` (serial-only) | parity vs plain KVCache (bit-exact), no measured win |
|
|
18
|
+
| `expert-trace/` | `MLX_BUN_EXPERT_TRACE` hook in gemma4 | closed study (expert-offload) |
|
|
@@ -88,7 +88,7 @@ button.ghost{background:var(--card);border:1px solid var(--bd);border-radius:8px
|
|
|
88
88
|
|
|
89
89
|
<script>
|
|
90
90
|
const ln=Math.log;
|
|
91
|
-
const UMIN=ln(1e-6); // bottom-left / plot floor (matches src/curve-sampler.ts CURVE_UMIN)
|
|
91
|
+
const UMIN=ln(1e-6); // bottom-left / plot floor (matches src/lab/curve/curve-sampler.ts CURVE_UMIN)
|
|
92
92
|
const ENGINE_BASE = location.protocol.startsWith("http") ? location.origin : "http://localhost:8080";
|
|
93
93
|
// ---- curve state: 5 movable control points, sorted ascending in p_in ----
|
|
94
94
|
function identityPoints(){ return [{x_pct:1e-4,y_pct:1e-4},{x_pct:0.1,y_pct:0.1},{x_pct:1,y_pct:1},{x_pct:9,y_pct:9},{x_pct:100,y_pct:100}]; }
|
|
@@ -102,7 +102,7 @@ const PROMPTS=[
|
|
|
102
102
|
"Describe the feeling of a city at 3am in one vivid sentence.",
|
|
103
103
|
];
|
|
104
104
|
|
|
105
|
-
// ---- math: monotone cubic (PCHIP), IDENTICAL to src/curve-sampler.ts ----
|
|
105
|
+
// ---- math: monotone cubic (PCHIP), IDENTICAL to src/lab/curve/curve-sampler.ts ----
|
|
106
106
|
const Lp=pt=>[ln(Math.max(pt.x_pct,1e-9)/100),ln(Math.max(pt.y_pct,1e-9)/100)];
|
|
107
107
|
function knotArrays(c){ const k=c.points.map(Lp).sort((a,b)=>a[0]-b[0]); return {X:k.map(q=>q[0]),Y:k.map(q=>q[1])}; }
|
|
108
108
|
function secants(c){ const {X,Y}=knotArrays(c),s=[]; for(let i=X.length-1;i>0;i--)s.push((Y[i]-Y[i-1])/(X[i]-X[i-1])); return s; }
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// BELOW lowers them (renorm makes the literal number an intent); the SLOPE across
|
|
17
17
|
// a region sets how evenly the tokens inside it spread (flat<1 even, steep>1 separated).
|
|
18
18
|
|
|
19
|
-
import { MlxArray } from "
|
|
20
|
-
import * as ops from "
|
|
19
|
+
import { MlxArray } from "../../mlx/array";
|
|
20
|
+
import * as ops from "../../mlx/ops";
|
|
21
21
|
|
|
22
22
|
export interface CurvePoint { x_pct: number; y_pct: number; }
|
|
23
23
|
export interface CurveParams {
|