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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Token provenance for rendered multi-turn prompts. A generated BPE token
|
|
2
|
+
* sequence need not equal encode(decode(ids)). Preserve the sequence that
|
|
3
|
+
* produced the saved inference state when its rendered text is unchanged.
|
|
4
|
+
* This stores only text and IDs; KV ownership and persistence live elsewhere. */
|
|
5
|
+
export interface PromptTokenHistory {
|
|
6
|
+
remember(tokens: readonly number[]): void;
|
|
7
|
+
resolve(rendered: string, canonical: number[]): number[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TokenCodec {
|
|
11
|
+
encode(text: string, addSpecialTokens?: boolean): number[];
|
|
12
|
+
decode(ids: number[], skipSpecialTokens?: boolean): string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class GeneratedTokenHistory implements PromptTokenHistory {
|
|
16
|
+
readonly #entries = new Map<string, number[]>();
|
|
17
|
+
#chars = 0;
|
|
18
|
+
constructor(readonly codec: TokenCodec, readonly maxChars = 2 ** 21, readonly maxEntries = 16) {}
|
|
19
|
+
|
|
20
|
+
remember(tokens: readonly number[]): void {
|
|
21
|
+
if (!tokens.length) return;
|
|
22
|
+
// Repeated requests can publish the same generated checkpoint. Its text
|
|
23
|
+
// and IDs are already owned here; refresh recency without decoding the
|
|
24
|
+
// entire conversation again on the request's completion path.
|
|
25
|
+
for (const [text, ids] of this.#entries) {
|
|
26
|
+
if (ids.length !== tokens.length || !ids.every((id, i) => id === tokens[i])) continue;
|
|
27
|
+
this.#entries.delete(text);
|
|
28
|
+
this.#entries.set(text, ids);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const ids = [...tokens], text = this.codec.decode(ids, false);
|
|
32
|
+
// An unfinished UTF-8 token is not a text boundary. A later checkpoint
|
|
33
|
+
// can supply a complete prefix instead.
|
|
34
|
+
if (!text.length || text.endsWith("\uFFFD") || text.length > this.maxChars) return;
|
|
35
|
+
if (this.#entries.delete(text)) this.#chars -= text.length;
|
|
36
|
+
this.#entries.set(text, ids); this.#chars += text.length;
|
|
37
|
+
while (this.#entries.size > this.maxEntries || this.#chars > this.maxChars) {
|
|
38
|
+
const oldest = this.#entries.keys().next().value!;
|
|
39
|
+
this.#entries.delete(oldest); this.#chars -= oldest.length;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
resolve(rendered: string, canonical: number[]): number[] {
|
|
44
|
+
let best: { text: string; ids: number[] } | undefined;
|
|
45
|
+
for (const [text, ids] of this.#entries) {
|
|
46
|
+
if (text.length >= rendered.length || (best && text.length <= best.text.length)) continue;
|
|
47
|
+
if (rendered.startsWith(text)) best = { text, ids };
|
|
48
|
+
}
|
|
49
|
+
if (!best) return canonical;
|
|
50
|
+
// Normal canonical output keeps the existing tokenizer fast path.
|
|
51
|
+
if (best.ids.every((id, i) => canonical[i] === id)) return canonical;
|
|
52
|
+
const ids = [...best.ids, ...this.codec.encode(rendered.slice(best.text.length), false)];
|
|
53
|
+
// Decoder cleanup/normalization (e.g. SentencePiece's leading-space
|
|
54
|
+
// rules) can make a standalone suffix unsafe to append.
|
|
55
|
+
return this.codec.decode(ids, false) === rendered ? ids : canonical;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -29,7 +29,7 @@ import { cleanupFailure, disposeResources } from "../engine/resources";
|
|
|
29
29
|
// server.py's all-caches-have-merge check). GLM's capability owns compressed
|
|
30
30
|
// MLA/DSA merge/extract/filter and byte projection; Qwen3.5's SSM cache owns
|
|
31
31
|
// its row state. The remaining request-shape exclusions include vision, LoRA,
|
|
32
|
-
// serial-only KV modes
|
|
32
|
+
// serial-only KV modes
|
|
33
33
|
// (reproducibility ⇒ solo, matching mlx-lm's _is_batchable). Temperature /
|
|
34
34
|
// top-p / top-k DO batch (each row samples with its own seed). Full-attention
|
|
35
35
|
// AND sliding-window (Gemma) models both batch — the scheduler assembles each
|
|
@@ -42,9 +42,9 @@ import { MlxArray } from "../mlx/array";
|
|
|
42
42
|
import type { RuntimeModel } from "../model/factory";
|
|
43
43
|
import type { GenerateOptions, GenerateStats, TokenLogprobs } from "../generate";
|
|
44
44
|
import type { KvScheme } from "../kv-scheme";
|
|
45
|
+
import { createRowSampling } from "../backends/mlx/row-sampling";
|
|
45
46
|
import { makeStepSampler } from "../sampler";
|
|
46
47
|
import type { RowPromptCache } from "../backends/mlx/batch-group";
|
|
47
|
-
import type { MlxMemoryBudget } from "../backends/mlx/memory-guard";
|
|
48
48
|
import { bindMlxGateway, type MlxGatewayBinding, type MlxBatchGroup } from "../backends/mlx/gateway-binding";
|
|
49
49
|
import type { RuntimeConfig } from "../runtime-config";
|
|
50
50
|
import type { ExecutionRequirements, ResolvedExecution } from "../contracts/execution";
|
|
@@ -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,12 +110,12 @@ 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
|
|
|
116
117
|
/** Per-token sink: returning `false` halts this generation (stop sequence).
|
|
117
|
-
* `logprobs` is
|
|
118
|
+
* `logprobs` is populated on either lane when the request asked for
|
|
118
119
|
* logprobs capture (GenerateOptions.logprobs / topLogprobs). */
|
|
119
120
|
export type OnToken = (
|
|
120
121
|
token: number,
|
|
@@ -172,7 +173,6 @@ export class GenerationGateway {
|
|
|
172
173
|
private readonly serialRun: SerialRun,
|
|
173
174
|
private readonly opts: {
|
|
174
175
|
kvBudgetBytes?: number;
|
|
175
|
-
memoryBudget?: MlxMemoryBudget;
|
|
176
176
|
checkpoints?: boolean;
|
|
177
177
|
stateCodecs?: import("../kv-store").CacheCodecProvider;
|
|
178
178
|
/** The server-wide KV scheme (server.ts kvScheme) — threaded to the
|
|
@@ -185,9 +185,7 @@ export class GenerationGateway {
|
|
|
185
185
|
* back on finish. Safe to share with the serial lane — both use it
|
|
186
186
|
* only inside this gateway's mutual-exclusion domain. */
|
|
187
187
|
promptCache?: RowPromptCache;
|
|
188
|
-
|
|
189
|
-
* need that same lock, so this hook must never run inside it. */
|
|
190
|
-
beforeSerial?: () => Promise<void>;
|
|
188
|
+
adapterNamespace?: (adapters: string[]) => string;
|
|
191
189
|
} = {},
|
|
192
190
|
) {
|
|
193
191
|
this.#binding = "createBatchGroup" in model ? model : bindMlxGateway(model);
|
|
@@ -235,16 +233,20 @@ export class GenerationGateway {
|
|
|
235
233
|
await this.onIdle();
|
|
236
234
|
}
|
|
237
235
|
|
|
238
|
-
/**
|
|
239
|
-
*
|
|
240
|
-
* timer when already idle). The SSD write-behind gates each per-tensor
|
|
241
|
-
* flush step on this so durability work never steals blocking
|
|
242
|
-
* GPU-sync + writeSync slices from an active decode (the 2026-07-07
|
|
243
|
-
* decode@ctx contamination — see server.ts's write-behind block). */
|
|
236
|
+
/** Observe idleness. Call runWhenIdle for background work that must also
|
|
237
|
+
* acquire ownership atomically with the idle check. */
|
|
244
238
|
async onIdle(pollMs = 20): Promise<void> {
|
|
245
239
|
while (this.busy) await new Promise<void>((r) => setTimeout(r, pollMs));
|
|
246
240
|
}
|
|
247
241
|
|
|
242
|
+
/** Background work waits without requesting a batch drain. The final busy
|
|
243
|
+
* check and mutex acquisition have no await between them, so a foreground
|
|
244
|
+
* request cannot enter between the check and ownership registration. */
|
|
245
|
+
async runWhenIdle<T>(fn: () => Promise<T>, pollMs = 20): Promise<T> {
|
|
246
|
+
while (this.busy) await new Promise<void>(resolve => setTimeout(resolve, pollMs));
|
|
247
|
+
return this.runExclusive(fn);
|
|
248
|
+
}
|
|
249
|
+
|
|
248
250
|
/** Cumulative rows routed to the batch lane since server start. The serial
|
|
249
251
|
* lane never advances this — /stats' race-free lane-routing observable
|
|
250
252
|
* (active/pending are instantaneous and read 0 once a request finishes). */
|
|
@@ -272,7 +274,7 @@ export class GenerationGateway {
|
|
|
272
274
|
const frozenShape = Object.freeze(shape);
|
|
273
275
|
const execution = this.#binding.plan(frozenShape, options, {
|
|
274
276
|
continuous: this.batchingEnabled,
|
|
275
|
-
quantizedBatch: shape.kvQuant && this.#kvBatchable(),
|
|
277
|
+
quantizedBatch: (shape.kvQuant || shape.turboQuant) && this.#kvBatchable(),
|
|
276
278
|
checkpoints: this.opts.checkpoints === true,
|
|
277
279
|
});
|
|
278
280
|
return Object.freeze({ shape: frozenShape, mechanism: execution.mechanism, execution });
|
|
@@ -317,6 +319,19 @@ export class GenerationGateway {
|
|
|
317
319
|
try { return await fn(); } finally { lease.dispose(); }
|
|
318
320
|
}
|
|
319
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
|
+
|
|
320
335
|
/** Run one generation on the appropriate lane. onToken is invoked per emitted
|
|
321
336
|
* token (its `false` halts); resolves with stats when the generation ends. */
|
|
322
337
|
async run(
|
|
@@ -330,10 +345,32 @@ export class GenerationGateway {
|
|
|
330
345
|
trace?: PromptResponseTrace,
|
|
331
346
|
): Promise<GenerateStats> {
|
|
332
347
|
let reservation;
|
|
348
|
+
const closeRequestWait = trace?.begin("engine.request_wait", { capacity: this.#batch });
|
|
333
349
|
try { reservation = await acquireReservation(this.#requests, signal); }
|
|
334
350
|
catch (error) { cleanupFailure(error, () => disposeUnstartedRequest(options, vision)); }
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
}
|
|
337
374
|
}
|
|
338
375
|
|
|
339
376
|
async #run(
|
|
@@ -346,7 +383,8 @@ export class GenerationGateway {
|
|
|
346
383
|
signal?: AbortSignal,
|
|
347
384
|
trace?: PromptResponseTrace,
|
|
348
385
|
): Promise<GenerateStats> {
|
|
349
|
-
const disposeUnstarted = () => disposeUnstartedRequest(options,
|
|
386
|
+
const disposeUnstarted = () => disposeUnstartedRequest(options,
|
|
387
|
+
placement.mechanism === "continuous" ? undefined : vision);
|
|
350
388
|
if (placement.shape !== shape) {
|
|
351
389
|
disposeUnstarted();
|
|
352
390
|
throw new Error("generation placement does not belong to this request shape");
|
|
@@ -390,7 +428,7 @@ export class GenerationGateway {
|
|
|
390
428
|
});
|
|
391
429
|
};
|
|
392
430
|
let started = false;
|
|
393
|
-
return
|
|
431
|
+
return this.runExclusive(async () => {
|
|
394
432
|
signal?.throwIfAborted();
|
|
395
433
|
started = true;
|
|
396
434
|
const stats = await this.serialRun(
|
|
@@ -398,7 +436,7 @@ export class GenerationGateway {
|
|
|
398
436
|
);
|
|
399
437
|
signal?.throwIfAborted();
|
|
400
438
|
return stats;
|
|
401
|
-
}, trace, signal)
|
|
439
|
+
}, trace, signal)
|
|
402
440
|
// Covers a serial waiter aborted before serialRun takes ownership.
|
|
403
441
|
// generate() also disposes defensively; the operation is idempotent.
|
|
404
442
|
.finally(() => {
|
|
@@ -407,16 +445,25 @@ export class GenerationGateway {
|
|
|
407
445
|
});
|
|
408
446
|
}
|
|
409
447
|
|
|
448
|
+
const adapters = options.adapters?.length ? [...options.adapters] : undefined;
|
|
449
|
+
const adapterKey = adapters ? JSON.stringify(adapters) : "";
|
|
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 : "";
|
|
454
|
+
const context = adapters
|
|
455
|
+
? this.#binding.bindAdapterContext?.(adapters, `adapters:${adapterKey}`) : undefined;
|
|
456
|
+
|
|
410
457
|
// The scheduler owns grammar ready/accept sequencing. StepSampler owns the
|
|
411
458
|
// shared processors -> mask -> logprobs -> sample -> history contract.
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
initialHistory: promptIds,
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
459
|
+
const method = this.#binding.methodRequest?.(placement.execution, options);
|
|
460
|
+
const continuation = method ? undefined : this.#binding.continuationRequest?.(placement.execution, options, promptIds, onToken);
|
|
461
|
+
const sampling = method ? undefined : continuation ?? createRowSampling(makeStepSampler(options, {
|
|
462
|
+
tokenRepresentation: "device", grammarWait: "external",
|
|
463
|
+
historyUpdate: "after-sample", initialHistory: promptIds,
|
|
464
|
+
captureSelectedLogprob: options.logprobs === true,
|
|
465
|
+
captureTopLogprobs: options.topLogprobs,
|
|
466
|
+
}), onToken);
|
|
420
467
|
|
|
421
468
|
let st;
|
|
422
469
|
this.#rowsSubmitted++;
|
|
@@ -425,13 +472,15 @@ export class GenerationGateway {
|
|
|
425
472
|
});
|
|
426
473
|
try {
|
|
427
474
|
st = await this.#ensureScheduler().submit({
|
|
428
|
-
promptIds,
|
|
475
|
+
promptIds, context, cacheNamespace, cacheSessionId: options.cacheSessionId, prefillChunkSize: options.prefillChunkSize,
|
|
476
|
+
...(vision ? { promptInput: this.#binding.mediaInput!(vision) } : {}),
|
|
477
|
+
continuation: continuation?.continuation,
|
|
478
|
+
statePolicy: this.#binding.statePolicy?.(placement.execution, options, promptIds.length + (options.maxTokens ?? 512)),
|
|
429
479
|
compiledDecode: placement.execution?.compiledDecode,
|
|
430
480
|
maxTokens: options.maxTokens ?? 512,
|
|
431
481
|
eosTokenIds: options.eosTokenIds ?? this.#binding.config.eosTokenIds,
|
|
432
|
-
sample,
|
|
433
|
-
|
|
434
|
-
onToken,
|
|
482
|
+
...(method ? { method, onToken } : { sample: sampling!.sample,
|
|
483
|
+
plainGreedy: sampling!.plainGreedy, onToken: sampling!.onToken }),
|
|
435
484
|
onAdmitted: closeAdmission,
|
|
436
485
|
trace,
|
|
437
486
|
...(signal ? { signal } : {}),
|
|
@@ -445,7 +494,7 @@ export class GenerationGateway {
|
|
|
445
494
|
});
|
|
446
495
|
} finally {
|
|
447
496
|
closeAdmission?.();
|
|
448
|
-
|
|
497
|
+
sampling?.dispose();
|
|
449
498
|
// The scheduler never owns per-row grammar state.
|
|
450
499
|
options.grammar?.dispose();
|
|
451
500
|
}
|
|
@@ -460,6 +509,8 @@ export class GenerationGateway {
|
|
|
460
509
|
prefillTps: st.prefillMs > 0 ? ((st.promptTokens - st.cachedTokens) / st.prefillMs) * 1000 : 0,
|
|
461
510
|
decodeTps: st.decodeMs > 0 && st.generatedTokens > 1 ? ((st.generatedTokens - 1) / st.decodeMs) * 1000 : 0,
|
|
462
511
|
cacheTokens: [],
|
|
512
|
+
...(st.spec ? { spec: st.spec } : {}),
|
|
513
|
+
...(st.fill ? { fill: st.fill } : {}),
|
|
463
514
|
};
|
|
464
515
|
}
|
|
465
516
|
|
|
@@ -470,7 +521,6 @@ export class GenerationGateway {
|
|
|
470
521
|
maxBatch: this.#batch,
|
|
471
522
|
stateCodecs: this.opts.stateCodecs,
|
|
472
523
|
kvBudgetBytes: this.opts.kvBudgetBytes,
|
|
473
|
-
memoryBudget: this.opts.memoryBudget,
|
|
474
524
|
// Phase 3.1: the batchable kvConfig composition is applied by the
|
|
475
525
|
// scheduler (solo rows convert at serial chunk boundaries, then
|
|
476
526
|
// merge as quantized triples). Only threaded when the scheme
|
|
@@ -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/model-host.ts
CHANGED
|
@@ -73,10 +73,9 @@ export interface ServerContext<Model = RuntimeModel> {
|
|
|
73
73
|
* optiq serve injects these as server defaults (gen_config.py);
|
|
74
74
|
* explicit request fields always win. */
|
|
75
75
|
genDefaults: GenSamplingDefaults;
|
|
76
|
-
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* through src/spec/serve-loop.ts. null = no draft configured. */
|
|
76
|
+
/** Spec-eligible requests use src/spec/serve-loop.ts; ordinary fallbacks
|
|
77
|
+
* use their own feature-compatible scheduling mechanism.
|
|
78
|
+
* null = no draft configured. */
|
|
80
79
|
draft?: {
|
|
81
80
|
provider: import("../spec/source").DraftProvider;
|
|
82
81
|
numDraftTokens: number;
|
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 } : {}),
|
|
@@ -33,12 +33,13 @@ export interface RequestPlanInput {
|
|
|
33
33
|
promptIds: number[];
|
|
34
34
|
options: GenerateOptions & { stopSequences: string[] };
|
|
35
35
|
requestedMaxTokens: number;
|
|
36
|
-
|
|
36
|
+
contextLimit: number | null;
|
|
37
37
|
stream: boolean;
|
|
38
38
|
wantLogprobs: boolean;
|
|
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;
|
|
@@ -114,11 +115,12 @@ export type PlanRequestResult = RequestPlan | RequestRejection;
|
|
|
114
115
|
* options, adapter selection, and lane shape are derived here once for chat
|
|
115
116
|
* and raw completions. */
|
|
116
117
|
export function planRequest(input: RequestPlanInput): PlanRequestResult {
|
|
117
|
-
const available = input.
|
|
118
|
+
const available = input.contextLimit === null ? Infinity
|
|
119
|
+
: input.contextLimit - input.promptIds.length;
|
|
118
120
|
if (available < 1) {
|
|
119
121
|
return new RequestRejection(
|
|
120
122
|
`prompt is ${input.promptIds.length} tokens but the memory budget caps ` +
|
|
121
|
-
`safe context at ${input.
|
|
123
|
+
`safe context at ${input.contextLimit} — no room to generate; ` +
|
|
122
124
|
"shorten the prompt or raise --memory-budget",
|
|
123
125
|
input.ownership,
|
|
124
126
|
);
|
|
@@ -136,6 +138,7 @@ export function planRequest(input: RequestPlanInput): PlanRequestResult {
|
|
|
136
138
|
|
|
137
139
|
const shape: RequestShape = {
|
|
138
140
|
hasVision: input.hasVision,
|
|
141
|
+
hasPreparedPrefixIdentity: input.hasPreparedPrefixIdentity,
|
|
139
142
|
hasAdapters: !!options.adapters?.length,
|
|
140
143
|
hasRepetitionPenalty: !!options.repetitionPenalty,
|
|
141
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,10 +94,10 @@ export function createRequestPrep(input: {
|
|
|
92
94
|
? Math.min(genTemp, NO_THINK_TEMPERATURE)
|
|
93
95
|
: genTemp;
|
|
94
96
|
return {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
//
|
|
97
|
+
cacheSessionId: typeof req.session_id === "string" ? req.session_id :
|
|
98
|
+
typeof req.prompt_cache_key === "string" ? req.prompt_cache_key : undefined,
|
|
99
|
+
// Unset means generate until EOS/stop. Only a client cap or an explicit
|
|
100
|
+
// operator limit constrains completion length; fit estimates do not.
|
|
99
101
|
maxTokens: req.max_completion_tokens ?? req.max_tokens ??
|
|
100
102
|
defaultGeneratedTokens,
|
|
101
103
|
temperature: req.temperature ?? serverOptions.defaultTemperature ?? defaultTemp,
|
|
@@ -193,7 +195,8 @@ export function createRequestPrep(input: {
|
|
|
193
195
|
const ids = ctx.tokenizer.encode(rendered);
|
|
194
196
|
// template includes <bos>; tokenizer post-processor also prepends one
|
|
195
197
|
const trimmed = ids[0] === ids[1] && ids[0] === ctx.tokenizer.bosTokenId ? ids.slice(1) : ids;
|
|
196
|
-
return { ids:
|
|
198
|
+
return { ids: input.tokenHistory?.resolve(rendered, trimmed) ?? trimmed,
|
|
199
|
+
startInThinking: promptEndsInOpenThink(rendered) };
|
|
197
200
|
};
|
|
198
201
|
|
|
199
202
|
/** STABLE cache boundary: the prompt's tail is a generation primer (e.g.
|
|
@@ -221,18 +224,32 @@ export function createRequestPrep(input: {
|
|
|
221
224
|
if (primer !== undefined) return Math.max(0, trimmed.length - primer);
|
|
222
225
|
let stableLen = trimmed.length;
|
|
223
226
|
try {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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));
|
|
236
253
|
// Memoize the mode's primer length (sanity-capped: a "primer" longer
|
|
237
254
|
// than 64 tokens means the probe diverged for content reasons —
|
|
238
255
|
// don't generalize that).
|
|
@@ -273,10 +290,10 @@ export function createRequestPrep(input: {
|
|
|
273
290
|
* when the feature is off or the request's shape refuses it.
|
|
274
291
|
*
|
|
275
292
|
* Refusals owned here (body-level); the ones only ChatStage can see —
|
|
276
|
-
* a compiled grammar, media prompts
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
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.
|
|
280
297
|
*
|
|
281
298
|
* MLX_BUN_FILL=echo additionally arms the echo index (K3c, Lab tier): the
|
|
282
299
|
* strict rows keep policy "assert", and copied spans ride the same
|
|
@@ -289,9 +306,7 @@ export function createRequestPrep(input: {
|
|
|
289
306
|
const mode = resolveFillMode();
|
|
290
307
|
if (mode === "off") return null;
|
|
291
308
|
if (!tools?.length) return null;
|
|
292
|
-
//
|
|
293
|
-
// skip the injected positions, so an identical request could not replay.
|
|
294
|
-
if (req.seed !== undefined) return null;
|
|
309
|
+
// Seed support is selected by the execution binding after placement.
|
|
295
310
|
// Injected tokens are never sampled — they have no logprob row.
|
|
296
311
|
if (req.logprobs === true) return null;
|
|
297
312
|
if (typeof req.top_logprobs === "number" && req.top_logprobs > 0) return null;
|
|
@@ -16,7 +16,7 @@ export class TextCompletionStage {
|
|
|
16
16
|
constructor(
|
|
17
17
|
private readonly ctx: ServingContext,
|
|
18
18
|
private readonly prep: RequestPrep,
|
|
19
|
-
private readonly
|
|
19
|
+
private readonly contextLimit: number | null,
|
|
20
20
|
/** `--max-tokens` / the GLM plan's reservation. No invented fallback:
|
|
21
21
|
* unset runs to EOS or the admitted context (with no template an EOS
|
|
22
22
|
* may never come — that run is bounded by admission, the real limit). */
|
|
@@ -81,7 +81,7 @@ export class TextCompletionStage {
|
|
|
81
81
|
promptIds,
|
|
82
82
|
options,
|
|
83
83
|
requestedMaxTokens,
|
|
84
|
-
|
|
84
|
+
contextLimit: this.contextLimit,
|
|
85
85
|
stream: request.stream,
|
|
86
86
|
wantLogprobs,
|
|
87
87
|
topLogprobs,
|