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
|
@@ -94,11 +94,12 @@ class AsyncMutex {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* path, bypassing the prompt cache. */
|
|
97
|
+
/** Prepared embeddings for vision/audio prompts. Qualified models consume
|
|
98
|
+
* this through shared preparation and decode; other bindings retain serial
|
|
99
|
+
* execution. Token-only prefix caching remains disabled for media. */
|
|
101
100
|
export type Vision = {
|
|
101
|
+
/** Producer identity includes all media and preceding rendered tokens. */
|
|
102
|
+
prefixIdentity?: string;
|
|
102
103
|
embeddings: MlxArray;
|
|
103
104
|
/** bool [L] image-token mask for the bidirectional attention overlay.
|
|
104
105
|
* Absent when the prompt carries ANY audio — audio(-containing) prompts
|
|
@@ -109,7 +110,7 @@ export type Vision = {
|
|
|
109
110
|
* where zeroing falls back to imageMask. */
|
|
110
111
|
multimodalMask?: MlxArray;
|
|
111
112
|
/** Qwen3.5/3.8 vision: the request's mRoPE positions + decode delta,
|
|
112
|
-
*
|
|
113
|
+
* owned by shared forward input, or scoped to the explicit serial run. */
|
|
113
114
|
mrope?: import("../model/qwen3-mrope").MropeRequestState;
|
|
114
115
|
};
|
|
115
116
|
|
|
@@ -318,6 +319,19 @@ export class GenerationGateway {
|
|
|
318
319
|
try { return await fn(); } finally { lease.dispose(); }
|
|
319
320
|
}
|
|
320
321
|
|
|
322
|
+
/** Preparation reads model weights without replacing active model state.
|
|
323
|
+
* A compatible backend can perform it between existing decode iterations. */
|
|
324
|
+
runPreparation<T>(work: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
325
|
+
if (this.#closed) return Promise.reject(new Error("generation gateway is closed"));
|
|
326
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
327
|
+
const group = this.batchingEnabled ? this.#ensureScheduler() : undefined;
|
|
328
|
+
return group?.runPreparation
|
|
329
|
+
? group.runPreparation(work, signal)
|
|
330
|
+
: this.runExclusive(work, undefined, signal);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
get mediaBatchingEnabled(): boolean { return this.batchingEnabled && !!this.#binding.mediaInput; }
|
|
334
|
+
|
|
321
335
|
/** Run one generation on the appropriate lane. onToken is invoked per emitted
|
|
322
336
|
* token (its `false` halts); resolves with stats when the generation ends. */
|
|
323
337
|
async run(
|
|
@@ -331,10 +345,32 @@ export class GenerationGateway {
|
|
|
331
345
|
trace?: PromptResponseTrace,
|
|
332
346
|
): Promise<GenerateStats> {
|
|
333
347
|
let reservation;
|
|
348
|
+
const closeRequestWait = trace?.begin("engine.request_wait", { capacity: this.#batch });
|
|
334
349
|
try { reservation = await acquireReservation(this.#requests, signal); }
|
|
335
350
|
catch (error) { cleanupFailure(error, () => disposeUnstartedRequest(options, vision)); }
|
|
336
|
-
|
|
337
|
-
|
|
351
|
+
finally { closeRequestWait?.(); }
|
|
352
|
+
let releasePrefix: (() => void) | undefined;
|
|
353
|
+
try {
|
|
354
|
+
try {
|
|
355
|
+
const adapters = options.adapters?.length
|
|
356
|
+
? this.opts.adapterNamespace?.(options.adapters) ?? JSON.stringify(options.adapters) : "";
|
|
357
|
+
const baseNamespace = this.#binding.prefixNamespace?.(placement.execution, options, adapters) ??
|
|
358
|
+
(placement.execution?.method === "speculative" ? null : adapters);
|
|
359
|
+
const namespace = baseNamespace !== null && vision?.prefixIdentity
|
|
360
|
+
? JSON.stringify(["prepared-prefix-v1", vision.prefixIdentity, baseNamespace]) : baseNamespace;
|
|
361
|
+
if ((!vision || placement.execution?.promptCache) && namespace !== null) {
|
|
362
|
+
const closePrefetch = this.opts.promptCache?.prefetch ? trace?.begin("cache.prefetch") : undefined;
|
|
363
|
+
try { releasePrefix = await this.opts.promptCache?.prefetch?.(promptIds, namespace, options.cacheSessionId); }
|
|
364
|
+
finally { closePrefetch?.(); }
|
|
365
|
+
}
|
|
366
|
+
} catch (error) { cleanupFailure(error, () => disposeUnstartedRequest(options,
|
|
367
|
+
placement.mechanism === "continuous" ? undefined : vision)); }
|
|
368
|
+
return await this.#run(promptIds, options, onToken, vision, shape, placement, signal, trace);
|
|
369
|
+
} finally {
|
|
370
|
+
disposeResources([{ dispose: () => releasePrefix?.() }, reservation,
|
|
371
|
+
...(placement.mechanism === "continuous" && vision
|
|
372
|
+
? [vision.embeddings, vision.imageMask, vision.multimodalMask].filter(value => value != null) : [])]);
|
|
373
|
+
}
|
|
338
374
|
}
|
|
339
375
|
|
|
340
376
|
async #run(
|
|
@@ -347,7 +383,8 @@ export class GenerationGateway {
|
|
|
347
383
|
signal?: AbortSignal,
|
|
348
384
|
trace?: PromptResponseTrace,
|
|
349
385
|
): Promise<GenerateStats> {
|
|
350
|
-
const disposeUnstarted = () => disposeUnstartedRequest(options,
|
|
386
|
+
const disposeUnstarted = () => disposeUnstartedRequest(options,
|
|
387
|
+
placement.mechanism === "continuous" ? undefined : vision);
|
|
351
388
|
if (placement.shape !== shape) {
|
|
352
389
|
disposeUnstarted();
|
|
353
390
|
throw new Error("generation placement does not belong to this request shape");
|
|
@@ -410,8 +447,10 @@ export class GenerationGateway {
|
|
|
410
447
|
|
|
411
448
|
const adapters = options.adapters?.length ? [...options.adapters] : undefined;
|
|
412
449
|
const adapterKey = adapters ? JSON.stringify(adapters) : "";
|
|
413
|
-
const
|
|
414
|
-
|
|
450
|
+
const adapterNamespace = () => adapters ? this.opts.adapterNamespace?.(adapters) ?? adapterKey : "";
|
|
451
|
+
const cacheNamespace = vision?.prefixIdentity
|
|
452
|
+
? () => JSON.stringify(["prepared-prefix-v1", vision.prefixIdentity, adapterNamespace()])
|
|
453
|
+
: adapters ? adapterNamespace : "";
|
|
415
454
|
const context = adapters
|
|
416
455
|
? this.#binding.bindAdapterContext?.(adapters, `adapters:${adapterKey}`) : undefined;
|
|
417
456
|
|
|
@@ -433,7 +472,8 @@ export class GenerationGateway {
|
|
|
433
472
|
});
|
|
434
473
|
try {
|
|
435
474
|
st = await this.#ensureScheduler().submit({
|
|
436
|
-
promptIds, context, cacheNamespace, prefillChunkSize: options.prefillChunkSize,
|
|
475
|
+
promptIds, context, cacheNamespace, cacheSessionId: options.cacheSessionId, prefillChunkSize: options.prefillChunkSize,
|
|
476
|
+
...(vision ? { promptInput: this.#binding.mediaInput!(vision) } : {}),
|
|
437
477
|
continuation: continuation?.continuation,
|
|
438
478
|
statePolicy: this.#binding.statePolicy?.(placement.execution, options, promptIds.length + (options.maxTokens ?? 512)),
|
|
439
479
|
compiledDecode: placement.execution?.compiledDecode,
|
|
@@ -470,6 +510,7 @@ export class GenerationGateway {
|
|
|
470
510
|
decodeTps: st.decodeMs > 0 && st.generatedTokens > 1 ? ((st.generatedTokens - 1) / st.decodeMs) * 1000 : 0,
|
|
471
511
|
cacheTokens: [],
|
|
472
512
|
...(st.spec ? { spec: st.spec } : {}),
|
|
513
|
+
...(st.fill ? { fill: st.fill } : {}),
|
|
473
514
|
};
|
|
474
515
|
}
|
|
475
516
|
|
|
@@ -21,9 +21,14 @@ export interface BuiltPrompt {
|
|
|
21
21
|
diffusionPixels: MlxArray | null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Enter the model's execution domain for native preparation. Fetching and
|
|
25
|
+
* container transcoding stay outside this callback. */
|
|
26
|
+
export type PromptNativeWork = <T>(work: () => Promise<T>) => Promise<T>;
|
|
27
|
+
|
|
24
28
|
export interface ModelPromptBuilder {
|
|
25
29
|
(body: ChatRequestParams, tools: ChatRequestParams["tools"] | null,
|
|
26
|
-
ownership: RequestOwnership, prep: RequestPrep
|
|
30
|
+
ownership: RequestOwnership, prep: RequestPrep, nativeWork?: PromptNativeWork,
|
|
31
|
+
objects?: import("../contracts/object-cache").ObjectCache<import("../backends/mlx/checkpoint-state").CheckpointAttachment[]>): Promise<BuiltPrompt>;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/** Model-owned native services. A replacement supplies this one binding;
|
|
@@ -35,6 +40,7 @@ export interface ModelServingBinding {
|
|
|
35
40
|
createSerial(services: MlxSerialServices): SerialRun;
|
|
36
41
|
readonly buildPrompt: ModelPromptBuilder;
|
|
37
42
|
restore(store: SsdCacheStore, entry: SsdIndexEntry): ReturnType<SsdCacheStore["restore"]>;
|
|
43
|
+
restoreAsync?(store: SsdCacheStore, entry: SsdIndexEntry): ReturnType<SsdCacheStore["restoreAsync"]>;
|
|
38
44
|
signal(promptIds: number[], bins: number, minimum: number): Promise<{ bins: number[]; vocab: number }>;
|
|
39
45
|
diagnostics(): Record<string, unknown>;
|
|
40
46
|
readonly discovery: { readonly adapters: boolean; readonly training: boolean;
|
package/src/serve/preparation.ts
CHANGED
|
@@ -17,11 +17,11 @@ export async function acquireReservation(pool: Pick<AdmissionPool, "acquire">, s
|
|
|
17
17
|
finally { signal?.removeEventListener("abort", abort); }
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/** Bound retained preparations independently from the execution queue. */
|
|
21
21
|
export function createPreparationExecutor(
|
|
22
|
-
run: PreparationExecutor["run"], grammarCapacity: number,
|
|
22
|
+
run: PreparationExecutor["run"], grammarCapacity: number, mediaCapacity = 1,
|
|
23
23
|
): PreparationExecutor & { close(): void } {
|
|
24
|
-
const media = new AdmissionPool(1);
|
|
24
|
+
const media = new AdmissionPool(Math.max(1, mediaCapacity));
|
|
25
25
|
const constraints = new AdmissionPool(Math.max(1, grammarCapacity));
|
|
26
26
|
return {
|
|
27
27
|
run,
|
|
@@ -10,16 +10,27 @@ export type P2RTracePhase =
|
|
|
10
10
|
| "request.prompt_prepare"
|
|
11
11
|
| "completion.total"
|
|
12
12
|
| "completion.placement"
|
|
13
|
+
| "engine.request_wait"
|
|
13
14
|
| "engine.admission_wait"
|
|
15
|
+
| "engine.mixed_forward"
|
|
14
16
|
| "cache.lookup_restore"
|
|
17
|
+
| "cache.prefetch"
|
|
15
18
|
| "prefill.total"
|
|
16
19
|
| "prefill.batch_setup"
|
|
17
20
|
| "prefill.chunk"
|
|
21
|
+
| "prefill.row_sync"
|
|
22
|
+
| "prefill.forward"
|
|
23
|
+
| "prefill.evaluate"
|
|
24
|
+
| "prefill.checkpoint"
|
|
25
|
+
| "prefill.project"
|
|
26
|
+
| "prefill.complete"
|
|
27
|
+
| "prefill.companion"
|
|
18
28
|
| "prefill.kv_maintenance"
|
|
19
29
|
| "token_zero.total"
|
|
20
30
|
| "token_zero.forward"
|
|
21
31
|
| "token_zero.head"
|
|
22
32
|
| "token_zero.sample"
|
|
33
|
+
| "response.token_route"
|
|
23
34
|
| "response.first_write"
|
|
24
35
|
| "response.final_write";
|
|
25
36
|
|
|
@@ -42,6 +53,8 @@ export interface P2RTraceRecord {
|
|
|
42
53
|
route: string;
|
|
43
54
|
clock: "monotonic-ms";
|
|
44
55
|
outcome: P2RTraceOutcome;
|
|
56
|
+
/** Process-local monotonic origin; aligns concurrent requests in one server. */
|
|
57
|
+
startedAtMs?: number;
|
|
45
58
|
totalMs: number;
|
|
46
59
|
events: P2RTraceEvent[];
|
|
47
60
|
attributes?: P2RTraceAttributes;
|
|
@@ -129,6 +142,7 @@ export class PromptResponseTrace {
|
|
|
129
142
|
route: this.route,
|
|
130
143
|
clock: "monotonic-ms",
|
|
131
144
|
outcome,
|
|
145
|
+
startedAtMs: this.#startedAt,
|
|
132
146
|
totalMs,
|
|
133
147
|
events: [...this.#events].sort((a, b) => a.startMs - b.startMs),
|
|
134
148
|
...(attributes ? { attributes } : {}),
|
|
@@ -39,6 +39,7 @@ export interface RequestPlanInput {
|
|
|
39
39
|
topLogprobs: number;
|
|
40
40
|
adapterIds: string[];
|
|
41
41
|
hasVision: boolean;
|
|
42
|
+
hasPreparedPrefixIdentity?: boolean;
|
|
42
43
|
userSeed: boolean;
|
|
43
44
|
hasGrammar: boolean;
|
|
44
45
|
hasDraft: boolean;
|
|
@@ -137,6 +138,7 @@ export function planRequest(input: RequestPlanInput): PlanRequestResult {
|
|
|
137
138
|
|
|
138
139
|
const shape: RequestShape = {
|
|
139
140
|
hasVision: input.hasVision,
|
|
141
|
+
hasPreparedPrefixIdentity: input.hasPreparedPrefixIdentity,
|
|
140
142
|
hasAdapters: !!options.adapters?.length,
|
|
141
143
|
hasRepetitionPenalty: !!options.repetitionPenalty,
|
|
142
144
|
hasLogitsExtras: !!(
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// tool-call stream router. Resolved ONCE per server (createRequestPrep) and
|
|
6
6
|
// captured by the route handlers. Extracted from src/server.ts (repo-taming
|
|
7
7
|
// Phase 4).
|
|
8
|
-
import type { ToolDefinition } from "../chat-template";
|
|
8
|
+
import type { ChatMessage, ToolDefinition } from "../chat-template";
|
|
9
9
|
import {
|
|
10
10
|
fillEchoConfig,
|
|
11
11
|
fillMaxSpan,
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type ChatRequestParams,
|
|
28
28
|
} from "./chat-request";
|
|
29
29
|
import type { ServingContext } from "./model-host";
|
|
30
|
+
import type { PromptTokenHistory } from "./generated-token-history";
|
|
30
31
|
import { selectToolStreamMode, ToolAwareStream, type ToolStreamMode } from "./token-streams";
|
|
31
32
|
|
|
32
33
|
/** The ServerOptions fields request preparation reads (server-wide
|
|
@@ -49,6 +50,7 @@ export function createRequestPrep(input: {
|
|
|
49
50
|
/** `--max-tokens` / the GLM memory plan's generation cap; undefined = the
|
|
50
51
|
* surface's own default (chat 65 536, raw completion 512). */
|
|
51
52
|
defaultGeneratedTokens: number | undefined;
|
|
53
|
+
tokenHistory?: PromptTokenHistory;
|
|
52
54
|
}) {
|
|
53
55
|
const { ctx, serverOptions, kvScheme, defaultGeneratedTokens } = input;
|
|
54
56
|
|
|
@@ -92,6 +94,8 @@ export function createRequestPrep(input: {
|
|
|
92
94
|
? Math.min(genTemp, NO_THINK_TEMPERATURE)
|
|
93
95
|
: genTemp;
|
|
94
96
|
return {
|
|
97
|
+
cacheSessionId: typeof req.session_id === "string" ? req.session_id :
|
|
98
|
+
typeof req.prompt_cache_key === "string" ? req.prompt_cache_key : undefined,
|
|
95
99
|
// Unset means generate until EOS/stop. Only a client cap or an explicit
|
|
96
100
|
// operator limit constrains completion length; fit estimates do not.
|
|
97
101
|
maxTokens: req.max_completion_tokens ?? req.max_tokens ??
|
|
@@ -191,7 +195,8 @@ export function createRequestPrep(input: {
|
|
|
191
195
|
const ids = ctx.tokenizer.encode(rendered);
|
|
192
196
|
// template includes <bos>; tokenizer post-processor also prepends one
|
|
193
197
|
const trimmed = ids[0] === ids[1] && ids[0] === ctx.tokenizer.bosTokenId ? ids.slice(1) : ids;
|
|
194
|
-
return { ids:
|
|
198
|
+
return { ids: input.tokenHistory?.resolve(rendered, trimmed) ?? trimmed,
|
|
199
|
+
startInThinking: promptEndsInOpenThink(rendered) };
|
|
195
200
|
};
|
|
196
201
|
|
|
197
202
|
/** STABLE cache boundary: the prompt's tail is a generation primer (e.g.
|
|
@@ -219,18 +224,32 @@ export function createRequestPrep(input: {
|
|
|
219
224
|
if (primer !== undefined) return Math.max(0, trimmed.length - primer);
|
|
220
225
|
let stableLen = trimmed.length;
|
|
221
226
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
const canonical = ctx.tokenizer.encode(ctx.template.render(normalizeMessages(req.messages), opts));
|
|
228
|
+
const current = canonical[0] === canonical[1] && canonical[0] === ctx.tokenizer.bosTokenId
|
|
229
|
+
? canonical.slice(1) : canonical;
|
|
230
|
+
const replies: ChatMessage[] = [{ role: "assistant", content: "x" }];
|
|
231
|
+
// Some templates retain an empty think primer for text replies but
|
|
232
|
+
// omit it before a tool call. Both reply forms must preserve the
|
|
233
|
+
// boundary retained by an untrimmable recurrent cache.
|
|
234
|
+
if (tools?.length) replies.push({ role: "assistant", content: null, tool_calls: [{
|
|
235
|
+
id: "cache-boundary-probe", type: "function",
|
|
236
|
+
function: { name: tools[0]!.function.name, arguments: {} },
|
|
237
|
+
}] });
|
|
238
|
+
let i = current.length;
|
|
239
|
+
for (const reply of replies) {
|
|
240
|
+
const probe = ctx.tokenizer.encode(ctx.template.render([
|
|
241
|
+
...normalizeMessages(req.messages), reply, { role: "user", content: "x" },
|
|
242
|
+
], opts));
|
|
243
|
+
const probeTrimmed = probe[0] === probe[1] && probe[0] === ctx.tokenizer.bosTokenId ? probe.slice(1) : probe;
|
|
244
|
+
let common = 0;
|
|
245
|
+
const n = Math.min(i, probeTrimmed.length);
|
|
246
|
+
while (common < n && current[common] === probeTrimmed[common]) common++;
|
|
247
|
+
i = common;
|
|
248
|
+
}
|
|
249
|
+
// Generated-history provenance may use a different BPE segmentation
|
|
250
|
+
// inside old turns. Measure the primer canonically, then subtract it
|
|
251
|
+
// from the actual request IDs that inference will receive.
|
|
252
|
+
stableLen = Math.max(0, trimmed.length - (current.length - i));
|
|
234
253
|
// Memoize the mode's primer length (sanity-capped: a "primer" longer
|
|
235
254
|
// than 64 tokens means the probe diverged for content reasons —
|
|
236
255
|
// don't generalize that).
|
|
@@ -271,10 +290,10 @@ export function createRequestPrep(input: {
|
|
|
271
290
|
* when the feature is off or the request's shape refuses it.
|
|
272
291
|
*
|
|
273
292
|
* Refusals owned here (body-level); the ones only ChatStage can see —
|
|
274
|
-
* a compiled grammar, media prompts
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
293
|
+
* a compiled grammar, media prompts and a mounted draft model — are applied
|
|
294
|
+
* there. The execution binding owns cache-format and method support. Strict
|
|
295
|
+
* shared execution keeps the sampler's absolute token positions, including
|
|
296
|
+
* known tokens; an explicit seed therefore remains replayable.
|
|
278
297
|
*
|
|
279
298
|
* MLX_BUN_FILL=echo additionally arms the echo index (K3c, Lab tier): the
|
|
280
299
|
* strict rows keep policy "assert", and copied spans ride the same
|
|
@@ -287,9 +306,7 @@ export function createRequestPrep(input: {
|
|
|
287
306
|
const mode = resolveFillMode();
|
|
288
307
|
if (mode === "off") return null;
|
|
289
308
|
if (!tools?.length) return null;
|
|
290
|
-
//
|
|
291
|
-
// skip the injected positions, so an identical request could not replay.
|
|
292
|
-
if (req.seed !== undefined) return null;
|
|
309
|
+
// Seed support is selected by the execution binding after placement.
|
|
293
310
|
// Injected tokens are never sampled — they have no logprob row.
|
|
294
311
|
if (req.logprobs === true) return null;
|
|
295
312
|
if (typeof req.top_logprobs === "number" && req.top_logprobs > 0) return null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Whisper-only server: `mlx-bun serve <whisper checkpoint>` starts this
|
|
2
|
+
// instead of the chat server. Routes: the audio routes, /v1/models,
|
|
3
|
+
// /v1, /health, /stats. No chat machinery, no prompt cache — the process
|
|
4
|
+
// idles at a few tens of MB with the weights paged out (see
|
|
5
|
+
// TranscriptionService). This is the footprint sotto wants from a
|
|
6
|
+
// dictation backend.
|
|
7
|
+
|
|
8
|
+
import type { Server } from "bun";
|
|
9
|
+
import pkgJson from "../../package.json" with { type: "json" };
|
|
10
|
+
import { handleAudioRoute } from "./audio-routes";
|
|
11
|
+
import { TranscriptionService } from "./transcription-service";
|
|
12
|
+
|
|
13
|
+
const pkgVersion = (pkgJson as { version: string }).version;
|
|
14
|
+
|
|
15
|
+
export interface TranscriptionServerOptions {
|
|
16
|
+
modelDir: string;
|
|
17
|
+
modelId: string;
|
|
18
|
+
port?: number;
|
|
19
|
+
hostname?: string;
|
|
20
|
+
unixSocket?: string;
|
|
21
|
+
idleUnloadSec?: number;
|
|
22
|
+
resident?: boolean;
|
|
23
|
+
/** Load the weights before serving (default: on first request). */
|
|
24
|
+
preload?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function createTranscriptionServer(opts: TranscriptionServerOptions): Promise<{ server: Server<unknown>; service: TranscriptionService }> {
|
|
28
|
+
const service = new TranscriptionService({
|
|
29
|
+
modelDir: opts.modelDir, modelId: opts.modelId, idleUnloadSec: opts.idleUnloadSec, resident: opts.resident,
|
|
30
|
+
});
|
|
31
|
+
if (opts.preload) await service.ensureLoaded();
|
|
32
|
+
const startedAt = Date.now();
|
|
33
|
+
const server = Bun.serve({
|
|
34
|
+
...(opts.unixSocket
|
|
35
|
+
? ({ unix: opts.unixSocket } as unknown as Record<string, never>)
|
|
36
|
+
: { port: opts.port ?? 0, ...(opts.hostname ? { hostname: opts.hostname } : {}) }),
|
|
37
|
+
idleTimeout: 0,
|
|
38
|
+
async fetch(request) {
|
|
39
|
+
const url = new URL(request.url);
|
|
40
|
+
const audio = await handleAudioRoute(url, request, { service: async () => service });
|
|
41
|
+
if (audio) return audio;
|
|
42
|
+
if (request.method === "GET" && url.pathname === "/health")
|
|
43
|
+
return Response.json({ status: "ok", transcription: { ...service.stats, sessions: service.sessionCount } });
|
|
44
|
+
if (request.method === "GET" && url.pathname === "/stats")
|
|
45
|
+
return Response.json({ model: opts.modelId, transcription: service.stats, uptime_s: (Date.now() - startedAt) / 1000 });
|
|
46
|
+
if (request.method === "GET" && url.pathname === "/v1")
|
|
47
|
+
return Response.json({
|
|
48
|
+
name: "mlx-bun", version: pkgVersion, model: opts.modelId, mode: "transcription",
|
|
49
|
+
endpoints: ["POST /v1/audio/transcriptions", "POST /v1/audio/translations", "POST /v1/audio/sessions", "POST /v1/audio/sessions/:id/audio", "POST /v1/audio/sessions/:id/finish", "DELETE /v1/audio/sessions/:id", "POST /admin/transcription/unload", "GET /v1/models", "GET /health", "GET /stats"],
|
|
50
|
+
});
|
|
51
|
+
if (request.method === "GET" && (url.pathname === "/v1/models" || url.pathname.startsWith("/v1/models/")))
|
|
52
|
+
return Response.json({
|
|
53
|
+
object: "list",
|
|
54
|
+
data: [{
|
|
55
|
+
id: opts.modelId, object: "model", created: Math.floor(startedAt / 1000), owned_by: "mlx-bun",
|
|
56
|
+
transcription: true, resident: service.resident,
|
|
57
|
+
capabilities: { transcription: true, translation: true, chat_completions: false },
|
|
58
|
+
}],
|
|
59
|
+
});
|
|
60
|
+
return Response.json({ error: { message: "not found" } }, { status: 404 });
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
return { server, service };
|
|
64
|
+
}
|