mlx-bun 0.3.0 → 0.4.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/docs/README.md +4 -1
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/design/batching.md +1321 -49
- package/docs/design/decode-speed-program.md +331 -1163
- package/docs/design/generic-model-support.md +79 -3
- package/docs/design/kv-cache.md +198 -54
- package/docs/design/speculative-decoding.md +387 -24
- package/docs/design/turboquant.md +123 -13
- package/docs/design/unified-engine-frontier-plan.md +144 -142
- package/docs/planning/PRODUCT_ROADMAP.md +16 -0
- package/docs/planning/release-notes-v0.4.0.md +45 -0
- package/docs/reference/benchmarks.md +2457 -6
- package/docs/reference/cli.md +41 -2
- package/docs/reference/distribution.md +16 -9
- package/docs/reference/environment.md +55 -5
- package/docs/reference/models.md +13 -4
- package/docs/reference/server-api.md +79 -25
- package/docs/reference/server-config.md +178 -84
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +1 -1
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/batch-group.ts +368 -224
- 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 +106 -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/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 +77 -18
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +133 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/model-serving.ts +1 -1
- package/src/backends/mlx/prefill-cohort.ts +89 -0
- package/src/backends/mlx/prefill-rows.ts +200 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/request-state-policy.ts +41 -0
- package/src/backends/mlx/rollback.ts +41 -3
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +35 -66
- package/src/backends/mlx/speculative-group.ts +359 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- 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 +165 -0
- package/src/cli.ts +11 -36
- package/src/contracts/execution.ts +11 -0
- package/src/contracts/prefix-cache.ts +23 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +18 -0
- package/src/engine/execution-plan.ts +18 -16
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +10 -5
- package/src/fit.ts +29 -15
- package/src/generate.ts +77 -173
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +70 -12
- package/src/kv-store.ts +70 -16
- package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
- package/src/lab/paged-kv/paged-kv.ts +25 -11
- package/src/mlx/array.ts +2 -1
- 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 +93 -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 +55 -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 +162 -80
- package/src/model/gemma4.ts +34 -26
- 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 +67 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3_5.ts +82 -42
- 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/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +83 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +42 -44
- package/src/sampler.ts +36 -25
- package/src/serve/chat-stage.ts +2 -2
- package/src/serve/generation-gateway.ts +39 -30
- package/src/serve/model-host.ts +3 -4
- package/src/serve/request-plan.ts +4 -3
- package/src/serve/request-prep.ts +2 -4
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/server.ts +64 -108
- package/src/spec/assistant-rows.ts +92 -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 +53 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +92 -20
- 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 +219 -0
- package/src/spec/qwen-mtp-source.ts +81 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +70 -68
- package/src/spec/source.ts +80 -15
- package/src/spec/two-model-rows.ts +61 -0
- package/src/spec/two-model.ts +43 -101
- package/src/ssd-cache.ts +33 -21
- package/src/ssd-durability.ts +11 -7
- package/src/web/app.js +57 -57
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
package/src/preflight.ts
CHANGED
|
@@ -32,6 +32,9 @@ export interface MachineState {
|
|
|
32
32
|
/** Foreign processes holding > bigRssMB resident (excludes kernel,
|
|
33
33
|
* WindowServer, and our own bun/python benchmark processes). */
|
|
34
34
|
bigProcesses: { rssMB: number; command: string }[];
|
|
35
|
+
/** Explicitly permitted CPU-only background commands; RSS limits still apply. */
|
|
36
|
+
allowedCpuProcesses?: string[];
|
|
37
|
+
backgroundCpuProcesses?: { rssMB: number; command: string }[];
|
|
35
38
|
ok: boolean;
|
|
36
39
|
problems: string[];
|
|
37
40
|
at: string;
|
|
@@ -41,6 +44,29 @@ export interface PreflightLimits {
|
|
|
41
44
|
maxSwapUsedMB?: number; // default 512
|
|
42
45
|
minFreePercent?: number; // default 35
|
|
43
46
|
bigRssMB?: number; // default 2048
|
|
47
|
+
allowCpuProcesses?: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function foreignProcessFindings(output: string, limits: PreflightLimits = {}) {
|
|
51
|
+
const bigProcesses: { rssMB: number; command: string }[] = [];
|
|
52
|
+
const backgroundCpuProcesses: { rssMB: number; command: string }[] = [];
|
|
53
|
+
for (const line of output.split("\n")) {
|
|
54
|
+
const m = line.match(/^\s*([\d.]+)\s+(\d+)\s+(.*)$/);
|
|
55
|
+
if (!m) continue;
|
|
56
|
+
const cpuPct = Number(m[1]);
|
|
57
|
+
const rssMB = Number(m[2]) / 1024;
|
|
58
|
+
const command = m[3]!;
|
|
59
|
+
if (rssMB < (limits.bigRssMB ?? 2048) && cpuPct < 50) continue;
|
|
60
|
+
if (/(bun|\.venv\/bin\/python|mlx_lm|optiq)/.test(command) ||
|
|
61
|
+
/(kernel_task|WindowServer|launchd|mds|spotlight)/i.test(command)) continue;
|
|
62
|
+
const finding = { rssMB: Math.round(rssMB),
|
|
63
|
+
command: command.slice(0, 80) + (cpuPct >= 50 ? ` (${cpuPct.toFixed(0)}% cpu)` : "") };
|
|
64
|
+
// Exact command match; permission to tolerate CPU activity never permits
|
|
65
|
+
// that process to consume the benchmark's memory headroom.
|
|
66
|
+
const allowed = rssMB < (limits.bigRssMB ?? 2048) && limits.allowCpuProcesses?.includes(command);
|
|
67
|
+
(allowed ? backgroundCpuProcesses : bigProcesses).push(finding);
|
|
68
|
+
}
|
|
69
|
+
return { bigProcesses, backgroundCpuProcesses };
|
|
44
70
|
}
|
|
45
71
|
|
|
46
72
|
function sh(cmd: string[]): string {
|
|
@@ -51,7 +77,6 @@ function sh(cmd: string[]): string {
|
|
|
51
77
|
export function checkMachine(limits: PreflightLimits = {}): MachineState {
|
|
52
78
|
const maxSwap = limits.maxSwapUsedMB ?? 512;
|
|
53
79
|
const minFree = limits.minFreePercent ?? 35;
|
|
54
|
-
const bigRss = limits.bigRssMB ?? 2048;
|
|
55
80
|
const problems: string[] = [];
|
|
56
81
|
|
|
57
82
|
// swap: "vm.swapusage: total = 7168.00M used = 6413.06M free = ..."
|
|
@@ -95,26 +120,15 @@ export function checkMachine(limits: PreflightLimits = {}): MachineState {
|
|
|
95
120
|
// one clean sample doesn't prove absence (the run-spread stability
|
|
96
121
|
// policy in bench-h2h.ts is the direct signal); a hot sample here is
|
|
97
122
|
// still a cheap early warning.
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const BIG_CPU_PCT = 50;
|
|
101
|
-
const bigProcesses: { rssMB: number; command: string }[] = [];
|
|
102
|
-
for (const line of sh(["ps", "axo", "pcpu=,rss=,command="]).split("\n")) {
|
|
103
|
-
const m = line.match(/^\s*([\d.]+)\s+(\d+)\s+(.*)$/);
|
|
104
|
-
if (!m) continue;
|
|
105
|
-
const cpuPct = Number(m[1]);
|
|
106
|
-
const rssMB = Number(m[2]) / 1024;
|
|
107
|
-
const command = m[3]!.slice(0, 80);
|
|
108
|
-
if (rssMB < bigRss && cpuPct < BIG_CPU_PCT) continue;
|
|
109
|
-
if (SELF_PATTERN.test(command) || SYSTEM_PATTERN.test(command)) continue;
|
|
110
|
-
bigProcesses.push({ rssMB: Math.round(rssMB), command: cpuPct >= BIG_CPU_PCT ? `${command} (${cpuPct.toFixed(0)}% cpu)` : command });
|
|
111
|
-
}
|
|
123
|
+
const { bigProcesses, backgroundCpuProcesses } = foreignProcessFindings(
|
|
124
|
+
sh(["ps", "axo", "pcpu=,rss=,command="]), limits);
|
|
112
125
|
for (const p of bigProcesses)
|
|
113
126
|
problems.push(`big process: ${p.rssMB} MB — ${p.command}`);
|
|
114
127
|
|
|
115
128
|
return {
|
|
116
129
|
swapUsedMB, freePercent, cpuSpeedLimit, gpuWiredLimitMB, loadAvg1m,
|
|
117
130
|
bigProcesses,
|
|
131
|
+
allowedCpuProcesses: limits.allowCpuProcesses ?? [], backgroundCpuProcesses,
|
|
118
132
|
ok: problems.length === 0,
|
|
119
133
|
problems,
|
|
120
134
|
at: new Date().toISOString(),
|
|
@@ -136,6 +150,8 @@ export function machineStateJson(s: MachineState): string {
|
|
|
136
150
|
gpu_wired_mb: s.gpuWiredLimitMB,
|
|
137
151
|
load1m: s.loadAvg1m,
|
|
138
152
|
big_procs: s.bigProcesses.length,
|
|
153
|
+
allowed_cpu_processes: s.allowedCpuProcesses ?? [],
|
|
154
|
+
background_cpu_processes: s.backgroundCpuProcesses ?? [],
|
|
139
155
|
ok: s.ok,
|
|
140
156
|
at: s.at,
|
|
141
157
|
});
|
package/src/prompt-cache.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type CheckpointAttachment, materializeCheckpoint, attachmentBytes, cloneAttachments, disposeAttachments } from "./backends/mlx/checkpoint-state";
|
|
2
|
+
import { leaseCacheState, minimumReusableOffset } from "./backends/mlx/state-views";
|
|
2
3
|
import { withResource, cleanupFailure, disposeResources, ownResource } from "./engine/resources";
|
|
3
4
|
// Byte-capped LRU prompt cache — the RAM tier of the Layer-0 KV store.
|
|
4
5
|
//
|
|
@@ -10,12 +11,9 @@ import { withResource, cleanupFailure, disposeResources, ownResource } from "./e
|
|
|
10
11
|
// const hit = cache.take(promptIds); // longest-prefix match
|
|
11
12
|
// generate(model, promptIds, { cache: hit?.caches ?? fresh })
|
|
12
13
|
// cache.put([...promptIds, ...generated], caches, ns, hit?.retain);
|
|
13
|
-
// take()
|
|
14
|
-
// and
|
|
15
|
-
//
|
|
16
|
-
// an exact hit, avoiding an extra resident recurrent snapshot. put() supersedes
|
|
17
|
-
// same-ns prefix-ancestors (when the new entry is trimmable) so a conversation
|
|
18
|
-
// stays one entry, not one per turn.
|
|
14
|
+
// take() lends zero-copy views and leaves every RAM donor in place, including
|
|
15
|
+
// recurrent and method state. SSD persistence runs independently of RAM reuse.
|
|
16
|
+
// put() supersedes same-ns prefix-ancestors when the new entry is trimmable.
|
|
19
17
|
//
|
|
20
18
|
// TIERING (Layer 0, unified-engine plan): when a ColdTier is attached,
|
|
21
19
|
// take() itself runs the two-tier dance — RAM peek vs cold find, restore
|
|
@@ -28,6 +26,7 @@ import { withResource, cleanupFailure, disposeResources, ownResource } from "./e
|
|
|
28
26
|
// snapshot for both lanes.
|
|
29
27
|
|
|
30
28
|
import type { Cache } from "./model/gemma4";
|
|
29
|
+
import type { PrefixCache, PrefixCacheHit } from "./contracts/prefix-cache";
|
|
31
30
|
import { cloneKvCaches } from "./kv-store";
|
|
32
31
|
|
|
33
32
|
/** Reference-counted release: wraps an entry's `retain` (e.g. an mmap
|
|
@@ -68,10 +67,10 @@ export interface ColdTier {
|
|
|
68
67
|
/** Materialize a found entry as GPU-visible caches (zero-copy COW mmap;
|
|
69
68
|
* pages fault in lazily). `retain` must run after the caches are
|
|
70
69
|
* disposed (it unmaps the backing file). Null on any failure. */
|
|
71
|
-
restore(handle: unknown): { tokens: number[]; caches: Cache[]; retain: () => void } | null;
|
|
70
|
+
restore(handle: unknown): { tokens: number[]; caches: Cache[]; attachments?: CheckpointAttachment[]; retain: () => void } | null;
|
|
72
71
|
/** Borrow live caches and finish persistence before returning. A false
|
|
73
|
-
* result prevents a required demotion
|
|
74
|
-
store(tokens: number[], caches: Cache[], ns: string): boolean | void;
|
|
72
|
+
* result prevents a required demotion. */
|
|
73
|
+
store(tokens: number[], caches: Cache[], ns: string, attachments?: CheckpointAttachment[]): boolean | void;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
/** Spill sink for eviction/demotion (write-behind, 2026-07-06).
|
|
@@ -95,9 +94,7 @@ export interface SpillSink {
|
|
|
95
94
|
spillSync?: (entry: PromptCacheEntry) => void;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
export interface PromptCacheEntry {
|
|
99
|
-
tokens: number[];
|
|
100
|
-
caches: Cache[];
|
|
97
|
+
export interface PromptCacheEntry extends PrefixCacheHit<Cache[], CheckpointAttachment[]> {
|
|
101
98
|
/** Namespace key — adapter spec for LoRA requests ("" = base model).
|
|
102
99
|
* KV computed under one adapter must never seed another's prefill. */
|
|
103
100
|
ns: string;
|
|
@@ -134,7 +131,7 @@ interface EntryRecord {
|
|
|
134
131
|
share: { acquire(): () => void };
|
|
135
132
|
}
|
|
136
133
|
|
|
137
|
-
export class PromptCache {
|
|
134
|
+
export class PromptCache implements PrefixCache<Cache[], CheckpointAttachment[]> {
|
|
138
135
|
readonly maxBytes: number;
|
|
139
136
|
#entries: EntryRecord[] = [];
|
|
140
137
|
#clock = 0;
|
|
@@ -161,6 +158,7 @@ export class PromptCache {
|
|
|
161
158
|
spill: SpillSink | ((entry: PromptCacheEntry) => void) | null = null,
|
|
162
159
|
cold: ColdTier | null = null,
|
|
163
160
|
clone: (caches: Cache[]) => Cache[] = cloneKvCaches,
|
|
161
|
+
private readonly materialize: (caches: Cache[], attachments?: CheckpointAttachment[]) => void = materializeCheckpoint,
|
|
164
162
|
) {
|
|
165
163
|
this.maxBytes = maxBytes;
|
|
166
164
|
this.#spillOwned = typeof spill === "function" ? null : spill?.spillOwned ?? null;
|
|
@@ -185,23 +183,26 @@ export class PromptCache {
|
|
|
185
183
|
// no backing mmap for retain to unmap, so nothing the clones
|
|
186
184
|
// could alias goes away.
|
|
187
185
|
let clones: Cache[] | null = null;
|
|
186
|
+
let attachments: CheckpointAttachment[] | undefined;
|
|
188
187
|
try {
|
|
189
188
|
clones = this.#clone(entry.caches);
|
|
190
|
-
|
|
189
|
+
attachments = cloneAttachments(entry.attachments);
|
|
190
|
+
this.#spillOwned({ tokens: [...entry.tokens], caches: clones, ns: entry.ns, attachments });
|
|
191
191
|
} catch {
|
|
192
192
|
// Failure degrades to no-spill; eviction/demotion never unwinds.
|
|
193
193
|
if (clones) for (const c of clones) try { c.dispose(); } catch {}
|
|
194
|
+
try { disposeAttachments(attachments); } catch {}
|
|
194
195
|
}
|
|
195
196
|
} else {
|
|
196
197
|
const spill =
|
|
197
198
|
this.#spillSync ??
|
|
198
199
|
(this.#cold
|
|
199
|
-
? (e: PromptCacheEntry) => this.#cold!.store(e.tokens, e.caches, e.ns)
|
|
200
|
+
? (e: PromptCacheEntry) => this.#cold!.store(e.tokens, e.caches, e.ns, e.attachments)
|
|
200
201
|
: null);
|
|
201
202
|
if (spill) try { spill(entry); } catch {}
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
|
-
disposeResources(entry.caches);
|
|
205
|
+
disposeResources([...entry.caches, { dispose: () => disposeAttachments(entry.attachments) }]);
|
|
205
206
|
entry.retain?.(); // backing release requires successful cache disposal
|
|
206
207
|
}
|
|
207
208
|
|
|
@@ -227,7 +228,7 @@ export class PromptCache {
|
|
|
227
228
|
// Keep the entry resident if persistence throws. A failed SSD write
|
|
228
229
|
// must not be presented as a successful demotion.
|
|
229
230
|
if (this.#cold) {
|
|
230
|
-
if (this.#cold.store(entry.tokens, entry.caches, entry.ns) === false) break;
|
|
231
|
+
if (this.#cold.store(entry.tokens, entry.caches, entry.ns, entry.attachments) === false) break;
|
|
231
232
|
}
|
|
232
233
|
else this.#spillSync?.(entry);
|
|
233
234
|
this.#entries.splice(oldest, 1);
|
|
@@ -239,9 +240,9 @@ export class PromptCache {
|
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
/** Find the entry with the longest usable common prefix of `prompt`
|
|
242
|
-
* across BOTH tiers.
|
|
243
|
-
*
|
|
244
|
-
*
|
|
243
|
+
* across BOTH tiers. Every RAM entry lends zero-copy views and retains its
|
|
244
|
+
* donor, including exact-boundary recurrent state and method attachments.
|
|
245
|
+
* The caller owns the returned views —
|
|
245
246
|
* dispose them or put() an extended entry, honoring `retain` either way.
|
|
246
247
|
*
|
|
247
248
|
* Usable prefix = common prefix capped at prompt.length - 1 (at least
|
|
@@ -262,9 +263,9 @@ export class PromptCache {
|
|
|
262
263
|
const e = this.#entries[i]!.entry;
|
|
263
264
|
if (e.ns !== ns) continue;
|
|
264
265
|
const p = Math.min(commonPrefixLength(e.tokens, prompt), prompt.length - 1);
|
|
265
|
-
if (p <= bestLen) continue;
|
|
266
|
+
if (p <= bestLen || p < minimumReusableOffset(e.caches)) continue;
|
|
266
267
|
const trimNeeded = e.tokens.length - p;
|
|
267
|
-
if (trimNeeded > 0 && !e.caches.every((c) => c.isTrimmable())) continue;
|
|
268
|
+
if (trimNeeded > 0 && (!!e.attachments?.length || !e.caches.every((c) => c.isTrimmable()))) continue;
|
|
268
269
|
bestLen = p;
|
|
269
270
|
bestIdx = i;
|
|
270
271
|
}
|
|
@@ -283,7 +284,8 @@ export class PromptCache {
|
|
|
283
284
|
this.#disposeEntry({ ...entry, ns }, false));
|
|
284
285
|
const result = withResource(owner, (entry) => {
|
|
285
286
|
const trimNeeded = entry.tokens.length - hit.prefixLen;
|
|
286
|
-
if (
|
|
287
|
+
if (hit.prefixLen < minimumReusableOffset(entry.caches)) return null;
|
|
288
|
+
if (trimNeeded > 0 && (!!entry.attachments?.length || !entry.caches.every((cache) => cache.isTrimmable()))) {
|
|
287
289
|
console.warn(
|
|
288
290
|
`[prompt-cache] cold entry unusable: ${entry.tokens.length} tokens, ` +
|
|
289
291
|
`needs trim ${trimNeeded} but untrimmable (wrapped ring/SSM) — re-prefilling`,
|
|
@@ -312,28 +314,22 @@ export class PromptCache {
|
|
|
312
314
|
const rec = this.#entries[bestIdx]!;
|
|
313
315
|
rec.lastUsed = ++this.#clock;
|
|
314
316
|
rec.lastUsedMs = Date.now();
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
// This
|
|
318
|
-
// Save the boundary before consumption: the extended recurrent state
|
|
319
|
-
// cannot be trimmed back, and a delayed writer can no longer find it
|
|
320
|
-
// in RAM after ownership moves. An already durable boundary is cheap.
|
|
321
|
-
if (rec.entry.caches.some((c) => c.signature() === "ssm")) {
|
|
322
|
-
if (this.#cold?.store(rec.entry.tokens, rec.entry.caches, ns) === false)
|
|
323
|
-
throw new Error("prompt-cache: failed to persist recurrent boundary before transfer");
|
|
324
|
-
this.#entries.splice(bestIdx, 1);
|
|
325
|
-
return rec.entry;
|
|
326
|
-
}
|
|
317
|
+
// Lend immutable views for every backend state. RAM retention and SSD
|
|
318
|
+
// durability remain cache responsibilities while the caller extends its
|
|
319
|
+
// own views. This keeps synchronous persistence out of a RAM hit.
|
|
327
320
|
const clones = this.#clone(rec.entry.caches);
|
|
321
|
+
let attachments: CheckpointAttachment[] | undefined;
|
|
328
322
|
const trimNeeded = rec.entry.tokens.length - bestLen;
|
|
329
323
|
try {
|
|
324
|
+
attachments = cloneAttachments(rec.entry.attachments);
|
|
330
325
|
if (trimNeeded > 0) for (const c of clones) c.trim(trimNeeded);
|
|
331
326
|
} catch (error) {
|
|
332
|
-
cleanupFailure(error, () => disposeResources(clones));
|
|
327
|
+
cleanupFailure(error, () => disposeResources([...clones, { dispose: () => disposeAttachments(attachments) }]));
|
|
333
328
|
}
|
|
334
329
|
return {
|
|
335
330
|
tokens: rec.entry.tokens.slice(0, bestLen),
|
|
336
331
|
caches: clones,
|
|
332
|
+
attachments,
|
|
337
333
|
ns,
|
|
338
334
|
retain: rec.share.acquire(),
|
|
339
335
|
};
|
|
@@ -346,9 +342,9 @@ export class PromptCache {
|
|
|
346
342
|
for (const { entry: e } of this.#entries) {
|
|
347
343
|
if (e.ns !== ns) continue;
|
|
348
344
|
const p = Math.min(commonPrefixLength(e.tokens, prompt), prompt.length - 1);
|
|
349
|
-
if (p <= best) continue;
|
|
345
|
+
if (p <= best || p < minimumReusableOffset(e.caches)) continue;
|
|
350
346
|
const trimNeeded = e.tokens.length - p; // same usability rule as take()
|
|
351
|
-
if (trimNeeded > 0 && !e.caches.every((c) => c.isTrimmable())) continue;
|
|
347
|
+
if (trimNeeded > 0 && (!!e.attachments?.length || !e.caches.every((c) => c.isTrimmable()))) continue;
|
|
352
348
|
best = p;
|
|
353
349
|
}
|
|
354
350
|
return best;
|
|
@@ -368,10 +364,12 @@ export class PromptCache {
|
|
|
368
364
|
/** Insert (or reinsert) an entry; evicts LRU entries over the byte cap
|
|
369
365
|
* (spilling each to the cold tier first, when one is attached). If the
|
|
370
366
|
* entry itself exceeds the cap it is spilled + disposed, not stored. */
|
|
371
|
-
put(tokens: number[], caches: Cache[], ns = "", retain?: () => void): void {
|
|
367
|
+
put(tokens: number[], caches: Cache[], ns = "", retain?: () => void, attachments?: CheckpointAttachment[]): void {
|
|
372
368
|
// All cache-provided validation runs before ownership is adopted.
|
|
373
|
-
const bytes = cacheBytes(caches);
|
|
374
|
-
const trimmable = caches.every((cache) => cache.isTrimmable());
|
|
369
|
+
const bytes = cacheBytes(caches) + attachmentBytes(attachments);
|
|
370
|
+
const trimmable = !attachments?.length && caches.every((cache) => cache.isTrimmable());
|
|
371
|
+
// Storage takes resolved snapshots, independently of their producer.
|
|
372
|
+
this.materialize(caches, attachments);
|
|
375
373
|
const discard = (entry: PromptCacheEntry, spill: boolean) => {
|
|
376
374
|
try { this.#disposeEntry(entry, spill); }
|
|
377
375
|
catch (error) {
|
|
@@ -380,7 +378,7 @@ export class PromptCache {
|
|
|
380
378
|
console.warn(`[prompt-cache] entry cleanup failed: ${error}`);
|
|
381
379
|
}
|
|
382
380
|
};
|
|
383
|
-
const entry: PromptCacheEntry = { tokens, caches, ns, retain };
|
|
381
|
+
const entry: PromptCacheEntry = { tokens, caches, ns, retain, attachments };
|
|
384
382
|
if (bytes > this.maxBytes) {
|
|
385
383
|
discard(entry, true);
|
|
386
384
|
return;
|
|
@@ -415,7 +413,7 @@ export class PromptCache {
|
|
|
415
413
|
if (trimmable) {
|
|
416
414
|
for (const old of [...this.#entries]) {
|
|
417
415
|
if (old === rec || old.entry.ns !== ns) continue;
|
|
418
|
-
if (old.entry.tokens.length >= tokens.length) continue;
|
|
416
|
+
if (old.entry.tokens.length >= tokens.length || old.entry.tokens.length < minimumReusableOffset(caches)) continue;
|
|
419
417
|
if (commonPrefixLength(old.entry.tokens, tokens) !== old.entry.tokens.length) continue;
|
|
420
418
|
this.#entries = this.#entries.filter((r) => r !== old);
|
|
421
419
|
discard(old.entry, false);
|
package/src/sampler.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { Dtype } from "./mlx/ffi";
|
|
12
12
|
import { MlxArray } from "./mlx/array";
|
|
13
13
|
import * as ops from "./mlx/ops";
|
|
14
|
+
import type { TokenLogprobs } from "./contracts/generation";
|
|
15
|
+
import type { SampledToken, SamplingSession } from "./contracts/sampling";
|
|
14
16
|
import { applyCurve, type CurveParams } from "./lab/curve/curve-sampler";
|
|
15
17
|
|
|
16
18
|
/** Resolved HLG sampling config (the user-facing knobs). The mid gain is NOT
|
|
@@ -120,10 +122,7 @@ export interface StepExtras {
|
|
|
120
122
|
topVals: MlxArray | null;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
export
|
|
124
|
-
token: T;
|
|
125
|
-
extras: StepExtras | null;
|
|
126
|
-
}
|
|
125
|
+
export type StepSample<T> = SampledToken<T, StepExtras>;
|
|
127
126
|
|
|
128
127
|
interface StepGrammar {
|
|
129
128
|
readonly isTerminated: boolean;
|
|
@@ -161,25 +160,8 @@ export interface NumberStepSamplerConfig extends StepSamplerConfigBase {
|
|
|
161
160
|
grammarWait: "before-sample";
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
export
|
|
165
|
-
|
|
166
|
-
readonly needsHistory: boolean;
|
|
167
|
-
sample(logits: MlxArray, step: number): StepSample<MlxArray>;
|
|
168
|
-
seedHistory(tokens: readonly number[]): void;
|
|
169
|
-
commitDevice(token: MlxArray): void;
|
|
170
|
-
commitNumbers(tokens: readonly number[]): void;
|
|
171
|
-
dispose(): void;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export interface NumberStepSampler {
|
|
175
|
-
readonly isPlainGreedy: boolean;
|
|
176
|
-
readonly needsHistory: boolean;
|
|
177
|
-
sample(logits: MlxArray, step: number): Promise<StepSample<number>>;
|
|
178
|
-
seedHistory(tokens: readonly number[]): void;
|
|
179
|
-
commitDevice(token: MlxArray): void;
|
|
180
|
-
commitNumbers(tokens: readonly number[]): void;
|
|
181
|
-
dispose(): void;
|
|
182
|
-
}
|
|
163
|
+
export type DeviceStepSampler = SamplingSession<MlxArray, MlxArray, StepSample<MlxArray>>;
|
|
164
|
+
export type NumberStepSampler = SamplingSession<MlxArray, MlxArray, Promise<StepSample<number>>>;
|
|
183
165
|
|
|
184
166
|
const GOLDEN = 0x9e3779b97f4a7c15n;
|
|
185
167
|
|
|
@@ -786,7 +768,7 @@ export function makeLogitsProcessors(opts: LogitsProcessorOptions = {}): LogitsP
|
|
|
786
768
|
return out;
|
|
787
769
|
}
|
|
788
770
|
|
|
789
|
-
/** Whether
|
|
771
|
+
/** Whether scores can use stateless greedy sampling across independent rows. */
|
|
790
772
|
export function isPlainGreedy(
|
|
791
773
|
opts: StepSamplerOptions,
|
|
792
774
|
hasProcessors = makeLogitsProcessors(opts).length > 0,
|
|
@@ -809,6 +791,23 @@ export function disposeStepExtras(extras: StepExtras | null): void {
|
|
|
809
791
|
extras.topVals?.dispose();
|
|
810
792
|
}
|
|
811
793
|
|
|
794
|
+
/** Read captured probabilities with their token, without adding casts behind
|
|
795
|
+
* the next decode step. Consumes the capture on success and failure. */
|
|
796
|
+
export function readStepExtras(extras: StepExtras | null): TokenLogprobs | undefined {
|
|
797
|
+
if (!extras) return undefined;
|
|
798
|
+
try {
|
|
799
|
+
const out: TokenLogprobs = {};
|
|
800
|
+
if (extras.sel) out.logprob = extras.sel.toFloat32Host()[0]!;
|
|
801
|
+
if (extras.topIdx && extras.topVals) {
|
|
802
|
+
const ids = extras.topIdx.toIntTokens();
|
|
803
|
+
const vals = extras.topVals.toFloat32Host();
|
|
804
|
+
out.top = Array.from(ids, (id, i) => ({ id, logprob: vals[i]! }))
|
|
805
|
+
.sort((a, b) => b.logprob - a.logprob);
|
|
806
|
+
}
|
|
807
|
+
return out;
|
|
808
|
+
} finally { disposeStepExtras(extras); }
|
|
809
|
+
}
|
|
810
|
+
|
|
812
811
|
export function makeStepSampler(
|
|
813
812
|
options: StepSamplerOptions,
|
|
814
813
|
config: DeviceStepSamplerConfig,
|
|
@@ -941,7 +940,10 @@ export function makeStepSampler(
|
|
|
941
940
|
if (config.initialHistory) seedHistory(config.initialHistory);
|
|
942
941
|
|
|
943
942
|
const common = {
|
|
943
|
+
independent: isPlainGreedy(options, processors.length > 0) && !capture && !config.sampler
|
|
944
|
+
? independentGreedySampling : undefined,
|
|
944
945
|
isPlainGreedy: isPlainGreedy(options, processors.length > 0),
|
|
946
|
+
capturesLogprobs: capture,
|
|
945
947
|
needsHistory: processors.length > 0,
|
|
946
948
|
seedHistory,
|
|
947
949
|
commitDevice,
|
|
@@ -989,7 +991,16 @@ export function makeStepSampler(
|
|
|
989
991
|
};
|
|
990
992
|
}
|
|
991
993
|
|
|
992
|
-
/**
|
|
994
|
+
/** Stateless sampling across rows or verification positions. Preserve the
|
|
995
|
+
* normalized-score argmax, including rounding-created ties. */
|
|
996
|
+
export const independentGreedySampling = Object.freeze({
|
|
997
|
+
sample(logits: MlxArray): MlxArray {
|
|
998
|
+
using logprobs = toLogprobs(logits);
|
|
999
|
+
return ops.argmaxAxis(logprobs, -1);
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
/** logits [..., V] → logprobs [..., V] (logits - logsumexp). */
|
|
993
1004
|
export function toLogprobs(logits: MlxArray): MlxArray {
|
|
994
1005
|
const lse = ops.logsumexpAxis(logits, -1, true);
|
|
995
1006
|
const out = ops.sub(logits, lse);
|
package/src/serve/chat-stage.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class ChatStage {
|
|
|
25
25
|
private readonly prep: RequestPrep,
|
|
26
26
|
private readonly promptCache: Pick<PromptCache, "peekPrefixLen">,
|
|
27
27
|
/** Admission ceiling (fit() / the GLM memory plan), resolved once. */
|
|
28
|
-
private readonly
|
|
28
|
+
private readonly contextLimit: number | null,
|
|
29
29
|
/** `serve --adapter <dir>` startup default; a request's explicit
|
|
30
30
|
* `adapter` (incl. "none") wins. */
|
|
31
31
|
private readonly defaultAdapter?: string,
|
|
@@ -165,7 +165,7 @@ export class ChatStage {
|
|
|
165
165
|
promptIds,
|
|
166
166
|
options,
|
|
167
167
|
requestedMaxTokens,
|
|
168
|
-
|
|
168
|
+
contextLimit: this.contextLimit,
|
|
169
169
|
stream: request.stream,
|
|
170
170
|
wantLogprobs,
|
|
171
171
|
topLogprobs,
|
|
@@ -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";
|
|
@@ -114,7 +114,7 @@ export type Vision = {
|
|
|
114
114
|
};
|
|
115
115
|
|
|
116
116
|
/** Per-token sink: returning `false` halts this generation (stop sequence).
|
|
117
|
-
* `logprobs` is
|
|
117
|
+
* `logprobs` is populated on either lane when the request asked for
|
|
118
118
|
* logprobs capture (GenerateOptions.logprobs / topLogprobs). */
|
|
119
119
|
export type OnToken = (
|
|
120
120
|
token: number,
|
|
@@ -172,7 +172,6 @@ export class GenerationGateway {
|
|
|
172
172
|
private readonly serialRun: SerialRun,
|
|
173
173
|
private readonly opts: {
|
|
174
174
|
kvBudgetBytes?: number;
|
|
175
|
-
memoryBudget?: MlxMemoryBudget;
|
|
176
175
|
checkpoints?: boolean;
|
|
177
176
|
stateCodecs?: import("../kv-store").CacheCodecProvider;
|
|
178
177
|
/** The server-wide KV scheme (server.ts kvScheme) — threaded to the
|
|
@@ -185,9 +184,7 @@ export class GenerationGateway {
|
|
|
185
184
|
* back on finish. Safe to share with the serial lane — both use it
|
|
186
185
|
* only inside this gateway's mutual-exclusion domain. */
|
|
187
186
|
promptCache?: RowPromptCache;
|
|
188
|
-
|
|
189
|
-
* need that same lock, so this hook must never run inside it. */
|
|
190
|
-
beforeSerial?: () => Promise<void>;
|
|
187
|
+
adapterNamespace?: (adapters: string[]) => string;
|
|
191
188
|
} = {},
|
|
192
189
|
) {
|
|
193
190
|
this.#binding = "createBatchGroup" in model ? model : bindMlxGateway(model);
|
|
@@ -235,16 +232,20 @@ export class GenerationGateway {
|
|
|
235
232
|
await this.onIdle();
|
|
236
233
|
}
|
|
237
234
|
|
|
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). */
|
|
235
|
+
/** Observe idleness. Call runWhenIdle for background work that must also
|
|
236
|
+
* acquire ownership atomically with the idle check. */
|
|
244
237
|
async onIdle(pollMs = 20): Promise<void> {
|
|
245
238
|
while (this.busy) await new Promise<void>((r) => setTimeout(r, pollMs));
|
|
246
239
|
}
|
|
247
240
|
|
|
241
|
+
/** Background work waits without requesting a batch drain. The final busy
|
|
242
|
+
* check and mutex acquisition have no await between them, so a foreground
|
|
243
|
+
* request cannot enter between the check and ownership registration. */
|
|
244
|
+
async runWhenIdle<T>(fn: () => Promise<T>, pollMs = 20): Promise<T> {
|
|
245
|
+
while (this.busy) await new Promise<void>(resolve => setTimeout(resolve, pollMs));
|
|
246
|
+
return this.runExclusive(fn);
|
|
247
|
+
}
|
|
248
|
+
|
|
248
249
|
/** Cumulative rows routed to the batch lane since server start. The serial
|
|
249
250
|
* lane never advances this — /stats' race-free lane-routing observable
|
|
250
251
|
* (active/pending are instantaneous and read 0 once a request finishes). */
|
|
@@ -272,7 +273,7 @@ export class GenerationGateway {
|
|
|
272
273
|
const frozenShape = Object.freeze(shape);
|
|
273
274
|
const execution = this.#binding.plan(frozenShape, options, {
|
|
274
275
|
continuous: this.batchingEnabled,
|
|
275
|
-
quantizedBatch: shape.kvQuant && this.#kvBatchable(),
|
|
276
|
+
quantizedBatch: (shape.kvQuant || shape.turboQuant) && this.#kvBatchable(),
|
|
276
277
|
checkpoints: this.opts.checkpoints === true,
|
|
277
278
|
});
|
|
278
279
|
return Object.freeze({ shape: frozenShape, mechanism: execution.mechanism, execution });
|
|
@@ -390,7 +391,7 @@ export class GenerationGateway {
|
|
|
390
391
|
});
|
|
391
392
|
};
|
|
392
393
|
let started = false;
|
|
393
|
-
return
|
|
394
|
+
return this.runExclusive(async () => {
|
|
394
395
|
signal?.throwIfAborted();
|
|
395
396
|
started = true;
|
|
396
397
|
const stats = await this.serialRun(
|
|
@@ -398,7 +399,7 @@ export class GenerationGateway {
|
|
|
398
399
|
);
|
|
399
400
|
signal?.throwIfAborted();
|
|
400
401
|
return stats;
|
|
401
|
-
}, trace, signal)
|
|
402
|
+
}, trace, signal)
|
|
402
403
|
// Covers a serial waiter aborted before serialRun takes ownership.
|
|
403
404
|
// generate() also disposes defensively; the operation is idempotent.
|
|
404
405
|
.finally(() => {
|
|
@@ -407,16 +408,23 @@ export class GenerationGateway {
|
|
|
407
408
|
});
|
|
408
409
|
}
|
|
409
410
|
|
|
411
|
+
const adapters = options.adapters?.length ? [...options.adapters] : undefined;
|
|
412
|
+
const adapterKey = adapters ? JSON.stringify(adapters) : "";
|
|
413
|
+
const cacheNamespace = adapters
|
|
414
|
+
? () => this.opts.adapterNamespace?.(adapters) ?? adapterKey : "";
|
|
415
|
+
const context = adapters
|
|
416
|
+
? this.#binding.bindAdapterContext?.(adapters, `adapters:${adapterKey}`) : undefined;
|
|
417
|
+
|
|
410
418
|
// The scheduler owns grammar ready/accept sequencing. StepSampler owns the
|
|
411
419
|
// shared processors -> mask -> logprobs -> sample -> history contract.
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
initialHistory: promptIds,
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
+
const method = this.#binding.methodRequest?.(placement.execution, options);
|
|
421
|
+
const continuation = method ? undefined : this.#binding.continuationRequest?.(placement.execution, options, promptIds, onToken);
|
|
422
|
+
const sampling = method ? undefined : continuation ?? createRowSampling(makeStepSampler(options, {
|
|
423
|
+
tokenRepresentation: "device", grammarWait: "external",
|
|
424
|
+
historyUpdate: "after-sample", initialHistory: promptIds,
|
|
425
|
+
captureSelectedLogprob: options.logprobs === true,
|
|
426
|
+
captureTopLogprobs: options.topLogprobs,
|
|
427
|
+
}), onToken);
|
|
420
428
|
|
|
421
429
|
let st;
|
|
422
430
|
this.#rowsSubmitted++;
|
|
@@ -425,13 +433,14 @@ export class GenerationGateway {
|
|
|
425
433
|
});
|
|
426
434
|
try {
|
|
427
435
|
st = await this.#ensureScheduler().submit({
|
|
428
|
-
promptIds,
|
|
436
|
+
promptIds, context, cacheNamespace, prefillChunkSize: options.prefillChunkSize,
|
|
437
|
+
continuation: continuation?.continuation,
|
|
438
|
+
statePolicy: this.#binding.statePolicy?.(placement.execution, options, promptIds.length + (options.maxTokens ?? 512)),
|
|
429
439
|
compiledDecode: placement.execution?.compiledDecode,
|
|
430
440
|
maxTokens: options.maxTokens ?? 512,
|
|
431
441
|
eosTokenIds: options.eosTokenIds ?? this.#binding.config.eosTokenIds,
|
|
432
|
-
sample,
|
|
433
|
-
|
|
434
|
-
onToken,
|
|
442
|
+
...(method ? { method, onToken } : { sample: sampling!.sample,
|
|
443
|
+
plainGreedy: sampling!.plainGreedy, onToken: sampling!.onToken }),
|
|
435
444
|
onAdmitted: closeAdmission,
|
|
436
445
|
trace,
|
|
437
446
|
...(signal ? { signal } : {}),
|
|
@@ -445,7 +454,7 @@ export class GenerationGateway {
|
|
|
445
454
|
});
|
|
446
455
|
} finally {
|
|
447
456
|
closeAdmission?.();
|
|
448
|
-
|
|
457
|
+
sampling?.dispose();
|
|
449
458
|
// The scheduler never owns per-row grammar state.
|
|
450
459
|
options.grammar?.dispose();
|
|
451
460
|
}
|
|
@@ -460,6 +469,7 @@ export class GenerationGateway {
|
|
|
460
469
|
prefillTps: st.prefillMs > 0 ? ((st.promptTokens - st.cachedTokens) / st.prefillMs) * 1000 : 0,
|
|
461
470
|
decodeTps: st.decodeMs > 0 && st.generatedTokens > 1 ? ((st.generatedTokens - 1) / st.decodeMs) * 1000 : 0,
|
|
462
471
|
cacheTokens: [],
|
|
472
|
+
...(st.spec ? { spec: st.spec } : {}),
|
|
463
473
|
};
|
|
464
474
|
}
|
|
465
475
|
|
|
@@ -470,7 +480,6 @@ export class GenerationGateway {
|
|
|
470
480
|
maxBatch: this.#batch,
|
|
471
481
|
stateCodecs: this.opts.stateCodecs,
|
|
472
482
|
kvBudgetBytes: this.opts.kvBudgetBytes,
|
|
473
|
-
memoryBudget: this.opts.memoryBudget,
|
|
474
483
|
// Phase 3.1: the batchable kvConfig composition is applied by the
|
|
475
484
|
// scheduler (solo rows convert at serial chunk boundaries, then
|
|
476
485
|
// merge as quantized triples). Only threaded when the scheme
|
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;
|
|
@@ -33,7 +33,7 @@ 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;
|
|
@@ -114,11 +114,12 @@ export type PlanRequestResult = RequestPlan | RequestRejection;
|
|
|
114
114
|
* options, adapter selection, and lane shape are derived here once for chat
|
|
115
115
|
* and raw completions. */
|
|
116
116
|
export function planRequest(input: RequestPlanInput): PlanRequestResult {
|
|
117
|
-
const available = input.
|
|
117
|
+
const available = input.contextLimit === null ? Infinity
|
|
118
|
+
: input.contextLimit - input.promptIds.length;
|
|
118
119
|
if (available < 1) {
|
|
119
120
|
return new RequestRejection(
|
|
120
121
|
`prompt is ${input.promptIds.length} tokens but the memory budget caps ` +
|
|
121
|
-
`safe context at ${input.
|
|
122
|
+
`safe context at ${input.contextLimit} — no room to generate; ` +
|
|
122
123
|
"shorten the prompt or raise --memory-budget",
|
|
123
124
|
input.ownership,
|
|
124
125
|
);
|