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/serve/isolate.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { ExecutionCoordinator } from "../engine/execution-coordinator";
|
|
2
|
+
import { AdmissionPool, AdmissionRejected } from "../engine/admission";
|
|
3
|
+
import { acquireReservation } from "./preparation";
|
|
4
|
+
import { createParentApplication } from "./parent-application";
|
|
5
|
+
import type { DisposableResource } from "../contracts/resources";
|
|
6
|
+
import { retainResponseLease } from "./response-lease";
|
|
7
|
+
import type { EngineHost } from "../contracts/host";
|
|
8
|
+
// Runtime isolation — the parent half (docs/reference/server-config.md).
|
|
2
9
|
//
|
|
3
10
|
// ARCHITECTURE (decision 2026-07-05, deviating from the doc's original
|
|
4
11
|
// structured-IPC sketch — recorded there): the inference ENGINE is the
|
|
@@ -33,11 +40,17 @@ import { join } from "node:path";
|
|
|
33
40
|
|
|
34
41
|
/** How to (re)start the engine child. `argv[0]` is the executable. */
|
|
35
42
|
export interface EngineSpec {
|
|
43
|
+
/** Parent-owned activity lease; covers startup and complete response bodies. */
|
|
44
|
+
acquire?: (signal: AbortSignal) => Promise<DisposableResource>;
|
|
36
45
|
argv: string[];
|
|
37
46
|
socketPath: string;
|
|
38
47
|
/** Max ms to wait for the child's /health after spawn (weights load —
|
|
39
48
|
* large models take a while). Default 15 min. */
|
|
40
49
|
readyTimeoutMs?: number;
|
|
50
|
+
/** Bound automatic restarts in a rolling window, including startup failure. */
|
|
51
|
+
maxRestarts?: number;
|
|
52
|
+
restartWindowMs?: number;
|
|
53
|
+
restartDelayMs?: number;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
const HOP_BY_HOP = new Set([
|
|
@@ -50,17 +63,23 @@ export function defaultSocketPath(): string {
|
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
/** The persistent engine child: spawn, health-gate, respawn-on-crash. */
|
|
53
|
-
export class EngineChild {
|
|
66
|
+
export class EngineChild implements EngineHost<Request, Response> {
|
|
54
67
|
readonly spec: EngineSpec;
|
|
55
68
|
#proc: ReturnType<typeof Bun.spawn> | null = null;
|
|
56
69
|
#ready: Promise<void>;
|
|
57
70
|
#stopping = false;
|
|
71
|
+
#lifetime = new AbortController();
|
|
58
72
|
restarts = 0;
|
|
59
73
|
#lastSpawnAt = 0;
|
|
74
|
+
#restartTimes: number[] = [];
|
|
75
|
+
#backoff: ReturnType<typeof setTimeout> | undefined;
|
|
76
|
+
#wakeBackoff: (() => void) | undefined;
|
|
77
|
+
#startups = new Set<Promise<void>>();
|
|
60
78
|
|
|
61
79
|
constructor(spec: EngineSpec) {
|
|
62
|
-
this.spec = spec;
|
|
80
|
+
this.spec = Object.freeze({ ...spec, argv: [...spec.argv] });
|
|
63
81
|
this.#ready = this.#spawn();
|
|
82
|
+
void this.#ready.catch(() => {}); // failures remain observable through ready/forward
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
get ready(): Promise<void> {
|
|
@@ -71,38 +90,78 @@ export class EngineChild {
|
|
|
71
90
|
return this.#proc?.pid ?? null;
|
|
72
91
|
}
|
|
73
92
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
#spawn(): Promise<void> {
|
|
94
|
+
const work = this.#start();
|
|
95
|
+
this.#startups.add(work);
|
|
96
|
+
void work.finally(() => this.#startups.delete(work)).catch(() => {});
|
|
97
|
+
return work;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async #start(): Promise<void> {
|
|
101
|
+
if (this.#stopping) throw new Error("engine host is closed");
|
|
102
|
+
const lease = await this.spec.acquire?.(this.#lifetime.signal);
|
|
103
|
+
let starting: ReturnType<typeof Bun.spawn> | undefined;
|
|
104
|
+
let healthy = false;
|
|
105
|
+
try {
|
|
106
|
+
if (this.#stopping) throw new Error("engine host is closed");
|
|
107
|
+
this.#lastSpawnAt = Date.now();
|
|
108
|
+
try { unlinkSync(this.spec.socketPath); } catch {}
|
|
109
|
+
const proc = Bun.spawn(this.spec.argv, {
|
|
110
|
+
stdio: ["ignore", "inherit", "inherit"], // load progress → user's terminal
|
|
111
|
+
env: process.env,
|
|
112
|
+
});
|
|
113
|
+
this.#proc = proc;
|
|
114
|
+
starting = proc;
|
|
115
|
+
void proc.exited.then((code) => {
|
|
116
|
+
if (this.#stopping || this.#proc !== proc) return;
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const window = this.spec.restartWindowMs ?? 60_000;
|
|
119
|
+
this.#restartTimes = this.#restartTimes.filter((time) => now - time < window);
|
|
120
|
+
if (this.#restartTimes.length >= (this.spec.maxRestarts ?? 3)) {
|
|
121
|
+
this.#ready = Promise.reject(new Error(`engine restart limit reached after exit ${code}`));
|
|
122
|
+
void this.#ready.catch(() => {});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.#restartTimes.push(now);
|
|
126
|
+
console.error(`[isolate] engine exited (code ${code}) — respawning`);
|
|
127
|
+
this.restarts++;
|
|
128
|
+
// Crash-loop backoff: an engine that dies within 10 s of spawning
|
|
129
|
+
// (bad flags, OOM on load) waits 5 s before the retry.
|
|
130
|
+
const delay = this.spec.restartDelayMs ?? (Date.now() - this.#lastSpawnAt < 10_000 ? 5_000 : 0);
|
|
131
|
+
this.#ready = new Promise<void>((resolve) => {
|
|
132
|
+
this.#wakeBackoff = resolve;
|
|
133
|
+
this.#backoff = setTimeout(resolve, delay);
|
|
134
|
+
}).then(() => {
|
|
135
|
+
this.#backoff = undefined; this.#wakeBackoff = undefined;
|
|
136
|
+
return this.#spawn();
|
|
137
|
+
});
|
|
138
|
+
void this.#ready.catch(() => {});
|
|
139
|
+
});
|
|
140
|
+
// Health-gate: poll the child's /health over the socket until it
|
|
141
|
+
// answers — model load happens behind this.
|
|
142
|
+
const deadline = Date.now() + (this.spec.readyTimeoutMs ?? 15 * 60_000);
|
|
143
|
+
while (Date.now() < deadline) {
|
|
144
|
+
if (this.#stopping || proc.killed || proc.exitCode !== null || this.#proc !== proc)
|
|
145
|
+
throw new Error("engine died during startup");
|
|
146
|
+
try {
|
|
147
|
+
const r = await fetch("http://engine/health", {
|
|
148
|
+
unix: this.spec.socketPath,
|
|
149
|
+
signal: AbortSignal.timeout(2_000),
|
|
150
|
+
} as RequestInit & { unix: string });
|
|
151
|
+
if (r.ok) { healthy = true; return; }
|
|
152
|
+
} catch { /* not up yet */ }
|
|
153
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
154
|
+
}
|
|
155
|
+
proc.kill();
|
|
156
|
+
throw new Error("engine did not become healthy in time");
|
|
157
|
+
} finally {
|
|
158
|
+
if (!healthy && starting && starting.exitCode === null) {
|
|
159
|
+
starting.kill();
|
|
160
|
+
const force = setTimeout(() => { if (starting!.exitCode === null) starting!.kill("SIGKILL"); }, 3000);
|
|
161
|
+
try { await starting.exited; } finally { clearTimeout(force); }
|
|
162
|
+
}
|
|
163
|
+
lease?.dispose();
|
|
104
164
|
}
|
|
105
|
-
throw new Error("engine did not become healthy in time");
|
|
106
165
|
}
|
|
107
166
|
|
|
108
167
|
/** Forward one request to the child; the response streams through.
|
|
@@ -124,33 +183,81 @@ export class EngineChild {
|
|
|
124
183
|
}
|
|
125
184
|
|
|
126
185
|
async #forwardOnce(request: Request): Promise<Response> {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
186
|
+
if (this.#stopping) throw new Error("engine host is closed");
|
|
187
|
+
request.signal.throwIfAborted();
|
|
188
|
+
await new Promise<void>((resolve, reject) => {
|
|
189
|
+
const abort = () => { request.signal.removeEventListener("abort", abort); reject(request.signal.reason); };
|
|
190
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
191
|
+
this.#ready.then(() => { request.signal.removeEventListener("abort", abort); resolve(); },
|
|
192
|
+
(error) => { request.signal.removeEventListener("abort", abort); reject(error); });
|
|
131
193
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
194
|
+
if (this.#stopping) throw new Error("engine host is closed");
|
|
195
|
+
request.signal.throwIfAborted();
|
|
196
|
+
const signal = AbortSignal.any([request.signal, this.#lifetime.signal]);
|
|
197
|
+
const lease = await this.spec.acquire?.(signal);
|
|
198
|
+
try {
|
|
199
|
+
signal.throwIfAborted();
|
|
200
|
+
const headers = new Headers();
|
|
201
|
+
request.headers.forEach((v, k) => {
|
|
202
|
+
if (!HOP_BY_HOP.has(k.toLowerCase())) headers.set(k, v);
|
|
203
|
+
});
|
|
204
|
+
const proxied = await fetch(request.url, {
|
|
205
|
+
method: request.method,
|
|
206
|
+
headers,
|
|
207
|
+
body: request.body,
|
|
208
|
+
signal, // client abort → child sees the disconnect
|
|
209
|
+
redirect: "manual",
|
|
210
|
+
unix: this.spec.socketPath,
|
|
211
|
+
// Streaming request bodies need half-duplex; buffered bodies ignore it.
|
|
212
|
+
duplex: "half",
|
|
213
|
+
} as RequestInit & { unix: string; duplex: string });
|
|
214
|
+
const outHeaders = new Headers();
|
|
215
|
+
proxied.headers.forEach((v, k) => {
|
|
216
|
+
if (!HOP_BY_HOP.has(k.toLowerCase())) outHeaders.set(k, v);
|
|
217
|
+
});
|
|
218
|
+
const response = new Response(proxied.body, { status: proxied.status, headers: outHeaders });
|
|
219
|
+
return lease ? retainResponseLease(response, lease) : response;
|
|
220
|
+
} catch (error) { lease?.dispose(); throw error; }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** A live UDS response owns the worker's native lease. Disconnect releases it
|
|
224
|
+
* even if the parent crashes. External cancellation applies to acquisition;
|
|
225
|
+
* the returned owner alone releases an admitted lease after job death. */
|
|
226
|
+
async reserveExecution(signal: AbortSignal): Promise<DisposableResource> {
|
|
227
|
+
if (!this.#proc || this.#proc.exitCode !== null) return { dispose() {} };
|
|
228
|
+
const abort = new AbortController();
|
|
229
|
+
const cancel = () => abort.abort(signal.reason);
|
|
230
|
+
signal.throwIfAborted();
|
|
231
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
232
|
+
try {
|
|
233
|
+
const response = await fetch("http://engine/admin/lease", {
|
|
234
|
+
method: "POST", unix: this.spec.socketPath, signal: abort.signal,
|
|
235
|
+
} as RequestInit & { unix: string });
|
|
236
|
+
if (!response.ok) { await response.body?.cancel(); throw new Error(`worker lease failed (${response.status})`); }
|
|
237
|
+
return { dispose() { abort.abort(); void response.body?.cancel().catch(() => {}); } };
|
|
238
|
+
} catch (error) { abort.abort(); throw error; }
|
|
239
|
+
finally { signal.removeEventListener("abort", cancel); }
|
|
147
240
|
}
|
|
148
241
|
|
|
149
242
|
stop(): void {
|
|
150
243
|
this.#stopping = true;
|
|
244
|
+
this.#lifetime.abort(new Error("engine host is closed"));
|
|
245
|
+
clearTimeout(this.#backoff);
|
|
246
|
+
this.#wakeBackoff?.();
|
|
151
247
|
this.#proc?.kill();
|
|
152
248
|
try { unlinkSync(this.spec.socketPath); } catch {}
|
|
153
249
|
}
|
|
250
|
+
|
|
251
|
+
async close(): Promise<void> {
|
|
252
|
+
this.stop();
|
|
253
|
+
const process = this.#proc;
|
|
254
|
+
const force = process ? setTimeout(() => { if (process.exitCode === null) process.kill("SIGKILL"); }, 3000) : undefined;
|
|
255
|
+
try {
|
|
256
|
+
// Startup owns an admission lease even before a child exists. Earlier
|
|
257
|
+
// crashed attempts can still be unwinding after #ready switches to a retry.
|
|
258
|
+
await Promise.allSettled([this.#ready, ...this.#startups, ...(process ? [process.exited] : [])]);
|
|
259
|
+
} finally { if (force) clearTimeout(force); }
|
|
260
|
+
}
|
|
154
261
|
}
|
|
155
262
|
|
|
156
263
|
/** Model resolution the parent injects (Registry-backed in production;
|
|
@@ -158,7 +265,7 @@ export class EngineChild {
|
|
|
158
265
|
* drop-in ignore semantics (served by the default model, like mlx-lm). */
|
|
159
266
|
export type ModelResolver = (query: string) => { repoId: string; path: string } | null;
|
|
160
267
|
|
|
161
|
-
/** Child-per-model pool (P2,
|
|
268
|
+
/** Child-per-model pool (P2, docs/reference/server-config.md): LRU residency over
|
|
162
269
|
* engine children. Eviction is graceful and lossless — POST /admin/drain
|
|
163
270
|
* (gateway quiesce + demote the prompt cache to the SSD tier, keyed by
|
|
164
271
|
* that model's own fingerprint dir) and only then stop the child; the
|
|
@@ -170,6 +277,9 @@ export class ModelPool {
|
|
|
170
277
|
#children = new Map<string, EngineChild>(); // repoId → child
|
|
171
278
|
#lru: string[] = []; // most-recent last
|
|
172
279
|
#spawning = new Map<string, Promise<EngineChild>>();
|
|
280
|
+
#coldStarts = new AdmissionPool(1);
|
|
281
|
+
#starting = new Set<EngineChild>();
|
|
282
|
+
#stopping = false;
|
|
173
283
|
readonly defaultKey: string;
|
|
174
284
|
|
|
175
285
|
constructor(
|
|
@@ -210,40 +320,55 @@ export class ModelPool {
|
|
|
210
320
|
/** Route a request's `model` field to a child, spawning on first use.
|
|
211
321
|
* undefined/empty/unknown → the default child (drop-in semantics). */
|
|
212
322
|
async childFor(modelField: string | undefined | null): Promise<EngineChild> {
|
|
213
|
-
if (
|
|
323
|
+
if (this.#stopping) throw new Error("model pool is closed");
|
|
324
|
+
modelField ||= this.defaultKey;
|
|
214
325
|
// Already resident under this exact id?
|
|
215
326
|
if (this.#children.has(modelField)) return this.#use(modelField);
|
|
216
327
|
const rec = (() => {
|
|
217
328
|
try { return this.opts.resolve(modelField); } catch { return null; }
|
|
218
329
|
})();
|
|
219
|
-
if (!rec) return this
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
330
|
+
if (!rec && modelField !== this.defaultKey) return this.childFor(this.defaultKey);
|
|
331
|
+
const resolved = rec ?? { repoId: this.defaultKey, path: "" };
|
|
332
|
+
if (this.#children.has(resolved.repoId)) return this.#use(resolved.repoId);
|
|
333
|
+
const inflight = this.#spawning.get(resolved.repoId);
|
|
334
|
+
if (inflight) return inflight.then(() => this.#use(resolved.repoId));
|
|
223
335
|
const spawnP = (async () => {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
336
|
+
const admission = await this.#coldStarts.acquire();
|
|
337
|
+
try {
|
|
338
|
+
if (this.#stopping) throw new Error("model pool is closed");
|
|
339
|
+
const sock = this.#socketFor(resolved.repoId);
|
|
340
|
+
const child = new EngineChild(resolved.repoId === this.defaultKey ? this.opts.defaultChild.spec : {
|
|
341
|
+
argv: [...this.opts.selfArgv, ...engineArgvForModel(this.opts.rawArgs, sock, resolved.path)],
|
|
227
342
|
socketPath: sock,
|
|
343
|
+
acquire: this.opts.defaultChild.spec.acquire,
|
|
228
344
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
345
|
+
this.#starting.add(child);
|
|
346
|
+
try {
|
|
347
|
+
await child.ready;
|
|
348
|
+
if (this.#stopping) throw new Error("model pool is closed");
|
|
349
|
+
this.#children.set(resolved.repoId, child);
|
|
350
|
+
this.#lru.push(resolved.repoId);
|
|
351
|
+
await this.#evictOverCap();
|
|
352
|
+
return child;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
await child.close();
|
|
355
|
+
throw error;
|
|
356
|
+
} finally { this.#starting.delete(child); }
|
|
357
|
+
} finally { admission.dispose(); }
|
|
235
358
|
})();
|
|
236
|
-
this.#spawning.set(
|
|
359
|
+
this.#spawning.set(resolved.repoId, spawnP);
|
|
237
360
|
try {
|
|
238
361
|
return await spawnP;
|
|
239
362
|
} finally {
|
|
240
|
-
this.#spawning.delete(
|
|
363
|
+
this.#spawning.delete(resolved.repoId);
|
|
241
364
|
}
|
|
242
365
|
}
|
|
243
366
|
|
|
244
367
|
#use(key: string): EngineChild {
|
|
245
368
|
this.#bump(key);
|
|
246
|
-
|
|
369
|
+
const child = this.#children.get(key);
|
|
370
|
+
if (!child) throw new Error(`model ${key} is no longer resident`);
|
|
371
|
+
return child;
|
|
247
372
|
}
|
|
248
373
|
|
|
249
374
|
/** Evict least-recently-used children over the cap: drain + demote (the
|
|
@@ -253,23 +378,38 @@ export class ModelPool {
|
|
|
253
378
|
const victim = this.#lru.find((k) => this.#children.has(k));
|
|
254
379
|
if (!victim || this.#children.size <= 1) return;
|
|
255
380
|
const child = this.#children.get(victim)!;
|
|
256
|
-
|
|
257
|
-
this.#lru = this.#lru.filter((k) => k !== victim);
|
|
381
|
+
const lease = await child.spec.acquire?.(AbortSignal.timeout(120_000));
|
|
258
382
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
383
|
+
// Another overlapping eviction may have completed while we waited.
|
|
384
|
+
if (this.#children.get(victim) !== child) continue;
|
|
385
|
+
this.#children.delete(victim);
|
|
386
|
+
this.#lru = this.#lru.filter((k) => k !== victim);
|
|
387
|
+
try {
|
|
388
|
+
const response = await fetch("http://engine/admin/drain", {
|
|
389
|
+
method: "POST", unix: child.spec.socketPath,
|
|
390
|
+
signal: AbortSignal.timeout(120_000),
|
|
391
|
+
} as RequestInit & { unix: string });
|
|
392
|
+
await response.arrayBuffer();
|
|
393
|
+
} catch { /* best-effort — state also persists via evict-spill */ }
|
|
394
|
+
await child.close();
|
|
395
|
+
console.log(`[isolate] evicted engine ${victim} (pool cap ${this.opts.poolMax})`);
|
|
396
|
+
} finally { lease?.dispose(); }
|
|
267
397
|
}
|
|
268
398
|
}
|
|
269
399
|
|
|
270
400
|
stopAll(): void {
|
|
271
|
-
|
|
401
|
+
this.#stopping = true;
|
|
402
|
+
this.#coldStarts.close();
|
|
403
|
+
for (const child of new Set([...this.#children.values(), ...this.#starting])) child.stop();
|
|
272
404
|
this.#children.clear();
|
|
405
|
+
this.#lru = [];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async close(): Promise<void> {
|
|
409
|
+
const children = new Set([...this.#children.values(), ...this.#starting]);
|
|
410
|
+
this.stopAll();
|
|
411
|
+
await Promise.all([...children].map((child) => child.close()));
|
|
412
|
+
await Promise.allSettled(this.#spawning.values());
|
|
273
413
|
}
|
|
274
414
|
}
|
|
275
415
|
|
|
@@ -280,6 +420,8 @@ const MODEL_ROUTED = new Set([
|
|
|
280
420
|
]);
|
|
281
421
|
|
|
282
422
|
export interface ProxyServerOptions {
|
|
423
|
+
/** Application-store injection for embedded hosts and model-free tests. */
|
|
424
|
+
createJobStore?: () => Promise<import("../jobs/db").JobStore>;
|
|
283
425
|
port: number;
|
|
284
426
|
hostname?: string;
|
|
285
427
|
engine: EngineSpec;
|
|
@@ -301,11 +443,61 @@ export function startProxyServer(opts: ProxyServerOptions): {
|
|
|
301
443
|
server: ReturnType<typeof Bun.serve>;
|
|
302
444
|
engine: EngineChild;
|
|
303
445
|
pool: ModelPool | null;
|
|
446
|
+
close(): Promise<void>;
|
|
304
447
|
} {
|
|
305
|
-
const
|
|
448
|
+
const coordinator = new ExecutionCoordinator();
|
|
449
|
+
const acquire = (mode: "shared" | "exclusive", signal: AbortSignal) =>
|
|
450
|
+
acquireReservation({ acquire: (cancellation) => coordinator.acquire(mode, cancellation) }, signal);
|
|
451
|
+
const engine = new EngineChild({ ...opts.engine, async acquire(signal) {
|
|
452
|
+
const parent = await acquire("shared", signal);
|
|
453
|
+
try {
|
|
454
|
+
const supplied = await opts.engine.acquire?.(signal);
|
|
455
|
+
return { dispose() { try { supplied?.dispose(); } finally { parent.dispose(); } } };
|
|
456
|
+
} catch (error) { parent.dispose(); throw error; }
|
|
457
|
+
} });
|
|
306
458
|
const pool = opts.pool
|
|
307
459
|
? new ModelPool({ ...opts.pool, defaultChild: engine })
|
|
308
460
|
: null;
|
|
461
|
+
const children = () => pool ? pool.residentKeys.map((key) => pool.child(key)!).filter(Boolean) : [engine];
|
|
462
|
+
const forwardRequest = async (request: Request): Promise<Response> => {
|
|
463
|
+
const url = new URL(request.url);
|
|
464
|
+
// P2 routing: generation endpoints carry `model` in the JSON body —
|
|
465
|
+
// buffer it (small), pick/spawn that model's child, forward the
|
|
466
|
+
// buffered body. Everything else rides the default engine.
|
|
467
|
+
if (pool && request.method === "POST" && MODEL_ROUTED.has(url.pathname)) {
|
|
468
|
+
const raw = await request.text();
|
|
469
|
+
let modelField: string | undefined;
|
|
470
|
+
try { modelField = (JSON.parse(raw) as { model?: string }).model; } catch { /* malformed → default child answers with its own 400 */ }
|
|
471
|
+
const target = await pool.childFor(modelField);
|
|
472
|
+
return await target.forward(new Request(request.url, {
|
|
473
|
+
method: request.method,
|
|
474
|
+
headers: request.headers,
|
|
475
|
+
body: raw,
|
|
476
|
+
signal: request.signal,
|
|
477
|
+
}));
|
|
478
|
+
}
|
|
479
|
+
return await (pool ? await pool.childFor(undefined) : engine).forward(request);
|
|
480
|
+
};
|
|
481
|
+
const application = createParentApplication({
|
|
482
|
+
createJobStore: opts.createJobStore,
|
|
483
|
+
serverPort: () => server.port,
|
|
484
|
+
async acquireGpu(signal) {
|
|
485
|
+
const lease = await acquire("exclusive", signal);
|
|
486
|
+
try {
|
|
487
|
+
// Network cancellation is not a native completion fence. Explicitly
|
|
488
|
+
// drain every resident worker before the subprocess may touch the GPU.
|
|
489
|
+
const results = await Promise.allSettled(children().map((child) => child.reserveExecution(signal)));
|
|
490
|
+
const held = results.filter((result) => result.status === "fulfilled").map((result) => result.value);
|
|
491
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
492
|
+
if (failed) { for (const worker of held) worker.dispose(); throw failed.reason; }
|
|
493
|
+
return { dispose() { for (const worker of held) worker.dispose(); lease.dispose(); } };
|
|
494
|
+
} catch (error) { lease.dispose(); throw error; }
|
|
495
|
+
},
|
|
496
|
+
invalidateLibrary() {
|
|
497
|
+
for (const child of children()) void child.forward(new Request("http://engine/library?refresh=1"))
|
|
498
|
+
.then((response) => response.arrayBuffer()).catch(() => {});
|
|
499
|
+
},
|
|
500
|
+
});
|
|
309
501
|
const server = Bun.serve({
|
|
310
502
|
port: opts.port,
|
|
311
503
|
...(opts.hostname ? { hostname: opts.hostname } : {}),
|
|
@@ -317,32 +509,26 @@ export function startProxyServer(opts: ProxyServerOptions): {
|
|
|
317
509
|
{ error: { message: "WebSocket chat is not proxied under --isolate yet; run without --isolate for the web chat UI" } },
|
|
318
510
|
{ status: 501 },
|
|
319
511
|
);
|
|
320
|
-
if (url.pathname === "/engine" && request.method === "GET")
|
|
512
|
+
if (url.pathname === "/engine" && request.method === "GET") {
|
|
513
|
+
// A pool may have evicted and recreated the default worker. Inspect
|
|
514
|
+
// current residency without starting a worker merely for diagnostics.
|
|
515
|
+
const current = pool ? pool.child(pool.defaultKey) : engine;
|
|
321
516
|
return Response.json({
|
|
322
517
|
isolated: true,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
518
|
+
response_store: application.responseStats,
|
|
519
|
+
pid: current?.pid ?? null,
|
|
520
|
+
restarts: current?.restarts ?? null,
|
|
521
|
+
socket: current?.spec.socketPath ?? null,
|
|
326
522
|
...(pool ? { pool: { resident: pool.residentKeys, default: pool.defaultKey } } : {}),
|
|
327
523
|
});
|
|
524
|
+
}
|
|
328
525
|
try {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (pool && request.method === "POST" && MODEL_ROUTED.has(url.pathname)) {
|
|
333
|
-
const raw = await request.text();
|
|
334
|
-
let modelField: string | undefined;
|
|
335
|
-
try { modelField = (JSON.parse(raw) as { model?: string }).model; } catch { /* malformed → default child answers with its own 400 */ }
|
|
336
|
-
const target = await pool.childFor(modelField);
|
|
337
|
-
return await target.forward(new Request(request.url, {
|
|
338
|
-
method: request.method,
|
|
339
|
-
headers: request.headers,
|
|
340
|
-
body: raw,
|
|
341
|
-
signal: request.signal,
|
|
342
|
-
}));
|
|
343
|
-
}
|
|
344
|
-
return await engine.forward(request);
|
|
526
|
+
const local = await application.handle(request, forwardRequest);
|
|
527
|
+
if (local) return local;
|
|
528
|
+
return await forwardRequest(request);
|
|
345
529
|
} catch (e) {
|
|
530
|
+
if (e instanceof AdmissionRejected) return Response.json(
|
|
531
|
+
{ error: { message: e.message, type: "resource_admission", code: "queue_full" } }, { status: 429 });
|
|
346
532
|
if (request.signal.aborted) return new Response(null, { status: 499 });
|
|
347
533
|
return Response.json(
|
|
348
534
|
{
|
|
@@ -356,7 +542,13 @@ export function startProxyServer(opts: ProxyServerOptions): {
|
|
|
356
542
|
}
|
|
357
543
|
},
|
|
358
544
|
});
|
|
359
|
-
|
|
545
|
+
let closing: Promise<void> | undefined;
|
|
546
|
+
return { server, engine, pool, close: () => closing ??= (async () => {
|
|
547
|
+
await server.stop(true);
|
|
548
|
+
coordinator.close();
|
|
549
|
+
await application.close();
|
|
550
|
+
if (pool) await pool.close(); else await engine.close();
|
|
551
|
+
})() };
|
|
360
552
|
}
|
|
361
553
|
|
|
362
554
|
/** serve's value-taking flags — needed to tell a flag VALUE from the bare
|
|
@@ -368,7 +560,7 @@ const SERVE_VALUE_FLAGS = new Set([
|
|
|
368
560
|
"--batch", "--decode-concurrency", "--kv-quant",
|
|
369
561
|
"--kv-group-size", "--quantized-kv-start", "--kv-budget",
|
|
370
562
|
"--memory-budget", "--prompt-cache", "--ssd-cache", "--ssd-cache-max",
|
|
371
|
-
"--ssd-demote-idle", "--adapter", "--max-tokens", "--unix",
|
|
563
|
+
"--ssd-demote-idle", "--generation-checkpoint", "--adapter", "--max-tokens", "--unix",
|
|
372
564
|
"--model-pool", "--compiled-decode", "--compiled-activations",
|
|
373
565
|
"--fused-sdpa", "--thinking",
|
|
374
566
|
]);
|
package/src/serve/lab-routes.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { JobStore } from "../jobs";
|
|
|
2
2
|
|
|
3
3
|
export interface LabRouteDeps {
|
|
4
4
|
ensureJobs(): Promise<JobStore>;
|
|
5
|
+
acquireGpu?: (signal: AbortSignal) => Promise<import("../contracts/resources").DisposableResource>;
|
|
5
6
|
serverPort(): number | undefined;
|
|
6
7
|
invalidateLibrary(): void;
|
|
7
8
|
}
|
|
@@ -103,6 +104,7 @@ export async function handleLabRoute(
|
|
|
103
104
|
"finetune",
|
|
104
105
|
{ ...body, adapter_path: adapterPath },
|
|
105
106
|
adapterPath,
|
|
107
|
+
{ acquire: deps.acquireGpu },
|
|
106
108
|
);
|
|
107
109
|
return Response.json({ ok: true, job_id: jobId, adapter_path: adapterPath });
|
|
108
110
|
}
|
|
@@ -332,6 +334,7 @@ async function submitQuantize(request: Request, deps: LabRouteDeps): Promise<Res
|
|
|
332
334
|
rotate_weights: body.rotate_weights,
|
|
333
335
|
rotation_seed: body.rotation_seed,
|
|
334
336
|
}, outDir, {
|
|
337
|
+
acquire: deps.acquireGpu,
|
|
335
338
|
onComplete: () => deps.invalidateLibrary(),
|
|
336
339
|
});
|
|
337
340
|
return Response.json({ ok: true, job_id: jobId, output_dir: outDir });
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
// a same-process debugging/telemetry aid, not a durable store — entries for
|
|
17
17
|
// long-dead requests must not leak memory over a long-running server.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
import type { Lane } from "../contracts/pi";
|
|
20
|
+
export type { Lane } from "../contracts/pi";
|
|
20
21
|
|
|
21
22
|
const MAX_ENTRIES = 512;
|
|
22
23
|
const lanes = new Map<string, Lane>();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { modelServingBinding } from "../backends/mlx/model-serving";
|
|
2
|
+
import type { ServingContext } from "./model-host";
|
|
2
3
|
import type { GenerationGateway } from "./generation-gateway";
|
|
3
|
-
import { embedMany, isEmbeddingModel } from "../embed";
|
|
4
4
|
import { listAvailableAdapters } from "../lora";
|
|
5
5
|
|
|
6
6
|
export type ModelAdminRoute =
|
|
@@ -29,7 +29,7 @@ export function matchModelAdminRoute(method: string, pathname: string): ModelAdm
|
|
|
29
29
|
export async function handleModelAdminRoute(
|
|
30
30
|
url: URL,
|
|
31
31
|
request: Request,
|
|
32
|
-
ctx:
|
|
32
|
+
ctx: ServingContext,
|
|
33
33
|
gateway: Pick<GenerationGateway, "runExclusive">,
|
|
34
34
|
): Promise<Response | null> {
|
|
35
35
|
const route = matchModelAdminRoute(request.method, url.pathname);
|
|
@@ -37,7 +37,9 @@ export async function handleModelAdminRoute(
|
|
|
37
37
|
|
|
38
38
|
switch (route.kind) {
|
|
39
39
|
case "embeddings": {
|
|
40
|
-
|
|
40
|
+
const binding = modelServingBinding(ctx);
|
|
41
|
+
const embed = binding.embed?.bind(binding);
|
|
42
|
+
if (!embed) {
|
|
41
43
|
return Response.json({
|
|
42
44
|
error: {
|
|
43
45
|
message: `served model "${ctx.modelId}" is not an embedding model; ` +
|
|
@@ -66,7 +68,7 @@ export async function handleModelAdminRoute(
|
|
|
66
68
|
}, { status: 400 });
|
|
67
69
|
}
|
|
68
70
|
const instruction = typeof body.instruction === "string" ? body.instruction : undefined;
|
|
69
|
-
const results =
|
|
71
|
+
const results = await gateway.runExclusive(async () => embed(inputs, instruction), undefined, request.signal);
|
|
70
72
|
let totalTokens = 0;
|
|
71
73
|
const data = results.map((result, index) => {
|
|
72
74
|
totalTokens += result.tokens;
|