mlx-bun 0.4.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 +55 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +220 -7
- package/docs/design/decode-speed-program.md +121 -66
- package/docs/design/generic-model-support.md +116 -0
- package/docs/design/kv-cache.md +380 -211
- package/docs/design/speculative-decoding.md +165 -26
- package/docs/design/turboquant.md +20 -7
- package/docs/design/unified-engine-frontier-plan.md +120 -13
- package/docs/planning/PRODUCT_ROADMAP.md +5 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3463 -3489
- package/docs/reference/cli.md +62 -1
- package/docs/reference/distribution.md +29 -4
- package/docs/reference/environment.md +104 -8
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +17 -5
- package/docs/reference/server-api.md +181 -11
- package/docs/reference/server-config.md +232 -105
- 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/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +111 -27
- package/src/backends/mlx/cache-layout.ts +3 -3
- package/src/backends/mlx/continuation-persistence.ts +5 -4
- 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/gateway-binding.ts +56 -12
- package/src/backends/mlx/kv-maintenance.ts +30 -2
- 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 +4 -3
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +15 -4
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +57 -13
- 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 +18 -5
- package/src/backends/mlx/rollback.ts +6 -3
- package/src/backends/mlx/serial-executor.ts +16 -7
- package/src/backends/mlx/speculative-group.ts +105 -26
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/target-layout-capability.ts +1 -1
- package/src/backends/mlx/transitioning-kv-rows.ts +8 -0
- package/src/cli.ts +322 -0
- package/src/contracts/execution.ts +10 -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 +13 -3
- package/src/contracts/scheduling.ts +9 -0
- package/src/engine/execution-plan.ts +23 -12
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/scheduler.ts +22 -2
- package/src/fill/fill-session.ts +14 -3
- package/src/generate.ts +28 -22
- package/src/grammar.ts +13 -0
- package/src/inference/prefill.ts +6 -0
- package/src/kv-scheme.ts +22 -0
- package/src/kv-store.ts +115 -79
- 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 +33 -5
- package/src/lab/paged-kv/paged-kv.ts +108 -42
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +37 -0
- 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/batched-turboquant-kv.ts +6 -5
- package/src/model/delayed-quantized-kv.ts +3 -3
- package/src/model/delayed-rotating-quantized-kv.ts +1 -1
- package/src/model/delayed-turboquant-kv.ts +6 -4
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-row.ts +1 -1
- package/src/model/gemma4-base.ts +25 -6
- package/src/model/gemma4.ts +82 -2
- package/src/model/kv-attention-view.ts +4 -1
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +99 -37
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +6 -0
- 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/prompt-cache.ts +265 -43
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +5 -4
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +17 -12
- 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 +52 -11
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +2 -0
- package/src/serve/request-prep.ts +38 -21
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +132 -164
- package/src/spec/assistant-rows.ts +1 -0
- package/src/spec/deepspec-rows.ts +1 -1
- package/src/spec/dflash-source.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +1 -0
- package/src/spec/ngram-source.ts +27 -5
- package/src/spec/projected-draft-rows.ts +2 -2
- package/src/spec/qwen-mtp-rows.ts +11 -0
- package/src/spec/qwen-mtp-source.ts +3 -0
- package/src/spec/serve-loop.ts +5 -3
- package/src/spec/source.ts +14 -1
- package/src/spec/two-model-rows.ts +1 -0
- package/src/spec/two-model.ts +2 -2
- package/src/ssd-cache.ts +76 -17
- package/src/ssd-durability.ts +20 -22
- 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 +160 -0
- 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/docs/{planning → archive/planning}/release-notes-v0.4.0.md +0 -0
package/src/server.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ContinuationPersistence } from "./backends/mlx/continuation-persistence";
|
|
2
2
|
import { modelServingBinding } from "./backends/mlx/model-serving";
|
|
3
|
+
import { handleAudioRoute } from "./serve/audio-routes";
|
|
4
|
+
import { TranscriptionService, defaultWhisperRecord } from "./serve/transcription-service";
|
|
3
5
|
import type { ServingContext } from "./serve/model-host";
|
|
4
6
|
export { generationCheckpointKey } from "./serve/checkpoint-identity";
|
|
5
7
|
// OpenAI-compatible HTTP server: /v1/chat/completions (+ SSE streaming)
|
|
@@ -26,7 +28,7 @@ import {
|
|
|
26
28
|
type GenerateOptions,
|
|
27
29
|
} from "./generate";
|
|
28
30
|
import { createPreparationExecutor } from "./serve/preparation";
|
|
29
|
-
import { cloneKvCaches, legacyCacheCodecs
|
|
31
|
+
import { cloneKvCaches, legacyCacheCodecs } from "./kv-store";
|
|
30
32
|
import type { Cache } from "./model/gemma4";
|
|
31
33
|
import { resolveKvScheme } from "./kv-scheme";
|
|
32
34
|
import { runtimeValue } from "./runtime-config";
|
|
@@ -49,10 +51,11 @@ import { handleLabRoute } from "./serve/lab-routes";
|
|
|
49
51
|
import { handleModelAdminRoute } from "./serve/model-admin-routes";
|
|
50
52
|
import { handleStaticRoute } from "./serve/static-routes";
|
|
51
53
|
|
|
52
|
-
import { PromptCache
|
|
54
|
+
import { PromptCache } from "./prompt-cache";
|
|
55
|
+
import { CostSizeRetention } from "./storage/retention-policy";
|
|
56
|
+
import { TieredPromptCache } from "./tiered-prompt-cache";
|
|
53
57
|
import { SsdCacheStore } from "./ssd-cache";
|
|
54
58
|
import {
|
|
55
|
-
SsdDurabilityCoordinator,
|
|
56
59
|
type DurabilityFlushResult,
|
|
57
60
|
type DurabilitySnapshotStats,
|
|
58
61
|
} from "./ssd-durability";
|
|
@@ -67,7 +70,7 @@ import {
|
|
|
67
70
|
type ResponsesRequest,
|
|
68
71
|
} from "./responses";
|
|
69
72
|
import { fit } from "./fit";
|
|
70
|
-
import { setMemoryLimit } from "./mlx/ffi";
|
|
73
|
+
import { activeMemory, maxRecommendedWorkingSetSize, setMemoryLimit } from "./mlx/ffi";
|
|
71
74
|
import { makePiWsHandler, type PiWsData } from "./pi-web";
|
|
72
75
|
import { ChatStage } from "./serve/chat-stage";
|
|
73
76
|
import { TextCompletionStage } from "./serve/text-completion-stage";
|
|
@@ -77,6 +80,7 @@ import {
|
|
|
77
80
|
chatCompletionJson, chatCompletionStream, textCompletionJson, textCompletionStream,
|
|
78
81
|
} from "./serve/openai-wire";
|
|
79
82
|
import { createRequestPrep } from "./serve/request-prep";
|
|
83
|
+
import { GeneratedTokenHistory } from "./serve/generated-token-history";
|
|
80
84
|
import {
|
|
81
85
|
detectDraftKind,
|
|
82
86
|
loadContext,
|
|
@@ -163,6 +167,16 @@ export interface ServerOptions {
|
|
|
163
167
|
defaultTemperature?: number;
|
|
164
168
|
defaultTopP?: number;
|
|
165
169
|
defaultTopK?: number;
|
|
170
|
+
/** Whisper checkpoint directory for /v1/audio/* (`--whisper-model`).
|
|
171
|
+
* Unset ⇒ the first downloaded `whisper` checkpoint is used on demand. */
|
|
172
|
+
whisperModelDir?: string;
|
|
173
|
+
/** Display id for the Whisper checkpoint. */
|
|
174
|
+
whisperModelId?: string;
|
|
175
|
+
/** Seconds idle before the Whisper weights are disposed
|
|
176
|
+
* (`--whisper-idle-unload`, default 300; 0 keeps them resident). */
|
|
177
|
+
whisperIdleUnloadSec?: number;
|
|
178
|
+
/** Keep the Whisper weights resident (`--whisper-resident`). */
|
|
179
|
+
whisperResident?: boolean;
|
|
166
180
|
/** Completion cap when the request omits max_tokens (`--max-tokens`).
|
|
167
181
|
* mlx_lm.server's flag; its default there is 512 — ours stays 65,536 so
|
|
168
182
|
* thinking traces never truncate. `--max-tokens 512` = mlx-lm behavior. */
|
|
@@ -322,8 +336,7 @@ export function createServer(
|
|
|
322
336
|
// The scheduler chooses its B=1 fast path or B=N step from active rows.
|
|
323
337
|
// Both full-attention (CPM) and
|
|
324
338
|
// sliding-window (Gemma) models batch — the scheduler assembles each layer's
|
|
325
|
-
// cache by attention type.
|
|
326
|
-
// unsupported explicit kv-quant) drain to the serial executor
|
|
339
|
+
// cache by attention type. Unsupported model/method combinations drain to the serial executor
|
|
327
340
|
// (see GenerationGateway.place). No inference setting is rewritten.
|
|
328
341
|
// DEFAULT 8 (flipped 2026-07-05, Josh's call, after GATE-B1-SPEED): a
|
|
329
342
|
// lone request through the batch lane IS the serial engine (adopted
|
|
@@ -397,10 +410,9 @@ export function createServer(
|
|
|
397
410
|
// quietly (kv-quant: the swap would drop the scheme; speculative
|
|
398
411
|
// provider composition with paged storage is not implemented).
|
|
399
412
|
if (serverOptions.pagedKv) {
|
|
400
|
-
if (kvScheme.
|
|
413
|
+
if (kvScheme.kvConfig?.length || kvScheme.turboQuant)
|
|
401
414
|
throw new Error(
|
|
402
|
-
`--paged-kv
|
|
403
|
-
`blocks are a documented follow-up, docs/design/kv-cache.md).`,
|
|
415
|
+
`--paged-kv supports bf16 and uniform affine KV4/KV8; per-layer and TurboQuant pages are not implemented.`,
|
|
404
416
|
);
|
|
405
417
|
if (ctx.draft)
|
|
406
418
|
throw new Error(
|
|
@@ -415,11 +427,7 @@ export function createServer(
|
|
|
415
427
|
const bs = serverOptions.pagedKv.blockSize;
|
|
416
428
|
if (bs !== undefined && (!Number.isInteger(bs) || bs <= 0))
|
|
417
429
|
throw new Error(`--paged-kv-block-size must be a positive integer (got ${bs})`);
|
|
418
|
-
|
|
419
|
-
console.warn(
|
|
420
|
-
"[paged-kv] --ssd-cache has no effect: paged requests bypass the prompt " +
|
|
421
|
-
"cache (v1 non-goal), so nothing reaches the SSD tier.",
|
|
422
|
-
);
|
|
430
|
+
|
|
423
431
|
}
|
|
424
432
|
|
|
425
433
|
// SSD cold tier (docs/design/kv-cache.md): prefix KV survives RAM
|
|
@@ -454,6 +462,8 @@ export function createServer(
|
|
|
454
462
|
tokenizerHash: Bun.hash(tokJson).toString(16),
|
|
455
463
|
modelId: ctx.modelId,
|
|
456
464
|
verify: serverOptions.ssdCacheVerify,
|
|
465
|
+
storage: { layout: runtimeValue("MLX_BUN_SSD_LAYOUT") === "blocks" ? "blocks" : "whole",
|
|
466
|
+
segmented: runtimeValue("MLX_BUN_SSD_SEGMENTED") !== "0" },
|
|
457
467
|
});
|
|
458
468
|
const recovered = ssdStore.scan();
|
|
459
469
|
const capacity = Number.isFinite(ssdStore.maxBytes)
|
|
@@ -475,6 +485,15 @@ export function createServer(
|
|
|
475
485
|
const h = ssdStore!.find(prompt, ns);
|
|
476
486
|
return h ? { prefixLen: h.prefixLen, handle: h.entry } : null;
|
|
477
487
|
},
|
|
488
|
+
findExact: (tokens: number[], ns: string) => {
|
|
489
|
+
const hit = ssdStore!.findExact(tokens, ns);
|
|
490
|
+
return hit ? { prefixLen: hit.prefixLen, handle: hit.entry } : null;
|
|
491
|
+
},
|
|
492
|
+
restoreObjectAsync: async (handle: unknown) => {
|
|
493
|
+
const loaded = await ssdStore!.restoreAsync(handle as import("./ssd-cache").SsdIndexEntry,
|
|
494
|
+
{ makeCache: () => [] });
|
|
495
|
+
return loaded ? { ...loaded, retain: () => {} } : null;
|
|
496
|
+
},
|
|
478
497
|
restore: (handle: unknown) => {
|
|
479
498
|
const loaded = serving.restore(ssdStore!, handle as import("./ssd-cache").SsdIndexEntry);
|
|
480
499
|
if (!loaded) return null;
|
|
@@ -484,6 +503,12 @@ export function createServer(
|
|
|
484
503
|
// as a no-op so callers' dispose ordering is unchanged.
|
|
485
504
|
return { tokens: loaded.tokens, caches: loaded.caches, attachments: loaded.attachments, retain: () => {} };
|
|
486
505
|
},
|
|
506
|
+
...(serving.restoreAsync && runtimeValue("MLX_BUN_SSD_PREFETCH") !== "0" ? {
|
|
507
|
+
restoreAsync: async (handle: unknown) => {
|
|
508
|
+
const loaded = await serving.restoreAsync!(ssdStore!, handle as import("./ssd-cache").SsdIndexEntry);
|
|
509
|
+
return loaded ? { ...loaded, retain: () => {} } : null;
|
|
510
|
+
},
|
|
511
|
+
} : {}),
|
|
487
512
|
store: (tokens: number[], caches: import("./model/gemma4").Cache[], ns: string,
|
|
488
513
|
attachments?: import("./backends/mlx/checkpoint-state").CheckpointAttachment[]) => {
|
|
489
514
|
if (ssdStore!.hasDurablePrefix(tokens, ns)) return true;
|
|
@@ -491,29 +516,13 @@ export function createServer(
|
|
|
491
516
|
},
|
|
492
517
|
}
|
|
493
518
|
: null;
|
|
494
|
-
const promptCache =
|
|
495
|
-
promptCacheCap,
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
// SpillQueue -> storeAsync (generation-locked per tensor, see below) ->
|
|
502
|
-
// clone disposal on every settle/drop path (that dispose is what
|
|
503
|
-
// actually frees the demoted GPU memory; the queue's byte cap keeps
|
|
504
|
-
// starved-gate retention bounded — 2026-07-07 review fix). spillQueue
|
|
505
|
-
// is declared with the snapshot scheduler below; safe to close over
|
|
506
|
-
// here because spills only fire at put()/demoteIdle time, long after
|
|
507
|
-
// init.
|
|
508
|
-
ssdStore
|
|
509
|
-
? {
|
|
510
|
-
spillOwned: (entry) =>
|
|
511
|
-
spillQueue!.enqueue({ tokens: entry.tokens, caches: entry.caches, ns: entry.ns, attachments: entry.attachments }),
|
|
512
|
-
}
|
|
513
|
-
: null,
|
|
514
|
-
coldTier,
|
|
515
|
-
cloneState,
|
|
516
|
-
);
|
|
519
|
+
const promptCache = ssdStore && coldTier
|
|
520
|
+
? new TieredPromptCache(promptCacheCap, ssdStore, coldTier, cloneState,
|
|
521
|
+
runtimeValue("MLX_BUN_SSD_WRITEBEHIND") !== "0")
|
|
522
|
+
: new PromptCache(promptCacheCap, null, null, cloneState);
|
|
523
|
+
if (runtimeValue("MLX_BUN_CACHE_RETENTION") === "cost-size") promptCache.retention = new CostSizeRetention();
|
|
524
|
+
const spillQueue = promptCache instanceof TieredPromptCache ? promptCache.spillQueue : null;
|
|
525
|
+
const durability = promptCache instanceof TieredPromptCache ? promptCache.durability : null;
|
|
517
526
|
// Responses-API store for previous_response_id resumption (Phase 11):
|
|
518
527
|
// TTL + byte-capped LRU, port of optiq/response_store.py. Pairs with
|
|
519
528
|
// the prompt cache: a resumed conversation re-renders the same prefix,
|
|
@@ -553,59 +562,8 @@ export function createServer(
|
|
|
553
562
|
serving.gateway.configureContinuation?.(executionServices);
|
|
554
563
|
const runGeneration = serving.createSerial(executionServices);
|
|
555
564
|
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
// clean exit): after a request completes, snapshot its still-RAM-resident
|
|
559
|
-
// entry to SSD off the request path. Debounced + coalesced per namespace —
|
|
560
|
-
// an agent hammering one conversation persists the settled state once
|
|
561
|
-
// things go quiet, not every turn. Runs under the gateway lock (byte
|
|
562
|
-
// extraction must not race a generation mutating the entry); if the entry
|
|
563
|
-
// was evicted (already spilled) or extended (a newer schedule pending)
|
|
564
|
-
// meanwhile, findExact misses and nothing is written.
|
|
565
|
-
// Keyed by namespace and exact tokens: a sub-second final [prompt+gen] put
|
|
566
|
-
// must not cancel the pending boundary snapshot, and unrelated prompts of
|
|
567
|
-
// equal length must not cancel each other. Exact reschedules still
|
|
568
|
-
// coalesce; stale keys self-clean when findExact misses.
|
|
569
|
-
//
|
|
570
|
-
// NON-BLOCKING (2026-07-06, the write-behind persistence contract): the
|
|
571
|
-
// gateway lock is held only for a zero-copy SNAPSHOT (findExact +
|
|
572
|
-
// cloneKvCaches — microseconds; entries are immutable so the clones are
|
|
573
|
-
// consistent forever). The flush itself runs OFF the lock via
|
|
574
|
-
// storeAsync, and writes chain serially so two multi-hundred-MB flushes
|
|
575
|
-
// never overlap.
|
|
576
|
-
//
|
|
577
|
-
// IDLE-GATED (2026-07-07, the decode@ctx fix): "off the lock" was not
|
|
578
|
-
// enough — every per-tensor flush step is a blocking GPU sync on the
|
|
579
|
-
// SAME stream decode uses (ops.contiguous enqueues a kernel, rawBytesView
|
|
580
|
-
// evals) plus a synchronous multi-MB writeSync, and the setImmediate
|
|
581
|
-
// pacing interleaved those slices exactly between decode tokens. A ~16k
|
|
582
|
-
// entry's flush overlapping the bench's ctx repeats depressed decode@ctx
|
|
583
|
-
// ~9% on e4b (mlx-lm runs no equivalent background work). Now every step
|
|
584
|
-
// — including the first — runs through gateway.runWhenIdle(), so the
|
|
585
|
-
// flush only progresses while NOTHING is generating and pauses when a
|
|
586
|
-
// request arrives mid-flush. Tradeoffs, accepted: durability waits for a
|
|
587
|
-
// quiet moment (single-user serving quiesces constantly; sustained
|
|
588
|
-
// hammering defers the flush AND the spill clones' GPU-memory release —
|
|
589
|
-
// bounded by the chain). MLX_BUN_SSD_WRITEBEHIND=0
|
|
590
|
-
// disables write-behind snapshots entirely — the paired-A/B lever + kill
|
|
591
|
-
// switch (restart survival then degrades to spill-on-evict only).
|
|
592
|
-
const writeBehindOn = runtimeValue("MLX_BUN_SSD_WRITEBEHIND") !== "0";
|
|
593
|
-
const ssdFlushStep = <T>(step: () => T): Promise<T> =>
|
|
594
|
-
gateway.runWhenIdle(async () => step());
|
|
595
|
-
// Bounded write-behind queue (2026-07-07 review fix — see SpillQueue in
|
|
596
|
-
// kv-store.ts): pending clones pin their entries' GPU buffers while the
|
|
597
|
-
// idle gate starves under sustained traffic, so QUEUED bytes are capped —
|
|
598
|
-
// over cap the oldest queued spill drops (clone disposed immediately, a
|
|
599
|
-
// future cache miss, never a wrong result) instead of accumulating past
|
|
600
|
-
// the prompt-cache cap. Default 2 GB (a quarter of the 8 GB RAM-cache
|
|
601
|
-
// default); MLX_BUN_SSD_SPILL_QUEUE_GB overrides. The durability
|
|
602
|
-
// coordinator retains a dirty record until the queue confirms the atomic
|
|
603
|
-
// store. Explicit flush and graceful shutdown can therefore retry a drop.
|
|
604
|
-
// `|| 2` would coerce an explicit "0" back to 2 GB — parse so 0 works
|
|
605
|
-
// (cap 0 = keep only the newest + in-flight clone pinned; the soft cap
|
|
606
|
-
// never drops the item just enqueued).
|
|
607
|
-
// The gateway exists before the spill queue because the SSD writer uses
|
|
608
|
-
// its idle boundary to avoid competing with generation.
|
|
565
|
+
// Cache policy owns RAM residency and SSD persistence independently of
|
|
566
|
+
// request scheduling. Published state is immutable.
|
|
609
567
|
const gateway = new GenerationGateway(serving.gateway, batch, runGeneration, {
|
|
610
568
|
kvBudgetBytes: serverOptions.kvBudgetBytes,
|
|
611
569
|
checkpoints: !!(serverOptions.generationCheckpointTokens && ssdStore),
|
|
@@ -617,29 +575,21 @@ export function createServer(
|
|
|
617
575
|
const spillQueueGbRaw = Number(runtimeValue("MLX_BUN_SSD_SPILL_QUEUE_GB"));
|
|
618
576
|
const spillQueueCapBytes =
|
|
619
577
|
(Number.isFinite(spillQueueGbRaw) && spillQueueGbRaw >= 0 ? spillQueueGbRaw : 2) * 1024 ** 3;
|
|
620
|
-
// Both queues share the configured total: reserve half for checkpoints
|
|
621
|
-
// only when that service exists. Each retains the existing soft-cap rule.
|
|
622
|
-
const checkpointQueueBytes = ssdStore && serverOptions.generationCheckpointTokens
|
|
623
|
-
? Math.floor(spillQueueCapBytes / 2) : 0;
|
|
624
578
|
if (ssdStore && serverOptions.generationCheckpointTokens)
|
|
625
|
-
checkpointPersistence = new ContinuationPersistence(ssdStore, {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
cloneState,
|
|
640
|
-
(tokens, ns) => ssdStore.hasDurablePrefix(tokens, ns),
|
|
641
|
-
)
|
|
642
|
-
: null;
|
|
579
|
+
checkpointPersistence = new ContinuationPersistence(ssdStore, {
|
|
580
|
+
maxBytes: spillQueueCapBytes,
|
|
581
|
+
});
|
|
582
|
+
// Keep allocator headroom for the next forward. Only optional cache
|
|
583
|
+
// snapshots are reclaimed; context, batch size and sampling stay intact.
|
|
584
|
+
// Do not count MLX's reusable pool as live state or await an SSD write.
|
|
585
|
+
const cacheWorkingSet = Math.min(maxRecommendedWorkingSetSize(), allocatorLimit ?? Infinity);
|
|
586
|
+
const cacheOverBudget = () => Math.max(activeMemory(),
|
|
587
|
+
ctx.model.weightsBytes + Math.max(promptCache.totalBytes, spillQueue?.pendingBytes ?? 0)) > cacheWorkingSet * 0.85;
|
|
588
|
+
promptCache.pressure = {
|
|
589
|
+
// File-backed weights may not yet appear in MLX's active allocations
|
|
590
|
+
// before the first forward. Account for their known footprint as well.
|
|
591
|
+
overBudget: cacheOverBudget,
|
|
592
|
+
};
|
|
643
593
|
const durabilityStats = (): DurabilitySnapshotStats => {
|
|
644
594
|
const stats = durability?.stats ?? {
|
|
645
595
|
pendingSnapshots: 0,
|
|
@@ -668,28 +618,21 @@ export function createServer(
|
|
|
668
618
|
elapsedMs: performance.now() - started,
|
|
669
619
|
};
|
|
670
620
|
};
|
|
671
|
-
//
|
|
672
|
-
|
|
673
|
-
|
|
621
|
+
// Remember exact generated IDs independently of cache persistence.
|
|
622
|
+
const tokenHistory = new GeneratedTokenHistory(ctx.tokenizer);
|
|
623
|
+
if (ssdStore) for (const tokens of ssdStore.tokenPrefixes()) tokenHistory.remember(tokens);
|
|
624
|
+
promptCache.onPut = (tokens) => {
|
|
625
|
+
tokenHistory.remember(tokens);
|
|
626
|
+
};
|
|
674
627
|
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
// bursts while every prefix stays reachable (take() restores via
|
|
678
|
-
// zero-copy mmap). The exclusive section below covers only the sweep's
|
|
679
|
-
// zero-copy clone + dispose (spillOwned above); the SSD write itself
|
|
680
|
-
// runs off-lock on ssdWriteChain. Swept only when the engine is TRULY idle: the
|
|
681
|
-
// runExclusive below registers as a serial waiter, which would DRAIN a
|
|
682
|
-
// running batch, so the activity check guards it (a momentary exclusive
|
|
683
|
-
// grab of an idle engine is free). 0 disables.
|
|
628
|
+
// Cache age, not scheduler activity, decides idle demotion. The cache
|
|
629
|
+
// queues any missing SSD copy and retains RAM until that copy commits.
|
|
684
630
|
const demoteIdleMs = (serverOptions.ssdDemoteIdleSec ?? (ssdStore ? 300 : 0)) * 1000;
|
|
685
631
|
let demoteTimer: ReturnType<typeof setInterval> | null = null;
|
|
686
632
|
if (ssdStore && demoteIdleMs > 0) {
|
|
687
633
|
demoteTimer = setInterval(() => {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const n = promptCache.demoteIdle(demoteIdleMs);
|
|
691
|
-
if (n > 0) console.log(`[ssd-cache] demoted ${n} idle entr${n === 1 ? "y" : "ies"} to disk`);
|
|
692
|
-
}).catch(() => {});
|
|
634
|
+
const n = promptCache.demoteIdle(demoteIdleMs);
|
|
635
|
+
if (n > 0) console.log(`[ssd-cache] demoted ${n} idle entr${n === 1 ? "y" : "ies"} to disk`);
|
|
693
636
|
}, Math.max(30_000, demoteIdleMs / 4));
|
|
694
637
|
demoteTimer.unref?.();
|
|
695
638
|
}
|
|
@@ -699,7 +642,33 @@ export function createServer(
|
|
|
699
642
|
|
|
700
643
|
// /library response cache (30 s) — registry + config reads only.
|
|
701
644
|
const startedAt = Date.now();
|
|
702
|
-
|
|
645
|
+
// Speech-to-text companion (docs/reference/server-api.md "Audio
|
|
646
|
+
// transcription"): a Whisper checkpoint served beside the chat model,
|
|
647
|
+
// loaded on the first /v1/audio request and paged out after
|
|
648
|
+
// --whisper-idle-unload seconds. Explicit --whisper-model wins; otherwise
|
|
649
|
+
// the first downloaded `whisper` checkpoint is resolved lazily.
|
|
650
|
+
let transcription: TranscriptionService | null = null;
|
|
651
|
+
let transcriptionResolved = false;
|
|
652
|
+
const transcriptionService = async (): Promise<TranscriptionService | null> => {
|
|
653
|
+
if (transcription || transcriptionResolved) return transcription;
|
|
654
|
+
transcriptionResolved = true;
|
|
655
|
+
const rec = serverOptions.whisperModelDir
|
|
656
|
+
? { path: serverOptions.whisperModelDir, repoId: serverOptions.whisperModelId ?? serverOptions.whisperModelDir }
|
|
657
|
+
: await defaultWhisperRecord();
|
|
658
|
+
if (!rec) return null;
|
|
659
|
+
transcription = new TranscriptionService({
|
|
660
|
+
modelDir: rec.path, modelId: rec.repoId,
|
|
661
|
+
idleUnloadSec: serverOptions.whisperIdleUnloadSec,
|
|
662
|
+
resident: serverOptions.whisperResident,
|
|
663
|
+
exclusive: (fn, signal) => gateway.runExclusive(fn, undefined, signal),
|
|
664
|
+
});
|
|
665
|
+
return transcription;
|
|
666
|
+
};
|
|
667
|
+
const transcriptionInfo = async () => {
|
|
668
|
+
const svc = await transcriptionService();
|
|
669
|
+
return svc ? { id: svc.modelId, resident: svc.resident } : null;
|
|
670
|
+
};
|
|
671
|
+
const discoveryRoutes = createDiscoveryRoutes(ctx, gateway, startedAt, transcriptionInfo);
|
|
703
672
|
|
|
704
673
|
// Captured so the WebSocket handler can resolve the bound (possibly
|
|
705
674
|
// ephemeral) port lazily for the loopback pi provider.
|
|
@@ -729,9 +698,10 @@ export function createServer(
|
|
|
729
698
|
// → inferenceStage.admit → inferenceStage.run → InferenceResult → wire
|
|
730
699
|
// (/v1/completions substitutes textStage; Anthropic and Responses reuse
|
|
731
700
|
// chatStage with their own wire formats.)
|
|
732
|
-
const prep = createRequestPrep({ ctx, serverOptions, kvScheme, defaultGeneratedTokens });
|
|
701
|
+
const prep = createRequestPrep({ ctx, serverOptions, kvScheme, defaultGeneratedTokens, tokenHistory });
|
|
733
702
|
const { templateOptionsFor } = prep;
|
|
734
|
-
const preparation = createPreparationExecutor((work, signal) => gateway.
|
|
703
|
+
const preparation = createPreparationExecutor((work, signal) => gateway.runPreparation(work, signal),
|
|
704
|
+
batch, gateway.mediaBatchingEnabled ? batch : 1);
|
|
735
705
|
const chatStage = new ChatStage(
|
|
736
706
|
ctx, prep, promptCache, contextLimit, serverOptions.defaultAdapter,
|
|
737
707
|
preparation, serving.buildPrompt);
|
|
@@ -739,6 +709,15 @@ export function createServer(
|
|
|
739
709
|
ctx, prep, contextLimit, defaultGeneratedTokens, serverOptions.defaultAdapter);
|
|
740
710
|
const inferenceStage = new InferenceStage(completionExecutor);
|
|
741
711
|
const openAiMeta = (id: string) => ({ id, created: Math.floor(Date.now() / 1000), model: ctx.modelId });
|
|
712
|
+
/** Carry application session affinity into the shared cache request fields. */
|
|
713
|
+
const applyCacheSession = (body: ChatRequestParams, request: Request, original: unknown = body) => {
|
|
714
|
+
const fields = original as { session_id?: unknown; prompt_cache_key?: unknown };
|
|
715
|
+
const session = typeof fields.session_id === "string" ? fields.session_id :
|
|
716
|
+
typeof fields.prompt_cache_key === "string" ? fields.prompt_cache_key :
|
|
717
|
+
request.headers.get("x-session-affinity") ?? request.headers.get("session_id") ?? undefined;
|
|
718
|
+
if (runtimeValue("MLX_BUN_SESSION_CACHE") !== "0") body.session_id = session;
|
|
719
|
+
else { delete body.session_id; delete body.prompt_cache_key; }
|
|
720
|
+
};
|
|
742
721
|
/** Parse the JSON body under the request's trace; a bad body is a 400. */
|
|
743
722
|
const parseBody = async <T,>(request: Request, trace: PromptResponseTrace | undefined): Promise<T | Response> => {
|
|
744
723
|
const closeBodyParse = trace?.begin("request.body_parse");
|
|
@@ -777,6 +756,7 @@ export function createServer(
|
|
|
777
756
|
// capability flags — true if a tower is loaded or loadable (lazy)
|
|
778
757
|
vision: !!(ctx.vision || ctx.loadVision),
|
|
779
758
|
audio: !!(ctx.audio || ctx.loadAudio),
|
|
759
|
+
transcription: async () => (await transcriptionService()) !== null,
|
|
780
760
|
thinking: ctx.template.supportsThinking,
|
|
781
761
|
// Model-author sampling defaults (generation_config.json, server-CLI
|
|
782
762
|
// overrides applied) for the sampling popover's per-model recommended
|
|
@@ -793,6 +773,12 @@ export function createServer(
|
|
|
793
773
|
async fetch(request, server) {
|
|
794
774
|
const url = new URL(request.url);
|
|
795
775
|
|
|
776
|
+
if (url.pathname === "/admin/cache/session/close" && request.method === "POST") {
|
|
777
|
+
const body = await parseBody<{ session_id?: string }>(request, undefined);
|
|
778
|
+
if (body instanceof Response) return body;
|
|
779
|
+
if (typeof body.session_id === "string") promptCache.closeSession(body.session_id);
|
|
780
|
+
return Response.json({ closed: typeof body.session_id === "string" });
|
|
781
|
+
}
|
|
796
782
|
if (url.pathname === "/admin/cache/flush" && request.method === "POST") {
|
|
797
783
|
const result = await flushDurability();
|
|
798
784
|
return Response.json(
|
|
@@ -824,6 +810,9 @@ export function createServer(
|
|
|
824
810
|
const modelAdminResponse = await handleModelAdminRoute(url, request, ctx, gateway);
|
|
825
811
|
if (modelAdminResponse) return modelAdminResponse;
|
|
826
812
|
|
|
813
|
+
const audioResponse = await handleAudioRoute(url, request, { service: transcriptionService });
|
|
814
|
+
if (audioResponse) return audioResponse;
|
|
815
|
+
|
|
827
816
|
if (url.pathname === "/fit" && request.method === "GET") {
|
|
828
817
|
// Fit assessment for the status page: this-machine report at the
|
|
829
818
|
// admission ceiling + the Apple SKU matrix at a fixed 32k.
|
|
@@ -934,30 +923,6 @@ export function createServer(
|
|
|
934
923
|
}
|
|
935
924
|
|
|
936
925
|
if (url.pathname === "/stats" && request.method === "GET") {
|
|
937
|
-
// Active KV scheme across ALL layers. Since Phase 9 rotating
|
|
938
|
-
// (sliding-window) caches quantize too, so every layer the
|
|
939
|
-
// scheme names counts — the old display filtered to
|
|
940
|
-
// full_attention and silently undercounted (e.g. 26B showed
|
|
941
|
-
// 5/30 quantized when its kv_config.json covers all 30).
|
|
942
|
-
const layerTypes = ctx.model.config.text.layerTypes;
|
|
943
|
-
const kvLayers: Record<string, number> = {};
|
|
944
|
-
let kvMode = "bf16";
|
|
945
|
-
if (kvScheme.turboQuant) {
|
|
946
|
-
// v1: full-attention layers only (sliding-window stays bf16 —
|
|
947
|
-
// docs/design/turboquant.md non-goal).
|
|
948
|
-
const fullAttn = layerTypes.filter((l) => l !== "sliding_attention").length;
|
|
949
|
-
kvMode = `turbo k${kvScheme.turboQuant.kBits}v${kvScheme.turboQuant.vBits}`;
|
|
950
|
-
kvLayers[`turbo-k${kvScheme.turboQuant.kBits}v${kvScheme.turboQuant.vBits}`] = fullAttn;
|
|
951
|
-
} else if (kvScheme.kvBits) {
|
|
952
|
-
kvMode = `uniform-kv${kvScheme.kvBits}`;
|
|
953
|
-
kvLayers[`kv${kvScheme.kvBits}`] = layerTypes.length;
|
|
954
|
-
} else if (kvScheme.kvConfig) {
|
|
955
|
-
kvMode = "mixed (kv_config.json)";
|
|
956
|
-
for (const e of kvScheme.kvConfig)
|
|
957
|
-
kvLayers[`kv${e.bits}`] = (kvLayers[`kv${e.bits}`] ?? 0) + 1;
|
|
958
|
-
}
|
|
959
|
-
const bf16Layers = layerTypes.length - Object.values(kvLayers).reduce((a, b) => a + b, 0);
|
|
960
|
-
const slidingLayers = layerTypes.filter((l) => l === "sliding_attention").length;
|
|
961
926
|
return Response.json({
|
|
962
927
|
server: {
|
|
963
928
|
owner: serverOptions.owner ?? "embedded",
|
|
@@ -970,6 +935,12 @@ export function createServer(
|
|
|
970
935
|
max_bytes: promptCache.maxBytes,
|
|
971
936
|
hits: promptCache.hits,
|
|
972
937
|
misses: promptCache.misses,
|
|
938
|
+
session_hits: promptCache.sessionHits,
|
|
939
|
+
session_misses: promptCache.sessionMisses,
|
|
940
|
+
prefix_scans: promptCache.prefixScans,
|
|
941
|
+
object_hits: promptCache.objectHits,
|
|
942
|
+
object_misses: promptCache.objectMisses,
|
|
943
|
+
object_restores: promptCache.objectRestores,
|
|
973
944
|
},
|
|
974
945
|
...(ssdStore ? {
|
|
975
946
|
ssd_cache: {
|
|
@@ -995,14 +966,7 @@ export function createServer(
|
|
|
995
966
|
max_bytes: responseStore.maxBytes,
|
|
996
967
|
ttl_ms: responseStore.ttlMs,
|
|
997
968
|
},
|
|
998
|
-
kv_quant:
|
|
999
|
-
mode: kvMode,
|
|
1000
|
-
layers: { ...kvLayers, ...(bf16Layers > 0 ? { bf16: bf16Layers } : {}) },
|
|
1001
|
-
attention: {
|
|
1002
|
-
global: layerTypes.length - slidingLayers,
|
|
1003
|
-
sliding_window: slidingLayers,
|
|
1004
|
-
},
|
|
1005
|
-
},
|
|
969
|
+
kv_quant: resolvedKvScheme.describe(ctx.model.config),
|
|
1006
970
|
admission: {
|
|
1007
971
|
max_safe_context: admission.maxSafeContext,
|
|
1008
972
|
enforced_context_tokens: contextLimit,
|
|
@@ -1146,6 +1110,7 @@ export function createServer(
|
|
|
1146
1110
|
});
|
|
1147
1111
|
const body = await parseBody<ChatRequestParams>(request, trace);
|
|
1148
1112
|
if (body instanceof Response) return body;
|
|
1113
|
+
applyCacheSession(body as ChatRequestParams, request);
|
|
1149
1114
|
const meta = openAiMeta(id);
|
|
1150
1115
|
const a = await admit(
|
|
1151
1116
|
inferenceStage, () => chatStage.run(new ChatRequest(body), id, request.signal), trace, "chat request");
|
|
@@ -1166,6 +1131,7 @@ export function createServer(
|
|
|
1166
1131
|
});
|
|
1167
1132
|
const body = await parseBody<TextCompletionParams>(request, trace);
|
|
1168
1133
|
if (body instanceof Response) return body;
|
|
1134
|
+
applyCacheSession(body as ChatRequestParams, request);
|
|
1169
1135
|
const meta = openAiMeta(id);
|
|
1170
1136
|
const a = await admit(
|
|
1171
1137
|
inferenceStage, () => textStage.run(new TextCompletionRequest(body), id), trace, "text completion");
|
|
@@ -1195,6 +1161,7 @@ export function createServer(
|
|
|
1195
1161
|
let chatBody: ChatRequestParams;
|
|
1196
1162
|
try {
|
|
1197
1163
|
chatBody = anthropicToChatBody(anthropicBody) as unknown as ChatRequestParams;
|
|
1164
|
+
applyCacheSession(chatBody, request, anthropicBody);
|
|
1198
1165
|
} catch (e) {
|
|
1199
1166
|
return anthropicError(400, (e as Error).message, {});
|
|
1200
1167
|
}
|
|
@@ -1250,6 +1217,7 @@ export function createServer(
|
|
|
1250
1217
|
let chatBody: ChatRequestParams;
|
|
1251
1218
|
try {
|
|
1252
1219
|
chatBody = responsesToChatBody(responsesBody) as unknown as ChatRequestParams;
|
|
1220
|
+
applyCacheSession(chatBody, request, responsesBody);
|
|
1253
1221
|
} catch (e) {
|
|
1254
1222
|
return responsesError(400, (e as Error).message, {});
|
|
1255
1223
|
}
|
|
@@ -86,6 +86,7 @@ class AssistantRows implements DraftPrefillGroup, DraftRowGroup {
|
|
|
86
86
|
|
|
87
87
|
export function assistantGroups(drafter: GemmaAssistantDrafter, namespace: string): GroupedDraftProvider {
|
|
88
88
|
return {
|
|
89
|
+
checkpointNamespace: () => namespace,
|
|
89
90
|
openPrefill: ({target,checkpoints}) => new AssistantRows(target.assistantRows!,drafter,namespace,checkpoints),
|
|
90
91
|
open: ({target,checkpoints}) => new AssistantRows(target.assistantRows!,drafter,namespace,checkpoints),
|
|
91
92
|
};
|
|
@@ -12,7 +12,7 @@ export function bindDeepspecTarget(target: TargetView, drafter: Pick<DeepspecDra
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function deepspecGroups(drafter: DeepspecDrafter, namespace: string): GroupedDraftProvider {
|
|
15
|
-
return projectedDraftGroups(target => {
|
|
15
|
+
return projectedDraftGroups(namespace, target => {
|
|
16
16
|
bindDeepspecTarget(target, drafter);
|
|
17
17
|
return {
|
|
18
18
|
namespace, schema: "deepspec-context-v1",
|
|
@@ -48,7 +48,7 @@ export class DflashProvider implements DraftProvider {
|
|
|
48
48
|
this.id = id;
|
|
49
49
|
this.weightsBytes = weightsBytes;
|
|
50
50
|
this.gamma = drafter.cfg.gamma;
|
|
51
|
-
this.grouped = projectedDraftGroups(target => {
|
|
51
|
+
this.grouped = projectedDraftGroups(namespace, target => {
|
|
52
52
|
if (!target.gemmaTaps) throw new Error("DSpark drafter requires a Gemma4 target");
|
|
53
53
|
const projection = target.gemmaTaps.projection;
|
|
54
54
|
const minConf = runtimeValue("MLX_BUN_DSPARK_MINCONF");
|
|
@@ -30,6 +30,7 @@ function lastToken(feed: readonly number[]): number {
|
|
|
30
30
|
|
|
31
31
|
export class Glm52NativeMtpProvider implements DraftProvider {
|
|
32
32
|
readonly grouped: GroupedDraftProvider = {
|
|
33
|
+
checkpointNamespace: () => "glm52-native-mtp-v1",
|
|
33
34
|
open: options => this.#openRows(options.target, options.sampling, options.checkpoints),
|
|
34
35
|
openPrefill: options => this.#openRows(options.target, null, options.checkpoints),
|
|
35
36
|
};
|
package/src/spec/ngram-source.ts
CHANGED
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
// speculation is structurally never worse than plain decode by more than the
|
|
39
39
|
// JS scan (~µs against 30k-token histories).
|
|
40
40
|
|
|
41
|
-
import {
|
|
41
|
+
import { runtimeFlag } from "../runtime-config";
|
|
42
42
|
import type { MlxArray } from "../mlx/array";
|
|
43
43
|
import * as ops from "../mlx/ops";
|
|
44
44
|
import type { CheckpointAttachment } from "../backends/mlx/checkpoint-state";
|
|
45
|
-
import type { DraftProvider, DraftSource, DraftRowCheckpoint, DraftRowGroup, DraftPrefillGroup, GroupedDraftProvider } from "./source";
|
|
45
|
+
import type { DraftProvider, DraftSource, DraftRowCheckpoint, DraftRowGroup, DraftPrefillGroup, GroupedDraftProvider, DraftRowConstraints } from "./source";
|
|
46
46
|
import { applyStateChanges } from "../engine/resources";
|
|
47
47
|
|
|
48
48
|
/** The matching policy is independent of the executor's feed convention. */
|
|
@@ -78,7 +78,8 @@ class NgramRows implements DraftRowGroup, DraftPrefillGroup {
|
|
|
78
78
|
readonly namespace: string;
|
|
79
79
|
#histories: number[][] = [];
|
|
80
80
|
#drafts: number[][] = [];
|
|
81
|
-
constructor(readonly max: number, readonly min: number, checkpoints: readonly (DraftRowCheckpoint | null)[]
|
|
81
|
+
constructor(readonly max: number, readonly min: number, checkpoints: readonly (DraftRowCheckpoint | null)[],
|
|
82
|
+
readonly constraints?: DraftRowConstraints) {
|
|
82
83
|
this.namespace = namespace(max, min);
|
|
83
84
|
this.append(checkpoints);
|
|
84
85
|
}
|
|
@@ -95,7 +96,12 @@ class NgramRows implements DraftRowGroup, DraftPrefillGroup {
|
|
|
95
96
|
for (let row = 0; row < this.rowCount; row++) this.#histories[row]!.push(...ids.slice(row * length, (row + 1) * length));
|
|
96
97
|
}
|
|
97
98
|
materialize(): void {}
|
|
98
|
-
draft(pending: readonly number[], depth: number): number[][] {
|
|
99
|
+
draft(pending: readonly number[], depth: number): number[][] | Promise<number[][]> {
|
|
100
|
+
if (this.constraints) {
|
|
101
|
+
for (const [row, history] of this.#histories.entries()) history.push(pending[row]!);
|
|
102
|
+
return Promise.all(this.#histories.map((_, row) => this.constraints!.propose(row, depth)))
|
|
103
|
+
.then(proposals => this.#drafts = proposals);
|
|
104
|
+
}
|
|
99
105
|
this.#drafts = this.#histories.map((history, row) => {
|
|
100
106
|
history.push(pending[row]!);
|
|
101
107
|
return proposeNgram(history, this.max, this.min, depth);
|
|
@@ -126,6 +132,7 @@ export class NgramProvider implements DraftProvider {
|
|
|
126
132
|
readonly max: number;
|
|
127
133
|
readonly min: number;
|
|
128
134
|
readonly grouped: GroupedDraftProvider = {
|
|
135
|
+
checkpointNamespace: () => namespace(this.max, this.min),
|
|
129
136
|
supportsTargetAdapters: true,
|
|
130
137
|
open: options => new NgramRows(this.max, this.min, options.checkpoints),
|
|
131
138
|
openPrefill: options => new NgramRows(this.max, this.min, options.checkpoints),
|
|
@@ -150,7 +157,7 @@ class NgramSource implements DraftSource {
|
|
|
150
157
|
readonly weightsBytes = 0;
|
|
151
158
|
#hist: number[] = [];
|
|
152
159
|
#lastDrafts: number[] = [];
|
|
153
|
-
readonly #tailSplit =
|
|
160
|
+
readonly #tailSplit = runtimeFlag("MLX_BUN_PREFILL_TAIL_SPLIT", true);
|
|
154
161
|
readonly checkpoint: NonNullable<DraftSource["checkpoint"]>;
|
|
155
162
|
|
|
156
163
|
constructor(
|
|
@@ -202,3 +209,18 @@ class NgramSource implements DraftSource {
|
|
|
202
209
|
|
|
203
210
|
dispose(): void {}
|
|
204
211
|
}
|
|
212
|
+
|
|
213
|
+
/** Constraint candidates share committed-history ownership with prompt lookup.
|
|
214
|
+
* They have no serial draft implementation and never perform ngram lookup. */
|
|
215
|
+
export function constraintDraftProvider(): { id: string; grouped: GroupedDraftProvider } {
|
|
216
|
+
return {
|
|
217
|
+
id: "grammar",
|
|
218
|
+
grouped: {
|
|
219
|
+
checkpointNamespace: () => namespace(3, 1),
|
|
220
|
+
supportsTargetAdapters: true,
|
|
221
|
+
openPrefill: options => new NgramRows(3, 1, options.checkpoints),
|
|
222
|
+
open: options => new NgramRows(3, 1, options.checkpoints,
|
|
223
|
+
options.constraints ?? { propose: async () => [] }),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -65,9 +65,9 @@ class ProjectedDraftRows implements DraftPrefillGroup, DraftRowGroup {
|
|
|
65
65
|
dispose(): void { this.context.dispose(); }
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export function projectedDraftGroups(bind: (target: TargetView) => ProjectedDraftMethod): GroupedDraftProvider {
|
|
68
|
+
export function projectedDraftGroups(namespace: string, bind: (target: TargetView) => ProjectedDraftMethod): GroupedDraftProvider {
|
|
69
69
|
const open = ({ target, checkpoints }: {
|
|
70
70
|
target: TargetView; checkpoints: readonly (DraftRowCheckpoint | null)[];
|
|
71
71
|
}) => new ProjectedDraftRows(bind(target), checkpoints);
|
|
72
|
-
return { open, openPrefill: open };
|
|
72
|
+
return { checkpointNamespace: () => namespace, open, openPrefill: open };
|
|
73
73
|
}
|
|
@@ -133,6 +133,17 @@ export class QwenMtpRows {
|
|
|
133
133
|
if (state.length) ops.evalAll(state);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/** Align the companion to verified external continuations through the same
|
|
137
|
+
* true-hidden prefill graph. Different rows retain different prefix lengths. */
|
|
138
|
+
consume(tokens: MlxArray, context: MlxArray, lengths: readonly number[]): void {
|
|
139
|
+
this.#cache.specRoundBegin();
|
|
140
|
+
this.prefill(tokens, context);
|
|
141
|
+
this.#cache.specRoundRollback([...lengths]);
|
|
142
|
+
using positions = ops.fromInt32(lengths.map(length => length - 1), [lengths.length, 1, 1]);
|
|
143
|
+
const hidden = ops.takeAlongAxis(context, positions, 1);
|
|
144
|
+
this.#hidden!.dispose(); this.#hidden = hidden;
|
|
145
|
+
}
|
|
146
|
+
|
|
136
147
|
/** Nonnegative depth, with every request's first pending token already known.
|
|
137
148
|
* Build the complete device dependency chain before reading proposal IDs. */
|
|
138
149
|
draft(pending: readonly number[], depth: number, steps: readonly number[]): number[][] {
|