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
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import type { OrdinaryContinuation } from "./continuation";
|
|
2
|
+
import type { MlxRequestStatePolicy } from "./request-state-policy";
|
|
1
3
|
import { AdmissionRejected } from "../../engine/admission";
|
|
2
|
-
import type { ExecutionGroup } from "../../contracts/scheduling";
|
|
4
|
+
import type { ExecutionContext, ExecutionGroup } from "../../contracts/scheduling";
|
|
3
5
|
import { driveExecutionGroup } from "../../engine/scheduler";
|
|
4
|
-
import { disposeResources } from "../../engine/resources";
|
|
6
|
+
import { disposeResources, withResource } from "../../engine/resources";
|
|
5
7
|
import type { DisposableResource } from "../../contracts/resources";
|
|
6
|
-
import {
|
|
7
|
-
import { executeMlxPrefillStep } from "./prefill";
|
|
8
|
-
import { enterMlxMemoryGuard, type MlxMemoryBudget } from "./memory-guard";
|
|
8
|
+
import { MlxPrefillCohort, type PrefillState } from "./prefill-cohort";
|
|
9
9
|
// Continuous-batching scheduler for `--batch N` serving (phase S2, the engine
|
|
10
10
|
// loop). Owns ONE running batch and drives it forward one decode step at a
|
|
11
11
|
// time, admitting waiting requests and evicting finished ones between steps —
|
|
@@ -87,14 +87,15 @@ import {
|
|
|
87
87
|
type QuantRow,
|
|
88
88
|
} from "../../model/batched-quant";
|
|
89
89
|
import { BatchedRotatingQuantCache } from "../../model/batched-rotating-quant";
|
|
90
|
-
import
|
|
90
|
+
import { cloneSingleRowState, leaseCacheStates } from "./state-views";
|
|
91
|
+
import { ownedCacheLayout } from "./cache-layout";
|
|
92
|
+
import { createKvMaintenance, type KvMaintenance } from "./kv-maintenance";
|
|
91
93
|
import type { KvScheme } from "../../kv-scheme";
|
|
92
94
|
import { BatchedRotatingCache } from "../../model/batched-rotating";
|
|
93
|
-
import { cloneKvCaches } from "../../kv-store";
|
|
94
95
|
import { SSMCache } from "../../model/qwen3-delta";
|
|
95
96
|
import type { RuntimeModel } from "../../model/factory";
|
|
96
97
|
import type { GrammarController } from "../../grammar";
|
|
97
|
-
import {
|
|
98
|
+
import { independentGreedySampling } from "../../sampler";
|
|
98
99
|
import { acquireModelWiredLimit } from "../../generate";
|
|
99
100
|
import { batchRowKvBytes } from "../../serve/kv-budget";
|
|
100
101
|
import type { PromptResponseTrace } from "../../serve/prompt-response-trace";
|
|
@@ -132,19 +133,64 @@ export function stepTraceReport(): string {
|
|
|
132
133
|
* step 0..maxTokens-1 for this row (never beyond maxTokens). */
|
|
133
134
|
export type RowSampler = (logits1V: MlxArray, step: number) => MlxArray;
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
/** A method prepares and advances rows; the shared executor owns queueing. */
|
|
137
|
+
export interface MlxGroupPreparation {
|
|
138
|
+
readonly rows: readonly Row[];
|
|
139
|
+
readonly tokenWeight?: number;
|
|
140
|
+
readonly canAdmit?: boolean;
|
|
141
|
+
admit?(row: Row): void;
|
|
142
|
+
advance(): Promise<boolean>;
|
|
143
|
+
dispose(): void;
|
|
144
|
+
}
|
|
145
|
+
export interface MlxGroupMethodHost {
|
|
146
|
+
readonly rows: readonly Row[];
|
|
147
|
+
readonly runtime: RuntimeConfig;
|
|
148
|
+
readonly prefillChunkSize: number;
|
|
149
|
+
readonly promptCache: RowPromptCache | undefined;
|
|
150
|
+
join(row: Row): void;
|
|
151
|
+
filterRows(keep: number[]): void;
|
|
152
|
+
publish(row: Row, token: number, logprobs?: import("../../generate").TokenLogprobs): Promise<void | boolean>;
|
|
153
|
+
finish(row: Row, reason: "stop" | "length"): void;
|
|
154
|
+
}
|
|
155
|
+
export interface MlxGroupedMethod {
|
|
156
|
+
prepare(row: Row): MlxGroupPreparation;
|
|
157
|
+
advance(): Promise<void>;
|
|
158
|
+
filterRows(keep: readonly number[], discard: boolean): void;
|
|
159
|
+
dispose(): void;
|
|
160
|
+
}
|
|
161
|
+
export interface MlxGroupMethodRequest {
|
|
162
|
+
/** Equal keys declare compatible method state, independent of queue size. */
|
|
163
|
+
readonly key: string;
|
|
164
|
+
readonly data: unknown;
|
|
165
|
+
open(host: MlxGroupMethodHost): MlxGroupedMethod;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type BatchRequest = BatchRequestFields & (
|
|
169
|
+
| { method: MlxGroupMethodRequest; sample?: RowSampler }
|
|
170
|
+
| { method?: undefined; sample: RowSampler }
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
interface BatchRequestFields {
|
|
174
|
+
/** Optional ordinary continuation policy; owns persistence and sampler recovery. */
|
|
175
|
+
continuation?: OrdinaryContinuation;
|
|
176
|
+
statePolicy?: MlxRequestStatePolicy;
|
|
177
|
+
context?: ExecutionContext;
|
|
178
|
+
/** Resolve loaded-state identity under the execution lease, after any
|
|
179
|
+
* queued adapter replacement has finished. */
|
|
180
|
+
cacheNamespace?: string | (() => string);
|
|
136
181
|
/** Resolved replay permission; standalone group callers retain auto mode. */
|
|
137
182
|
compiledDecode?: boolean;
|
|
138
183
|
promptIds: number[];
|
|
184
|
+
/** Request policy overrides the execution group's captured prefill default. */
|
|
185
|
+
prefillChunkSize?: number;
|
|
139
186
|
maxTokens: number;
|
|
140
187
|
eosTokenIds: number[];
|
|
141
|
-
sample: RowSampler;
|
|
142
188
|
/** Called per emitted (non-EOS) token, in order. Returning `false` halts this
|
|
143
189
|
* row (a decoded-text stop sequence fired) — matches generate()'s onToken
|
|
144
190
|
* contract. EOS terminates the row WITHOUT an onToken call. Throwing evicts
|
|
145
191
|
* THIS row only (its submit promise rejects; siblings continue). May be
|
|
146
192
|
* async; keep it cheap — it runs inline in the step loop. */
|
|
147
|
-
onToken: (token: number) => void | boolean | Promise<void | boolean>;
|
|
193
|
+
onToken: (token: number, logprobs?: import("../../generate").TokenLogprobs) => void | boolean | Promise<void | boolean>;
|
|
148
194
|
/** Diagnostic-only request-local trace. */
|
|
149
195
|
trace?: PromptResponseTrace;
|
|
150
196
|
/** Closes the gateway's queue/admission span when this row leaves pending. */
|
|
@@ -178,6 +224,7 @@ export interface BatchRequest {
|
|
|
178
224
|
}
|
|
179
225
|
|
|
180
226
|
export interface BatchStats {
|
|
227
|
+
spec?: import("../../generate").GenerateStats["spec"];
|
|
181
228
|
promptTokens: number;
|
|
182
229
|
generatedTokens: number;
|
|
183
230
|
/** Prompt tokens served from the prompt cache (Phase 3.2): a joiner's solo
|
|
@@ -200,14 +247,10 @@ export interface BatchStats {
|
|
|
200
247
|
* finish INSIDE a multi-row batch get their KV EXTRACTED per row into
|
|
201
248
|
* fresh serial caches first (mlx-lm server.py:872 extract_cache — the
|
|
202
249
|
* cross-request reuse concurrent agents live on). */
|
|
203
|
-
export
|
|
204
|
-
take(prompt: number[], ns?: string): {
|
|
205
|
-
tokens: number[]; caches: Cache[]; retain?: () => void;
|
|
206
|
-
} | null;
|
|
207
|
-
put(tokens: number[], caches: Cache[], ns?: string, retain?: () => void): void;
|
|
208
|
-
}
|
|
250
|
+
export type RowPromptCache = import("./checkpoint-state").MlxPrefixCache;
|
|
209
251
|
|
|
210
|
-
interface Row {
|
|
252
|
+
export interface Row {
|
|
253
|
+
spec?: import("../../generate").GenerateStats["spec"];
|
|
211
254
|
req: BatchRequest;
|
|
212
255
|
resolve: (s: BatchStats) => void;
|
|
213
256
|
reject: (e: unknown) => void;
|
|
@@ -220,6 +263,8 @@ interface Row {
|
|
|
220
263
|
/** performance.now() marks for BatchStats timing (0 = not reached). */
|
|
221
264
|
admittedAt: number;
|
|
222
265
|
firstTokenAt: number;
|
|
266
|
+
closeTokenZero?: () => void;
|
|
267
|
+
cacheNamespace?: string;
|
|
223
268
|
/** Generated tokens whose KV actually entered the cache (fed as a step
|
|
224
269
|
* input) — serial generate()'s `forwarded` list. promptIds+fed is the
|
|
225
270
|
* exact token coverage of the row's caches, the put() entry key.
|
|
@@ -242,30 +287,14 @@ interface Row {
|
|
|
242
287
|
merged: boolean;
|
|
243
288
|
}
|
|
244
289
|
|
|
245
|
-
/** A joiner mid-prefill: `pos` prompt tokens are already in `solo`. Advanced
|
|
246
|
-
* one chunk per loop iteration, interleaved with batch decode steps. */
|
|
247
|
-
interface PrefillState {
|
|
248
|
-
row: Row;
|
|
249
|
-
solo: Cache[];
|
|
250
|
-
pos: number;
|
|
251
|
-
/** Release hook carried by an SSD-restored prompt-cache entry (must run
|
|
252
|
-
* only after `solo`/the adopted caches are disposed — see PromptCacheEntry). */
|
|
253
|
-
retain?: () => void;
|
|
254
|
-
/** Pending boundary-snapshot position: #prefillChunk splits its chunking
|
|
255
|
-
* exactly here, clones + put()s the trim-free prefix entry, then nulls
|
|
256
|
-
* it. null = no snapshot for this row (short prompt / cached past it). */
|
|
257
|
-
snapAt: number | null;
|
|
258
|
-
closePrefill?: () => void;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
290
|
/** Held while the batch is active; the serial fallback acquires the same lock. */
|
|
262
291
|
export interface ExclusiveLock {
|
|
263
292
|
acquire(): Promise<() => void>;
|
|
264
293
|
}
|
|
265
294
|
|
|
266
295
|
export interface MlxBatchExecutionGroupOptions {
|
|
267
|
-
/**
|
|
268
|
-
|
|
296
|
+
/** Scheduling work budget for extending an existing preparation cohort. */
|
|
297
|
+
prefillBatchTokenLimit?: number;
|
|
269
298
|
runtime?: RuntimeConfig;
|
|
270
299
|
maxQueued?: number;
|
|
271
300
|
stateCodecs?: import("../../kv-store").CacheCodecProvider;
|
|
@@ -290,6 +319,8 @@ export interface MlxBatchExecutionGroupOptions {
|
|
|
290
319
|
* the same chunk boundaries as the serial path, so a row's quantized bytes
|
|
291
320
|
* preserve the L2 composition. Unsupported schemes fail at construction. */
|
|
292
321
|
kvScheme?: KvScheme;
|
|
322
|
+
/** Model binding owns support for precision transitions. */
|
|
323
|
+
kvBatchCapabilities?: { delayedAffine?: boolean };
|
|
293
324
|
/** Prompt-cache hook (Phase 3.2): admission take()s the longest usable
|
|
294
325
|
* prefix into the joiner's solo caches (suffix-only prefill — the
|
|
295
326
|
* multi-turn chat TTFT path); rows that finish never-merged put() their
|
|
@@ -320,7 +351,8 @@ export class MlxBatchExecutionGroup {
|
|
|
320
351
|
#inners: LayerInner[] | null = null; // per-layer batched KV; null when empty
|
|
321
352
|
#fullLeftPad: number[] = []; // per-row padding for FULL layers (rot self-tracks)
|
|
322
353
|
#pending: Row[] = [];
|
|
323
|
-
#prefill:
|
|
354
|
+
#prefill: MlxGroupPreparation | null = null; // the (single) joiner mid-prefill
|
|
355
|
+
#preparationPublishedOutput = false;
|
|
324
356
|
/** Sampled-but-unread token array [B], aligned with #running — the decode
|
|
325
357
|
* pipeline register. Filtered/disposed alongside the batched KV. */
|
|
326
358
|
#pendingToks: MlxArray | null = null;
|
|
@@ -329,6 +361,8 @@ export class MlxBatchExecutionGroup {
|
|
|
329
361
|
* accounting must skip it — see Row.fed/fedTainted). Filtered/cleared in
|
|
330
362
|
* lockstep with the register. */
|
|
331
363
|
#pendingReal: boolean[] | null = null;
|
|
364
|
+
#method: MlxGroupedMethod | undefined;
|
|
365
|
+
#methodKey: string | undefined;
|
|
332
366
|
#steps = 0; // decode-step counter (clearCache cadence)
|
|
333
367
|
#looping = false;
|
|
334
368
|
#closed = false;
|
|
@@ -338,24 +372,29 @@ export class MlxBatchExecutionGroup {
|
|
|
338
372
|
readonly #lock: ExclusiveLock | undefined;
|
|
339
373
|
readonly #admissionHeld: (() => boolean) | undefined;
|
|
340
374
|
readonly #prefillChunkSize: number;
|
|
375
|
+
readonly #prefillBatchTokenLimit: number;
|
|
341
376
|
readonly #prefillTailSplit: boolean;
|
|
342
377
|
readonly #kvBudgetBytes: number | undefined;
|
|
343
|
-
readonly #
|
|
344
|
-
|
|
378
|
+
readonly #defaultPromptCache: RowPromptCache | undefined;
|
|
379
|
+
#statePolicy?: MlxRequestStatePolicy;
|
|
380
|
+
get #promptCache(): RowPromptCache | undefined {
|
|
381
|
+
return this.#statePolicy ? this.#statePolicy.promptCache : this.#defaultPromptCache;
|
|
382
|
+
}
|
|
345
383
|
/** Retain hook of the currently-ADOPTED row's cache entry (at most one:
|
|
346
384
|
* only a lone adopted row holds un-copied entry caches). Runs after the
|
|
347
385
|
* adopted caches are disposed, or transfers back on put(). */
|
|
348
386
|
#adoptedRetain: (() => void) | null = null;
|
|
387
|
+
#contextKey: string | undefined;
|
|
388
|
+
#releaseContext: (() => void) | undefined;
|
|
349
389
|
readonly #kinds: ("full" | "rot" | "ssm" | "owned-batch")[];
|
|
350
390
|
readonly #rotMaxSize: number[]; // per-layer sliding window (rot layers only)
|
|
351
391
|
readonly #compressedProjectors: Array<(tokens: number) => number> | null;
|
|
352
392
|
readonly #batchCacheMaxTokens: number | null;
|
|
353
393
|
readonly #kvScheme: KvScheme | undefined;
|
|
354
|
-
|
|
355
|
-
readonly #kvByLayer: Map<number, KvQuantSpec> | null;
|
|
394
|
+
readonly #maintainKv: KvMaintenance | null;
|
|
356
395
|
/** Compiled decode runner for the B=1 serial-class case (Phase 3.2) —
|
|
357
396
|
* same eligibility gate as generate.ts (gemma dense, kill switch
|
|
358
|
-
* MLX_BUN_COMPILED_DECODE);
|
|
397
|
+
* MLX_BUN_COMPILED_DECODE); adapter requests disable replay in their plan. Set
|
|
359
398
|
* to null permanently on a failed step (serial disables per
|
|
360
399
|
* generation; the scheduler is one long-lived "generation"). */
|
|
361
400
|
#compiled: CompiledDecode | null;
|
|
@@ -370,17 +409,19 @@ export class MlxBatchExecutionGroup {
|
|
|
370
409
|
this.#maxBatch = Math.max(1, Math.floor(opts.maxBatch));
|
|
371
410
|
this.#lock = opts.lock;
|
|
372
411
|
this.#admissionHeld = opts.admissionHeld;
|
|
373
|
-
this.#prefillChunkSize = Math.max(1, Math.floor(opts.prefillChunkSize ??
|
|
412
|
+
this.#prefillChunkSize = Math.max(1, Math.floor(opts.prefillChunkSize ??
|
|
413
|
+
this.#runtime.number("MLX_BUN_RD_PREFILL_CHUNK", 2048)));
|
|
414
|
+
this.#prefillBatchTokenLimit = opts.prefillBatchTokenLimit ?? 2048;
|
|
374
415
|
this.#prefillTailSplit = this.#runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true);
|
|
375
416
|
this.#kvBudgetBytes = opts.kvBudgetBytes;
|
|
376
|
-
this.#
|
|
377
|
-
this.#promptCache = opts.promptCache;
|
|
417
|
+
this.#defaultPromptCache = opts.promptCache;
|
|
378
418
|
this.#kvScheme = opts.kvScheme;
|
|
379
419
|
const proto = withRuntimeConfig(this.#runtime, () => model.makeCache()); // fresh caches hold no buffers
|
|
380
420
|
if (this.#kvScheme && !this.#kvScheme.batchable(
|
|
381
421
|
model.config,
|
|
382
422
|
(layerIdx) =>
|
|
383
423
|
isPlainKvCache(proto[layerIdx]) || isRotatingPlainCache(proto[layerIdx]),
|
|
424
|
+
proto.length, opts.kvBatchCapabilities,
|
|
384
425
|
)) {
|
|
385
426
|
for (const cache of proto) cache.dispose();
|
|
386
427
|
throw new Error(`unsupported KV scheme for batch scheduler: ${this.#kvScheme.kind}`);
|
|
@@ -398,11 +439,10 @@ export class MlxBatchExecutionGroup {
|
|
|
398
439
|
? proto.map((cache) => (tokens: number) => cache.projectedBytes(tokens))
|
|
399
440
|
: null;
|
|
400
441
|
this.#batchCacheMaxTokens = proto.every(isBatchableCache)
|
|
401
|
-
? Math.min(...proto.map((cache) => cache.maxTokens ??
|
|
442
|
+
? Math.min(...proto.map((cache) => cache.maxTokens ?? Infinity))
|
|
402
443
|
: null;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
? new Map(kvConfig.map((entry) => [entry.layerIdx, entry]))
|
|
444
|
+
this.#maintainKv = this.#kvScheme?.quantized
|
|
445
|
+
? createKvMaintenance(this.#kvScheme.options)
|
|
406
446
|
: null;
|
|
407
447
|
this.#rotMaxSize = proto.map((c) => (isRotatingPlainCache(c) ? c.maxSize : 0));
|
|
408
448
|
for (const c of proto) c.dispose();
|
|
@@ -418,7 +458,7 @@ export class MlxBatchExecutionGroup {
|
|
|
418
458
|
}
|
|
419
459
|
|
|
420
460
|
get pendingRows(): number {
|
|
421
|
-
return this.#pending.length + (this.#prefill
|
|
461
|
+
return this.#pending.length + (this.#prefill?.rows.length ?? 0);
|
|
422
462
|
}
|
|
423
463
|
|
|
424
464
|
/** Projected KV bytes of one row at its worst case (full prompt + full
|
|
@@ -437,14 +477,6 @@ export class MlxBatchExecutionGroup {
|
|
|
437
477
|
);
|
|
438
478
|
}
|
|
439
479
|
|
|
440
|
-
async #withMemoryGuard<T>(promptTokens: number, run: () => Promise<T>): Promise<T> {
|
|
441
|
-
if (!this.#memoryBudget) return run();
|
|
442
|
-
const guard = enterMlxMemoryGuard(this.model, this.#memoryBudget,
|
|
443
|
-
promptTokens, this.#prefillChunkSize);
|
|
444
|
-
try { guard.check(); return await run(); }
|
|
445
|
-
finally { guard.close(); }
|
|
446
|
-
}
|
|
447
|
-
|
|
448
480
|
async #forwardHidden(ids: MlxArray, cache: Cache[]): Promise<MlxArray> {
|
|
449
481
|
const asyncModel = this.model as RuntimeModel & {
|
|
450
482
|
forwardHiddenAsync?: (ids: MlxArray, cache: Cache[]) => Promise<MlxArray>;
|
|
@@ -457,7 +489,7 @@ export class MlxBatchExecutionGroup {
|
|
|
457
489
|
/** Projected aggregate KV of everything admitted (running + mid-prefill). */
|
|
458
490
|
get projectedKvBytes(): number {
|
|
459
491
|
let total = this.#running.reduce((a, r) => a + this.#rowKvBytes(r), 0);
|
|
460
|
-
if (this.#prefill) total += this.#rowKvBytes(
|
|
492
|
+
if (this.#prefill) for (const row of this.#prefill.rows) total += this.#rowKvBytes(row);
|
|
461
493
|
return total;
|
|
462
494
|
}
|
|
463
495
|
|
|
@@ -551,6 +583,14 @@ export class MlxBatchExecutionGroup {
|
|
|
551
583
|
get active() { return scheduler.#running.length; },
|
|
552
584
|
get queued() { return scheduler.#pending.length; },
|
|
553
585
|
get preparing() { return scheduler.#prefill !== null; },
|
|
586
|
+
get preparingRows() { return scheduler.#prefill?.rows.length ?? 0; },
|
|
587
|
+
get canPrepareMore() { return scheduler.#prefill?.canAdmit === true; },
|
|
588
|
+
get preparingTokens() { return scheduler.#prefill?.tokenWeight ?? 0; },
|
|
589
|
+
// Cache restoration belongs to preparation. A queued request's prompt
|
|
590
|
+
// length is an upper bound until its cache owner resolves the prefix.
|
|
591
|
+
get nextPreparationTokens() { return scheduler.#pending[0]?.promptTokens ?? 0; },
|
|
592
|
+
get maxPreparationTokens() { return scheduler.#prefillBatchTokenLimit; },
|
|
593
|
+
get preparationPublishedOutput() { return scheduler.#preparationPublishedOutput; },
|
|
554
594
|
get maxActive() { return scheduler.#maxBatch; },
|
|
555
595
|
get admissionHeld() { return scheduler.#admissionHeld?.() === true; },
|
|
556
596
|
get closed() { return scheduler.#closed; },
|
|
@@ -563,18 +603,22 @@ export class MlxBatchExecutionGroup {
|
|
|
563
603
|
}
|
|
564
604
|
},
|
|
565
605
|
admitNext: () => this.#admitNext(),
|
|
566
|
-
canBurst: () => this.#
|
|
567
|
-
|
|
606
|
+
canBurst: () => this.#contextCompatible(this.#pending[0]!) &&
|
|
607
|
+
(this.#kvBudgetBytes === undefined ||
|
|
608
|
+
this.projectedKvBytes + this.#rowKvBytes(this.#pending[0]!) <= this.#kvBudgetBytes),
|
|
568
609
|
advancePreparation: () => this.#advancePreparation(),
|
|
569
|
-
advance: () => this.#
|
|
570
|
-
Math.max(0, ...this.#running.map((row) => row.promptTokens + row.sampled)),
|
|
571
|
-
() => this.#step()),
|
|
610
|
+
advance: () => this.#method ? this.#method.advance() : this.#step(),
|
|
572
611
|
failActive: (error) => {
|
|
573
612
|
for (const row of this.#running) row.reject(error);
|
|
574
613
|
this.#applyFilter([], true); // failed state never enters the prefix store
|
|
575
614
|
},
|
|
576
615
|
failAll: (error) => this.#failAll(error),
|
|
577
|
-
reserveResidency: () =>
|
|
616
|
+
reserveResidency: () => {
|
|
617
|
+
const release = acquireModelWiredLimit(this.model);
|
|
618
|
+
return () => disposeResources([
|
|
619
|
+
{ dispose: () => this.#leaveContext() }, { dispose: release },
|
|
620
|
+
]);
|
|
621
|
+
},
|
|
578
622
|
...(this.#lock ? { acquireExecution: () => this.#lock!.acquire() } : {}),
|
|
579
623
|
waitForWork: async () => {
|
|
580
624
|
await new Promise<void>((resolve) => { this.#wake = resolve; });
|
|
@@ -589,66 +633,106 @@ export class MlxBatchExecutionGroup {
|
|
|
589
633
|
} finally { this.#looping = false; }
|
|
590
634
|
}
|
|
591
635
|
|
|
636
|
+
#contextCompatible(row: Row): boolean {
|
|
637
|
+
return (!this.#running.length && !this.#prefill) || ((row.req.context?.key ?? "") === this.#contextKey && row.req.method?.key === this.#methodKey && row.req.statePolicy?.key === this.#statePolicy?.key);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
#leaveContext(): void {
|
|
641
|
+
const release = this.#releaseContext;
|
|
642
|
+
this.#releaseContext = undefined;
|
|
643
|
+
this.#contextKey = undefined;
|
|
644
|
+
release?.();
|
|
645
|
+
}
|
|
646
|
+
|
|
592
647
|
#admitNext(): boolean {
|
|
593
|
-
if (!this.#kvAdmits(this.#pending[0]!)) return false;
|
|
648
|
+
if (!this.#contextCompatible(this.#pending[0]!) || !this.#kvAdmits(this.#pending[0]!)) return false;
|
|
594
649
|
const row = this.#pending.shift()!;
|
|
595
|
-
let owned: { caches: Cache[]; retain?: () => void } | undefined;
|
|
596
|
-
let closePrefill: (() => void) | undefined;
|
|
597
650
|
try {
|
|
651
|
+
this.#statePolicy = row.req.statePolicy;
|
|
652
|
+
row.cacheNamespace = typeof row.req.cacheNamespace === "function"
|
|
653
|
+
? row.req.cacheNamespace() : row.req.cacheNamespace;
|
|
654
|
+
const key = row.req.context?.key ?? "";
|
|
655
|
+
if (key !== this.#contextKey) {
|
|
656
|
+
this.#leaveContext();
|
|
657
|
+
this.#releaseContext = row.req.context?.enter();
|
|
658
|
+
this.#contextKey = key;
|
|
659
|
+
}
|
|
598
660
|
row.req.onAdmitted?.();
|
|
599
661
|
row.admittedAt = performance.now();
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
662
|
+
if (row.req.method?.key !== this.#methodKey) {
|
|
663
|
+
this.#method?.dispose(); this.#method = undefined; this.#methodKey = undefined;
|
|
664
|
+
if (row.req.method) {
|
|
665
|
+
const executor = this;
|
|
666
|
+
this.#method = row.req.method.open({
|
|
667
|
+
get rows() { return executor.#running; }, runtime: this.#runtime,
|
|
668
|
+
prefillChunkSize: this.#prefillChunkSize, promptCache: this.#promptCache,
|
|
669
|
+
join: row => { this.#running.push(row); },
|
|
670
|
+
filterRows: keep => this.#applyFilter(keep),
|
|
671
|
+
publish: async (row, token, logprobs) => {
|
|
672
|
+
row.req.signal?.throwIfAborted();
|
|
673
|
+
this.#firstOutput(row);
|
|
674
|
+
return row.req.onToken(token, logprobs);
|
|
675
|
+
},
|
|
676
|
+
finish: (row, reason) => this.#finish(row, reason),
|
|
677
|
+
});
|
|
678
|
+
this.#methodKey = row.req.method.key;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (this.#prefill) {
|
|
682
|
+
this.#prefill.admit!(row);
|
|
683
|
+
} else if (this.#method) {
|
|
684
|
+
this.#prefill = this.#method.prepare(row);
|
|
685
|
+
} else {
|
|
686
|
+
const preparation = new MlxPrefillCohort({
|
|
687
|
+
model: this.model, chunkSize: this.#prefillChunkSize, tailSplit: this.#prefillTailSplit,
|
|
688
|
+
promptCache: this.#promptCache, stateCodecs: this.#stateCodecs, maintain: this.#maintainKv ?? undefined,
|
|
689
|
+
forward: (ids, caches) => this.#forwardHidden(ids, caches),
|
|
690
|
+
project: (hidden, caches, completed) => this.#projectPrefill(hidden, caches, completed),
|
|
691
|
+
complete: (state, logits) => this.#completePrefill(state, logits),
|
|
692
|
+
resume: state => this.#completePrefill(state, null),
|
|
693
|
+
reject: (failed, error) => this.#rejectPreparingRows([failed], error),
|
|
694
|
+
});
|
|
695
|
+
preparation.admit(row); this.#prefill = preparation;
|
|
696
|
+
}
|
|
697
|
+
} catch (error) { row.reject(error); }
|
|
625
698
|
return true;
|
|
626
699
|
}
|
|
627
700
|
|
|
701
|
+
#rejectPreparingRows(rows: readonly Row[], error: unknown): void {
|
|
702
|
+
const retiring = new Set(rows);
|
|
703
|
+
const keep = this.#running.flatMap((row, index) => retiring.has(row) ? [] : [index]);
|
|
704
|
+
if (keep.length !== this.#running.length) this.#applyFilter(keep, true);
|
|
705
|
+
for (const row of rows) row.reject(error);
|
|
706
|
+
}
|
|
707
|
+
|
|
628
708
|
async #advancePreparation(): Promise<void> {
|
|
629
|
-
const p = this.#prefill
|
|
709
|
+
const p = this.#prefill!, rows = p.rows;
|
|
710
|
+
this.#preparationPublishedOutput = false;
|
|
630
711
|
try {
|
|
631
|
-
if (await
|
|
632
|
-
p.closePrefill?.();
|
|
633
|
-
this.#prefill = null;
|
|
634
|
-
}
|
|
712
|
+
if (await p.advance()) this.#prefill = null;
|
|
635
713
|
} catch (error) {
|
|
636
714
|
this.#prefill = null;
|
|
637
715
|
let failure = error;
|
|
638
|
-
try {
|
|
716
|
+
try { p.dispose(); }
|
|
639
717
|
catch (cleanupError) { failure = new AggregateError([error, cleanupError], "batch prefill and cleanup failed"); }
|
|
640
|
-
|
|
718
|
+
try { this.#rejectPreparingRows(rows, failure); }
|
|
719
|
+
catch (cleanupError) {
|
|
720
|
+
failure = new AggregateError([failure, cleanupError], "batch admission retirement failed");
|
|
721
|
+
for (const row of rows) row.reject(failure);
|
|
722
|
+
this.#failAll(failure);
|
|
723
|
+
}
|
|
641
724
|
}
|
|
642
725
|
}
|
|
643
726
|
|
|
644
727
|
#failAll(error: unknown): void {
|
|
645
728
|
const p = this.#prefill;
|
|
646
|
-
const rows = new Set([...this.#running, ...this.#pending, ...(p
|
|
729
|
+
const rows = new Set([...this.#running, ...this.#pending, ...(p?.rows ?? [])]);
|
|
647
730
|
const retain = this.#adoptedRetain;
|
|
648
|
-
const resources = [
|
|
649
|
-
{ dispose: () =>
|
|
731
|
+
const resources = [p, this.#method, ...(this.#inners ?? []), this.#pendingToks,
|
|
732
|
+
{ dispose: () => retain?.() }]
|
|
650
733
|
.filter((resource): resource is DisposableResource => resource != null);
|
|
651
734
|
this.#pending = []; this.#running = []; this.#prefill = null;
|
|
735
|
+
this.#method = undefined; this.#methodKey = undefined;
|
|
652
736
|
this.#inners = null; this.#pendingToks = null; this.#pendingReal = null;
|
|
653
737
|
this.#fullLeftPad = []; this.#adoptedRetain = null;
|
|
654
738
|
for (const row of rows) row.reject(error);
|
|
@@ -665,110 +749,105 @@ export class MlxBatchExecutionGroup {
|
|
|
665
749
|
* bytes bit-exact vs serial `--kv-quant config`. Gateway placement and the
|
|
666
750
|
* constructor both guarantee every named cache can convert. */
|
|
667
751
|
#quantizeSolo(solo: Cache[], trace?: PromptResponseTrace): void {
|
|
668
|
-
if (!this.#
|
|
752
|
+
if (!this.#maintainKv) return;
|
|
669
753
|
const close = trace?.begin("prefill.kv_maintenance", {
|
|
670
754
|
mechanism: "continuous",
|
|
671
755
|
});
|
|
672
756
|
try {
|
|
673
|
-
|
|
674
|
-
const e = this.#kvByLayer.get(i);
|
|
675
|
-
const c = solo[i]!;
|
|
676
|
-
if (!e || c.offset === 0) continue;
|
|
677
|
-
// Milestone 2: rotating layers convert too — RotatingKVCache.
|
|
678
|
-
// toQuantized → RotatingQuantizedKVCache, exactly maybeQuantizeKv's
|
|
679
|
-
// dispatch (generate.ts). Converted caches don't match either class
|
|
680
|
-
// (the four cache classes are siblings), so re-conversion never fires.
|
|
681
|
-
if (!(isPlainKvCache(c) || isRotatingPlainCache(c))) continue;
|
|
682
|
-
const q = c.toQuantized(e.groupSize, e.bits);
|
|
683
|
-
solo[i] = q;
|
|
684
|
-
ops.evalAll(q.state());
|
|
685
|
-
clearCache();
|
|
686
|
-
}
|
|
757
|
+
this.#maintainKv(solo);
|
|
687
758
|
} finally {
|
|
688
759
|
close?.();
|
|
689
760
|
}
|
|
690
761
|
}
|
|
691
762
|
|
|
692
|
-
/**
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
p.row.req.signal?.throwIfAborted();
|
|
698
|
-
const prompt = p.row.req.promptIds;
|
|
699
|
-
const chunkStart = p.pos;
|
|
700
|
-
const closeChunk = p.row.req.trace?.begin("prefill.chunk", {
|
|
701
|
-
mechanism: "continuous",
|
|
702
|
-
startToken: chunkStart,
|
|
703
|
-
});
|
|
704
|
-
try {
|
|
705
|
-
let h: MlxArray;
|
|
706
|
-
while (true) {
|
|
707
|
-
p.row.req.signal?.throwIfAborted();
|
|
708
|
-
const step = nextPrefillStep({ length: prompt.length, position: p.pos,
|
|
709
|
-
chunkSize: this.#prefillChunkSize, tailSplit: this.#prefillTailSplit, snapshotAt: p.snapAt });
|
|
710
|
-
const hidden = await executeMlxPrefillStep(
|
|
711
|
-
(ids, caches) => this.#forwardHidden(ids, caches), p.solo, prompt, step,
|
|
712
|
-
() => this.#quantizeSolo(p.solo, p.row.req.trace),
|
|
713
|
-
);
|
|
714
|
-
p.pos = step.end;
|
|
715
|
-
if (hidden) { h = hidden; break; }
|
|
716
|
-
if (step.snapshot) {
|
|
717
|
-
try { this.#promptCache!.put(prompt.slice(0, step.end), cloneKvCaches(p.solo, this.#stateCodecs)); }
|
|
718
|
-
catch (error) { console.warn(`batch-lane boundary snapshot skipped: ${(error as Error).message}`); }
|
|
719
|
-
p.snapAt = null;
|
|
720
|
-
}
|
|
721
|
-
if (step.batchYield) return false;
|
|
763
|
+
/** The target owns projection geometry; samplers receive individual rows. */
|
|
764
|
+
#projectPrefill(hidden: MlxArray, caches: Cache[], completed: readonly PrefillState[]): MlxArray {
|
|
765
|
+
for (const p of completed) {
|
|
766
|
+
p.closePrefill?.(); p.closePrefill = undefined;
|
|
767
|
+
p.row.closeTokenZero = p.row.req.trace?.begin("token_zero.total", { mechanism: "continuous" });
|
|
722
768
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const forceAttribution = !!p.row.req.trace &&
|
|
733
|
-
this.#runtime.value("MLX_BUN_P2R_SYNC") === "1";
|
|
734
|
-
if (forceAttribution) {
|
|
735
|
-
const closeForward = p.row.req.trace!.begin("token_zero.forward", {
|
|
736
|
-
mechanism: "continuous",
|
|
737
|
-
activeBytes: activeMemory(),
|
|
738
|
-
cacheBytes: cacheMemory(),
|
|
739
|
-
peakBytes: peakMemory(),
|
|
740
|
-
});
|
|
741
|
-
ops.evalAll([h, ...p.solo.flatMap((c) => c.state())]);
|
|
742
|
-
closeForward();
|
|
769
|
+
const attributed = this.#runtime.value("MLX_BUN_P2R_SYNC") === "1"
|
|
770
|
+
? completed.flatMap(p => p.row.req.trace ? [p.row.req.trace] : []) : [];
|
|
771
|
+
const begin = (phase: "token_zero.forward" | "token_zero.head") => attributed.map(trace => trace.begin(phase, {
|
|
772
|
+
mechanism: "continuous", activeBytes: activeMemory(), cacheBytes: cacheMemory(), peakBytes: peakMemory(),
|
|
773
|
+
}));
|
|
774
|
+
if (attributed.length) {
|
|
775
|
+
const closes = begin("token_zero.forward");
|
|
776
|
+
try { withResource(leaseCacheStates(caches), state => ops.evalAll([hidden, ...state])); }
|
|
777
|
+
finally { for (const close of closes) close(); }
|
|
743
778
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
779
|
+
// Project before selecting rows: M4 quantized projection can use different
|
|
780
|
+
// arithmetic at M=1. Tail-split preparation reaches this with N=1.
|
|
781
|
+
const logits = this.model.logitsFromHidden(hidden);
|
|
782
|
+
try {
|
|
783
|
+
if (attributed.length) {
|
|
784
|
+
const closes = begin("token_zero.head");
|
|
785
|
+
try { ops.evalAll([logits]); } finally { for (const close of closes) close(); }
|
|
786
|
+
}
|
|
787
|
+
return logits;
|
|
788
|
+
} catch (error) { logits.dispose(); throw error; }
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Sample borrowed logits from the shared target projection. */
|
|
792
|
+
async #completePrefill(p: PrefillState, logits: MlxArray | null): Promise<void> {
|
|
793
|
+
if (p.continuation) {
|
|
794
|
+
const saved = p.continuation, row = p.row;
|
|
795
|
+
row.req.continuation!.resumeSampling(saved);
|
|
796
|
+
const interval = row.req.continuation!.interval;
|
|
797
|
+
this.#checkpointAt.set(row, (Math.floor(saved.generatedTokens / interval) + 1) * interval);
|
|
798
|
+
row.firstTokenAt = performance.now();
|
|
799
|
+
row.closeTokenZero?.(); row.closeTokenZero = undefined;
|
|
800
|
+
this.#preparationPublishedOutput = true;
|
|
801
|
+
for (const token of saved.cacheTokens.slice(row.req.promptIds.length)) {
|
|
802
|
+
row.req.signal?.throwIfAborted();
|
|
803
|
+
if (await row.req.onToken(token) === false)
|
|
804
|
+
throw new Error("saved generation prefix triggered a terminal stop while replaying");
|
|
805
|
+
}
|
|
806
|
+
row.fed = saved.cacheTokens.slice(row.req.promptIds.length);
|
|
807
|
+
row.generated = saved.generatedTokens + 1;
|
|
808
|
+
row.sampled = saved.generatedTokens + 1;
|
|
809
|
+
row.cachedTokens = Math.min(saved.cacheTokens.length, row.promptTokens);
|
|
810
|
+
const stop = await this.#emit(row, saved.pendingToken);
|
|
811
|
+
if (stop !== "continue") {
|
|
812
|
+
this.#putOrDispose(p.solo, saved.cacheTokens, p.retain, row.cacheNamespace);
|
|
813
|
+
p.solo = [];
|
|
814
|
+
this.#finish(row, stop);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
await this.#mergeJoiner(p);
|
|
818
|
+
return;
|
|
758
819
|
}
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
820
|
+
if (!logits) throw new Error("ordinary prefill completed without logits");
|
|
821
|
+
const forceAttribution = !!p.row.req.trace && this.#runtime.value("MLX_BUN_P2R_SYNC") === "1";
|
|
822
|
+
const V = logits.shape[2]!;
|
|
823
|
+
const last2 = ops.reshape(logits, [1, V]);
|
|
762
824
|
const closeSample = forceAttribution
|
|
763
825
|
? p.row.req.trace!.begin("token_zero.sample", { mechanism: "continuous" })
|
|
764
826
|
: undefined;
|
|
765
|
-
|
|
827
|
+
let sampled: MlxArray;
|
|
828
|
+
try { sampled = p.row.req.sample!(last2, 0); } finally { last2.dispose(); }
|
|
829
|
+
// The first admission can seed the existing device-token register. Its
|
|
830
|
+
// first decode forward then overlaps token-zero readback, just like every
|
|
831
|
+
// later pipeline step. Grammar and explicit eager-output modes retain
|
|
832
|
+
// their read-before-advance order.
|
|
833
|
+
if (!this.#inners && !p.row.req.grammar && !this.#noPipeline &&
|
|
834
|
+
this.#runtime.value("MLX_BUN_EARLY_FIRST_TOKEN") !== "1" && !forceAttribution) {
|
|
835
|
+
let owned: MlxArray | null = sampled;
|
|
836
|
+
try {
|
|
837
|
+
ops.asyncEvalAll([sampled]);
|
|
838
|
+
p.row.sampled = 1;
|
|
839
|
+
this.#quantizeSolo(p.solo, p.row.req.trace);
|
|
840
|
+
await this.#mergeJoiner(p);
|
|
841
|
+
this.#pendingToks = sampled;
|
|
842
|
+
this.#pendingReal = [true];
|
|
843
|
+
owned = null;
|
|
844
|
+
return;
|
|
845
|
+
} finally { owned?.dispose(); }
|
|
846
|
+
}
|
|
847
|
+
const tok = this.#readToken(sampled);
|
|
766
848
|
closeSample?.();
|
|
767
|
-
closeTokenZero?.();
|
|
768
|
-
last2.dispose();
|
|
769
849
|
p.row.sampled = 1;
|
|
770
850
|
p.row.generated = 1;
|
|
771
|
-
clearCache(); // drop the prefill transients (serial's token-0 clear)
|
|
772
851
|
|
|
773
852
|
// Grammar (B1): the mask0 was applied inside the sample closure (the
|
|
774
853
|
// controller is primed at compile). After reading token 0, advance the
|
|
@@ -782,17 +861,17 @@ export class MlxBatchExecutionGroup {
|
|
|
782
861
|
const stop = await this.#emit(p.row, tok);
|
|
783
862
|
// Token 0 was sampled but never fed — the caches cover exactly the
|
|
784
863
|
// prompt, a clean prompt-only entry (put-or-dispose).
|
|
785
|
-
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain);
|
|
864
|
+
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain, p.row.cacheNamespace);
|
|
786
865
|
this.#finish(p.row, stop === "continue" ? "stop" : stop);
|
|
787
|
-
return
|
|
866
|
+
return;
|
|
788
867
|
}
|
|
789
868
|
}
|
|
790
869
|
|
|
791
870
|
const stop = await this.#emit(p.row, tok);
|
|
792
871
|
if (stop !== "continue") {
|
|
793
|
-
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain);
|
|
872
|
+
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain, p.row.cacheNamespace);
|
|
794
873
|
this.#finish(p.row, stop);
|
|
795
|
-
return
|
|
874
|
+
return;
|
|
796
875
|
}
|
|
797
876
|
// Default tail-split path: the caches were already converted at the head
|
|
798
877
|
// boundary above (oracle composition: prefill ids[:-1] → convert → L=1
|
|
@@ -803,10 +882,7 @@ export class MlxBatchExecutionGroup {
|
|
|
803
882
|
// (before decode step 1 == before the merge).
|
|
804
883
|
this.#quantizeSolo(p.solo, p.row.req.trace);
|
|
805
884
|
await this.#mergeJoiner(p);
|
|
806
|
-
return
|
|
807
|
-
} finally {
|
|
808
|
-
closeChunk?.();
|
|
809
|
-
}
|
|
885
|
+
return;
|
|
810
886
|
}
|
|
811
887
|
|
|
812
888
|
/** Merge a fully-prefilled joiner with the running batch, layer by layer
|
|
@@ -814,6 +890,7 @@ export class MlxBatchExecutionGroup {
|
|
|
814
890
|
* first so the row set is settled and the next step starts cold. */
|
|
815
891
|
async #mergeJoiner(p: PrefillState): Promise<void> {
|
|
816
892
|
await this.#flushPipeline();
|
|
893
|
+
this.#maintainKv?.prepareBatch?.(p.solo);
|
|
817
894
|
|
|
818
895
|
// ADOPT, don't copy (unified-engine plan Phase 3.2): a row joining an
|
|
819
896
|
// EMPTY batch keeps its solo caches as the batch inners — a pointer
|
|
@@ -826,9 +903,9 @@ export class MlxBatchExecutionGroup {
|
|
|
826
903
|
// take/put become possible for it. The rot branch below knows how to
|
|
827
904
|
// treat an adopted RotatingKVCache as the merge's first row.
|
|
828
905
|
if (!this.#inners) {
|
|
829
|
-
this.#inners = p.solo as LayerInner[];
|
|
906
|
+
this.#inners = p.solo as LayerInner[]; p.solo = [];
|
|
830
907
|
this.#fullLeftPad = [0];
|
|
831
|
-
this.#adoptedRetain = p.retain ?? null;
|
|
908
|
+
this.#adoptedRetain = p.retain ?? null; p.retain = undefined;
|
|
832
909
|
this.#running.push(p.row);
|
|
833
910
|
return;
|
|
834
911
|
}
|
|
@@ -839,17 +916,17 @@ export class MlxBatchExecutionGroup {
|
|
|
839
916
|
const newInners: LayerInner[] = [];
|
|
840
917
|
let newFullPad = this.#fullLeftPad;
|
|
841
918
|
for (let layer = 0; layer < this.#kinds.length; layer++) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
throw new Error(`batch-capable layer ${layer} lost its cache capability`);
|
|
846
|
-
const merged = solo.makeEmptyBatch();
|
|
919
|
+
const solo = p.solo[layer]!;
|
|
920
|
+
const merged = ownedCacheLayout(solo);
|
|
921
|
+
if (merged) {
|
|
847
922
|
const previous = prev?.[layer];
|
|
848
923
|
merged.mergeRows(previous ? [previous, solo] : [solo]);
|
|
849
924
|
newFullPad = [...merged.leftPad];
|
|
850
925
|
newInners.push(merged);
|
|
851
926
|
continue;
|
|
852
927
|
}
|
|
928
|
+
if (this.#kinds[layer] === "owned-batch")
|
|
929
|
+
throw new Error(`batch-capable layer ${layer} lost its cache capability`);
|
|
853
930
|
if (this.#kinds[layer] === "ssm") {
|
|
854
931
|
// No temporal axis, no left-pad: B-axis concat of the state slots.
|
|
855
932
|
// mergeRows steals the solo arrays when the batch starts cold, so the
|
|
@@ -1049,8 +1126,9 @@ export class MlxBatchExecutionGroup {
|
|
|
1049
1126
|
// run its retain now. The joiner's likewise after its solo dispose.
|
|
1050
1127
|
this.#adoptedRetain?.();
|
|
1051
1128
|
this.#adoptedRetain = null;
|
|
1052
|
-
|
|
1053
|
-
p.retain
|
|
1129
|
+
const completed = p.solo; p.solo = [];
|
|
1130
|
+
const release = p.retain; p.retain = undefined;
|
|
1131
|
+
disposeResources([...completed, { dispose: () => release?.() }]);
|
|
1054
1132
|
// Every row in a REAL merge has its KV interleaved in batched buffers —
|
|
1055
1133
|
// no longer prompt-cache put() candidates.
|
|
1056
1134
|
for (const r of this.#running) r.merged = true;
|
|
@@ -1197,10 +1275,8 @@ export class MlxBatchExecutionGroup {
|
|
|
1197
1275
|
if (vecOk) {
|
|
1198
1276
|
const flat = ops.reshape(lg, [B, V]);
|
|
1199
1277
|
lg.dispose();
|
|
1200
|
-
|
|
1201
|
-
flat.dispose();
|
|
1202
|
-
nextToks = ops.argmaxAxis(lp, -1); // [B]
|
|
1203
|
-
lp.dispose();
|
|
1278
|
+
try { nextToks = independentGreedySampling.sample(flat); }
|
|
1279
|
+
finally { flat.dispose(); }
|
|
1204
1280
|
for (const row of rows) if (row.sampled < row.req.maxTokens) row.sampled++;
|
|
1205
1281
|
} else {
|
|
1206
1282
|
const sampled: MlxArray[] = [];
|
|
@@ -1215,7 +1291,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1215
1291
|
const rl = lg.slice([b, 0, 0], [b + 1, 1, V]);
|
|
1216
1292
|
const rl2 = ops.reshape(rl, [1, V]);
|
|
1217
1293
|
rl.dispose();
|
|
1218
|
-
sampled.push(row.req.sample(rl2, row.sampled));
|
|
1294
|
+
sampled.push(row.req.sample!(rl2, row.sampled));
|
|
1219
1295
|
row.sampled++;
|
|
1220
1296
|
rl2.dispose();
|
|
1221
1297
|
}
|
|
@@ -1246,6 +1322,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1246
1322
|
// set from process start `prev` is always null, so the flush below IS the
|
|
1247
1323
|
// whole phase 2. Same math either way (pipelining is scheduling).
|
|
1248
1324
|
if (this.#noPipeline) {
|
|
1325
|
+
if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
|
|
1249
1326
|
await this.#flushPipeline();
|
|
1250
1327
|
return;
|
|
1251
1328
|
}
|
|
@@ -1266,9 +1343,37 @@ export class MlxBatchExecutionGroup {
|
|
|
1266
1343
|
await this.#emitRows(toks); // also filters #pendingToks on eviction
|
|
1267
1344
|
if (this.#stepTrace) { STEP_T.emit += performance.now() - tEmit; STEP_T.n++; }
|
|
1268
1345
|
}
|
|
1346
|
+
if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
|
|
1269
1347
|
if (this.#stepTrace) STEP_T.lastEnd = performance.now();
|
|
1270
1348
|
}
|
|
1271
1349
|
|
|
1350
|
+
readonly #checkpointAt = new WeakMap<Row, number>();
|
|
1351
|
+
/** Cache extraction belongs to this existing ordinary numerical driver;
|
|
1352
|
+
* the optional request port owns checkpoint eligibility and persistence. */
|
|
1353
|
+
#captureContinuations(): void {
|
|
1354
|
+
if (!this.#pendingToks || !this.#running.some(row => row.req.continuation)) return;
|
|
1355
|
+
let pending: number[] | undefined;
|
|
1356
|
+
const failed = new Set<number>();
|
|
1357
|
+
for (let index = 0; index < this.#running.length; index++) {
|
|
1358
|
+
const row = this.#running[index]!, policy = row.req.continuation;
|
|
1359
|
+
if (!policy || row.fedTainted || row.fed.length !== row.generated ||
|
|
1360
|
+
row.generated < (this.#checkpointAt.get(row) ?? policy.interval) ||
|
|
1361
|
+
this.#pendingReal?.[index] === false) continue;
|
|
1362
|
+
pending ??= this.#pendingToks.toIntTokens();
|
|
1363
|
+
const tokens = [...row.req.promptIds, ...row.fed];
|
|
1364
|
+
const caches = this.#running.length === 1 && !row.merged
|
|
1365
|
+
? cloneSingleRowState(this.#inners! as Cache[], this.#stateCodecs)
|
|
1366
|
+
: this.#extractRowCaches(index, tokens.length);
|
|
1367
|
+
if (!caches) continue;
|
|
1368
|
+
try {
|
|
1369
|
+
policy.captureOwned({ caches, cacheTokens: tokens, generatedTokens: row.generated,
|
|
1370
|
+
pendingToken: pending[index]! });
|
|
1371
|
+
this.#checkpointAt.set(row, (Math.floor(row.generated / policy.interval) + 1) * policy.interval);
|
|
1372
|
+
} catch (error) { row.reject(error); failed.add(index); }
|
|
1373
|
+
}
|
|
1374
|
+
if (failed.size) this.#applyFilter(this.#running.flatMap((_, index) => failed.has(index) ? [] : [index]), true);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1272
1377
|
/** Read out the pipeline register (if any): emit its tokens and evict
|
|
1273
1378
|
* finished rows, leaving the pipeline cold. Called before a join merges. */
|
|
1274
1379
|
async #flushPipeline(): Promise<void> {
|
|
@@ -1382,7 +1487,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1382
1487
|
const rl = lg.slice([b, 0, 0], [b + 1, 1, V]);
|
|
1383
1488
|
const rl2 = ops.reshape(rl, [1, V]);
|
|
1384
1489
|
rl.dispose();
|
|
1385
|
-
sampled.push(row.req.sample(rl2, row.sampled));
|
|
1490
|
+
sampled.push(row.req.sample!(rl2, row.sampled));
|
|
1386
1491
|
row.sampled++;
|
|
1387
1492
|
rl2.dispose();
|
|
1388
1493
|
}
|
|
@@ -1408,6 +1513,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1408
1513
|
// rows finish("stop") via #emit's isTerminated check; the filter evicts.
|
|
1409
1514
|
// Cold start: prevVals empty (prefill emitted tok0) → nothing to emit.
|
|
1410
1515
|
if (prevVals.length) await this.#emitRows(prevVals);
|
|
1516
|
+
if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
|
|
1411
1517
|
}
|
|
1412
1518
|
|
|
1413
1519
|
/** Emit one read-back token per running row; evict finished rows. A row's
|
|
@@ -1420,7 +1526,6 @@ export class MlxBatchExecutionGroup {
|
|
|
1420
1526
|
for (let b = 0; b < B; b++) {
|
|
1421
1527
|
const row = rows[b]!;
|
|
1422
1528
|
row.generated++;
|
|
1423
|
-
if (row.generated === 1) row.firstTokenAt = performance.now();
|
|
1424
1529
|
let disp: "continue" | "stop" | "length";
|
|
1425
1530
|
try {
|
|
1426
1531
|
disp = await this.#emit(row, toks[b]!);
|
|
@@ -1452,10 +1557,12 @@ export class MlxBatchExecutionGroup {
|
|
|
1452
1557
|
/** Account one sampled token for a row. Mirrors generate(): EOS terminates
|
|
1453
1558
|
* WITHOUT an onToken call; otherwise onToken(token) runs and `false` halts;
|
|
1454
1559
|
* reaching maxTokens ends with "length". Advances row.current on continue. */
|
|
1455
|
-
async #emit(row: Row, token: number): Promise<"continue" | "stop" | "length"> {
|
|
1560
|
+
async #emit(row: Row, token: number, logprobs?: import("../../generate").TokenLogprobs): Promise<"continue" | "stop" | "length"> {
|
|
1456
1561
|
row.req.signal?.throwIfAborted();
|
|
1562
|
+
this.#firstOutput(row);
|
|
1563
|
+
if (row.generated === 1) clearCache();
|
|
1457
1564
|
if (row.req.eosTokenIds.includes(token)) return "stop";
|
|
1458
|
-
const cont = await row.req.onToken(token);
|
|
1565
|
+
const cont = await row.req.onToken(token, logprobs);
|
|
1459
1566
|
if (cont === false) return "stop";
|
|
1460
1567
|
// Grammar termination: the matcher is satisfied (e.g. closing `}` accepted).
|
|
1461
1568
|
// The final token has been delivered via onToken above; halt with "stop" so
|
|
@@ -1466,10 +1573,21 @@ export class MlxBatchExecutionGroup {
|
|
|
1466
1573
|
return "continue";
|
|
1467
1574
|
}
|
|
1468
1575
|
|
|
1576
|
+
#firstOutput(row: Row): void {
|
|
1577
|
+
if (row.generated === 1) {
|
|
1578
|
+
if (this.#prefill?.rows.includes(row)) this.#preparationPublishedOutput = true;
|
|
1579
|
+
row.firstTokenAt = performance.now();
|
|
1580
|
+
row.closeTokenZero?.();
|
|
1581
|
+
row.closeTokenZero = undefined;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1469
1585
|
#finish(row: Row, reason: "stop" | "length"): void {
|
|
1586
|
+
row.req.continuation?.complete();
|
|
1470
1587
|
const now = performance.now();
|
|
1471
1588
|
const first = row.firstTokenAt || now;
|
|
1472
1589
|
row.resolve({
|
|
1590
|
+
...(row.spec ? { spec: row.spec } : {}),
|
|
1473
1591
|
promptTokens: row.promptTokens,
|
|
1474
1592
|
generatedTokens: row.generated,
|
|
1475
1593
|
cachedTokens: row.cachedTokens,
|
|
@@ -1484,13 +1602,28 @@ export class MlxBatchExecutionGroup {
|
|
|
1484
1602
|
* offset lines up (defensive — a mismatch means the accounting is wrong
|
|
1485
1603
|
* and the entry would corrupt future hits), else dispose. `retain` rides
|
|
1486
1604
|
* along per the PromptCacheEntry contract (runs after dispose). */
|
|
1487
|
-
#putOrDispose(caches: Cache[], tokens: number[], retain?: () => void): void {
|
|
1605
|
+
#putOrDispose(caches: Cache[], tokens: number[], retain?: () => void, namespace = ""): void {
|
|
1488
1606
|
if (this.#promptCache) {
|
|
1607
|
+
// A never-merged row can still own a row layout. Persistence receives
|
|
1608
|
+
// its existing serial representation, just like merged-row retirement.
|
|
1609
|
+
const extracted: Cache[] = [], replaced: Cache[] = [];
|
|
1610
|
+
let persistent: Cache[];
|
|
1611
|
+
try {
|
|
1612
|
+
persistent = caches.map(cache => {
|
|
1613
|
+
if (!isBatchableCache(cache) || cache.batchSize !== 1) return cache;
|
|
1614
|
+
const row = cache.extractRow(0);
|
|
1615
|
+
extracted.push(row); replaced.push(cache);
|
|
1616
|
+
return row;
|
|
1617
|
+
});
|
|
1618
|
+
} catch (error) { disposeResources(extracted); throw error; }
|
|
1619
|
+
try { disposeResources(replaced); }
|
|
1620
|
+
catch (error) { disposeResources(extracted); throw error; }
|
|
1621
|
+
caches = persistent;
|
|
1489
1622
|
const withOff = caches.find(
|
|
1490
1623
|
(c) => typeof (c as { offset?: unknown }).offset === "number",
|
|
1491
1624
|
) as { offset: number } | undefined;
|
|
1492
1625
|
if (withOff && withOff.offset === tokens.length) {
|
|
1493
|
-
this.#promptCache.put(tokens, caches,
|
|
1626
|
+
this.#promptCache.put(tokens, caches, namespace, retain);
|
|
1494
1627
|
return;
|
|
1495
1628
|
}
|
|
1496
1629
|
}
|
|
@@ -1515,8 +1648,8 @@ export class MlxBatchExecutionGroup {
|
|
|
1515
1648
|
// slices depend on this step's KV writes): async — the batched source
|
|
1516
1649
|
// buffers free once the copies land, instead of being pinned by a lazy
|
|
1517
1650
|
// graph inside an idle cache entry.
|
|
1518
|
-
|
|
1519
|
-
this.#putOrDispose(caches, tokens);
|
|
1651
|
+
withResource(leaseCacheStates(caches), state => ops.asyncEvalAll([...state]));
|
|
1652
|
+
this.#putOrDispose(caches, tokens, undefined, row.cacheNamespace);
|
|
1520
1653
|
}
|
|
1521
1654
|
|
|
1522
1655
|
/** Row `b` of every layer as OWNED serial-class caches, or null when a
|
|
@@ -1562,6 +1695,11 @@ export class MlxBatchExecutionGroup {
|
|
|
1562
1695
|
/** Evict rows not in `keep` (sorted ascending) from the batched KV and the
|
|
1563
1696
|
* pipeline register. */
|
|
1564
1697
|
#applyFilter(keep: number[], dropOnly = false): void {
|
|
1698
|
+
if (this.#method) {
|
|
1699
|
+
this.#method.filterRows(keep, dropOnly);
|
|
1700
|
+
this.#running = keep.map(row => this.#running[row]!);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1565
1703
|
const inners = this.#inners!;
|
|
1566
1704
|
if (keep.length === 0) {
|
|
1567
1705
|
// Prompt-cache put (Phase 3.2): a lone NEVER-MERGED row's inners are
|
|
@@ -1579,6 +1717,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1579
1717
|
inners as Cache[],
|
|
1580
1718
|
[...solo.req.promptIds, ...solo.fed],
|
|
1581
1719
|
this.#adoptedRetain ?? undefined,
|
|
1720
|
+
solo.cacheNamespace,
|
|
1582
1721
|
);
|
|
1583
1722
|
} else {
|
|
1584
1723
|
for (const c of inners) c.dispose();
|
|
@@ -1593,6 +1732,11 @@ export class MlxBatchExecutionGroup {
|
|
|
1593
1732
|
this.#pendingReal = null;
|
|
1594
1733
|
return;
|
|
1595
1734
|
}
|
|
1735
|
+
const keptFullPad = keep.map((i) => this.#fullLeftPad[i]!);
|
|
1736
|
+
// mlx-lm BatchKVCache.filter removes padding shared by all survivors.
|
|
1737
|
+
// Full-attention width and padding move together; each row's absolute
|
|
1738
|
+
// RoPE position stays unchanged. Rotating caches keep their own layout.
|
|
1739
|
+
const trimFull = this.#kinds.includes("full") ? Math.min(...keptFullPad) : 0;
|
|
1596
1740
|
const out: LayerInner[] = [];
|
|
1597
1741
|
for (const inner of inners) {
|
|
1598
1742
|
if (isBatchableCache(inner)) {
|
|
@@ -1607,18 +1751,18 @@ export class MlxBatchExecutionGroup {
|
|
|
1607
1751
|
throw new Error("applyFilter: adopted serial rotating cache cannot be row-filtered");
|
|
1608
1752
|
} else if (isQuantizedKvCache(inner)) {
|
|
1609
1753
|
const [k0, v0] = inner.temporalView();
|
|
1610
|
-
const f = filterQuantRows(k0, v0, keep);
|
|
1754
|
+
const f = filterQuantRows(k0, v0, keep, trimFull);
|
|
1611
1755
|
for (const t of [k0, v0]) { t.packed.dispose(); t.scales.dispose(); t.biases.dispose(); }
|
|
1612
1756
|
const c = new QuantizedKVCache(inner.groupSize, inner.bits);
|
|
1613
|
-
c.restoreState(f.keys, f.values, inner.offset);
|
|
1757
|
+
c.restoreState(f.keys, f.values, inner.offset - trimFull);
|
|
1614
1758
|
out.push(c);
|
|
1615
1759
|
inner.dispose();
|
|
1616
1760
|
} else if (isPlainKvCache(inner)) {
|
|
1617
1761
|
const [k0, v0] = inner.temporalView();
|
|
1618
|
-
const f = filterKVRows(k0, v0, keep);
|
|
1762
|
+
const f = filterKVRows(k0, v0, keep, trimFull);
|
|
1619
1763
|
k0.dispose(); v0.dispose();
|
|
1620
1764
|
const c = new KVCache();
|
|
1621
|
-
c.restoreState(f.keys, f.values, inner.offset);
|
|
1765
|
+
c.restoreState(f.keys, f.values, inner.offset - trimFull);
|
|
1622
1766
|
out.push(c);
|
|
1623
1767
|
inner.dispose();
|
|
1624
1768
|
} else {
|
|
@@ -1626,7 +1770,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1626
1770
|
}
|
|
1627
1771
|
}
|
|
1628
1772
|
this.#inners = out;
|
|
1629
|
-
this.#fullLeftPad =
|
|
1773
|
+
this.#fullLeftPad = keptFullPad.map((padding) => padding - trimFull);
|
|
1630
1774
|
this.#running = keep.map((i) => this.#running[i]!);
|
|
1631
1775
|
if (this.#pendingToks) {
|
|
1632
1776
|
const idx = ops.fromInt32(keep, [keep.length]);
|