mlx-bun 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- package/src/weights.ts +91 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Runtime isolation — the AI may crash, the UI never may
|
|
2
|
+
|
|
3
|
+
Status: **design** (not started)
|
|
4
|
+
Owner: serving layer / runtime
|
|
5
|
+
Default today: inference runs **in-process**, on the server's event-loop
|
|
6
|
+
thread. This doc proposes moving it to a **subprocess**.
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
One `mlx-bun` process plays three or four roles at once:
|
|
11
|
+
|
|
12
|
+
1. **Serves the web UI** (static assets, the chat page).
|
|
13
|
+
2. **Serves the API** (`/v1/*` for the web UI, the CLI, and external
|
|
14
|
+
agents).
|
|
15
|
+
3. **Runs the AI** (weight load, prefill, decode — the heavy GPU work).
|
|
16
|
+
|
|
17
|
+
One executable doing all of this is the product's whole charm, and for
|
|
18
|
+
most people, most of the time, it is perfect. It breaks in one specific
|
|
19
|
+
way: when the AI is working hard — say an agentic pipeline classifying a
|
|
20
|
+
photo library — the **web UI lags**. Page refreshes stall, `/status`
|
|
21
|
+
polls hang, chat messages queue behind the GPU.
|
|
22
|
+
|
|
23
|
+
Waiting for a *chat reply* is fine and expected (local inference takes
|
|
24
|
+
time; hosted services take up to a minute too). A **laggy UI is neither
|
|
25
|
+
normal nor acceptable.** The UI must stay responsive no matter what the
|
|
26
|
+
AI is doing — and if the AI process dies (an uncatchable GPU OOM), the UI
|
|
27
|
+
must survive and report it, not go down with it.
|
|
28
|
+
|
|
29
|
+
## Root cause — blocking work on the event-loop thread
|
|
30
|
+
|
|
31
|
+
The server is a single Bun event loop. The governing principle of an
|
|
32
|
+
event loop is: **the loop thread may only ever do non-blocking work.**
|
|
33
|
+
We violate it by running the synchronous MLX FFI eval loop *on* that
|
|
34
|
+
thread.
|
|
35
|
+
|
|
36
|
+
Concretely:
|
|
37
|
+
|
|
38
|
+
- **The per-token GPU read is a blocking FFI call.** Each decode step
|
|
39
|
+
ends with `ops.itemUint32(cur)` → `a.eval()` → `mlx_eval`
|
|
40
|
+
(`src/mlx/ops.ts:471`), which parks the single JS thread until the GPU
|
|
41
|
+
returns. Compute is dispatched async (`asyncEvalAll` + decode
|
|
42
|
+
pipelining, `src/generate.ts:386`) — good — but reading the value back
|
|
43
|
+
over `bun:ffi` is a hard, synchronous wait on the main thread. There is
|
|
44
|
+
no thread handoff.
|
|
45
|
+
- **The loop yields only *microtasks*, not *macrotasks*.** Bun services
|
|
46
|
+
I/O — accepting connections, answering `/status`, serving the page —
|
|
47
|
+
only at macrotask boundaries. A bare `yield` in the async generator
|
|
48
|
+
plus an `await` of an already-resolved value drains the microtask
|
|
49
|
+
queue without ever reaching the I/O phase.
|
|
50
|
+
- **Streaming partially hides this; non-streaming does not.** The SSE
|
|
51
|
+
path's `onToken` returns `new Promise(r => setImmediate(r))` ~every
|
|
52
|
+
25 ms (`src/server.ts:1341`) — a real macrotask yield, so the page
|
|
53
|
+
breathes. The **non-streaming** path's `onToken` returns `undefined`
|
|
54
|
+
every token (`src/server.ts:1407`), so the whole completion is one
|
|
55
|
+
blocking-FFI-plus-microtask chain and the loop is starved start to
|
|
56
|
+
finish.
|
|
57
|
+
- **Prefill never yields at all.** The chunked prompt eval calls
|
|
58
|
+
`ops.evalAll(...)` synchronously with no `await` between chunks
|
|
59
|
+
(`src/generate.ts:294`). A long prompt freezes the page for the full
|
|
60
|
+
prefill *even in streaming mode*.
|
|
61
|
+
|
|
62
|
+
### Why yielding alone is not the fix
|
|
63
|
+
|
|
64
|
+
A `setImmediate` yield can interleave I/O *between* tokens, but it
|
|
65
|
+
**cannot interrupt an in-flight `mlx_eval`** — that C call is atomic from
|
|
66
|
+
JS's view. Each token blocks for tens of ms; a prefill blocks one big
|
|
67
|
+
chunk (hundreds of ms to seconds) with no yield point possible inside the
|
|
68
|
+
kernel. So perfect yielding gives the UI thin inter-token slices, not
|
|
69
|
+
isolation. Adequate for "chat plus occasional"; inadequate for "agentic
|
|
70
|
+
pipeline running flat-out while I refresh the page." For true isolation
|
|
71
|
+
the blocking work must leave the thread.
|
|
72
|
+
|
|
73
|
+
## Decision — subprocess, not worker thread
|
|
74
|
+
|
|
75
|
+
Move the AI runtime off the event-loop thread. Two ways:
|
|
76
|
+
|
|
77
|
+
- **Worker thread** (Bun `Worker`): shares the process, cheapest IPC
|
|
78
|
+
(a `SharedArrayBuffer` token ring). But a GPU OOM / SIGTRAP — which our
|
|
79
|
+
own notes flag as **uncatchable** (CLAUDE.md "async GPU exec OOM is
|
|
80
|
+
uncatchable") — takes down the whole process, **including the UI
|
|
81
|
+
server**.
|
|
82
|
+
- **Subprocess** (`Bun.spawn`): crash isolation. An MLX OOM kills the
|
|
83
|
+
inference process; the UI/API server survives, reports it, and
|
|
84
|
+
respawns. Heavier IPC, but on a single-user local box a pipe/socket is
|
|
85
|
+
trivially fast next to token cadence.
|
|
86
|
+
|
|
87
|
+
**Decision: subprocess.** The product rule is "the AI may crash, the UI
|
|
88
|
+
never may," and only address-space isolation delivers that against an
|
|
89
|
+
uncatchable fault. It also reuses machinery the repo has already proven.
|
|
90
|
+
|
|
91
|
+
## We already built the blueprint — for jobs
|
|
92
|
+
|
|
93
|
+
`src/jobs/runner.ts` already does exactly this for the *other* heavy GPU
|
|
94
|
+
consumers (quantize / finetune):
|
|
95
|
+
|
|
96
|
+
- `submitSubprocess()` creates a job row and `Bun.spawn`s the binary as a
|
|
97
|
+
child (`src/jobs/runner.ts:210`, entry `src/jobs/job-entry.ts`).
|
|
98
|
+
- A single global **GPU lease** (`gpuLeaseHolder`) serializes GPU access;
|
|
99
|
+
`isGpuBusy()` / `currentGpuJob()` (`src/jobs/runner.ts:159`) expose it,
|
|
100
|
+
**and the server already gates token generation on it** so
|
|
101
|
+
quantize/finetune don't fight live inference.
|
|
102
|
+
- IPC is a file-backed SQLite job store + stdout/stderr line pumping into
|
|
103
|
+
a log + an SSE tail (`src/jobs/sse.ts`).
|
|
104
|
+
|
|
105
|
+
So the pattern — heavy GPU work → its own process → server thread stays
|
|
106
|
+
free, serialized by one lease — is established and tested. **Inference is
|
|
107
|
+
the one heavy consumer still running inline on the event-loop thread.**
|
|
108
|
+
It is the exception, and it is the one people hit on every message.
|
|
109
|
+
|
|
110
|
+
## The seam already exists
|
|
111
|
+
|
|
112
|
+
Request handlers do not call `generate()` directly — they call
|
|
113
|
+
`gateway.run(promptIds, options, onToken, vision, shape)`
|
|
114
|
+
(`src/serve/generation-gateway.ts:108`, call sites `src/server.ts:1331`
|
|
115
|
+
and `:1407`). The `GenerationGateway` is the abstraction boundary between
|
|
116
|
+
the HTTP layer and execution. **We replace what is behind it** (inline
|
|
117
|
+
`generate()` → an IPC client that talks to the inference subprocess); the
|
|
118
|
+
HTTP/SSE layer, the `onToken` contract, and the `false`-halts-generation
|
|
119
|
+
semantics are unchanged.
|
|
120
|
+
|
|
121
|
+
## Target architecture
|
|
122
|
+
|
|
123
|
+
- **Main process = pure reactor.** `Bun.serve` does static UI + the API
|
|
124
|
+
surface + routing + tokenization/templating/SSE encoding (all cheap,
|
|
125
|
+
non-blocking JS). **Zero MLX calls.** Once generation is off-thread,
|
|
126
|
+
nothing else on the main thread blocks — static serves and `/status`
|
|
127
|
+
are already async; they lag today only because the loop is
|
|
128
|
+
monopolized. So this alone makes the UI instant.
|
|
129
|
+
- **Inference runtime = a persistent subprocess** owning the model +
|
|
130
|
+
Metal context, doing prefill/decode, streaming tokens back. Persistent
|
|
131
|
+
(not spawn-per-request): weight load is the cost, can't pay it per
|
|
132
|
+
request. One process, one model, one GPU.
|
|
133
|
+
- **One GPU lease governs all consumers** — live inference, agentic API
|
|
134
|
+
calls, quantize, finetune. The GPU is the single serialized resource
|
|
135
|
+
(matching the single-user reality: chat waiting behind the photo
|
|
136
|
+
pipeline is the *acceptable* kind of waiting), while UI/API I/O is
|
|
137
|
+
fully parallel and never waits.
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
┌──────────────────────────────────────────────┐
|
|
141
|
+
│ main process (event loop — never blocks) │
|
|
142
|
+
│ Bun.serve: static UI · /v1/* API · routing │
|
|
143
|
+
│ tokenize · template · SSE encode │
|
|
144
|
+
│ │ gateway.run() = IPC client │
|
|
145
|
+
└────────┼──────────────────────────────────────┘
|
|
146
|
+
│ request ↓ ↑ token stream
|
|
147
|
+
┌────────┼──────────────────────────────────────┐
|
|
148
|
+
│ inference subprocess (may block freely) │
|
|
149
|
+
│ owns model + Metal · prefill · decode │
|
|
150
|
+
│ holds the GPU lease while generating │
|
|
151
|
+
└────────────────────────────────────────────────┘
|
|
152
|
+
(crash here ⇒ parent respawns; UI untouched)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## IPC sketch
|
|
156
|
+
|
|
157
|
+
- **Transport:** Bun supports a structured `ipc` channel on `Bun.spawn`
|
|
158
|
+
(parent ⇄ child message passing); a unix domain socket is the
|
|
159
|
+
fallback. Tokens are slow relative to message passing, so per-token
|
|
160
|
+
messages are fine; batch a few if needed. (Jobs use stdout-line + the
|
|
161
|
+
SQLite store, which is right for fire-and-forget long jobs but too
|
|
162
|
+
coarse for low-latency per-token streaming — inference wants the
|
|
163
|
+
message channel.)
|
|
164
|
+
- **Request → child:** `{ promptIds, options, shape }`. Tokenization and
|
|
165
|
+
template rendering stay in the parent (cheap); the child receives
|
|
166
|
+
token ids.
|
|
167
|
+
- **Child → parent:** a stream of `{ token }` messages, then a final
|
|
168
|
+
`{ stats }`. The parent's gateway client invokes the handler's existing
|
|
169
|
+
`onToken` per message — per-row SSE fan-out keeps working unchanged.
|
|
170
|
+
- **Cancellation / backpressure:** `onToken` returning `false` (stop
|
|
171
|
+
sequence) or a client disconnect must abort the in-flight generation —
|
|
172
|
+
send a cancel message; the child checks it at the decode-loop boundary
|
|
173
|
+
(the natural place `generate()` already yields).
|
|
174
|
+
- **Crash handling:** child `exited` non-zero ⇒ fail any in-flight
|
|
175
|
+
request with a clean 5xx, respawn the child, reload the model. The UI
|
|
176
|
+
server never noticed.
|
|
177
|
+
|
|
178
|
+
## Lease reconciliation (the one genuinely new problem)
|
|
179
|
+
|
|
180
|
+
Jobs are *transient* (spawn → run → exit, lease released on exit). The
|
|
181
|
+
inference worker is *persistent* (resident, holds the model in RAM). They
|
|
182
|
+
must share one GPU coherently:
|
|
183
|
+
|
|
184
|
+
- A quantize/finetune job needs the GPU while the inference worker is
|
|
185
|
+
resident. Options: (a) the inference worker **drains and yields** the
|
|
186
|
+
lease for the job's duration (finish current gen, pause admission,
|
|
187
|
+
release), then resumes; (b) the persistent worker **also runs the
|
|
188
|
+
jobs** (one GPU owner, no cross-process lease at all). (b) is cleaner
|
|
189
|
+
long-term but couples job lifecycle into the inference process; (a)
|
|
190
|
+
keeps jobs as-is. **Open — pick during phase 3.**
|
|
191
|
+
- On 24 GB, the resident model already dominates memory; a concurrent
|
|
192
|
+
finetune that also needs multi-GB is an OOM footgun regardless of
|
|
193
|
+
process layout. The lease must remain *exclusive*, never truly
|
|
194
|
+
concurrent on the GPU.
|
|
195
|
+
|
|
196
|
+
## What this unlocks / simplifies
|
|
197
|
+
|
|
198
|
+
- **Delete the yield hacks.** Once the loop blocks only in the child, the
|
|
199
|
+
`setImmediate` throttle (`src/server.ts:1341`) and any prefill
|
|
200
|
+
chunk-yielding (`src/generate.ts:294`) are no longer needed for UI
|
|
201
|
+
responsiveness — the hot path gets cleaner and marginally faster.
|
|
202
|
+
- **Folds into model hot-swap** (see PRODUCT_ROADMAP "Model hot-swap from
|
|
203
|
+
the UI"): the model lives in the worker, so `loadContext` and swap
|
|
204
|
+
happen there while the main thread serves a live "swapping…" state.
|
|
205
|
+
These two items converge on "the model is not on the main thread."
|
|
206
|
+
- **Flags the next blocker:** vision preprocessing (`Bun.Image` decode,
|
|
207
|
+
`src/vision/preprocess.ts`) is also main-thread CPU work and should
|
|
208
|
+
ride into the worker, or it becomes the next source of lag.
|
|
209
|
+
|
|
210
|
+
## Staging (incremental, not big-bang)
|
|
211
|
+
|
|
212
|
+
- **Phase 1 — stop the bleeding (in-process).** Macrotask-yield the
|
|
213
|
+
non-streaming `onToken` (`src/server.ts:1407`) and the prefill chunk
|
|
214
|
+
loop (`src/generate.ts:294`), mirroring what streaming already does.
|
|
215
|
+
Trivial change; kills the acute hang for the common case. Does **not**
|
|
216
|
+
isolate the UI under sustained load — that's Phase 2.
|
|
217
|
+
- **Phase 2 — inference subprocess behind the gateway.** Stand up the
|
|
218
|
+
persistent inference child + IPC; make `GenerationGateway` an IPC
|
|
219
|
+
client. Main process makes zero MLX calls. The HTTP layer is untouched.
|
|
220
|
+
This is the real fix.
|
|
221
|
+
- **Phase 3 — unify the GPU lease.** Bring inference under the same lease
|
|
222
|
+
as quantize/finetune so all GPU consumers schedule coherently; resolve
|
|
223
|
+
the persistent-vs-transient reconciliation above.
|
|
224
|
+
|
|
225
|
+
## Exit criteria
|
|
226
|
+
|
|
227
|
+
- Under a sustained generation load (e.g. a tight `/v1` loop), a parallel
|
|
228
|
+
web-UI page load and `/status` poll return in **< 50 ms** (today: stalls
|
|
229
|
+
for the full completion in non-streaming, inter-token in streaming).
|
|
230
|
+
- Killing the inference process mid-generation leaves the UI server up; a
|
|
231
|
+
fresh page load and `/status` still succeed, and the in-flight request
|
|
232
|
+
returns a clean error rather than a hang.
|
|
233
|
+
- No throughput regression vs the in-process path on the single-stream
|
|
234
|
+
decode benchmark (token IPC overhead is below noise).
|
|
235
|
+
|
|
236
|
+
## Open questions
|
|
237
|
+
|
|
238
|
+
- Bun `ipc` channel vs unix socket vs `SharedArrayBuffer` ring for the
|
|
239
|
+
token stream — measure latency/overhead before committing.
|
|
240
|
+
- One inference worker only (single GPU, single model) — confirmed by the
|
|
241
|
+
single-user model. Multiple models = future, and pairs with hot-swap.
|
|
242
|
+
- Lease reconciliation strategy (a) drain-and-yield vs (b) worker runs
|
|
243
|
+
jobs — decide in Phase 3.
|
|
244
|
+
- Where does prompt-cache live — in the worker (it holds KV bytes, must be
|
|
245
|
+
byte-capped per CLAUDE.md). Almost certainly the worker.
|
|
246
|
+
- Startup UX: parent up and serving the UI immediately; worker model load
|
|
247
|
+
happens behind a "loading…" state the parent reports.
|