mlx-bun 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -32
- package/docs/README.md +55 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +220 -7
- package/docs/design/decode-speed-program.md +121 -66
- package/docs/design/generic-model-support.md +116 -0
- package/docs/design/kv-cache.md +380 -211
- package/docs/design/speculative-decoding.md +165 -26
- package/docs/design/turboquant.md +20 -7
- package/docs/design/unified-engine-frontier-plan.md +120 -13
- package/docs/planning/PRODUCT_ROADMAP.md +5 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3463 -3489
- package/docs/reference/cli.md +62 -1
- package/docs/reference/distribution.md +29 -4
- package/docs/reference/environment.md +104 -8
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +17 -5
- package/docs/reference/server-api.md +181 -11
- package/docs/reference/server-config.md +232 -105
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +111 -27
- package/src/backends/mlx/cache-layout.ts +3 -3
- package/src/backends/mlx/continuation-persistence.ts +5 -4
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/gateway-binding.ts +56 -12
- package/src/backends/mlx/kv-maintenance.ts +30 -2
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +4 -3
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +15 -4
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +57 -13
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +18 -5
- package/src/backends/mlx/rollback.ts +6 -3
- package/src/backends/mlx/serial-executor.ts +16 -7
- package/src/backends/mlx/speculative-group.ts +105 -26
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/target-layout-capability.ts +1 -1
- package/src/backends/mlx/transitioning-kv-rows.ts +8 -0
- package/src/cli.ts +322 -0
- package/src/contracts/execution.ts +10 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +13 -3
- package/src/contracts/scheduling.ts +9 -0
- package/src/engine/execution-plan.ts +23 -12
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/scheduler.ts +22 -2
- package/src/fill/fill-session.ts +14 -3
- package/src/generate.ts +28 -22
- package/src/grammar.ts +13 -0
- package/src/inference/prefill.ts +6 -0
- package/src/kv-scheme.ts +22 -0
- package/src/kv-store.ts +115 -79
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +33 -5
- package/src/lab/paged-kv/paged-kv.ts +108 -42
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +37 -0
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/batched-turboquant-kv.ts +6 -5
- package/src/model/delayed-quantized-kv.ts +3 -3
- package/src/model/delayed-rotating-quantized-kv.ts +1 -1
- package/src/model/delayed-turboquant-kv.ts +6 -4
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-row.ts +1 -1
- package/src/model/gemma4-base.ts +25 -6
- package/src/model/gemma4.ts +82 -2
- package/src/model/kv-attention-view.ts +4 -1
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +99 -37
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +6 -0
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/prompt-cache.ts +265 -43
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +5 -4
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +17 -12
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +52 -11
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +2 -0
- package/src/serve/request-prep.ts +38 -21
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +132 -164
- package/src/spec/assistant-rows.ts +1 -0
- package/src/spec/deepspec-rows.ts +1 -1
- package/src/spec/dflash-source.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +1 -0
- package/src/spec/ngram-source.ts +27 -5
- package/src/spec/projected-draft-rows.ts +2 -2
- package/src/spec/qwen-mtp-rows.ts +11 -0
- package/src/spec/qwen-mtp-source.ts +3 -0
- package/src/spec/serve-loop.ts +5 -3
- package/src/spec/source.ts +14 -1
- package/src/spec/two-model-rows.ts +1 -0
- package/src/spec/two-model.ts +2 -2
- package/src/ssd-cache.ts +76 -17
- package/src/ssd-durability.ts +20 -22
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +160 -0
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- /package/docs/{planning → archive/planning}/release-notes-v0.4.0.md +0 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import type { GenerateOptions } from "../../generate";
|
|
2
|
+
import type { RuntimeModel } from "../../model/factory";
|
|
3
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
4
|
+
import type { MlxArray } from "../../mlx/array";
|
|
5
|
+
import * as ops from "../../mlx/ops";
|
|
6
|
+
import { clearCache } from "../../mlx/ffi";
|
|
7
|
+
import { makeStepSampler, type DeviceStepSampler, type NumberStepSampler } from "../../sampler";
|
|
8
|
+
import type { FillSession, Proposal } from "../../fill/fill-session";
|
|
9
|
+
import { disposeResources, cleanupFailure, withResource } from "../../engine/resources";
|
|
10
|
+
import { snapshotGenerationPolicy } from "./request-policy";
|
|
11
|
+
import { MlxPrefillCohort } from "./prefill-cohort";
|
|
12
|
+
import { MlxStateRows } from "./state-rows";
|
|
13
|
+
import { targetCacheLayout } from "./cache-layout";
|
|
14
|
+
import { leaseCacheStates } from "./state-views";
|
|
15
|
+
import { createKvMaintenance, type KvMaintenance } from "./kv-maintenance";
|
|
16
|
+
import { bindLegacyAutoregressiveModel, supportsCommittedAppendCache, type MlxAutoregressiveBinding, type MlxTokenAppend } from "./autoregressive";
|
|
17
|
+
import { appendHiddenRows } from "./fill-append";
|
|
18
|
+
import { advanceSpeculativeOutputs } from "./speculative-round";
|
|
19
|
+
import { bindRowCacheRollback } from "./rollback";
|
|
20
|
+
import { bindSpeculativeTargetModel, type MlxSpeculativeTargetBinding } from "./speculative";
|
|
21
|
+
import type { MlxForwardWork } from "./mixed-iteration";
|
|
22
|
+
import type { MlxGroupedMethod, MlxGroupMethodHost, MlxGroupMethodRequest,
|
|
23
|
+
MlxGroupPreparation, Row } from "./batch-group";
|
|
24
|
+
|
|
25
|
+
interface RequestState {
|
|
26
|
+
sampling: DeviceStepSampler;
|
|
27
|
+
fill: FillSession;
|
|
28
|
+
proposal?: { value: Proposal; emitted: number };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Strict continuations share preparation, row layouts and queueing with other
|
|
32
|
+
* methods. Sampled tokens remain pipelined; committed tokens bypass sampling.
|
|
33
|
+
* This binding is qualified independently before serving advertises it. */
|
|
34
|
+
export function bindFillGroupRequests(model: RuntimeModel) {
|
|
35
|
+
return (input: GenerateOptions): MlxGroupMethodRequest => {
|
|
36
|
+
const options = snapshotGenerationPolicy(input);
|
|
37
|
+
return {
|
|
38
|
+
key: JSON.stringify(["fill", options.kvBits ?? null, options.kvGroupSize ?? 64,
|
|
39
|
+
options.quantizedKvStart ?? null, options.turboQuant ?? null, options.kvConfig ?? null]),
|
|
40
|
+
data: options,
|
|
41
|
+
open: host => new FillGroup(host, model, options),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class FillGroup implements MlxGroupedMethod {
|
|
47
|
+
readonly #binding: MlxAutoregressiveBinding;
|
|
48
|
+
readonly #maintain: KvMaintenance;
|
|
49
|
+
readonly #append: MlxTokenAppend | null | undefined;
|
|
50
|
+
readonly #verify: MlxSpeculativeTargetBinding;
|
|
51
|
+
readonly #requests = new Map<Row, RequestState>();
|
|
52
|
+
#target: MlxStateRows<ReturnType<typeof targetCacheLayout>> | null = null;
|
|
53
|
+
#pending: MlxArray | null = null;
|
|
54
|
+
#known: boolean[] = [];
|
|
55
|
+
#real: boolean[] = [];
|
|
56
|
+
#steps = 0;
|
|
57
|
+
|
|
58
|
+
constructor(readonly host: MlxGroupMethodHost, readonly model: RuntimeModel, options: GenerateOptions) {
|
|
59
|
+
this.#binding = bindLegacyAutoregressiveModel(model);
|
|
60
|
+
this.#verify = bindSpeculativeTargetModel(model);
|
|
61
|
+
this.#maintain = createKvMaintenance(options);
|
|
62
|
+
const append = this.#binding.createAppend?.({ hasAdapters: !!this.#binding.adapters?.active.length, pagedKv: false });
|
|
63
|
+
this.#append = supportsCommittedAppendCache(append, options) ? append : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get runningTokens(): number { return this.host.rows.length * Math.max(1, this.#burstLength(), this.#verifyLength()); }
|
|
67
|
+
|
|
68
|
+
#verifyLength(): number {
|
|
69
|
+
if (!this.#pending || !this.host.rows.length ||
|
|
70
|
+
this.host.rows.some(row => this.#requests.get(row)!.proposal?.value.policy === "assert")) return 0;
|
|
71
|
+
return Math.max(...this.host.rows.map(row => {
|
|
72
|
+
const proposal = this.#requests.get(row)!.proposal;
|
|
73
|
+
return proposal ? proposal.value.ids.length - proposal.emitted : 0;
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#burstLength(): number {
|
|
78
|
+
const rows = this.host.rows;
|
|
79
|
+
if (!rows.length || !this.#pending || this.#known.some(known => !known) ||
|
|
80
|
+
(this.#append?.maxChunkSize(this.#target!.caches, rows.length) ?? 1) <= 1) return 0;
|
|
81
|
+
return Math.min(...rows.map(row => {
|
|
82
|
+
const proposal = this.#requests.get(row)!.proposal;
|
|
83
|
+
return proposal?.value.policy === "assert" ? proposal.value.ids.length - proposal.emitted : 0;
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
prepare(first: Row): MlxGroupPreparation {
|
|
88
|
+
const ready: { row: Row; caches: Cache[]; retain?: () => void }[] = [];
|
|
89
|
+
const target = new MlxPrefillCohort({
|
|
90
|
+
model: this.model, chunkSize: this.host.prefillChunkSize,
|
|
91
|
+
tailSplit: this.host.runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true),
|
|
92
|
+
promptCache: this.host.promptCache, maintain: this.#maintain,
|
|
93
|
+
forward: async (ids, caches) => this.#binding.graph.forwardHidden(ids, caches),
|
|
94
|
+
project: hidden => this.#binding.graph.projectLogits(hidden, { type: "all" }),
|
|
95
|
+
complete: async (state, logits) => {
|
|
96
|
+
const row = state.row;
|
|
97
|
+
const request = this.#requests.get(row)!;
|
|
98
|
+
using scores = ops.reshape(logits, [1, logits.shape.at(-1)!]);
|
|
99
|
+
using token = request.sampling.sample(scores, 0).token;
|
|
100
|
+
row.generated = row.sampled = 1;
|
|
101
|
+
try {
|
|
102
|
+
const reason = await this.#emit(row, token.toIntTokens()[0]!, false);
|
|
103
|
+
this.#maintain(state.solo);
|
|
104
|
+
if (reason) {
|
|
105
|
+
this.#put(row, state.solo, state.retain);
|
|
106
|
+
state.solo = []; state.retain = undefined;
|
|
107
|
+
this.#closeRequest(row); this.host.finish(row, reason);
|
|
108
|
+
} else {
|
|
109
|
+
ready.push({ row, caches: state.solo, retain: state.retain });
|
|
110
|
+
}
|
|
111
|
+
} catch (error) { this.#closeRequest(row); throw error; }
|
|
112
|
+
},
|
|
113
|
+
reject: (row, error) => { this.#closeRequest(row); row.reject(error); },
|
|
114
|
+
});
|
|
115
|
+
const admit = (row: Row) => {
|
|
116
|
+
const options = row.req.method!.data as GenerateOptions;
|
|
117
|
+
const request: RequestState = { fill: options.fill!,
|
|
118
|
+
sampling: makeStepSampler(options, { tokenRepresentation: "device", grammarWait: "external",
|
|
119
|
+
historyUpdate: "manual", initialHistory: row.req.promptIds }) };
|
|
120
|
+
row.fill = request.fill.stats;
|
|
121
|
+
this.#requests.set(row, request);
|
|
122
|
+
try { target.admit(row); }
|
|
123
|
+
catch (error) { this.#closeRequest(row); throw error; }
|
|
124
|
+
};
|
|
125
|
+
const preparation: MlxGroupPreparation = {
|
|
126
|
+
get rows() { return [...target.rows, ...ready.map(state => state.row)]; },
|
|
127
|
+
get canAdmit() { return target.canAdmit; },
|
|
128
|
+
get tokenWeight() { return target.tokenWeight + ready.reduce((sum, state) => sum + state.row.promptTokens - state.row.cachedTokens, 0); },
|
|
129
|
+
admit,
|
|
130
|
+
advance: async work => {
|
|
131
|
+
if (!ready.length) {
|
|
132
|
+
const done = await target.advance(work);
|
|
133
|
+
return done && !ready.length;
|
|
134
|
+
}
|
|
135
|
+
// Flush before membership changes. Prepared output was already
|
|
136
|
+
// published on the preceding scheduler iteration.
|
|
137
|
+
await this.#flush();
|
|
138
|
+
while (ready.length) {
|
|
139
|
+
const state = ready.shift()!;
|
|
140
|
+
try {
|
|
141
|
+
if (state.row.req.signal?.aborted) {
|
|
142
|
+
this.#closeRequest(state.row); state.row.reject(state.row.req.signal.reason); continue;
|
|
143
|
+
}
|
|
144
|
+
this.#maintain.prepareBatch?.(state.caches);
|
|
145
|
+
this.#target ??= new MlxStateRows(state.caches.map(targetCacheLayout));
|
|
146
|
+
this.#target.append(state.caches);
|
|
147
|
+
// Copies must settle before releasing a restored SSD lease.
|
|
148
|
+
if (state.retain) withResource(leaseCacheStates(this.#target.caches), arrays => ops.evalAll([...arrays]));
|
|
149
|
+
this.host.join(state.row);
|
|
150
|
+
} finally { disposeResources([...state.caches, { dispose: () => state.retain?.() }]); }
|
|
151
|
+
}
|
|
152
|
+
return !target.rows.length;
|
|
153
|
+
},
|
|
154
|
+
dispose: () => {
|
|
155
|
+
const rows = preparation.rows;
|
|
156
|
+
disposeResources([target, ...ready.splice(0).flatMap(state => [...state.caches,
|
|
157
|
+
{ dispose: () => state.retain?.() }]), { dispose: () => {
|
|
158
|
+
for (const row of rows) if (!this.host.rows.includes(row)) this.#closeRequest(row);
|
|
159
|
+
} }]);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
try { admit(first); return preparation; }
|
|
163
|
+
catch (error) { return cleanupFailure(error, () => preparation.dispose()); }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async advance(work?: MlxForwardWork): Promise<void> {
|
|
167
|
+
const live = this.host.rows.flatMap((row, index) => row.req.signal?.aborted ? [] : [index]);
|
|
168
|
+
if (live.length !== this.host.rows.length) {
|
|
169
|
+
for (const row of this.host.rows) if (row.req.signal?.aborted) row.reject(row.req.signal.reason);
|
|
170
|
+
this.host.filterRows(live);
|
|
171
|
+
}
|
|
172
|
+
const rows = [...this.host.rows], B = rows.length;
|
|
173
|
+
if (!B) return;
|
|
174
|
+
const verifyLength = this.#verifyLength();
|
|
175
|
+
if (!work && verifyLength > 1) { await this.#verifyKnown(verifyLength); return; }
|
|
176
|
+
const burst = this.#burstLength();
|
|
177
|
+
if (!work && burst > 1) { await this.#appendKnown(burst); return; }
|
|
178
|
+
const previous = this.#pending, known = this.#known, real = this.#real;
|
|
179
|
+
const anyLive = rows.some(row => row.sampled < row.req.maxTokens);
|
|
180
|
+
let next: MlxArray | null = null;
|
|
181
|
+
const nextKnown: boolean[] = [], nextReal: boolean[] = [];
|
|
182
|
+
if (anyLive) {
|
|
183
|
+
using input = previous ? ops.reshape(previous, [B, 1]) : ops.fromInt32(rows.map(row => row.current), [B, 1]);
|
|
184
|
+
using hidden = await (work ? work(input, this.#target!.caches) : this.#binding.graph.forwardHidden(input, this.#target!.caches));
|
|
185
|
+
const selected = rows.map((row, index) => {
|
|
186
|
+
const request = this.#requests.get(row)!;
|
|
187
|
+
// The next selected token is not committed until it becomes input.
|
|
188
|
+
// Discarding a speculative sample for an assert cannot pollute history.
|
|
189
|
+
if (request.sampling.needsHistory) {
|
|
190
|
+
using current = input.slice([index, 0], [index + 1, 1]);
|
|
191
|
+
request.sampling.commitDevice(current);
|
|
192
|
+
}
|
|
193
|
+
const value = request.proposal?.value.policy === "assert"
|
|
194
|
+
? request.proposal.value.ids[request.proposal.emitted + Number(!!previous && known[index])] : undefined;
|
|
195
|
+
nextKnown.push(value !== undefined); nextReal.push(row.sampled < row.req.maxTokens);
|
|
196
|
+
return value;
|
|
197
|
+
});
|
|
198
|
+
const needScores = rows.some((_row, index) => nextReal[index] && !nextKnown[index]);
|
|
199
|
+
using logits = needScores ? this.#binding.graph.projectLogits(hidden, { type: "last" }) : null;
|
|
200
|
+
const tokens: MlxArray[] = [];
|
|
201
|
+
try {
|
|
202
|
+
for (let index = 0; index < B; index++) {
|
|
203
|
+
const row = rows[index]!;
|
|
204
|
+
if (!nextReal[index]) { tokens.push(ops.fromInt32([0], [1])); continue; }
|
|
205
|
+
if (nextKnown[index]) tokens.push(ops.fromInt32([selected[index]!], [1]));
|
|
206
|
+
else {
|
|
207
|
+
using slice = logits!.slice([index, 0, 0], [index + 1, 1, logits!.shape[2]!]);
|
|
208
|
+
using scores = ops.reshape(slice, [1, slice.shape[2]!]);
|
|
209
|
+
tokens.push(this.#requests.get(row)!.sampling.sample(scores, row.sampled).token);
|
|
210
|
+
}
|
|
211
|
+
row.sampled++;
|
|
212
|
+
}
|
|
213
|
+
next = ops.concatAxis(tokens, 0);
|
|
214
|
+
// Constant known tokens have no dependency on the model graph.
|
|
215
|
+
// Submit state explicitly when the vocabulary projection is omitted.
|
|
216
|
+
if (needScores) ops.asyncEvalAll([next]);
|
|
217
|
+
else withResource(leaseCacheStates(this.#target!.caches), state => ops.asyncEvalAll([next!, hidden, ...state]));
|
|
218
|
+
} catch (error) { next?.dispose(); next = null; throw error; }
|
|
219
|
+
finally { disposeResources(tokens); }
|
|
220
|
+
}
|
|
221
|
+
this.#pending = next; this.#known = nextKnown; this.#real = nextReal;
|
|
222
|
+
if (previous) {
|
|
223
|
+
let values: number[];
|
|
224
|
+
try { values = previous.toIntTokens(); } finally { previous.dispose(); }
|
|
225
|
+
if (anyLive) for (let index = 0; index < B; index++) {
|
|
226
|
+
if (real[index]) rows[index]!.fed.push(values[index]!);
|
|
227
|
+
else rows[index]!.fedTainted = true;
|
|
228
|
+
}
|
|
229
|
+
await this.#emitRows(values, known);
|
|
230
|
+
} else if (anyLive) for (const row of rows) row.fed.push(row.current);
|
|
231
|
+
this.#checkPendingProposals();
|
|
232
|
+
if (++this.#steps % 256 === 0) clearCache();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The pending sample already verifies an echo's first token. Asserted
|
|
236
|
+
* continuations keep their append policy; other rows can join a verified
|
|
237
|
+
* span with an empty proposal through the existing row verifier. */
|
|
238
|
+
#checkPendingProposals(): void {
|
|
239
|
+
if (!this.#pending) return;
|
|
240
|
+
const rows = this.host.rows;
|
|
241
|
+
if (!rows.some(row => this.#requests.get(row)!.proposal?.value.policy === "verify")) return;
|
|
242
|
+
const tokens = this.#pending.toIntTokens();
|
|
243
|
+
for (const [index, row] of rows.entries()) {
|
|
244
|
+
const request = this.#requests.get(row)!, proposal = request.proposal;
|
|
245
|
+
if (proposal?.value.policy !== "verify") continue;
|
|
246
|
+
if (tokens[index] === proposal.value.ids[proposal.emitted]) this.#known[index] = true;
|
|
247
|
+
else {
|
|
248
|
+
request.fill.commit(proposal.value, proposal.emitted);
|
|
249
|
+
request.proposal = undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async #verifyKnown(width: number): Promise<void> {
|
|
255
|
+
// Publish the already verified pending token before changing geometry.
|
|
256
|
+
await this.#flush();
|
|
257
|
+
const rows = [...this.host.rows];
|
|
258
|
+
if (!rows.length) return;
|
|
259
|
+
// Publishing an ordinary row can discover a strict template span. Let
|
|
260
|
+
// its append policy choose the next operation before verifying echoes.
|
|
261
|
+
if (rows.some(row => this.#requests.get(row)!.proposal?.value.policy === "assert")) return;
|
|
262
|
+
const pending = rows.map(row => row.current);
|
|
263
|
+
const proposals = rows.map(row => {
|
|
264
|
+
const proposal = this.#requests.get(row)!.proposal;
|
|
265
|
+
// A newly discovered proposal cannot exceed this iteration's declared
|
|
266
|
+
// token work. Its remaining continuation stays request-owned.
|
|
267
|
+
return proposal?.value.policy === "verify"
|
|
268
|
+
? proposal.value.ids.slice(proposal.emitted, proposal.emitted + width - 1) : [];
|
|
269
|
+
});
|
|
270
|
+
const depth = Math.max(...proposals.map(ids => ids.length));
|
|
271
|
+
const samplers = rows.map(row => {
|
|
272
|
+
const sampling = this.#requests.get(row)!.sampling;
|
|
273
|
+
sampling.commitNumbers([row.current]);
|
|
274
|
+
let previous: number | undefined;
|
|
275
|
+
// A second sample is requested only after the preceding candidate was
|
|
276
|
+
// accepted. Leave the final correction out of manual processor history
|
|
277
|
+
// until the next forward actually consumes it.
|
|
278
|
+
return { ...sampling,
|
|
279
|
+
async sample(scores, step) {
|
|
280
|
+
if (previous !== undefined) sampling.commitNumbers([previous]);
|
|
281
|
+
const result = sampling.sample(scores, step);
|
|
282
|
+
try { previous = result.token.toIntTokens()[0]!; return { token: previous, extras: result.extras }; }
|
|
283
|
+
finally { result.token.dispose(); }
|
|
284
|
+
},
|
|
285
|
+
} satisfies NumberStepSampler;
|
|
286
|
+
});
|
|
287
|
+
const halted = new Set<Row>();
|
|
288
|
+
const rollback = bindRowCacheRollback(this.#target!.caches, rows.length);
|
|
289
|
+
const verifying = rows.filter(row => this.#requests.get(row)!.proposal?.value.policy === "verify")
|
|
290
|
+
.map(row => this.#requests.get(row)!.fill);
|
|
291
|
+
const pin = this.#verify.pinVerify?.();
|
|
292
|
+
try {
|
|
293
|
+
const completed = await advanceSpeculativeOutputs(rows.map((row, index) => ({
|
|
294
|
+
pending: pending[index]!, step: row.sampled, remaining: row.req.maxTokens - row.generated,
|
|
295
|
+
eosTokenIds: row.req.eosTokenIds, sampling: samplers[index]!,
|
|
296
|
+
output: { commit: async ids => {
|
|
297
|
+
const proposal = this.#requests.get(row)!.proposal;
|
|
298
|
+
const known = proposal?.value.policy === "verify" && ids[0] === proposal.value.ids[proposal.emitted];
|
|
299
|
+
if (!known) this.#settleProposal(row);
|
|
300
|
+
row.generated++;
|
|
301
|
+
const reason = await this.#emit(row, ids[0]!, known);
|
|
302
|
+
if (reason === "stop") { halted.add(row); return false; }
|
|
303
|
+
} },
|
|
304
|
+
})), depth, { draft: () => proposals, commit: () => {} }, {
|
|
305
|
+
transaction: { ...rollback,
|
|
306
|
+
begin(depth) {
|
|
307
|
+
const start = performance.now(); rollback.begin(depth);
|
|
308
|
+
const elapsed = performance.now() - start;
|
|
309
|
+
for (const fill of verifying) fill.noteVerifyEvent(elapsed);
|
|
310
|
+
},
|
|
311
|
+
resolve(accepted) {
|
|
312
|
+
const start = performance.now(); rollback.resolve(accepted);
|
|
313
|
+
const elapsed = performance.now() - start;
|
|
314
|
+
for (const fill of verifying) fill.stats.checkpointMs += elapsed;
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
forward: async ids => {
|
|
318
|
+
const result = await this.#verify.forward(ids, this.#target!.caches);
|
|
319
|
+
const context = result.ctxML ?? result.hidden;
|
|
320
|
+
try { return { logits: this.#verify.projectLogits(result.hidden), context }; }
|
|
321
|
+
catch (error) { context.dispose(); throw error; }
|
|
322
|
+
finally { if (result.ctxML) result.hidden.dispose(); }
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
const keep: number[] = [];
|
|
326
|
+
for (const [index, row] of rows.entries()) {
|
|
327
|
+
const output = completed.outputs[index]!, round = completed.rounds[index]!;
|
|
328
|
+
if (output.kind !== "failed" && output.kind !== "cancelled")
|
|
329
|
+
row.fed.push(pending[index]!, ...round.drafts.slice(0, output.accepted));
|
|
330
|
+
row.sampled += round.acceptance.emitted.length + Number(round.acceptance.sawEos);
|
|
331
|
+
if (output.kind === "continue") { row.current = output.pending; keep.push(index); }
|
|
332
|
+
else if (output.kind === "failed") row.reject(output.error);
|
|
333
|
+
else if (output.kind === "cancelled") row.reject(row.req.signal?.reason ?? new Error(output.reason));
|
|
334
|
+
else {
|
|
335
|
+
if (!halted.has(row) && round.acceptance.sawEos && row.generated < row.req.maxTokens) row.generated++;
|
|
336
|
+
this.#put(row, this.#target!.extractRow(index));
|
|
337
|
+
this.#settleProposal(row); this.host.finish(row, output.kind);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (keep.length !== rows.length) this.host.filterRows(keep);
|
|
341
|
+
if (++this.#steps % 256 === 0) clearCache();
|
|
342
|
+
} finally { pin?.close(); }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Multi-position arithmetic is model-owned. Wider cohorts whose model
|
|
346
|
+
* declares one position keep the pipelined single-position implementation. */
|
|
347
|
+
async #appendKnown(length: number): Promise<void> {
|
|
348
|
+
const rows = [...this.host.rows], B = rows.length;
|
|
349
|
+
const spans = new Map(rows.map(row => {
|
|
350
|
+
const proposal = this.#requests.get(row)!.proposal!;
|
|
351
|
+
return [row, proposal.value.ids.slice(proposal.emitted, proposal.emitted + length)] as const;
|
|
352
|
+
}));
|
|
353
|
+
using input = ops.fromInt32(rows.flatMap(row => spans.get(row)!), [B, length]);
|
|
354
|
+
using hidden = await appendHiddenRows(this.#append!.forwardHidden.bind(this.#append), this.#target!.caches, input,
|
|
355
|
+
(caches, count) => Math.min(this.#append!.maxChunkSize(caches, count),
|
|
356
|
+
this.#maintain.maxAppendTokens?.(caches) ?? Infinity,
|
|
357
|
+
...rows.map(row => this.#requests.get(row)!.fill.appendChunkSize || Infinity)));
|
|
358
|
+
const nextKnown: boolean[] = [], nextReal: boolean[] = [];
|
|
359
|
+
const selected = rows.map(row => {
|
|
360
|
+
const request = this.#requests.get(row)!, proposal = request.proposal!;
|
|
361
|
+
request.sampling.commitNumbers(spans.get(row)!);
|
|
362
|
+
row.fed.push(...spans.get(row)!);
|
|
363
|
+
const next = proposal.value.ids[proposal.emitted + length];
|
|
364
|
+
const real = row.generated + length < row.req.maxTokens;
|
|
365
|
+
nextKnown.push(next !== undefined); nextReal.push(real);
|
|
366
|
+
row.sampled = row.generated + length + Number(real);
|
|
367
|
+
return next;
|
|
368
|
+
});
|
|
369
|
+
const needScores = rows.some((_row, index) => nextReal[index] && !nextKnown[index]);
|
|
370
|
+
using logits = needScores ? this.#binding.graph.projectLogits(hidden, { type: "last" }) : null;
|
|
371
|
+
const tokens: MlxArray[] = [];
|
|
372
|
+
let next: MlxArray | null = null;
|
|
373
|
+
try {
|
|
374
|
+
for (const [index, row] of rows.entries()) {
|
|
375
|
+
if (!nextReal[index] || nextKnown[index]) tokens.push(ops.fromInt32([selected[index] ?? 0], [1]));
|
|
376
|
+
else {
|
|
377
|
+
using slice = logits!.slice([index, 0, 0], [index + 1, 1, logits!.shape[2]!]);
|
|
378
|
+
using scores = ops.reshape(slice, [1, slice.shape[2]!]);
|
|
379
|
+
tokens.push(this.#requests.get(row)!.sampling.sample(scores, row.generated + length).token);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
next = ops.concatAxis(tokens, 0);
|
|
383
|
+
if (needScores) ops.asyncEvalAll([next]);
|
|
384
|
+
else withResource(leaseCacheStates(this.#target!.caches), state => ops.asyncEvalAll([hidden, ...state]));
|
|
385
|
+
this.#pending!.dispose(); this.#pending = next; next = null;
|
|
386
|
+
this.#known = nextKnown; this.#real = nextReal;
|
|
387
|
+
} finally { next?.dispose(); disposeResources(tokens); }
|
|
388
|
+
for (let column = 0; column < length && this.host.rows.length; column++) {
|
|
389
|
+
const active = [...this.host.rows];
|
|
390
|
+
await this.#emitRows(active.map(row => spans.get(row)![column]!), active.map(() => true));
|
|
391
|
+
}
|
|
392
|
+
if (++this.#steps % 256 === 0) clearCache();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async #emit(row: Row, token: number, known: boolean): Promise<"stop" | "length" | undefined> {
|
|
396
|
+
row.req.signal?.throwIfAborted();
|
|
397
|
+
const request = this.#requests.get(row)!;
|
|
398
|
+
if (row.req.eosTokenIds.includes(token)) return "stop";
|
|
399
|
+
// Account a known token even when the consumer stops on this token.
|
|
400
|
+
if (known) {
|
|
401
|
+
const proposal = request.proposal!;
|
|
402
|
+
if (++proposal.emitted === proposal.value.ids.length) {
|
|
403
|
+
request.fill.commit(proposal.value, proposal.emitted); request.proposal = undefined;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!known) {
|
|
407
|
+
const proposal = request.fill.push(token, row.req.maxTokens - row.generated);
|
|
408
|
+
if (proposal) {
|
|
409
|
+
request.proposal = { value: proposal, emitted: 0 };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const more = await this.host.publish(row, token);
|
|
413
|
+
if (more === false) return "stop";
|
|
414
|
+
if (row.generated >= row.req.maxTokens) return "length";
|
|
415
|
+
row.current = token;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async #emitRows(values: number[], known: readonly boolean[]): Promise<void> {
|
|
419
|
+
const rows = [...this.host.rows], keep: number[] = [];
|
|
420
|
+
const replacement = new Map<number, number>();
|
|
421
|
+
for (let index = 0; index < rows.length; index++) {
|
|
422
|
+
const row = rows[index]!;
|
|
423
|
+
row.generated++;
|
|
424
|
+
try {
|
|
425
|
+
const reason = await this.#emit(row, values[index]!, known[index] ?? false);
|
|
426
|
+
if (reason) {
|
|
427
|
+
this.#put(row, this.#target!.extractRow(index));
|
|
428
|
+
this.#settleProposal(row); this.host.finish(row, reason);
|
|
429
|
+
} else {
|
|
430
|
+
keep.push(index);
|
|
431
|
+
const request = this.#requests.get(row)!;
|
|
432
|
+
if (!known[index] && request.proposal?.value.policy === "assert" && this.#pending) {
|
|
433
|
+
replacement.set(index, request.proposal.value.ids[0]!);
|
|
434
|
+
request.fill.noteWastedSample(); this.#known[index] = true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch (error) { row.reject(error); }
|
|
438
|
+
}
|
|
439
|
+
if (replacement.size) {
|
|
440
|
+
const parts: MlxArray[] = [];
|
|
441
|
+
try {
|
|
442
|
+
for (let index = 0; index < rows.length; index++) parts.push(replacement.has(index)
|
|
443
|
+
? ops.fromInt32([replacement.get(index)!], [1]) : this.#pending!.slice([index], [index + 1]));
|
|
444
|
+
const next = ops.concatAxis(parts, 0); this.#pending!.dispose(); this.#pending = next;
|
|
445
|
+
} finally { disposeResources(parts); }
|
|
446
|
+
}
|
|
447
|
+
if (keep.length !== rows.length) this.host.filterRows(keep);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async #flush(): Promise<void> {
|
|
451
|
+
const pending = this.#pending, known = this.#known;
|
|
452
|
+
if (!pending) return;
|
|
453
|
+
this.#pending = null; this.#known = []; this.#real = [];
|
|
454
|
+
let values: number[];
|
|
455
|
+
try { values = pending.toIntTokens(); } finally { pending.dispose(); }
|
|
456
|
+
await this.#emitRows(values, known);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#put(row: Row, caches: Cache[], retain?: () => void): void {
|
|
460
|
+
try {
|
|
461
|
+
if (this.host.promptCache && !row.fedTainted && !row.req.signal?.aborted) {
|
|
462
|
+
this.host.promptCache.put([...row.req.promptIds, ...row.fed], caches,
|
|
463
|
+
row.cacheNamespace, retain, undefined, row.req.cacheSessionId);
|
|
464
|
+
caches = []; retain = undefined;
|
|
465
|
+
}
|
|
466
|
+
} finally { disposeResources([...caches, { dispose: () => retain?.() }]); }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#settleProposal(row: Row): void {
|
|
470
|
+
const request = this.#requests.get(row);
|
|
471
|
+
if (request?.proposal) {
|
|
472
|
+
const proposal = request.proposal; request.proposal = undefined;
|
|
473
|
+
request.fill.commit(proposal.value, proposal.emitted);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
#closeRequest(row: Row): void {
|
|
477
|
+
this.#settleProposal(row);
|
|
478
|
+
const request = this.#requests.get(row); this.#requests.delete(row); request?.sampling.dispose();
|
|
479
|
+
}
|
|
480
|
+
filterRows(keep: readonly number[], _discard: boolean): void {
|
|
481
|
+
if (this.#pending) {
|
|
482
|
+
using indices = ops.fromInt32([...keep], [keep.length]);
|
|
483
|
+
const next = keep.length ? ops.takeAxis(this.#pending, indices, 0) : null;
|
|
484
|
+
this.#pending.dispose(); this.#pending = next;
|
|
485
|
+
}
|
|
486
|
+
this.#known = keep.map(index => this.#known[index]!); this.#real = keep.map(index => this.#real[index]!);
|
|
487
|
+
this.#target?.filterRows(keep);
|
|
488
|
+
const retained = new Set(keep.map(index => this.host.rows[index]!));
|
|
489
|
+
for (const row of this.host.rows) if (!retained.has(row)) this.#closeRequest(row);
|
|
490
|
+
}
|
|
491
|
+
dispose(): void {
|
|
492
|
+
const pending = this.#pending, target = this.#target;
|
|
493
|
+
this.#pending = null; this.#target = null;
|
|
494
|
+
disposeResources([...(pending ? [pending] : []), ...(target ? [target] : []),
|
|
495
|
+
{ dispose: () => { for (const row of this.#requests.keys()) this.#closeRequest(row); } }]);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { speculativePrefixNamespace, captureSpeculativeOptions } from "../../spec/cache-identity";
|
|
1
2
|
import { createOrdinaryContinuationRequest } from "./continuation-request";
|
|
2
3
|
import type { MlxSerialServices } from "./serial-executor";
|
|
3
|
-
import { bindPagedRequestState, type MlxRequestStatePolicy } from "./request-state-policy";
|
|
4
|
+
import { bindPagedRequestState, pagedPrefixNamespace, type MlxRequestStatePolicy } from "./request-state-policy";
|
|
4
5
|
import type { ExecutionContext } from "../../contracts/scheduling";
|
|
5
6
|
import type { ModelConfig } from "../../config";
|
|
6
7
|
import type { KvScheme } from "../../kv-scheme";
|
|
@@ -16,18 +17,26 @@ import { disposeResources } from "../../engine/resources";
|
|
|
16
17
|
import { legacyCompiledDecodeAvailable } from "./autoregressive";
|
|
17
18
|
import { MlxBatchExecutionGroup, type MlxBatchExecutionGroupOptions, type MlxGroupMethodRequest } from "./batch-group";
|
|
18
19
|
import type { DraftProvider } from "../../spec/source";
|
|
20
|
+
import { constraintDraftProvider } from "../../spec/ngram-source";
|
|
19
21
|
import { targetRowLayoutFactory } from "./target-layout-capability";
|
|
20
22
|
import { bindSpeculativeGroupRequests } from "./speculative-group";
|
|
23
|
+
import { bindFillGroupRequests } from "./fill-group";
|
|
21
24
|
import type { GenerateOptions } from "../../generate";
|
|
22
25
|
import type { ExecutionRequirements, ResolvedExecution } from "../../contracts/execution";
|
|
23
26
|
import { resolveExecution } from "../../engine/execution-plan";
|
|
27
|
+
import { bindEmbeddingsInput, type MlxPromptInput } from "./prompt-input";
|
|
28
|
+
import { bindQwenMediaInput } from "./qwen-prompt-input";
|
|
29
|
+
import type { Vision } from "../../serve/generation-gateway";
|
|
24
30
|
|
|
25
31
|
export interface MlxBatchGroup extends Pick<MlxBatchExecutionGroup,
|
|
26
|
-
"activeRows" | "pendingRows" | "projectedKvBytes" | "kvBudgetBytes" | "submit" | "kick" | "close"> {
|
|
32
|
+
"activeRows" | "pendingRows" | "projectedKvBytes" | "kvBudgetBytes" | "submit" | "kick" | "close"> {
|
|
33
|
+
runPreparation?<T>(work: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
34
|
+
}
|
|
27
35
|
|
|
28
36
|
/** A model implementation owns capability checks and the execution group.
|
|
29
37
|
* Scheduling never inspects concrete model/cache classes. */
|
|
30
38
|
export interface MlxGatewayBinding {
|
|
39
|
+
mediaInput?(input: Vision): MlxPromptInput;
|
|
31
40
|
configureContinuation?(services: MlxSerialServices): void;
|
|
32
41
|
continuationRequest?(execution: ResolvedExecution | undefined, options: GenerateOptions, prompt: number[],
|
|
33
42
|
onToken: Parameters<typeof createOrdinaryContinuationRequest>[0]["onToken"]): ReturnType<typeof createOrdinaryContinuationRequest> | undefined;
|
|
@@ -39,6 +48,7 @@ export interface MlxGatewayBinding {
|
|
|
39
48
|
cachesBatchable(): boolean;
|
|
40
49
|
kvBatchable(scheme: KvScheme): boolean;
|
|
41
50
|
statePolicy?(execution: ResolvedExecution | undefined, options: GenerateOptions, capacityTokens: number): MlxRequestStatePolicy | undefined;
|
|
51
|
+
prefixNamespace?(execution: ResolvedExecution | undefined, options: GenerateOptions, adapters: string): string | null;
|
|
42
52
|
methodRequest?(execution: ResolvedExecution | undefined, options: GenerateOptions): MlxGroupMethodRequest | undefined;
|
|
43
53
|
createBatchGroup(options: MlxBatchExecutionGroupOptions): MlxBatchGroup;
|
|
44
54
|
}
|
|
@@ -64,8 +74,20 @@ export function bindMlxGateway(model: RuntimeModel, draft?: { provider: DraftPro
|
|
|
64
74
|
};
|
|
65
75
|
const speculative = draft?.provider.grouped && cachesBatchable() && supportsTargetRows()
|
|
66
76
|
? bindSpeculativeGroupRequests(model, draft.provider, draft.numDraftTokens) : undefined;
|
|
77
|
+
const grammarProvider = runtime.flag("MLX_BUN_GRAMMAR_JUMP", false) && cachesBatchable() && supportsTargetRows()
|
|
78
|
+
? constraintDraftProvider() : undefined;
|
|
79
|
+
const grammarProposals = grammarProvider ? bindSpeculativeGroupRequests(model, grammarProvider,
|
|
80
|
+
Math.max(1, Math.trunc(runtime.number("MLX_BUN_GRAMMAR_DRAFT_TOKENS", 3)))) : undefined;
|
|
67
81
|
const adapterState = "loraState" in model ? model.loraState : undefined;
|
|
82
|
+
const fillRequests = supportsTargetRows() ? bindFillGroupRequests(model) : undefined;
|
|
83
|
+
const mediaInput = model instanceof Gemma4Model ? (input: Vision) =>
|
|
84
|
+
bindEmbeddingsInput((ids, caches, start) => start > 0 ? model.forwardHidden(ids, caches)
|
|
85
|
+
: model.forwardEmbeddings(input.embeddings,
|
|
86
|
+
caches, input.imageMask ?? null, ids, input.multimodalMask ?? null))
|
|
87
|
+
: model instanceof Qwen35Model ? (input: Vision) => bindQwenMediaInput(model, input.embeddings, input.mrope!)
|
|
88
|
+
: undefined;
|
|
68
89
|
return {
|
|
90
|
+
mediaInput,
|
|
69
91
|
config: model.config, runtime,
|
|
70
92
|
configureContinuation: services => { continuationServices = services; },
|
|
71
93
|
continuationRequest(execution, options, prompt, onToken) {
|
|
@@ -78,8 +100,23 @@ export function bindMlxGateway(model: RuntimeModel, draft?: { provider: DraftPro
|
|
|
78
100
|
restore: entry => services.checkpoints!.restore(entry, model),
|
|
79
101
|
interval: services.checkpointEveryTokens!, identity: services.identity });
|
|
80
102
|
},
|
|
81
|
-
statePolicy: (execution, options, capacity) =>
|
|
82
|
-
|
|
103
|
+
statePolicy: (execution, options, capacity) => {
|
|
104
|
+
// Media token IDs alone do not identify the prepared embeddings.
|
|
105
|
+
// Preserve the existing uncached media policy through the state port.
|
|
106
|
+
if (execution?.method === "autoregressive" && !execution.promptCache)
|
|
107
|
+
return { key: "uncached-prepared-input", create: () => model.makeCache() };
|
|
108
|
+
return execution?.pagedKv ? bindPagedRequestState(model, options, capacity, continuationServices?.promptCache, runtime) : undefined;
|
|
109
|
+
},
|
|
110
|
+
prefixNamespace: (execution, options, adapters) => {
|
|
111
|
+
if (execution?.pagedKv) return pagedPrefixNamespace(options, adapters, runtime.flag("MLX_BUN_PAGED_ATTN", false));
|
|
112
|
+
if (execution?.method !== "speculative") return adapters;
|
|
113
|
+
const namespace = (execution.grammarJump ? grammarProvider : draft?.provider)?.grouped?.checkpointNamespace?.();
|
|
114
|
+
return namespace === undefined ? null : speculativePrefixNamespace(namespace, adapters, captureSpeculativeOptions(options));
|
|
115
|
+
},
|
|
116
|
+
methodRequest: (execution, options) => execution?.method === "speculative"
|
|
117
|
+
? (execution.grammarJump ? grammarProposals : speculative)?.(
|
|
118
|
+
options.fill && !execution.fill ? { ...options, fill: undefined } : options)
|
|
119
|
+
: execution?.fill ? fillRequests?.(options) : undefined,
|
|
83
120
|
...(adapterState ? { bindAdapterContext(adapters: string[], key: string): ExecutionContext {
|
|
84
121
|
const selected = [...adapters];
|
|
85
122
|
return { key, enter() {
|
|
@@ -89,26 +126,33 @@ export function bindMlxGateway(model: RuntimeModel, draft?: { provider: DraftPro
|
|
|
89
126
|
} };
|
|
90
127
|
} } : {}),
|
|
91
128
|
plan(request, options, scheduling) {
|
|
129
|
+
const sharedMethod = request.hasDraft ? speculative : grammarProposals;
|
|
130
|
+
const provider = request.hasDraft ? draft?.provider : grammarProvider;
|
|
92
131
|
return resolveExecution(request, {
|
|
93
132
|
...scheduling,
|
|
94
133
|
sharedCheckpoints: !!continuationServices?.checkpointPersistence &&
|
|
95
134
|
!request.hasDraft && !request.hasVision && !request.hasGrammar &&
|
|
96
135
|
!request.wantsLogprobs && !options.fill && !options.pagedKv,
|
|
97
136
|
adapterBatch: !!adapterState, pagedBatch: model instanceof Gemma4Model,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
mediaBatch: !!mediaInput,
|
|
138
|
+
mediaPrefixCache: runtime.flag("MLX_BUN_MEDIA_PREFIX_CACHE", true),
|
|
139
|
+
groupedMethods: sharedMethod ? ["autoregressive", "speculative"] : ["autoregressive"],
|
|
140
|
+
sharedGrammarProposals: !!grammarProposals,
|
|
141
|
+
sharedFill: !!fillRequests && !!options.fill,
|
|
142
|
+
sharedSpeculativeEcho: !!options.fill?.plan.echo && provider?.grouped?.supportsExternalTokens === true,
|
|
143
|
+
speculativeLogprobs: scheduling.continuous && !!sharedMethod,
|
|
144
|
+
sharedSpeculativeAdapters: scheduling.continuous && !!sharedMethod && !!adapterState &&
|
|
145
|
+
provider?.grouped?.supportsTargetAdapters === true,
|
|
102
146
|
turboQuantBatch: scheduling.quantizedBatch,
|
|
103
|
-
speculativeTurboQuant: scheduling.continuous && !!
|
|
147
|
+
speculativeTurboQuant: scheduling.continuous && !!sharedMethod && !!options.turboQuant,
|
|
104
148
|
method: model instanceof DiffusionGemmaModel ? "denoising" : "autoregressive",
|
|
105
149
|
compiledDecode: legacyCompiledDecodeAvailable(model),
|
|
106
150
|
grammarBatch: runtime.value("MLX_BUN_GRAMMAR_BATCH") !== "0",
|
|
107
151
|
speculativeKvQuant: (!(model instanceof Qwen35Model) || runtime.flag("MLX_BUN_QWEN_SPEC_KV4", true)) && (
|
|
108
|
-
(scheduling.continuous && !!
|
|
152
|
+
(scheduling.continuous && !!sharedMethod && (options.kvBits === 4 || options.kvBits === 8 || !!options.kvConfig?.length)) ||
|
|
109
153
|
(!options.kvConfig?.length && model instanceof Qwen35Model &&
|
|
110
|
-
(options.kvBits === 4 || (scheduling.continuous && !!
|
|
111
|
-
(options.quantizedKvStart === 0 || (scheduling.continuous && !!
|
|
154
|
+
(options.kvBits === 4 || (scheduling.continuous && !!sharedMethod && options.kvBits === 8)) &&
|
|
155
|
+
(options.quantizedKvStart === 0 || (scheduling.continuous && !!sharedMethod)))
|
|
112
156
|
),
|
|
113
157
|
}, {
|
|
114
158
|
pagedKv: !!options.pagedKv, fill: !!options.fill,
|