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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Glm52Cache } from "../../model/glm52-cache";
|
|
2
|
+
import { SpeculativeRotatingKVCache } from "../../model/speculative-rotating-kv";
|
|
3
|
+
import { RotatingAffineLayout, SpeculativeRotatingAffineLayout } from "../../model/rotating-kv-layout";
|
|
4
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
5
|
+
import { KVCache, QuantizedKVCache, TurboQuantKVCache, RotatingKVCache, RotatingQuantizedKVCache } from "../../model/gemma4-base";
|
|
6
|
+
import { DelayedRotatingQuantizedKVCache } from "../../model/delayed-rotating-quantized-kv";
|
|
7
|
+
import { DelayedQuantizedKVCache } from "../../model/delayed-quantized-kv";
|
|
8
|
+
import { DelayedTurboQuantKVCache } from "../../model/delayed-turboquant-kv";
|
|
9
|
+
import { SSMCache } from "../../model/qwen3-delta";
|
|
10
|
+
import { BatchedSSMCache } from "../../model/batched-ssm";
|
|
11
|
+
import { BatchedKVCache } from "../../model/batched-kv";
|
|
12
|
+
import { BatchedQuantizedKVCache } from "../../model/batched-quantized-kv";
|
|
13
|
+
import { BatchedTurboQuantKVCache } from "../../model/batched-turboquant-kv";
|
|
14
|
+
|
|
15
|
+
export type TargetRowLayout = Glm52Cache | DelayedRotatingQuantizedKVCache | RotatingAffineLayout | SpeculativeRotatingKVCache | BatchedKVCache | BatchedQuantizedKVCache | BatchedTurboQuantKVCache |
|
|
16
|
+
BatchedSSMCache | DelayedTurboQuantKVCache | DelayedQuantizedKVCache;
|
|
17
|
+
|
|
18
|
+
/** The storage binding advertises a row transaction factory. The gateway can
|
|
19
|
+
* compose supported model graphs without identifying their model family. */
|
|
20
|
+
export function targetRowLayoutFactory(cache: Cache): (() => TargetRowLayout) | undefined {
|
|
21
|
+
if (cache instanceof Glm52Cache) return () => cache.makeEmptyBatch();
|
|
22
|
+
if (cache instanceof DelayedRotatingQuantizedKVCache)
|
|
23
|
+
return () => new DelayedRotatingQuantizedKVCache(cache.maxSize, cache.groupSize, cache.bits, cache.start, cache.maintain, undefined, true);
|
|
24
|
+
if (cache instanceof RotatingAffineLayout || cache instanceof RotatingQuantizedKVCache)
|
|
25
|
+
return () => new SpeculativeRotatingAffineLayout(cache.maxSize, cache.groupSize, cache.bits);
|
|
26
|
+
if (cache instanceof RotatingKVCache || cache instanceof SpeculativeRotatingKVCache) return () => new SpeculativeRotatingKVCache(cache.maxSize);
|
|
27
|
+
if (cache instanceof DelayedTurboQuantKVCache || cache instanceof DelayedQuantizedKVCache ||
|
|
28
|
+
cache instanceof BatchedTurboQuantKVCache || cache instanceof BatchedKVCache ||
|
|
29
|
+
cache instanceof BatchedQuantizedKVCache || cache instanceof BatchedSSMCache) return () => cache.makeEmptyBatch();
|
|
30
|
+
if (cache instanceof TurboQuantKVCache) return () => new BatchedTurboQuantKVCache(cache.kBits, cache.vBits);
|
|
31
|
+
if (cache instanceof SSMCache) return () => new BatchedSSMCache();
|
|
32
|
+
if (cache instanceof QuantizedKVCache) return () => new BatchedQuantizedKVCache(cache.groupSize, cache.bits);
|
|
33
|
+
if (cache instanceof KVCache) return () => new BatchedKVCache();
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { MlxArray } from "../../mlx/array";
|
|
2
|
+
import * as ops from "../../mlx/ops";
|
|
3
|
+
import type { BatchableCache, Cache, Mask } from "../../model/gemma4-base";
|
|
4
|
+
import { KvTensorRows } from "./kv-tensor-rows";
|
|
5
|
+
import { leaseCacheStates, minimumReusableOffset } from "./state-views";
|
|
6
|
+
import { withResource, disposeResources } from "../../engine/resources";
|
|
7
|
+
import { cloneKvCaches } from "../../kv-store";
|
|
8
|
+
|
|
9
|
+
/** Encoded layouts retain physical row alignment across precision changes. */
|
|
10
|
+
export interface TransitionedKvLayout extends BatchableCache {
|
|
11
|
+
alignRows?(leftPad: readonly number[]): void;
|
|
12
|
+
}
|
|
13
|
+
export interface SpeculativeTransitionedKvLayout extends TransitionedKvLayout {
|
|
14
|
+
specRoundBegin(): void;
|
|
15
|
+
specRoundCommit(): void;
|
|
16
|
+
specRoundRollback(keep: number | readonly number[]): void;
|
|
17
|
+
}
|
|
18
|
+
export interface KvRowTransition<Layout extends TransitionedKvLayout> {
|
|
19
|
+
readonly signature: string;
|
|
20
|
+
maintain(rows: Cache[]): void;
|
|
21
|
+
converted(row: Cache): boolean;
|
|
22
|
+
makeLayout(): Layout;
|
|
23
|
+
packRows?(layout: Layout, rows: readonly Cache[]): void;
|
|
24
|
+
prepareRows?(rows: Cache[]): Cache[];
|
|
25
|
+
extractRow?(row: Cache): Cache;
|
|
26
|
+
rollbackRow?(row: Cache, before: number, keep: number, preserveColumns: boolean): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export interface TransitioningKvPositions {
|
|
31
|
+
readonly rowOffsets: readonly number[];
|
|
32
|
+
readonly leftPad: readonly number[];
|
|
33
|
+
readonly offset: number;
|
|
34
|
+
readonly batchSize: number | null;
|
|
35
|
+
readonly ropeOffsetArr: MlxArray | undefined;
|
|
36
|
+
sync(rows: readonly Cache[], preserve: boolean): void;
|
|
37
|
+
filterRows(keep: readonly number[]): void;
|
|
38
|
+
makeMask(tokens: number, window: number | null): Mask;
|
|
39
|
+
dispose(): void;
|
|
40
|
+
}
|
|
41
|
+
export class FullKvPositions extends KvTensorRows implements TransitioningKvPositions {
|
|
42
|
+
sync(rows: readonly Cache[], preserve: boolean): void {
|
|
43
|
+
this.mergeRows(preserve
|
|
44
|
+
? [{ planes: [], rowOffsets: rows.map(row => row.offset), leftPad: [...this.leftPad] }]
|
|
45
|
+
: rows.map(row => ({ planes: [], rowOffsets: [row.offset], leftPad: [0] })));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Owns membership, committed positions and precision transitions. Codec and
|
|
50
|
+
* attention adapters own tensor access; scheduling and persistence use the
|
|
51
|
+
* existing batch and checkpoint contracts. Row conversion never examines an
|
|
52
|
+
* uncommitted speculative suffix. */
|
|
53
|
+
export abstract class TransitioningKvRows<Layout extends TransitionedKvLayout> implements BatchableCache {
|
|
54
|
+
protected readonly positions: TransitioningKvPositions;
|
|
55
|
+
protected rows: Cache[] = [];
|
|
56
|
+
protected packed?: Layout;
|
|
57
|
+
#prefilling = false;
|
|
58
|
+
|
|
59
|
+
readonly stateNeedsDispose = true;
|
|
60
|
+
constructor(readonly transition: KvRowTransition<Layout>, row?: Cache, positions: TransitioningKvPositions = new FullKvPositions()) {
|
|
61
|
+
this.positions = positions;
|
|
62
|
+
if (row) { this.rows = transition.prepareRows?.([row]) ?? [row]; this.syncPositions(); }
|
|
63
|
+
}
|
|
64
|
+
signature(): string { return this.transition.signature; }
|
|
65
|
+
get prefillMaintenance() { return this; }
|
|
66
|
+
beginPrefill(): void { this.#prefilling = true; }
|
|
67
|
+
commitPrefill(indices: readonly number[]): void {
|
|
68
|
+
if (this.packed) return;
|
|
69
|
+
for (const index of indices) {
|
|
70
|
+
const row = [this.rows[index]!];
|
|
71
|
+
try { this.transition.maintain(row); }
|
|
72
|
+
finally { this.rows[index] = row[0]!; }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
endPrefill(): void { this.#prefilling = false; }
|
|
76
|
+
get minimumReusableOffset(): number { return this.packed?.minimumReusableOffset ?? minimumReusableOffset(this.rows); }
|
|
77
|
+
get rowOffsets(): readonly number[] { return this.packed?.rowOffsets ?? this.positions.rowOffsets; }
|
|
78
|
+
get leftPad(): readonly number[] { return this.packed?.leftPad ?? this.positions.leftPad; }
|
|
79
|
+
get offset(): number { return this.packed?.offset ?? this.positions.offset; }
|
|
80
|
+
get batchSize(): number | null { return this.packed?.batchSize ?? this.positions.batchSize; }
|
|
81
|
+
get ropeOffsetArr(): MlxArray | undefined { return this.packed ? this.packed.ropeOffsetArr : this.positions.ropeOffsetArr; }
|
|
82
|
+
bytesPerToken(): number { return this.packed?.bytesPerToken?.() ?? Math.max(0, ...this.rows.map(row => row.bytesPerToken?.() ?? 0)); }
|
|
83
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
84
|
+
makeMask(tokens: number, window: number | null): Mask { return (this.packed ?? this.positions).makeMask(tokens, window); }
|
|
85
|
+
isTrimmable(): boolean { return this.packed?.isTrimmable() ?? this.rows.every(row => row.isTrimmable()); }
|
|
86
|
+
trim(count: number): void {
|
|
87
|
+
if (this.packed) this.packed.trim(count);
|
|
88
|
+
else { for (const row of this.rows) row.trim(count); this.syncPositions(true); }
|
|
89
|
+
}
|
|
90
|
+
protected syncPositions(preserve = false): void { this.positions.sync(this.rows, preserve); }
|
|
91
|
+
protected advance(): void {
|
|
92
|
+
if (this.packed) return;
|
|
93
|
+
if (!this.#prefilling) this.transition.maintain(this.rows);
|
|
94
|
+
if (this.rows.length && this.rows.every(row => this.transition.converted(row))) {
|
|
95
|
+
const packed = this.transition.makeLayout();
|
|
96
|
+
try {
|
|
97
|
+
if (this.transition.packRows) this.transition.packRows(packed, this.rows);
|
|
98
|
+
else packed.mergeRows(this.rows);
|
|
99
|
+
packed.alignRows?.(this.positions.leftPad);
|
|
100
|
+
}
|
|
101
|
+
catch (error) { packed.dispose(); throw error; }
|
|
102
|
+
const previous = this.rows; this.rows = []; this.packed = packed;
|
|
103
|
+
this.positions.dispose(); disposeResources(previous);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
abstract updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray];
|
|
107
|
+
abstract makeEmptyBatch(): TransitioningKvRows<Layout>;
|
|
108
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
109
|
+
const owned: Cache[] = [];
|
|
110
|
+
try {
|
|
111
|
+
for (const row of rows) {
|
|
112
|
+
if (row instanceof TransitioningKvRows) {
|
|
113
|
+
for (let index = 0; index < row.batchSize!; index++) owned.push(row.extractRow(index));
|
|
114
|
+
} else owned.push(...cloneKvCaches([row]));
|
|
115
|
+
}
|
|
116
|
+
} catch (error) { disposeResources(owned); throw error; }
|
|
117
|
+
let prepared: Cache[];
|
|
118
|
+
try { prepared = this.transition.prepareRows?.(owned) ?? owned; }
|
|
119
|
+
catch (error) { disposeResources(owned); throw error; }
|
|
120
|
+
this.dispose(); this.rows = prepared; this.syncPositions();
|
|
121
|
+
}
|
|
122
|
+
filterRows(keep: readonly number[]): void {
|
|
123
|
+
if (this.packed) { this.packed.filterRows(keep); return; }
|
|
124
|
+
const previous = this.rows; this.rows = keep.map(index => previous[index]!);
|
|
125
|
+
disposeResources(previous.filter((_, index) => !keep.includes(index))); this.positions.filterRows(keep);
|
|
126
|
+
}
|
|
127
|
+
extractRow(row: number): Cache { return this.packed?.extractRow(row) ?? (this.transition.extractRow?.(this.rows[row]!) ?? cloneKvCaches([this.rows[row]!])[0]!); }
|
|
128
|
+
state(): MlxArray[] {
|
|
129
|
+
return withResource(leaseCacheStates(this.packed ? [this.packed] : this.rows), arrays => {
|
|
130
|
+
const views: MlxArray[] = [];
|
|
131
|
+
try { for (const array of arrays) views.push(ops.contiguous(array)); return views; }
|
|
132
|
+
catch (error) { disposeResources(views); throw error; }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
dispose(): void {
|
|
136
|
+
this.#prefilling = false;
|
|
137
|
+
const previous = this.rows; this.rows = [];
|
|
138
|
+
this.packed?.dispose(); this.packed = undefined; this.positions.dispose(); disposeResources(previous);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Rollback is a method capability, separate from ordinary cache membership. */
|
|
143
|
+
export abstract class SpeculativeTransitioningKvRows<Layout extends SpeculativeTransitionedKvLayout> extends TransitioningKvRows<Layout> {
|
|
144
|
+
#beforeRound?: number[];
|
|
145
|
+
specRoundBegin(): void {
|
|
146
|
+
this.advance(); this.#beforeRound = [...this.rowOffsets]; this.packed?.specRoundBegin();
|
|
147
|
+
}
|
|
148
|
+
specRoundCommit(): void {
|
|
149
|
+
this.packed?.specRoundCommit(); this.#beforeRound = undefined; this.advance();
|
|
150
|
+
}
|
|
151
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
152
|
+
if (this.packed) this.packed.specRoundRollback(keep);
|
|
153
|
+
else {
|
|
154
|
+
for (const [index, row] of this.rows.entries()) {
|
|
155
|
+
const count = typeof keep === "number" ? keep : keep[index]!;
|
|
156
|
+
const before = this.#beforeRound![index]!;
|
|
157
|
+
if (this.transition.rollbackRow) this.transition.rollbackRow(row, before, count, this.rows.length > 1);
|
|
158
|
+
else row.trim(row.offset - before - count);
|
|
159
|
+
}
|
|
160
|
+
this.syncPositions(true);
|
|
161
|
+
}
|
|
162
|
+
this.#beforeRound = undefined; this.advance();
|
|
163
|
+
}
|
|
164
|
+
override dispose(): void { this.#beforeRound = undefined; super.dispose(); }
|
|
165
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -120,10 +120,11 @@ const SERVER_FLAGS = `Server options:
|
|
|
120
120
|
--ssd-cache-verify Verify tensor hashes on every restore (reads all
|
|
121
121
|
bytes eagerly — integrity paranoia only)
|
|
122
122
|
--generation-checkpoint <tokens>
|
|
123
|
-
|
|
123
|
+
Queue an in-flight generation snapshot to the SSD
|
|
124
124
|
cache every N emitted tokens. Repeating the same
|
|
125
125
|
request after restart replays and resumes it.
|
|
126
|
-
Requires --ssd-cache
|
|
126
|
+
Requires --ssd-cache; supports ordinary requests
|
|
127
|
+
with shared cache layouts.
|
|
127
128
|
--isolate Run the inference engine as a CHILD process and
|
|
128
129
|
keep this process a pure UI/API proxy — the UI
|
|
129
130
|
stays instant under GPU load and survives engine
|
|
@@ -169,8 +170,8 @@ Model & quality:
|
|
|
169
170
|
auto-detected — a full same-tokenizer model
|
|
170
171
|
(mlx_lm.server parity), a Gemma "-assistant"
|
|
171
172
|
KV-borrowing drafter, or a DSpark checkpoint.
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
Supported draft providers use shared batched
|
|
174
|
+
execution; ordinary requests can batch too.
|
|
174
175
|
--draft-kind <kind> Override draft-artifact detection:
|
|
175
176
|
two-model | assistant | dspark | deepspec | mtp | ngram
|
|
176
177
|
(deepspec = DeepSeek's released Gemma4DSparkModel
|
|
@@ -205,9 +206,8 @@ Model & quality:
|
|
|
205
206
|
only; sliding-window layers stay bf16)]
|
|
206
207
|
--paged-kv OPTIONAL vLLM-style paged KV storage (block pool +
|
|
207
208
|
gather before the stock SDPA; env MLX_BUN_PAGED_KV=1).
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
--kv-quant, --draft-model; bypasses the prompt
|
|
209
|
+
Gemma4-family, bf16; supports --batch N.
|
|
210
|
+
Refuses --kv-quant and --draft-model; bypasses the prompt
|
|
211
211
|
cache; runs uncompiled decode. Bit-exact with the
|
|
212
212
|
plain path; expect a small decode cost at batch=1
|
|
213
213
|
(the gather copy). docs/design/kv-cache.md
|
|
@@ -1012,16 +1012,11 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
1012
1012
|
if (route.kvQuant !== undefined) serverOptions.kvQuant = route.kvQuant;
|
|
1013
1013
|
if (route.turboQuant !== undefined) serverOptions.turboQuant = route.turboQuant;
|
|
1014
1014
|
// --paged-kv (env: MLX_BUN_PAGED_KV=1): OPTIONAL vLLM-style block-pool KV
|
|
1015
|
-
// storage, default off (docs/design/kv-cache.md).
|
|
1016
|
-
//
|
|
1017
|
-
// --batch N>1 is refused loudly by createServer rather than downgraded.
|
|
1015
|
+
// storage, default off (docs/design/kv-cache.md). The state policy owns
|
|
1016
|
+
// block pools and cache reuse; paging preserves the selected batch size.
|
|
1018
1017
|
if (flag("paged-kv") || runtimeValue("MLX_BUN_PAGED_KV") === "1") {
|
|
1019
1018
|
const bsRaw = opt("paged-kv-block-size");
|
|
1020
1019
|
serverOptions.pagedKv = bsRaw !== null ? { blockSize: Number(bsRaw) } : {};
|
|
1021
|
-
if (batchRaw === null) {
|
|
1022
|
-
serverOptions.batch = 1;
|
|
1023
|
-
console.log("[paged-kv] serial-only in v1 — pinning --batch 1");
|
|
1024
|
-
}
|
|
1025
1020
|
}
|
|
1026
1021
|
// Bind loopback unless asked otherwise (mlx_lm.server parity); --host
|
|
1027
1022
|
// 0.0.0.0 is the explicit opt-in for LAN exposure. The chat-UI open and
|
|
@@ -1646,25 +1641,6 @@ switch (cmd) {
|
|
|
1646
1641
|
const config = await loadModelConfig(m.path);
|
|
1647
1642
|
const report = fit(config, m.sizeBytes, 8192, undefined, undefined, m.expertsBytes);
|
|
1648
1643
|
sFit.done(`${style.bold(m.repoId)} ${style.dim(`· ${gb(m.sizeBytes)} · ~${report.predictedDecodeTps.toFixed(0)} tok/s predicted`)}${picked ? style.dim(" · auto-picked (override: mlx-bun serve <query>)") : ""}`);
|
|
1649
|
-
// Near-ceiling advisory (admission doctrine: advise, never refuse). A
|
|
1650
|
-
// model near the DEFAULT Metal wired ceiling (~75% of RAM) dies with an
|
|
1651
|
-
// UNCATCHABLE async-GPU-OOM on long prefills (M4 24 GB × 17 GB model,
|
|
1652
|
-
// 2026-08-20) — surface the standard remedy up front instead.
|
|
1653
|
-
{
|
|
1654
|
-
const { maxRecommendedWorkingSetSize } = await import("./mlx/ffi");
|
|
1655
|
-
const ceiling = maxRecommendedWorkingSetSize();
|
|
1656
|
-
const sysctlRaw = Bun.spawnSync(["sysctl", "-n", "iogpu.wired_limit_mb"]).stdout.toString().trim();
|
|
1657
|
-
const limitIsDefault = sysctlRaw === "0" || sysctlRaw === "";
|
|
1658
|
-
if (limitIsDefault && ceiling > 0 && m.sizeBytes > 0.8 * ceiling) {
|
|
1659
|
-
const ramGb = Number(Bun.spawnSync(["sysctl", "-n", "hw.memsize"]).stdout.toString().trim()) / 2 ** 30;
|
|
1660
|
-
const suggestMb = Math.floor((ramGb - 2.5) * 1024);
|
|
1661
|
-
console.error(style.dim(
|
|
1662
|
-
` ! ${gb(m.sizeBytes)} model vs ~${gb(ceiling)} default GPU ceiling — long prompts can\n` +
|
|
1663
|
-
` hit an uncatchable Metal OOM. If that happens, raise the limit first:\n` +
|
|
1664
|
-
` sudo sysctl iogpu.wired_limit_mb=${suggestMb} (resets on reboot)`,
|
|
1665
|
-
));
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
1644
|
const sNative = step("native runtime");
|
|
1669
1645
|
await ensureNative(sNative);
|
|
1670
1646
|
sNative.done("native runtime ready");
|
|
@@ -1684,9 +1660,8 @@ switch (cmd) {
|
|
|
1684
1660
|
}
|
|
1685
1661
|
// Speculative decoding (mlx_lm.server parity): --draft-model <path|query>
|
|
1686
1662
|
// resolves like the main model; --num-draft-tokens defaults to 3 (the
|
|
1687
|
-
// server's default upstream — mlx_lm.generate's is 2).
|
|
1688
|
-
//
|
|
1689
|
-
// is_batchable = draft is None).
|
|
1663
|
+
// server's default upstream — mlx_lm.generate's is 2). Qualified providers
|
|
1664
|
+
// use shared execution; ordinary requests retain their own placement.
|
|
1690
1665
|
const draftQuery = opt("draft-model");
|
|
1691
1666
|
let draftModelDir: string | undefined;
|
|
1692
1667
|
if (draftQuery !== null) {
|
|
@@ -17,9 +17,20 @@ export interface ExecutionCapabilities {
|
|
|
17
17
|
readonly continuous: boolean;
|
|
18
18
|
readonly quantizedBatch: boolean;
|
|
19
19
|
readonly grammarBatch: boolean;
|
|
20
|
+
readonly adapterBatch?: boolean;
|
|
21
|
+
readonly pagedBatch?: boolean;
|
|
20
22
|
readonly checkpoints: boolean;
|
|
23
|
+
/** Bound ordinary driver can restore/capture this request configuration. */
|
|
24
|
+
readonly sharedCheckpoints?: boolean;
|
|
21
25
|
/** Model-qualified speculative execution can retain this request's KV codec. */
|
|
22
26
|
readonly speculativeKvQuant?: boolean;
|
|
27
|
+
readonly speculativeTurboQuant?: boolean;
|
|
28
|
+
readonly turboQuantBatch?: boolean;
|
|
29
|
+
readonly speculativeLogprobs?: boolean;
|
|
30
|
+
/** The grouped provider supports the target's mounted adapter context. */
|
|
31
|
+
readonly sharedSpeculativeAdapters?: boolean;
|
|
32
|
+
/** Methods supplied by the model's shared execution binding. */
|
|
33
|
+
readonly groupedMethods?: readonly string[];
|
|
23
34
|
/** A graph-owned compiled step exists; cache geometry can still decline it. */
|
|
24
35
|
readonly compiledDecode?: boolean;
|
|
25
36
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A reusable prefix and the backend/method state that covers its tokens.
|
|
2
|
+
* The caller owns a returned hit. After releasing or returning its state,
|
|
3
|
+
* it must release any retained backing through retain. */
|
|
4
|
+
export interface PrefixCacheHit<State, Attachment = never> {
|
|
5
|
+
tokens: number[];
|
|
6
|
+
caches: State;
|
|
7
|
+
attachments?: Attachment;
|
|
8
|
+
retain?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Storage is independent of request scheduling and the method producing
|
|
12
|
+
* state. Both prefill and decode can publish processed-token prefixes.
|
|
13
|
+
* State is opaque here; its backend/method defines layout and alignment.
|
|
14
|
+
* Implementations own prefix selection, retention and storage tiers. */
|
|
15
|
+
export interface PrefixCache<State, Attachment = never> {
|
|
16
|
+
/** Return owned state for the longest usable prefix, or null on a miss.
|
|
17
|
+
* Namespace distinguishes incompatible artifacts, methods and adapters. */
|
|
18
|
+
take(prompt: number[], ns?: string): PrefixCacheHit<State, Attachment> | null;
|
|
19
|
+
/** Transfer state and its optional backing release to storage. If this
|
|
20
|
+
* throws, ownership stays with the caller; successful return transfers
|
|
21
|
+
* ownership even when storage immediately spills or disposes the entry. */
|
|
22
|
+
put(tokens: number[], caches: State, ns?: string, retain?: () => void, attachments?: Attachment): void;
|
|
23
|
+
}
|
|
@@ -16,3 +16,10 @@ export interface StateView<T> {
|
|
|
16
16
|
export interface DisposableResource {
|
|
17
17
|
dispose(): void;
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
/** A prepared replacement for live state. Commit once between method rounds;
|
|
21
|
+
* it publishes already-built state without graph work or validation. Dispose
|
|
22
|
+
* releases the unpublished replacement, or the superseded state after commit. */
|
|
23
|
+
export interface PreparedStateChange extends DisposableResource {
|
|
24
|
+
commit(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Sampling owns score processing, token selection, and request-local history.
|
|
2
|
+
* Score/token representations and sync/device execution belong to the backend;
|
|
3
|
+
* this contract has no scheduling, cache, or tensor-runtime dependency. */
|
|
4
|
+
export interface SampledToken<Token, Metadata> {
|
|
5
|
+
token: Token;
|
|
6
|
+
extras: Metadata | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SamplingSession<Scores, DeviceToken, Result> {
|
|
10
|
+
/** Optional stateless operation shared across compatible rows and positions.
|
|
11
|
+
* It borrows scores and returns owned device tokens, without advancing history. */
|
|
12
|
+
readonly independent?: { sample(scores: Scores): DeviceToken };
|
|
13
|
+
readonly isPlainGreedy: boolean;
|
|
14
|
+
readonly capturesLogprobs: boolean;
|
|
15
|
+
readonly needsHistory: boolean;
|
|
16
|
+
/** Borrows scores; transfers ownership of the result to the caller. */
|
|
17
|
+
sample(logits: Scores, step: number): Result;
|
|
18
|
+
seedHistory(tokens: readonly number[]): void;
|
|
19
|
+
/** Commit accepted tokens when configured for manual history updates. */
|
|
20
|
+
commitDevice(token: DeviceToken): void;
|
|
21
|
+
commitNumbers(tokens: readonly number[]): void;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
/** Backend-owned context shared by compatible requests. A key identifies
|
|
2
|
+
* equivalent execution state; enter returns its scoped release. Scheduling
|
|
3
|
+
* observes group readiness without inspecting this context. */
|
|
4
|
+
export interface ExecutionContext {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
enter(): () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
/** A backend execution group exposes bounded work units and readiness. Queue
|
|
2
10
|
* payloads, tensors, cache merging and sampling remain inside its adapter. */
|
|
3
11
|
export interface ExecutionGroup {
|
|
4
12
|
readonly active: number;
|
|
5
13
|
readonly queued: number;
|
|
6
14
|
readonly preparing: boolean;
|
|
15
|
+
readonly preparingRows?: number;
|
|
16
|
+
/** The preparation owner can accept another row before its next work unit. */
|
|
17
|
+
readonly canPrepareMore?: boolean;
|
|
18
|
+
/** Work estimates affect grouping only. A request larger than this budget
|
|
19
|
+
* still runs alone; it is never rejected or sent to another executor. */
|
|
20
|
+
readonly preparingTokens?: number;
|
|
21
|
+
readonly nextPreparationTokens?: number;
|
|
22
|
+
readonly maxPreparationTokens?: number;
|
|
23
|
+
/** The latest preparation published output that can be flushed by yielding. */
|
|
24
|
+
readonly preparationPublishedOutput?: boolean;
|
|
7
25
|
readonly maxActive: number;
|
|
8
26
|
readonly admissionHeld: boolean;
|
|
9
27
|
readonly closed: boolean;
|
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
import type { ExecutionCapabilities, ExecutionFeatures, ExecutionRequirements, ResolvedExecution } from "../contracts/execution";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* placement even when this request must use the ordinary AR method. */
|
|
3
|
+
/** Select the request method, then place it using executor capabilities. */
|
|
5
4
|
export function resolveExecution(
|
|
6
5
|
request: ExecutionRequirements, capabilities: ExecutionCapabilities,
|
|
7
6
|
features: ExecutionFeatures = { pagedKv: false, fill: false },
|
|
8
7
|
): ResolvedExecution {
|
|
9
8
|
const reasons: string[] = [];
|
|
10
|
-
const
|
|
9
|
+
const sharedRequestExclusions = [
|
|
11
10
|
[!capabilities.continuous, "continuous-unavailable"],
|
|
12
|
-
[capabilities.method !== "autoregressive", "method-requires-serial"],
|
|
13
11
|
[request.hasVision, "media-requires-serial"],
|
|
14
|
-
[request.hasAdapters, "adapters-require-serial"],
|
|
15
|
-
[request.wantsLogprobs, "logprobs-require-serial"],
|
|
16
|
-
[request.userSeed, "explicit-seed-requires-serial"],
|
|
12
|
+
[request.hasAdapters && !capabilities.adapterBatch, "adapters-require-serial"],
|
|
17
13
|
[request.kvQuant && !capabilities.quantizedBatch, "kv-scheme-requires-serial"],
|
|
18
|
-
[request.turboQuant, "turbo-kv-requires-serial"],
|
|
19
|
-
[request.hasDraft, "configured-draft-requires-serial"],
|
|
14
|
+
[request.turboQuant && !capabilities.turboQuantBatch, "turbo-kv-requires-serial"],
|
|
20
15
|
[request.hasGrammar && !capabilities.grammarBatch, "grammar-batching-disabled"],
|
|
21
|
-
[features.pagedKv, "paged-kv-requires-serial"],
|
|
16
|
+
[features.pagedKv && !capabilities.pagedBatch, "paged-kv-requires-serial"],
|
|
22
17
|
] as const;
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
const sharedAdapterMethod = capabilities.sharedSpeculativeAdapters === true &&
|
|
19
|
+
!sharedRequestExclusions.some(([excluded]) => excluded) &&
|
|
20
|
+
capabilities.groupedMethods?.includes("speculative") === true;
|
|
25
21
|
const speculative = capabilities.method === "autoregressive" && request.hasDraft &&
|
|
26
|
-
!request.hasVision && !request.hasAdapters && !request.wantsLogprobs &&
|
|
22
|
+
!request.hasVision && (!request.hasAdapters || sharedAdapterMethod) && (!request.wantsLogprobs || capabilities.speculativeLogprobs === true) &&
|
|
27
23
|
(!request.kvQuant || capabilities.speculativeKvQuant === true) &&
|
|
28
|
-
!request.turboQuant && !features.pagedKv;
|
|
29
|
-
if (request.hasDraft && !speculative) reasons.push("draft-incompatible-with-request");
|
|
24
|
+
(!request.turboQuant || capabilities.speculativeTurboQuant === true) && !features.pagedKv;
|
|
30
25
|
const method = speculative ? "speculative" : capabilities.method;
|
|
26
|
+
const continuousExclusions = [sharedRequestExclusions[0],
|
|
27
|
+
[!(capabilities.groupedMethods ?? ["autoregressive"]).includes(method), "method-requires-serial"],
|
|
28
|
+
...sharedRequestExclusions.slice(1),
|
|
29
|
+
] as const;
|
|
30
|
+
for (const [excluded, reason] of continuousExclusions) if (excluded) reasons.push(reason);
|
|
31
|
+
const mechanism = reasons.length ? "serial" : "continuous";
|
|
32
|
+
if (request.hasDraft && !speculative) reasons.push("draft-incompatible-with-request");
|
|
31
33
|
const pagedKv = features.pagedKv && !request.hasVision && !request.hasAdapters;
|
|
32
34
|
if (features.pagedKv && !pagedKv) reasons.push("paged-kv-bypassed-for-media-or-adapters");
|
|
33
35
|
const promptCache = method !== "speculative" && !request.hasVision && !pagedKv;
|
|
@@ -36,7 +38,7 @@ export function resolveExecution(
|
|
|
36
38
|
!request.wantsLogprobs && !request.kvQuant && !request.turboQuant;
|
|
37
39
|
if (features.fill && !fill) reasons.push("fill-incompatible-with-request");
|
|
38
40
|
const checkpoint = capabilities.checkpoints && method === "autoregressive" &&
|
|
39
|
-
mechanism === "serial" && promptCache && !request.hasGrammar &&
|
|
41
|
+
(mechanism === "serial" || capabilities.sharedCheckpoints === true) && promptCache && !request.hasGrammar &&
|
|
40
42
|
!features.fill && !request.wantsLogprobs;
|
|
41
43
|
const compiledDecode = features.compiledDecode === true && capabilities.compiledDecode === true &&
|
|
42
44
|
method === "autoregressive" && !request.hasAdapters && !pagedKv;
|
package/src/engine/resources.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DisposableResource, ResourceOwner } from "../contracts/resources";
|
|
1
|
+
import type { DisposableResource, PreparedStateChange, ResourceOwner } from "../contracts/resources";
|
|
2
2
|
|
|
3
3
|
export function ownResource<T>(value: T, release: (value: T) => void): ResourceOwner<T> {
|
|
4
4
|
let held: { value: T } | undefined = { value };
|
|
@@ -35,6 +35,17 @@ export function cleanupFailure(error: unknown, cleanup: () => void): never {
|
|
|
35
35
|
throw error;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Complete every preparation before publishing any owner's replacement.
|
|
39
|
+
* Old state is released only after all owners have committed. */
|
|
40
|
+
export function applyStateChanges(prepare: readonly (() => PreparedStateChange)[]): void {
|
|
41
|
+
const changes: PreparedStateChange[] = [];
|
|
42
|
+
try {
|
|
43
|
+
for (const build of prepare) changes.push(build());
|
|
44
|
+
for (const change of changes) change.commit();
|
|
45
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(changes.splice(0))); }
|
|
46
|
+
disposeResources(changes);
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
export function withResource<T, R>(owner: Pick<ResourceOwner<T>, "borrow" | "close">, use: (value: T) => R): R {
|
|
39
50
|
try { return use(owner.borrow()); }
|
|
40
51
|
catch (error) { cleanupFailure(error, () => owner.close()); }
|
package/src/engine/scheduler.ts
CHANGED
|
@@ -30,15 +30,20 @@ export async function driveExecutionGroup(
|
|
|
30
30
|
|
|
31
31
|
if (!group.preparing && !held && group.queued && group.active < group.maxActive)
|
|
32
32
|
group.admitNext();
|
|
33
|
+
while (group.preparing && group.canPrepareMore && !held && group.queued &&
|
|
34
|
+
group.active + (group.preparingRows ?? 1) < group.maxActive && group.canBurst() &&
|
|
35
|
+
(group.preparingTokens ?? 0) + (group.nextPreparationTokens ?? 0) <= (group.maxPreparationTokens ?? Infinity)) {
|
|
36
|
+
if (!group.admitNext()) break;
|
|
37
|
+
}
|
|
33
38
|
if (group.preparing) {
|
|
34
39
|
const activeBefore = group.active;
|
|
35
40
|
await group.advancePreparation();
|
|
36
|
-
// A
|
|
37
|
-
// Flush
|
|
38
|
-
// group
|
|
41
|
+
// A bounded preparation may publish output before its state work ends.
|
|
42
|
+
// Flush it before the next work unit; queued short admissions still
|
|
43
|
+
// form a group across yields. Re-enter policy to observe new work,
|
|
39
44
|
// cancellation, admission holds and shutdown.
|
|
40
|
-
if (
|
|
41
|
-
group.active === 1 && !group.queued) {
|
|
45
|
+
if (group.preparationPublishedOutput ||
|
|
46
|
+
(yieldAfterPreparation && !activeBefore && !group.preparing && group.active === 1 && !group.queued)) {
|
|
42
47
|
lastYield = clock.now();
|
|
43
48
|
await clock.yield();
|
|
44
49
|
continue;
|
package/src/fit.ts
CHANGED
|
@@ -13,12 +13,12 @@ import type { ModelConfig } from "./config";
|
|
|
13
13
|
import type { MemoryPlan } from "./admission-plan";
|
|
14
14
|
import {
|
|
15
15
|
kvBytesAt,
|
|
16
|
-
kvGeometry,
|
|
17
16
|
kvQuantBytesPerElement,
|
|
17
|
+
sdpaFallbackBytes,
|
|
18
18
|
type KvSchemeOptions,
|
|
19
19
|
} from "./kv-scheme";
|
|
20
20
|
|
|
21
|
-
export { kvBytesAt, kvQuantBytesPerElement } from "./kv-scheme";
|
|
21
|
+
export { kvBytesAt, kvQuantBytesPerElement, sdpaFallbackBytes } from "./kv-scheme";
|
|
22
22
|
export type FitKvScheme = KvSchemeOptions;
|
|
23
23
|
|
|
24
24
|
/** Decode-efficiency vs theoretical bandwidth ceiling, measured on the
|
|
@@ -161,23 +161,37 @@ export function fit(
|
|
|
161
161
|
kvScheme?: FitKvScheme,
|
|
162
162
|
): FitReport {
|
|
163
163
|
const usable = usableBytes ?? machine.ramBytes * WIRED_FRACTION;
|
|
164
|
-
|
|
164
|
+
// Prefill transient = the calibrated per-chunk-token constant + the
|
|
165
|
+
// SDPA-fallback scores tensor (heads × min(chunk, ctx) × ctx × 2 B) for
|
|
166
|
+
// attention layers whose head dim MLX cannot fuse (kv-scheme.ts). The
|
|
167
|
+
// second term is what grows with context on Qwen3.x (head_dim 256) and
|
|
168
|
+
// Gemma 4 (256/512) and was unbilled before 2026-09-08.
|
|
169
|
+
const transientAt = (n: number) =>
|
|
170
|
+
Math.min(chunk, n) * TRANSIENT_PER_TOKEN + sdpaFallbackBytes(config, chunk, n);
|
|
171
|
+
const transient = transientAt(ctx);
|
|
165
172
|
const kv = kvBytesAt(config, ctx, kvScheme);
|
|
166
173
|
const total = weightsBytes + kv + transient;
|
|
167
174
|
|
|
168
|
-
// solve max context: weights + kv(
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
const
|
|
175
|
+
// solve max context: weights + kv(n) + transient(n) ≤ usable. The same
|
|
176
|
+
// formula `fits` uses, so the solved ceiling always fits. It is monotone
|
|
177
|
+
// in n (KV linear, sliding saturating at the window, scores quadratic
|
|
178
|
+
// below the chunk then linear), so bisect over [0, maxPositionEmbeddings].
|
|
179
|
+
const totalAt = (n: number) => weightsBytes + kvBytesAt(config, n, kvScheme) + transientAt(n);
|
|
173
180
|
let maxCtx = 0;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
:
|
|
180
|
-
|
|
181
|
+
{
|
|
182
|
+
let lo = 0;
|
|
183
|
+
let hi = Math.max(0, Math.floor(config.text.maxPositionEmbeddings));
|
|
184
|
+
if (totalAt(hi) <= usable) lo = hi;
|
|
185
|
+
else {
|
|
186
|
+
// invariant: totalAt(lo) ≤ usable (lo = 0 may itself not fit → 0)
|
|
187
|
+
if (totalAt(0) > usable) hi = 0;
|
|
188
|
+
while (hi - lo > 1) {
|
|
189
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
190
|
+
if (totalAt(mid) <= usable) lo = mid;
|
|
191
|
+
else hi = mid;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
maxCtx = lo;
|
|
181
195
|
}
|
|
182
196
|
|
|
183
197
|
// decode reads all weights + the KV cache once per token — except MoE
|