mlx-bun 0.3.0 → 0.5.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 +57 -32
- package/docs/README.md +58 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +1535 -50
- package/docs/design/decode-speed-program.md +394 -1171
- package/docs/design/generic-model-support.md +195 -3
- package/docs/design/kv-cache.md +538 -225
- package/docs/design/speculative-decoding.md +552 -50
- package/docs/design/turboquant.md +140 -17
- package/docs/design/unified-engine-frontier-plan.md +259 -150
- package/docs/planning/PRODUCT_ROADMAP.md +21 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3537 -1112
- package/docs/reference/cli.md +102 -2
- package/docs/reference/distribution.md +45 -13
- package/docs/reference/environment.md +159 -13
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +28 -7
- package/docs/reference/server-api.md +257 -33
- package/docs/reference/server-config.md +372 -151
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +464 -236
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +107 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +122 -19
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +161 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +5 -4
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +100 -0
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +244 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +54 -0
- package/src/backends/mlx/rollback.ts +45 -4
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +51 -73
- package/src/backends/mlx/speculative-group.ts +438 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
- package/src/cli.ts +333 -36
- package/src/contracts/execution.ts +21 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +33 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/engine/execution-plan.ts +38 -25
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +32 -7
- package/src/fill/fill-session.ts +14 -3
- package/src/fit.ts +29 -15
- package/src/generate.ts +105 -195
- package/src/grammar.ts +13 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/prefill.ts +6 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +92 -12
- package/src/kv-store.ts +183 -93
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
- package/src/lab/paged-kv/paged-kv.ts +126 -46
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +39 -1
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +94 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +57 -0
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +183 -82
- package/src/model/gemma4.ts +116 -28
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +70 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +177 -75
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +4 -4
- package/src/model/turboquant-codec.ts +89 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +306 -86
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +37 -25
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +19 -14
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +88 -38
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/model-host.ts +3 -4
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +6 -3
- package/src/serve/request-prep.ts +40 -25
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +185 -261
- package/src/spec/assistant-rows.ts +93 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +115 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +230 -0
- package/src/spec/qwen-mtp-source.ts +84 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +73 -69
- package/src/spec/source.ts +93 -15
- package/src/spec/two-model-rows.ts +62 -0
- package/src/spec/two-model.ts +45 -103
- package/src/ssd-cache.ts +107 -36
- package/src/ssd-durability.ts +29 -27
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +217 -57
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
package/src/cli.ts
CHANGED
|
@@ -74,6 +74,8 @@ Commands:
|
|
|
74
74
|
generate One-shot raw text generation from a local model (no server)
|
|
75
75
|
benchmark Measure decode/prefill speed of OUR stack on this machine
|
|
76
76
|
embed Text embeddings from a local embedding model (Qwen3-Embedding)
|
|
77
|
+
transcribe Speech-to-text from an audio file with a local Whisper model
|
|
78
|
+
dictate Push-to-talk: stream your microphone into Whisper, print/copy/type the text
|
|
77
79
|
evals Show recent benchmark runs (all stacks)
|
|
78
80
|
help Show help for a command (also: mlx-bun <command> --help)
|
|
79
81
|
|
|
@@ -120,10 +122,11 @@ const SERVER_FLAGS = `Server options:
|
|
|
120
122
|
--ssd-cache-verify Verify tensor hashes on every restore (reads all
|
|
121
123
|
bytes eagerly — integrity paranoia only)
|
|
122
124
|
--generation-checkpoint <tokens>
|
|
123
|
-
|
|
125
|
+
Queue an in-flight generation snapshot to the SSD
|
|
124
126
|
cache every N emitted tokens. Repeating the same
|
|
125
127
|
request after restart replays and resumes it.
|
|
126
|
-
Requires --ssd-cache
|
|
128
|
+
Requires --ssd-cache; supports ordinary requests
|
|
129
|
+
with shared cache layouts.
|
|
127
130
|
--isolate Run the inference engine as a CHILD process and
|
|
128
131
|
keep this process a pure UI/API proxy — the UI
|
|
129
132
|
stays instant under GPU load and survives engine
|
|
@@ -163,14 +166,27 @@ Model & quality:
|
|
|
163
166
|
request's \`adapter\` field, incl. "none", wins).
|
|
164
167
|
--adapter-path is accepted as the mlx_lm.server
|
|
165
168
|
alias. Hot-swap via POST /v1/adapters unchanged.
|
|
169
|
+
--whisper-model <p|query> Whisper checkpoint for /v1/audio/transcriptions
|
|
170
|
+
[default: the first downloaded whisper model,
|
|
171
|
+
resolved on the first request]. Serving a whisper
|
|
172
|
+
model ALONE starts a transcription-only server.
|
|
173
|
+
--preload Transcription-only server: load the Whisper
|
|
174
|
+
weights before listening instead of on the first
|
|
175
|
+
request.
|
|
176
|
+
--whisper-idle-unload <s> Keep the Whisper weights for this many idle seconds
|
|
177
|
+
after a take before disposing them. [default: 0 —
|
|
178
|
+
released right after every take, so the chat model
|
|
179
|
+
has the memory back before it prefills; a cold
|
|
180
|
+
take costs ~150 ms more than a warm one]
|
|
181
|
+
--whisper-resident Never dispose the Whisper weights (sotto-style).
|
|
166
182
|
--draft-model <query> Speculative decoding: a drafter proposes tokens
|
|
167
183
|
the main model verifies (exact results, faster
|
|
168
184
|
decode when drafts land). The artifact's kind is
|
|
169
185
|
auto-detected — a full same-tokenizer model
|
|
170
186
|
(mlx_lm.server parity), a Gemma "-assistant"
|
|
171
187
|
KV-borrowing drafter, or a DSpark checkpoint.
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
Supported draft providers use shared batched
|
|
189
|
+
execution; ordinary requests can batch too.
|
|
174
190
|
--draft-kind <kind> Override draft-artifact detection:
|
|
175
191
|
two-model | assistant | dspark | deepspec | mtp | ngram
|
|
176
192
|
(deepspec = DeepSeek's released Gemma4DSparkModel
|
|
@@ -205,9 +221,8 @@ Model & quality:
|
|
|
205
221
|
only; sliding-window layers stay bf16)]
|
|
206
222
|
--paged-kv OPTIONAL vLLM-style paged KV storage (block pool +
|
|
207
223
|
gather before the stock SDPA; env MLX_BUN_PAGED_KV=1).
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
--kv-quant, --draft-model; bypasses the prompt
|
|
224
|
+
Gemma4-family, bf16; supports --batch N.
|
|
225
|
+
Refuses --kv-quant and --draft-model; bypasses the prompt
|
|
211
226
|
cache; runs uncompiled decode. Bit-exact with the
|
|
212
227
|
plain path; expect a small decode cost at batch=1
|
|
213
228
|
(the gather copy). docs/design/kv-cache.md
|
|
@@ -268,6 +283,74 @@ Kill switches (bit-exact A/B levers — each selects a slower same-parity path):
|
|
|
268
283
|
bit-exact with the resident path`;
|
|
269
284
|
|
|
270
285
|
const HELP: Record<string, string> = {
|
|
286
|
+
dictate: `mlx-bun dictate — push-to-talk dictation from your microphone
|
|
287
|
+
|
|
288
|
+
Usage: mlx-bun dictate [query] [options]
|
|
289
|
+
|
|
290
|
+
Captures the mic through the AVAudioEngine sidecar (16 kHz mono; macOS asks
|
|
291
|
+
for Microphone permission on first use) and streams it into a Whisper
|
|
292
|
+
transcription session while you speak — every finished 30 s window is
|
|
293
|
+
transcribed during capture, so the result lands about one window after you
|
|
294
|
+
stop. Silence never runs Whisper (Silero VAD).
|
|
295
|
+
|
|
296
|
+
Modes:
|
|
297
|
+
(default) Press Enter to start a take, Enter again to stop;
|
|
298
|
+
q + Enter or Ctrl-C quits
|
|
299
|
+
--hotkey [keycode] Hold a key instead (default 61 = Right Option, like
|
|
300
|
+
sotto); needs Input Monitoring for your terminal
|
|
301
|
+
Backend:
|
|
302
|
+
[query] / --model <p> Whisper model for in-process transcription (default:
|
|
303
|
+
the first downloaded whisper checkpoint)
|
|
304
|
+
--server <url> Use a running mlx-bun server's /v1/audio/sessions
|
|
305
|
+
instead of loading the model here
|
|
306
|
+
--idle-unload <s> Release the weights after this many idle seconds
|
|
307
|
+
(0 = right after each take; default 30); --resident
|
|
308
|
+
keeps them loaded
|
|
309
|
+
Decoding:
|
|
310
|
+
--language <code> default en; "auto" detects
|
|
311
|
+
--beam-size <n> beam search (default greedy)
|
|
312
|
+
--prompt "…" initial prompt / --vocabulary "a,b,c" hint terms
|
|
313
|
+
--no-vad skip the Silero gate
|
|
314
|
+
Output (any combination; default prints):
|
|
315
|
+
--copy copy the transcript to the clipboard (pbcopy)
|
|
316
|
+
--type type it into the frontmost app via System Events
|
|
317
|
+
keystrokes (needs Accessibility for your terminal);
|
|
318
|
+
--type-delay <s> waits first so you can Cmd-Tab
|
|
319
|
+
(default 1 in Enter mode, 0 with --hotkey)`,
|
|
320
|
+
transcribe: `mlx-bun transcribe — speech-to-text with a local Whisper model (no server)
|
|
321
|
+
|
|
322
|
+
Usage: mlx-bun transcribe <audio-file> [query] [options]
|
|
323
|
+
|
|
324
|
+
Loads a Whisper checkpoint (mlx-community/whisper-*) and prints the
|
|
325
|
+
transcript. WAV is decoded natively; any other container CoreAudio reads
|
|
326
|
+
(mp3/m4a/flac/ogg/aiff/…) is transcoded with afconvert first.
|
|
327
|
+
|
|
328
|
+
Model:
|
|
329
|
+
[query] / --model <p> Whisper model; omitted = the first downloaded
|
|
330
|
+
\`whisper\` checkpoint (errors with an mlx-bun get
|
|
331
|
+
suggestion when none)
|
|
332
|
+
Decoding:
|
|
333
|
+
--language <code> ISO code or name ("en", "japanese"); "auto" or omitted
|
|
334
|
+
detects the language from the first 30 s
|
|
335
|
+
--task translate Translate to English instead of transcribing
|
|
336
|
+
--beam-size <n> Beam search width (default: greedy)
|
|
337
|
+
--temperature <t> Sampling temperature; the default runs the
|
|
338
|
+
(0, 0.2, …, 1.0) fallback ladder like mlx-whisper
|
|
339
|
+
--no-fallback Temperature 0 only, no retry ladder
|
|
340
|
+
--prompt "…" Initial prompt (vocabulary hints / style)
|
|
341
|
+
--no-timestamps Decode text only (<|notimestamps|>)
|
|
342
|
+
--no-condition Do not condition each window on the previous text
|
|
343
|
+
--vad Silero VAD gate: skip Whisper when no speech is
|
|
344
|
+
detected (weights: ggml-org/whisper-vad in the HF cache
|
|
345
|
+
or --vad-model <path>); --vad-trim crops to speech
|
|
346
|
+
--word-timestamps Word-level timestamps (verbose_json / verbose output)
|
|
347
|
+
--faithful Run the oracle-parity graph instead of the fast path
|
|
348
|
+
--audio-ctx <n> Lab: encode only n of the 1500 encoder positions
|
|
349
|
+
(whisper.cpp -ac); cheaper for short clips, degrades
|
|
350
|
+
below ~1024 — off by default
|
|
351
|
+
Output:
|
|
352
|
+
--format <f> text (default) · json · verbose_json · srt · vtt
|
|
353
|
+
--verbose Print each segment with its time range as it decodes`,
|
|
271
354
|
pi: `mlx-bun pi — drop into a coding-agent session on a local model
|
|
272
355
|
|
|
273
356
|
Usage: mlx-bun pi [options] [message...]
|
|
@@ -1012,22 +1095,28 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
1012
1095
|
if (route.kvQuant !== undefined) serverOptions.kvQuant = route.kvQuant;
|
|
1013
1096
|
if (route.turboQuant !== undefined) serverOptions.turboQuant = route.turboQuant;
|
|
1014
1097
|
// --paged-kv (env: MLX_BUN_PAGED_KV=1): OPTIONAL vLLM-style block-pool KV
|
|
1015
|
-
// storage, default off (docs/design/kv-cache.md).
|
|
1016
|
-
//
|
|
1017
|
-
// --batch N>1 is refused loudly by createServer rather than downgraded.
|
|
1098
|
+
// storage, default off (docs/design/kv-cache.md). The state policy owns
|
|
1099
|
+
// block pools and cache reuse; paging preserves the selected batch size.
|
|
1018
1100
|
if (flag("paged-kv") || runtimeValue("MLX_BUN_PAGED_KV") === "1") {
|
|
1019
1101
|
const bsRaw = opt("paged-kv-block-size");
|
|
1020
1102
|
serverOptions.pagedKv = bsRaw !== null ? { blockSize: Number(bsRaw) } : {};
|
|
1021
|
-
if (batchRaw === null) {
|
|
1022
|
-
serverOptions.batch = 1;
|
|
1023
|
-
console.log("[paged-kv] serial-only in v1 — pinning --batch 1");
|
|
1024
|
-
}
|
|
1025
1103
|
}
|
|
1026
1104
|
// Bind loopback unless asked otherwise (mlx_lm.server parity); --host
|
|
1027
1105
|
// 0.0.0.0 is the explicit opt-in for LAN exposure. The chat-UI open and
|
|
1028
1106
|
// `mlx-bun pi` attach both go through localhost, so loopback-only is
|
|
1029
1107
|
// transparent to them.
|
|
1030
1108
|
serverOptions.hostname = opt("host", "127.0.0.1")!;
|
|
1109
|
+
// Speech-to-text companion: explicit checkpoint + residency policy.
|
|
1110
|
+
const whisperIdleRaw = opt("whisper-idle-unload");
|
|
1111
|
+
if (whisperIdleRaw !== null) {
|
|
1112
|
+
const n = Number(whisperIdleRaw);
|
|
1113
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
1114
|
+
console.error(`--whisper-idle-unload expects seconds >= 0 (got "${whisperIdleRaw}")`);
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
serverOptions.whisperIdleUnloadSec = n;
|
|
1118
|
+
}
|
|
1119
|
+
if (flag("whisper-resident")) serverOptions.whisperResident = true;
|
|
1031
1120
|
// Server-wide default for the chat template's enable_thinking variable
|
|
1032
1121
|
// (CPM/MiniCPM5 and other hybrid-reasoning models). Unset ⇒ the model's
|
|
1033
1122
|
// own default (false for MiniCPM5). onOff accepts true|false|on|off|1|0.
|
|
@@ -1642,29 +1731,44 @@ switch (cmd) {
|
|
|
1642
1731
|
// straight from that path; anything else resolves like the positional
|
|
1643
1732
|
// fuzzy query. Precedence: --model > positional > --query > auto-pick.
|
|
1644
1733
|
const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
|
|
1734
|
+
if (m.modelType === "whisper") {
|
|
1735
|
+
// Transcription-only server: no chat model, weights paged in on demand.
|
|
1736
|
+
const sNative0 = step("native runtime");
|
|
1737
|
+
await ensureNative(sNative0);
|
|
1738
|
+
sNative0.done("native runtime ready");
|
|
1739
|
+
const { createTranscriptionServer } = await import("./serve/transcription-server");
|
|
1740
|
+
const { server, service } = await createTranscriptionServer({
|
|
1741
|
+
modelDir: m.path, modelId: m.repoId, port: rt.port,
|
|
1742
|
+
...(rt.serverOptions.hostname ? { hostname: rt.serverOptions.hostname } : {}),
|
|
1743
|
+
...(rt.serverOptions.unixSocket ? { unixSocket: rt.serverOptions.unixSocket } : {}),
|
|
1744
|
+
idleUnloadSec: rt.serverOptions.whisperIdleUnloadSec,
|
|
1745
|
+
resident: rt.serverOptions.whisperResident,
|
|
1746
|
+
preload: flag("preload"),
|
|
1747
|
+
});
|
|
1748
|
+
const where = rt.serverOptions.unixSocket ?? `http://${rt.serverOptions.hostname === "0.0.0.0" ? "localhost" : rt.serverOptions.hostname}:${server.port}`;
|
|
1749
|
+
box([
|
|
1750
|
+
`${style.bold(m.repoId)} ${style.dim("· transcription-only server")}`,
|
|
1751
|
+
`POST ${where}/v1/audio/transcriptions ${style.dim(`· ${service.stats.idle_unload_sec === null ? "always resident" : service.idleUnloadSec === 0 ? "released after every take" : `idle unload ${service.idleUnloadSec}s`}${service.resident ? " · loaded" : " · loads on first request"}`)}`,
|
|
1752
|
+
]);
|
|
1753
|
+
const stop = () => { service.dispose(); server.stop(true); process.exit(0); };
|
|
1754
|
+
process.once("SIGINT", stop);
|
|
1755
|
+
process.once("SIGTERM", stop);
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
const whisperQuery = opt("whisper-model");
|
|
1759
|
+
if (whisperQuery !== null) {
|
|
1760
|
+
const { m: wm } = await resolveModelAuto(whisperQuery);
|
|
1761
|
+
if (wm.modelType !== "whisper") {
|
|
1762
|
+
console.error(`--whisper-model ${whisperQuery} resolved to ${wm.repoId} (model_type ${wm.modelType}), not a Whisper checkpoint`);
|
|
1763
|
+
process.exit(1);
|
|
1764
|
+
}
|
|
1765
|
+
rt.serverOptions.whisperModelDir = wm.path;
|
|
1766
|
+
rt.serverOptions.whisperModelId = wm.repoId;
|
|
1767
|
+
}
|
|
1645
1768
|
const sFit = step("assessing fit");
|
|
1646
1769
|
const config = await loadModelConfig(m.path);
|
|
1647
1770
|
const report = fit(config, m.sizeBytes, 8192, undefined, undefined, m.expertsBytes);
|
|
1648
1771
|
sFit.done(`${style.bold(m.repoId)} ${style.dim(`· ${gb(m.sizeBytes)} · ~${report.predictedDecodeTps.toFixed(0)} tok/s predicted`)}${picked ? style.dim(" · auto-picked (override: mlx-bun serve <query>)") : ""}`);
|
|
1649
|
-
// Near-ceiling advisory (admission doctrine: advise, never refuse). A
|
|
1650
|
-
// model near the DEFAULT Metal wired ceiling (~75% of RAM) dies with an
|
|
1651
|
-
// UNCATCHABLE async-GPU-OOM on long prefills (M4 24 GB × 17 GB model,
|
|
1652
|
-
// 2026-08-20) — surface the standard remedy up front instead.
|
|
1653
|
-
{
|
|
1654
|
-
const { maxRecommendedWorkingSetSize } = await import("./mlx/ffi");
|
|
1655
|
-
const ceiling = maxRecommendedWorkingSetSize();
|
|
1656
|
-
const sysctlRaw = Bun.spawnSync(["sysctl", "-n", "iogpu.wired_limit_mb"]).stdout.toString().trim();
|
|
1657
|
-
const limitIsDefault = sysctlRaw === "0" || sysctlRaw === "";
|
|
1658
|
-
if (limitIsDefault && ceiling > 0 && m.sizeBytes > 0.8 * ceiling) {
|
|
1659
|
-
const ramGb = Number(Bun.spawnSync(["sysctl", "-n", "hw.memsize"]).stdout.toString().trim()) / 2 ** 30;
|
|
1660
|
-
const suggestMb = Math.floor((ramGb - 2.5) * 1024);
|
|
1661
|
-
console.error(style.dim(
|
|
1662
|
-
` ! ${gb(m.sizeBytes)} model vs ~${gb(ceiling)} default GPU ceiling — long prompts can\n` +
|
|
1663
|
-
` hit an uncatchable Metal OOM. If that happens, raise the limit first:\n` +
|
|
1664
|
-
` sudo sysctl iogpu.wired_limit_mb=${suggestMb} (resets on reboot)`,
|
|
1665
|
-
));
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
1772
|
const sNative = step("native runtime");
|
|
1669
1773
|
await ensureNative(sNative);
|
|
1670
1774
|
sNative.done("native runtime ready");
|
|
@@ -1684,9 +1788,8 @@ switch (cmd) {
|
|
|
1684
1788
|
}
|
|
1685
1789
|
// Speculative decoding (mlx_lm.server parity): --draft-model <path|query>
|
|
1686
1790
|
// resolves like the main model; --num-draft-tokens defaults to 3 (the
|
|
1687
|
-
// server's default upstream — mlx_lm.generate's is 2).
|
|
1688
|
-
//
|
|
1689
|
-
// is_batchable = draft is None).
|
|
1791
|
+
// server's default upstream — mlx_lm.generate's is 2). Qualified providers
|
|
1792
|
+
// use shared execution; ordinary requests retain their own placement.
|
|
1690
1793
|
const draftQuery = opt("draft-model");
|
|
1691
1794
|
let draftModelDir: string | undefined;
|
|
1692
1795
|
if (draftQuery !== null) {
|
|
@@ -1908,6 +2011,200 @@ switch (cmd) {
|
|
|
1908
2011
|
break;
|
|
1909
2012
|
}
|
|
1910
2013
|
|
|
2014
|
+
case "transcribe": {
|
|
2015
|
+
// Speech-to-text, one-shot, no server: audio file → Whisper → text.
|
|
2016
|
+
const { openWhisperModel } = await import("./model/factory");
|
|
2017
|
+
const { loadWhisperTokenizer } = await import("./audio/whisper-tokenizer");
|
|
2018
|
+
const { WhisperTranscriber } = await import("./audio/whisper-transcribe");
|
|
2019
|
+
const { formatTranscription, TRANSCRIPTION_FORMATS } = await import("./audio/whisper-format");
|
|
2020
|
+
const { TranscriptionService } = await import("./serve/transcription-service");
|
|
2021
|
+
const { formatTimestamp } = await import("./audio/whisper-format");
|
|
2022
|
+
|
|
2023
|
+
const audioPath = positional(0);
|
|
2024
|
+
if (!audioPath) {
|
|
2025
|
+
console.error("usage: mlx-bun transcribe <audio-file> [query] [--language en] [--format text|json|verbose_json|srt|vtt]");
|
|
2026
|
+
process.exit(1);
|
|
2027
|
+
}
|
|
2028
|
+
const format = (opt("format") ?? "text") as import("./audio/whisper-format").TranscriptionFormat;
|
|
2029
|
+
if (!TRANSCRIPTION_FORMATS.includes(format)) {
|
|
2030
|
+
console.error(`--format must be one of ${TRANSCRIPTION_FORMATS.join(", ")}`);
|
|
2031
|
+
process.exit(1);
|
|
2032
|
+
}
|
|
2033
|
+
const bytes = new Uint8Array(await Bun.file(audioPath).arrayBuffer());
|
|
2034
|
+
const samples = await TranscriptionService.decodeAudio(bytes);
|
|
2035
|
+
const durationSeconds = samples.length / 16_000;
|
|
2036
|
+
|
|
2037
|
+
const modelQuery = opt("model") ?? positional(1) ?? opt("query");
|
|
2038
|
+
let modelDir: string;
|
|
2039
|
+
if (modelQuery && existsSync(join(modelQuery, "config.json"))) modelDir = modelQuery;
|
|
2040
|
+
else if (modelQuery) modelDir = (await resolveModelAuto(modelQuery)).m.path;
|
|
2041
|
+
else {
|
|
2042
|
+
const reg = new Registry();
|
|
2043
|
+
if (reg.list().length === 0) await reg.scan();
|
|
2044
|
+
const w = reg.list().find((r) => r.modelType === "whisper");
|
|
2045
|
+
if (!w) {
|
|
2046
|
+
console.error("no Whisper model downloaded — try: mlx-bun get mlx-community/whisper-large-v3-turbo");
|
|
2047
|
+
process.exit(1);
|
|
2048
|
+
}
|
|
2049
|
+
modelDir = w.path;
|
|
2050
|
+
}
|
|
2051
|
+
let vadGate: { segments: { start: number; end: number }[] } | null = null;
|
|
2052
|
+
if (flag("vad")) {
|
|
2053
|
+
const { SileroVad } = await import("./audio/silero-vad");
|
|
2054
|
+
const tv = performance.now();
|
|
2055
|
+
const vad = SileroVad.load(opt("vad-model"));
|
|
2056
|
+
const { segments } = vad.detect(samples, { threshold: Number(opt("vad-threshold") ?? 0.5) });
|
|
2057
|
+
vadGate = { segments };
|
|
2058
|
+
if (flag("verbose")) console.error(`vad: ${segments.length} speech segment(s) in ${(performance.now() - tv).toFixed(0)} ms`);
|
|
2059
|
+
if (segments.length === 0) { process.stdout.write(format === "text" ? "\n" : JSON.stringify({ text: "", vad: { speech: false, segments: [] } }) + "\n"); break; }
|
|
2060
|
+
}
|
|
2061
|
+
const { model } = await openWhisperModel(modelDir);
|
|
2062
|
+
const tok = await loadWhisperTokenizer(modelDir, model.dims.nVocab);
|
|
2063
|
+
const transcriber = new WhisperTranscriber(model, tok);
|
|
2064
|
+
void vadGate;
|
|
2065
|
+
const langOpt = opt("language");
|
|
2066
|
+
const task = opt("task") === "translate" ? "translate" as const : "transcribe" as const;
|
|
2067
|
+
const beam = opt("beam-size");
|
|
2068
|
+
const temperature = opt("temperature");
|
|
2069
|
+
const verbose = flag("verbose");
|
|
2070
|
+
const t0 = performance.now();
|
|
2071
|
+
const result = await transcriber.transcribe(samples, {
|
|
2072
|
+
language: !langOpt || langOpt === "auto" ? null : langOpt,
|
|
2073
|
+
task,
|
|
2074
|
+
beamSize: beam ? Number(beam) : null,
|
|
2075
|
+
...(temperature ? { temperature: Number(temperature) } : flag("no-fallback") ? { temperature: 0 } : {}),
|
|
2076
|
+
initialPrompt: opt("prompt"),
|
|
2077
|
+
withoutTimestamps: flag("no-timestamps"),
|
|
2078
|
+
conditionOnPreviousText: !flag("no-condition"),
|
|
2079
|
+
fast: !flag("faithful"),
|
|
2080
|
+
wordTimestamps: flag("word-timestamps"),
|
|
2081
|
+
audioCtx: opt("audio-ctx") ? Number(opt("audio-ctx")) : null,
|
|
2082
|
+
onSegment: verbose
|
|
2083
|
+
? (s) => console.error(`[${formatTimestamp(s.start, ".")} --> ${formatTimestamp(s.end, ".")}] ${s.text.trim()}`)
|
|
2084
|
+
: undefined,
|
|
2085
|
+
});
|
|
2086
|
+
const elapsed = (performance.now() - t0) / 1000;
|
|
2087
|
+
if (verbose)
|
|
2088
|
+
console.error(`${durationSeconds.toFixed(2)} s audio in ${elapsed.toFixed(3)} s (${(durationSeconds / elapsed).toFixed(1)}× realtime), language ${result.language}`);
|
|
2089
|
+
process.stdout.write(formatTranscription(result, format, durationSeconds, task));
|
|
2090
|
+
transcriber.dispose();
|
|
2091
|
+
model.dispose();
|
|
2092
|
+
break;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
case "dictate": {
|
|
2096
|
+
const { startMicCapture } = await import("./audio/mic-capture");
|
|
2097
|
+
const { TranscriptionService, defaultWhisperRecord } = await import("./serve/transcription-service");
|
|
2098
|
+
const { style } = await import("./tui");
|
|
2099
|
+
const serverUrl = opt("server");
|
|
2100
|
+
const hotkeyFlag = argv.includes("--hotkey");
|
|
2101
|
+
const hotkey = hotkeyFlag ? Number(opt("hotkey") ?? 61) || 61 : null;
|
|
2102
|
+
const language = opt("language") ?? "en";
|
|
2103
|
+
const beam = opt("beam-size");
|
|
2104
|
+
const vocabulary = opt("vocabulary")?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2105
|
+
const params = {
|
|
2106
|
+
language: language === "auto" ? null : language,
|
|
2107
|
+
beamSize: beam ? Number(beam) : null,
|
|
2108
|
+
temperature: 0,
|
|
2109
|
+
prompt: opt("prompt"),
|
|
2110
|
+
vocabulary,
|
|
2111
|
+
vad: flag("no-vad") ? null : { threshold: 0.5, minSpeechMs: 120 },
|
|
2112
|
+
};
|
|
2113
|
+
const typeDelay = Number(opt("type-delay") ?? (hotkey ? 0 : 1));
|
|
2114
|
+
|
|
2115
|
+
// backend: in-process service or a running server's sessions
|
|
2116
|
+
let service: InstanceType<typeof TranscriptionService> | null = null;
|
|
2117
|
+
if (!serverUrl) {
|
|
2118
|
+
const q = opt("model") ?? positional(0) ?? opt("query");
|
|
2119
|
+
let modelDir: string; let modelId: string;
|
|
2120
|
+
if (q && existsSync(join(q, "config.json"))) { modelDir = q; modelId = q; }
|
|
2121
|
+
else if (q) { const { m } = await resolveModelAuto(q); modelDir = m.path; modelId = m.repoId; }
|
|
2122
|
+
else {
|
|
2123
|
+
const rec = await defaultWhisperRecord();
|
|
2124
|
+
if (!rec) { console.error("no Whisper model downloaded — try: mlx-bun get mlx-community/whisper-large-v3-turbo"); process.exit(1); }
|
|
2125
|
+
modelDir = rec.path; modelId = rec.repoId;
|
|
2126
|
+
}
|
|
2127
|
+
const idle = opt("idle-unload");
|
|
2128
|
+
service = new TranscriptionService({ modelDir, modelId, idleUnloadSec: idle !== null ? Number(idle) : 30, resident: flag("resident"), log: () => {} });
|
|
2129
|
+
process.stderr.write(style.dim("loading whisper… "));
|
|
2130
|
+
await service.ensureLoaded();
|
|
2131
|
+
service.vad();
|
|
2132
|
+
process.stderr.write(style.dim("ready\n"));
|
|
2133
|
+
}
|
|
2134
|
+
const base = serverUrl?.replace(/\/$/, "");
|
|
2135
|
+
interface Take { feed(pcm: Float32Array): Promise<void>; finish(): Promise<{ text: string; ms: number }>; }
|
|
2136
|
+
const startTake = async (): Promise<Take> => {
|
|
2137
|
+
if (service) {
|
|
2138
|
+
const session = await service.createSession(params);
|
|
2139
|
+
return {
|
|
2140
|
+
feed: async (pcm) => { await session.append(pcm); },
|
|
2141
|
+
finish: async () => { const t0 = performance.now(); const o = await session.finish(); return { text: o.result.text.trim(), ms: performance.now() - t0 }; },
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
const r = await fetch(`${base}/v1/audio/sessions`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ language: params.language, beam_size: params.beamSize, temperature: 0, prompt: params.prompt, vocabulary, vad: !!params.vad, vad_min_speech_ms: 120 }) });
|
|
2145
|
+
if (!r.ok) throw new Error(`session create failed: ${r.status} ${await r.text()}`);
|
|
2146
|
+
const { id } = await r.json() as { id: string };
|
|
2147
|
+
return {
|
|
2148
|
+
feed: async (pcm) => { await fetch(`${base}/v1/audio/sessions/${id}/audio`, { method: "POST", headers: { "content-type": "audio/pcm;rate=16000" }, body: new Uint8Array(pcm.buffer.slice(pcm.byteOffset, pcm.byteOffset + pcm.byteLength) as ArrayBuffer) }); },
|
|
2149
|
+
finish: async () => { const t0 = performance.now(); const f = await fetch(`${base}/v1/audio/sessions/${id}/finish`, { method: "POST" }); const b = await f.json() as { text: string }; return { text: (b.text ?? "").trim(), ms: performance.now() - t0 }; },
|
|
2150
|
+
};
|
|
2151
|
+
};
|
|
2152
|
+
|
|
2153
|
+
const deliver = async (text: string) => {
|
|
2154
|
+
if (!text) { console.log(style.dim("(no speech)")); return; }
|
|
2155
|
+
console.log(text);
|
|
2156
|
+
if (flag("copy")) { const p = Bun.spawn(["pbcopy"], { stdin: "pipe" }); p.stdin.write(text); p.stdin.end(); await p.exited; }
|
|
2157
|
+
if (flag("type")) {
|
|
2158
|
+
if (typeDelay > 0) await Bun.sleep(typeDelay * 1000);
|
|
2159
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2160
|
+
const r = Bun.spawnSync(["osascript", "-e", `tell application "System Events" to keystroke "${escaped}"`], { stderr: "pipe" });
|
|
2161
|
+
if (r.exitCode !== 0) console.error(style.dim(`typing failed (grant Accessibility to your terminal): ${r.stderr.toString().trim()}`));
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
|
|
2165
|
+
const mic = await startMicCapture({ hotkey });
|
|
2166
|
+
let take: Take | null = null;
|
|
2167
|
+
let pending: Float32Array[] = []; let pendingLen = 0;
|
|
2168
|
+
let feeding: Promise<void> = Promise.resolve();
|
|
2169
|
+
const flush = () => {
|
|
2170
|
+
if (!take || pendingLen === 0) return;
|
|
2171
|
+
const buf = new Float32Array(pendingLen); let o = 0; for (const p of pending) { buf.set(p, o); o += p.length; }
|
|
2172
|
+
pending = []; pendingLen = 0;
|
|
2173
|
+
const t = take;
|
|
2174
|
+
feeding = feeding.then(() => t.feed(buf)).catch((e) => console.error(style.dim(`feed failed: ${(e as Error).message}`)));
|
|
2175
|
+
};
|
|
2176
|
+
const begin = async () => { if (take) return; take = await startTake(); pending = []; pendingLen = 0; process.stderr.write(style.bold("● recording") + style.dim(hotkey ? " (release to stop)\n" : " (Enter to stop)\n")); };
|
|
2177
|
+
const end = async () => {
|
|
2178
|
+
if (!take) return;
|
|
2179
|
+
flush();
|
|
2180
|
+
await feeding;
|
|
2181
|
+
const t = take; take = null;
|
|
2182
|
+
process.stderr.write(style.dim("transcribing… "));
|
|
2183
|
+
try { const { text, ms } = await t.finish(); process.stderr.write(style.dim(`${ms.toFixed(0)} ms\n`)); await deliver(text); }
|
|
2184
|
+
catch (e) { console.error(`transcription failed: ${(e as Error).message}`); }
|
|
2185
|
+
};
|
|
2186
|
+
if (!hotkey) {
|
|
2187
|
+
console.error(style.dim("Enter: start/stop a take · q: quit"));
|
|
2188
|
+
(async () => {
|
|
2189
|
+
for await (const line of console) {
|
|
2190
|
+
if (line.trim() === "q") { mic.stop(); process.exit(0); }
|
|
2191
|
+
if (take) await end(); else await begin();
|
|
2192
|
+
}
|
|
2193
|
+
})();
|
|
2194
|
+
}
|
|
2195
|
+
process.once("SIGINT", () => { mic.stop(); process.exit(0); });
|
|
2196
|
+
for await (const ev of mic.events) {
|
|
2197
|
+
if (ev.kind === "ready") { if (hotkey) console.error(style.dim(`hold key ${hotkey} to talk · Ctrl-C quits · mic ${ev.info}`)); }
|
|
2198
|
+
else if (ev.kind === "error") { console.error(ev.message); mic.stop(); process.exit(1); }
|
|
2199
|
+
else if (ev.kind === "hotkey") { if (ev.down) await begin(); else await end(); }
|
|
2200
|
+
else if (ev.kind === "pcm" && take) {
|
|
2201
|
+
pending.push(ev.samples); pendingLen += ev.samples.length;
|
|
2202
|
+
if (pendingLen >= 16_000 / 4) flush(); // every 250 ms of audio
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
break;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
1911
2208
|
case "bench":
|
|
1912
2209
|
case "benchmark": {
|
|
1913
2210
|
const { banner, step, box, style } = await import("./tui");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Request facts only. Native resources remain with the preparation owner. */
|
|
2
2
|
export interface ExecutionRequirements {
|
|
3
3
|
readonly hasVision: boolean;
|
|
4
|
+
readonly hasPreparedPrefixIdentity?: boolean;
|
|
4
5
|
readonly hasAdapters: boolean;
|
|
5
6
|
readonly hasRepetitionPenalty: boolean;
|
|
6
7
|
readonly userSeed: boolean;
|
|
@@ -17,9 +18,29 @@ export interface ExecutionCapabilities {
|
|
|
17
18
|
readonly continuous: boolean;
|
|
18
19
|
readonly quantizedBatch: boolean;
|
|
19
20
|
readonly grammarBatch: boolean;
|
|
21
|
+
readonly adapterBatch?: boolean;
|
|
22
|
+
/** Prepared media can enter the ordinary shared execution group. */
|
|
23
|
+
readonly mediaBatch?: boolean;
|
|
24
|
+
readonly mediaPrefixCache?: boolean;
|
|
25
|
+
readonly pagedBatch?: boolean;
|
|
20
26
|
readonly checkpoints: boolean;
|
|
27
|
+
/** Bound ordinary driver can restore/capture this request configuration. */
|
|
28
|
+
readonly sharedCheckpoints?: boolean;
|
|
21
29
|
/** Model-qualified speculative execution can retain this request's KV codec. */
|
|
22
30
|
readonly speculativeKvQuant?: boolean;
|
|
31
|
+
readonly speculativeTurboQuant?: boolean;
|
|
32
|
+
readonly turboQuantBatch?: boolean;
|
|
33
|
+
readonly speculativeLogprobs?: boolean;
|
|
34
|
+
/** The grouped provider supports the target's mounted adapter context. */
|
|
35
|
+
readonly sharedSpeculativeAdapters?: boolean;
|
|
36
|
+
/** Methods supplied by the model's shared execution binding. */
|
|
37
|
+
readonly groupedMethods?: readonly string[];
|
|
38
|
+
/** A shared method accepts request-owned grammar continuation proposals. */
|
|
39
|
+
readonly sharedGrammarProposals?: boolean;
|
|
40
|
+
/** A grouped method can consume this request's strict known continuations. */
|
|
41
|
+
readonly sharedFill?: boolean;
|
|
42
|
+
/** A mounted provider can consume verified external continuations. */
|
|
43
|
+
readonly sharedSpeculativeEcho?: boolean;
|
|
23
44
|
/** A graph-owned compiled step exists; cache geometry can still decline it. */
|
|
24
45
|
readonly compiledDecode?: boolean;
|
|
25
46
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Exact reusable work, independent of token-prefix matching. The same cache
|
|
2
|
+
* owner chooses residency and persistence for objects and conversation state. */
|
|
3
|
+
export interface ObjectCache<Value> {
|
|
4
|
+
/** Borrow immutable data through an owned lease. A miss permits recompute. */
|
|
5
|
+
take(key: string): Promise<{ value: Value; dispose(): void } | null>;
|
|
6
|
+
/** Successful return transfers ownership, including immediate eviction. */
|
|
7
|
+
put(key: string, value: Value): void;
|
|
8
|
+
}
|
|
@@ -13,7 +13,7 @@ export function piModelDefinition(options: PiModelOptions = {}) {
|
|
|
13
13
|
name: options.name ?? "mlx-bun (local)",
|
|
14
14
|
api: "openai-completions" as const,
|
|
15
15
|
reasoning: options.reasoning ?? false,
|
|
16
|
-
compat: { supportsDeveloperRole: false,
|
|
16
|
+
compat: { supportsDeveloperRole: false, sendSessionAffinityHeaders: true,
|
|
17
17
|
...(options.reasoning ? { thinkingFormat: "qwen-chat-template" as const } : {}) },
|
|
18
18
|
input: (options.vision ? ["text", "image"] : ["text"]) as ("text" | "image")[],
|
|
19
19
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
package/src/contracts/pi.ts
CHANGED
|
@@ -160,7 +160,9 @@ export type ServerMessage =
|
|
|
160
160
|
// provider declaration — clients use it instead of probing for a 400.
|
|
161
161
|
// `thinking`: whether the model has a switchable reasoning channel (drives
|
|
162
162
|
// the UI's thinking on/off toggle; false hides it).
|
|
163
|
-
|
|
163
|
+
// transcription: a Whisper checkpoint is configured or resolvable for
|
|
164
|
+
// /v1/audio/sessions — drives the composer's hold-to-talk mic button.
|
|
165
|
+
| { type: "ready"; model: string; vision: boolean; audio: boolean; thinking: boolean; genDefaults: ReadyGenDefaults; transcription?: boolean }
|
|
164
166
|
| { type: "turn_start" }
|
|
165
167
|
| { type: "text_delta"; delta: string }
|
|
166
168
|
| { type: "thinking_delta"; delta: string }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** A reusable prefix and the backend/method state that covers its tokens.
|
|
2
|
+
* The caller owns a returned hit. After releasing or returning its state,
|
|
3
|
+
* it must release any retained backing through retain. */
|
|
4
|
+
export interface PrefixCacheHit<State, Attachment = never> {
|
|
5
|
+
tokens: number[];
|
|
6
|
+
caches: State;
|
|
7
|
+
attachments?: Attachment;
|
|
8
|
+
retain?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Storage is independent of request scheduling and the method producing
|
|
12
|
+
* state. Both prefill and decode can publish processed-token prefixes.
|
|
13
|
+
* State is opaque here; its backend/method defines layout and alignment.
|
|
14
|
+
* Implementations own prefix selection, retention and storage tiers. */
|
|
15
|
+
export interface PrefixCache<State, Attachment = never> {
|
|
16
|
+
/** Give optional retained state back to the allocator at an execution
|
|
17
|
+
* boundary. This never rejects or changes the active request. */
|
|
18
|
+
reclaim?(): void;
|
|
19
|
+
/** Drop session affinity without deleting reusable checkpoints. */
|
|
20
|
+
closeSession?(sessionId: string): void;
|
|
21
|
+
/** Prepare a reusable prefix before execution. The returned release ends
|
|
22
|
+
* request interest; cache residency and IO remain implementation-owned. */
|
|
23
|
+
prefetch?(prompt: number[], ns?: string, sessionId?: string): Promise<() => void>;
|
|
24
|
+
/** Return owned state for a usable session continuation, otherwise the
|
|
25
|
+
* longest usable prefix, or null on a miss. Session identity is an affinity
|
|
26
|
+
* hint and never changes the content or numerical namespace.
|
|
27
|
+
* Namespace distinguishes incompatible artifacts, methods and adapters. */
|
|
28
|
+
take(prompt: number[], ns?: string, sessionId?: string): PrefixCacheHit<State, Attachment> | null;
|
|
29
|
+
/** Transfer state and its optional backing release to storage. If this
|
|
30
|
+
* throws, ownership stays with the caller; successful return transfers
|
|
31
|
+
* ownership even when storage immediately spills or disposes the entry. */
|
|
32
|
+
put(tokens: number[], caches: State, ns?: string, retain?: () => void, attachments?: Attachment, sessionId?: string): void;
|
|
33
|
+
}
|
|
@@ -16,3 +16,10 @@ export interface StateView<T> {
|
|
|
16
16
|
export interface DisposableResource {
|
|
17
17
|
dispose(): void;
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
/** A prepared replacement for live state. Commit once between method rounds;
|
|
21
|
+
* it publishes already-built state without graph work or validation. Dispose
|
|
22
|
+
* releases the unpublished replacement, or the superseded state after commit. */
|
|
23
|
+
export interface PreparedStateChange extends DisposableResource {
|
|
24
|
+
commit(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Sampling owns score processing, token selection, and request-local history.
|
|
2
|
+
* Score/token representations and sync/device execution belong to the backend;
|
|
3
|
+
* this contract has no scheduling, cache, or tensor-runtime dependency. */
|
|
4
|
+
export interface SampledToken<Token, Metadata> {
|
|
5
|
+
token: Token;
|
|
6
|
+
extras: Metadata | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SamplingSession<Scores, DeviceToken, Result> {
|
|
10
|
+
/** Optional stateless operation shared across compatible rows and positions.
|
|
11
|
+
* It borrows scores and returns owned device tokens, without advancing history. */
|
|
12
|
+
readonly independent?: { sample(scores: Scores): DeviceToken };
|
|
13
|
+
readonly isPlainGreedy: boolean;
|
|
14
|
+
readonly capturesLogprobs: boolean;
|
|
15
|
+
readonly needsHistory: boolean;
|
|
16
|
+
/** Borrows scores; transfers ownership of the result to the caller. */
|
|
17
|
+
sample(logits: Scores, step: number): Result;
|
|
18
|
+
seedHistory(tokens: readonly number[]): void;
|
|
19
|
+
/** Commit accepted tokens when configured for manual history updates. */
|
|
20
|
+
commitDevice(token: DeviceToken): void;
|
|
21
|
+
commitNumbers(tokens: readonly number[]): void;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
+
/** Backend-owned context shared by compatible requests. A key identifies
|
|
2
|
+
* equivalent execution state; enter returns its scoped release. Scheduling
|
|
3
|
+
* observes group readiness without inspecting this context. */
|
|
4
|
+
export interface ExecutionContext {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
enter(): () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
/** A backend execution group exposes bounded work units and readiness. Queue
|
|
2
10
|
* payloads, tensors, cache merging and sampling remain inside its adapter. */
|
|
3
11
|
export interface ExecutionGroup {
|
|
12
|
+
/** Native preparation may borrow the execution lease between work units.
|
|
13
|
+
* Payloads and task failures belong to the backend's queue. */
|
|
14
|
+
readonly pendingTasks?: number;
|
|
15
|
+
advanceTask?(): Promise<void>;
|
|
4
16
|
readonly active: number;
|
|
5
17
|
readonly queued: number;
|
|
6
18
|
readonly preparing: boolean;
|
|
19
|
+
readonly preparingRows?: number;
|
|
20
|
+
/** The preparation owner can accept another row before its next work unit. */
|
|
21
|
+
readonly canPrepareMore?: boolean;
|
|
22
|
+
/** Work estimates affect grouping only. A request larger than this budget
|
|
23
|
+
* still runs alone; it is never rejected or sent to another executor. */
|
|
24
|
+
readonly preparingTokens?: number;
|
|
25
|
+
readonly nextPreparationTokens?: number;
|
|
26
|
+
readonly maxPreparationTokens?: number;
|
|
27
|
+
/** The latest preparation published output that can be flushed by yielding. */
|
|
28
|
+
readonly preparationPublishedOutput?: boolean;
|
|
7
29
|
readonly maxActive: number;
|
|
8
30
|
readonly admissionHeld: boolean;
|
|
9
31
|
readonly closed: boolean;
|
|
@@ -12,6 +34,11 @@ export interface ExecutionGroup {
|
|
|
12
34
|
admitNext(): boolean;
|
|
13
35
|
/** Read-only budget check used to preserve admission before decode. */
|
|
14
36
|
canBurst(): boolean;
|
|
37
|
+
/** Method-owned token demand for a compatible mixed iteration. Absent when
|
|
38
|
+
* the backend has no packed implementation for the current work. */
|
|
39
|
+
readonly mixedPreparation?: { readonly runningTokens: number; readonly minimumPreparationTokens: number };
|
|
40
|
+
readonly maxIterationTokens?: number;
|
|
41
|
+
advanceMixed?(tokenBudget: number): Promise<void>;
|
|
15
42
|
advancePreparation(): Promise<void>;
|
|
16
43
|
advance(): Promise<void>;
|
|
17
44
|
failActive(error: unknown): void;
|