mlx-bun 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -32
- package/docs/README.md +58 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +1535 -50
- package/docs/design/decode-speed-program.md +394 -1171
- package/docs/design/generic-model-support.md +195 -3
- package/docs/design/kv-cache.md +538 -225
- package/docs/design/speculative-decoding.md +552 -50
- package/docs/design/turboquant.md +140 -17
- package/docs/design/unified-engine-frontier-plan.md +259 -150
- package/docs/planning/PRODUCT_ROADMAP.md +21 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3537 -1112
- package/docs/reference/cli.md +102 -2
- package/docs/reference/distribution.md +45 -13
- package/docs/reference/environment.md +159 -13
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +28 -7
- package/docs/reference/server-api.md +257 -33
- package/docs/reference/server-config.md +372 -151
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +464 -236
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +107 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +122 -19
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +161 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +5 -4
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +100 -0
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +244 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +54 -0
- package/src/backends/mlx/rollback.ts +45 -4
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +51 -73
- package/src/backends/mlx/speculative-group.ts +438 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
- package/src/cli.ts +333 -36
- package/src/contracts/execution.ts +21 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +33 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/engine/execution-plan.ts +38 -25
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +32 -7
- package/src/fill/fill-session.ts +14 -3
- package/src/fit.ts +29 -15
- package/src/generate.ts +105 -195
- package/src/grammar.ts +13 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/prefill.ts +6 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +92 -12
- package/src/kv-store.ts +183 -93
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
- package/src/lab/paged-kv/paged-kv.ts +126 -46
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +39 -1
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +94 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +57 -0
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +183 -82
- package/src/model/gemma4.ts +116 -28
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +70 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +177 -75
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +4 -4
- package/src/model/turboquant-codec.ts +89 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +306 -86
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +37 -25
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +19 -14
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +88 -38
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/model-host.ts +3 -4
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +6 -3
- package/src/serve/request-prep.ts +40 -25
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +185 -261
- package/src/spec/assistant-rows.ts +93 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +115 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +230 -0
- package/src/spec/qwen-mtp-source.ts +84 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +73 -69
- package/src/spec/source.ts +93 -15
- package/src/spec/two-model-rows.ts +62 -0
- package/src/spec/two-model.ts +45 -103
- package/src/ssd-cache.ts +107 -36
- package/src/ssd-durability.ts +29 -27
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +217 -57
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { runMixedTokenIteration, type MlxForwardWork, type MlxPreparationWork } from "./mixed-iteration";
|
|
2
|
+
import type { MixedTokenModel } from "../../model/token-groups";
|
|
3
|
+
import type { OrdinaryContinuation } from "./continuation";
|
|
4
|
+
import type { MlxRequestStatePolicy } from "./request-state-policy";
|
|
1
5
|
import { AdmissionRejected } from "../../engine/admission";
|
|
2
|
-
import type { ExecutionGroup } from "../../contracts/scheduling";
|
|
6
|
+
import type { ExecutionContext, ExecutionGroup } from "../../contracts/scheduling";
|
|
3
7
|
import { driveExecutionGroup } from "../../engine/scheduler";
|
|
4
|
-
import { disposeResources } from "../../engine/resources";
|
|
8
|
+
import { disposeResources, withResource } from "../../engine/resources";
|
|
5
9
|
import type { DisposableResource } from "../../contracts/resources";
|
|
6
|
-
import {
|
|
7
|
-
import { executeMlxPrefillStep } from "./prefill";
|
|
8
|
-
import { enterMlxMemoryGuard, type MlxMemoryBudget } from "./memory-guard";
|
|
10
|
+
import { MlxPrefillCohort, type PrefillState } from "./prefill-cohort";
|
|
9
11
|
// Continuous-batching scheduler for `--batch N` serving (phase S2, the engine
|
|
10
12
|
// loop). Owns ONE running batch and drives it forward one decode step at a
|
|
11
13
|
// time, admitting waiting requests and evicting finished ones between steps —
|
|
@@ -63,6 +65,8 @@ import { enterMlxMemoryGuard, type MlxMemoryBudget } from "./memory-guard";
|
|
|
63
65
|
// optimization is a later refinement (batching-v2-plan item a).
|
|
64
66
|
|
|
65
67
|
import { MlxArray } from "../../mlx/array";
|
|
68
|
+
import type { PrefillPolicy } from "../../inference/prefill";
|
|
69
|
+
import { resolveMlxPrefillPolicy } from "./prefill-policy";
|
|
66
70
|
import * as ops from "../../mlx/ops";
|
|
67
71
|
import { activeMemory, cacheMemory, clearCache, Dtype, peakMemory } from "../../mlx/ffi";
|
|
68
72
|
import { runtimeConfig, withRuntimeConfig, type RuntimeConfig } from "../../runtime-config";
|
|
@@ -87,14 +91,17 @@ import {
|
|
|
87
91
|
type QuantRow,
|
|
88
92
|
} from "../../model/batched-quant";
|
|
89
93
|
import { BatchedRotatingQuantCache } from "../../model/batched-rotating-quant";
|
|
90
|
-
import
|
|
94
|
+
import { cloneSingleRowState, leaseCacheStates } from "./state-views";
|
|
95
|
+
import { ownedCacheLayout } from "./cache-layout";
|
|
96
|
+
import { createKvMaintenance, type KvMaintenance } from "./kv-maintenance";
|
|
91
97
|
import type { KvScheme } from "../../kv-scheme";
|
|
92
98
|
import { BatchedRotatingCache } from "../../model/batched-rotating";
|
|
93
|
-
import { cloneKvCaches } from "../../kv-store";
|
|
94
99
|
import { SSMCache } from "../../model/qwen3-delta";
|
|
95
100
|
import type { RuntimeModel } from "../../model/factory";
|
|
96
101
|
import type { GrammarController } from "../../grammar";
|
|
97
|
-
import {
|
|
102
|
+
import { independentGreedySampling } from "../../sampler";
|
|
103
|
+
import { ExecutionTasks } from "../../engine/execution-tasks";
|
|
104
|
+
import { CancellationSource, GenerationCancelled } from "../../engine/cancellation";
|
|
98
105
|
import { acquireModelWiredLimit } from "../../generate";
|
|
99
106
|
import { batchRowKvBytes } from "../../serve/kv-budget";
|
|
100
107
|
import type { PromptResponseTrace } from "../../serve/prompt-response-trace";
|
|
@@ -132,19 +139,70 @@ export function stepTraceReport(): string {
|
|
|
132
139
|
* step 0..maxTokens-1 for this row (never beyond maxTokens). */
|
|
133
140
|
export type RowSampler = (logits1V: MlxArray, step: number) => MlxArray;
|
|
134
141
|
|
|
135
|
-
|
|
142
|
+
/** A method prepares and advances rows; the shared executor owns queueing. */
|
|
143
|
+
export interface MlxGroupPreparation {
|
|
144
|
+
readonly rows: readonly Row[];
|
|
145
|
+
readonly tokenWeight?: number;
|
|
146
|
+
readonly canAdmit?: boolean;
|
|
147
|
+
readonly supportsMixedWork?: boolean;
|
|
148
|
+
admit?(row: Row): void;
|
|
149
|
+
advance(work?: MlxPreparationWork): Promise<boolean>;
|
|
150
|
+
dispose(): void;
|
|
151
|
+
}
|
|
152
|
+
export interface MlxGroupMethodHost {
|
|
153
|
+
readonly rows: readonly Row[];
|
|
154
|
+
readonly runtime: RuntimeConfig;
|
|
155
|
+
readonly prefillChunkSize: number;
|
|
156
|
+
readonly promptCache: RowPromptCache | undefined;
|
|
157
|
+
join(row: Row): void;
|
|
158
|
+
filterRows(keep: number[]): void;
|
|
159
|
+
publish(row: Row, token: number, logprobs?: import("../../generate").TokenLogprobs): Promise<void | boolean>;
|
|
160
|
+
finish(row: Row, reason: "stop" | "length"): void;
|
|
161
|
+
}
|
|
162
|
+
export interface MlxGroupedMethod {
|
|
163
|
+
/** Maximum target tokens in the next iteration, including candidates. */
|
|
164
|
+
readonly runningTokens?: number;
|
|
165
|
+
prepare(row: Row): MlxGroupPreparation;
|
|
166
|
+
advance(work?: MlxForwardWork): Promise<void>;
|
|
167
|
+
filterRows(keep: readonly number[], discard: boolean): void;
|
|
168
|
+
dispose(): void;
|
|
169
|
+
}
|
|
170
|
+
export interface MlxGroupMethodRequest {
|
|
171
|
+
/** Equal keys declare compatible method state, independent of queue size. */
|
|
172
|
+
readonly key: string;
|
|
173
|
+
readonly data: unknown;
|
|
174
|
+
open(host: MlxGroupMethodHost): MlxGroupedMethod;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type BatchRequest = BatchRequestFields & (
|
|
178
|
+
| { method: MlxGroupMethodRequest; sample?: RowSampler }
|
|
179
|
+
| { method?: undefined; sample: RowSampler }
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
interface BatchRequestFields {
|
|
183
|
+
/** Borrowed prepared input; its owner retains it through preparation. */
|
|
184
|
+
promptInput?: import("./prompt-input").MlxPromptInput;
|
|
185
|
+
cacheSessionId?: string;
|
|
186
|
+
/** Optional ordinary continuation policy; owns persistence and sampler recovery. */
|
|
187
|
+
continuation?: OrdinaryContinuation;
|
|
188
|
+
statePolicy?: MlxRequestStatePolicy;
|
|
189
|
+
context?: ExecutionContext;
|
|
190
|
+
/** Resolve loaded-state identity under the execution lease, after any
|
|
191
|
+
* queued adapter replacement has finished. */
|
|
192
|
+
cacheNamespace?: string | (() => string);
|
|
136
193
|
/** Resolved replay permission; standalone group callers retain auto mode. */
|
|
137
194
|
compiledDecode?: boolean;
|
|
138
195
|
promptIds: number[];
|
|
196
|
+
/** Request policy overrides the execution group's captured prefill default. */
|
|
197
|
+
prefillChunkSize?: number;
|
|
139
198
|
maxTokens: number;
|
|
140
199
|
eosTokenIds: number[];
|
|
141
|
-
sample: RowSampler;
|
|
142
200
|
/** Called per emitted (non-EOS) token, in order. Returning `false` halts this
|
|
143
201
|
* row (a decoded-text stop sequence fired) — matches generate()'s onToken
|
|
144
202
|
* contract. EOS terminates the row WITHOUT an onToken call. Throwing evicts
|
|
145
203
|
* THIS row only (its submit promise rejects; siblings continue). May be
|
|
146
204
|
* async; keep it cheap — it runs inline in the step loop. */
|
|
147
|
-
onToken: (token: number) => void | boolean | Promise<void | boolean>;
|
|
205
|
+
onToken: (token: number, logprobs?: import("../../generate").TokenLogprobs) => void | boolean | Promise<void | boolean>;
|
|
148
206
|
/** Diagnostic-only request-local trace. */
|
|
149
207
|
trace?: PromptResponseTrace;
|
|
150
208
|
/** Closes the gateway's queue/admission span when this row leaves pending. */
|
|
@@ -178,6 +236,8 @@ export interface BatchRequest {
|
|
|
178
236
|
}
|
|
179
237
|
|
|
180
238
|
export interface BatchStats {
|
|
239
|
+
spec?: import("../../generate").GenerateStats["spec"];
|
|
240
|
+
fill?: import("../../generate").GenerateStats["fill"];
|
|
181
241
|
promptTokens: number;
|
|
182
242
|
generatedTokens: number;
|
|
183
243
|
/** Prompt tokens served from the prompt cache (Phase 3.2): a joiner's solo
|
|
@@ -192,6 +252,8 @@ export interface BatchStats {
|
|
|
192
252
|
decodeMs: number;
|
|
193
253
|
}
|
|
194
254
|
|
|
255
|
+
let nextMixedWorkId = 0;
|
|
256
|
+
|
|
195
257
|
/** The slice of PromptCache the scheduler drives (structural — the server's
|
|
196
258
|
* PromptCache satisfies it). take() on admission (any joiner: a restored
|
|
197
259
|
* prefix + suffix prefill is byte-safe whether the row later merges or
|
|
@@ -200,14 +262,11 @@ export interface BatchStats {
|
|
|
200
262
|
* finish INSIDE a multi-row batch get their KV EXTRACTED per row into
|
|
201
263
|
* fresh serial caches first (mlx-lm server.py:872 extract_cache — the
|
|
202
264
|
* 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
|
-
}
|
|
265
|
+
export type RowPromptCache = import("./checkpoint-state").MlxPrefixCache;
|
|
209
266
|
|
|
210
|
-
interface Row {
|
|
267
|
+
export interface Row {
|
|
268
|
+
spec?: import("../../generate").GenerateStats["spec"];
|
|
269
|
+
fill?: import("../../generate").GenerateStats["fill"];
|
|
211
270
|
req: BatchRequest;
|
|
212
271
|
resolve: (s: BatchStats) => void;
|
|
213
272
|
reject: (e: unknown) => void;
|
|
@@ -220,6 +279,8 @@ interface Row {
|
|
|
220
279
|
/** performance.now() marks for BatchStats timing (0 = not reached). */
|
|
221
280
|
admittedAt: number;
|
|
222
281
|
firstTokenAt: number;
|
|
282
|
+
closeTokenZero?: () => void;
|
|
283
|
+
cacheNamespace?: string;
|
|
223
284
|
/** Generated tokens whose KV actually entered the cache (fed as a step
|
|
224
285
|
* input) — serial generate()'s `forwarded` list. promptIds+fed is the
|
|
225
286
|
* exact token coverage of the row's caches, the put() entry key.
|
|
@@ -242,30 +303,14 @@ interface Row {
|
|
|
242
303
|
merged: boolean;
|
|
243
304
|
}
|
|
244
305
|
|
|
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
306
|
/** Held while the batch is active; the serial fallback acquires the same lock. */
|
|
262
307
|
export interface ExclusiveLock {
|
|
263
308
|
acquire(): Promise<() => void>;
|
|
264
309
|
}
|
|
265
310
|
|
|
266
311
|
export interface MlxBatchExecutionGroupOptions {
|
|
267
|
-
/**
|
|
268
|
-
|
|
312
|
+
/** Scheduling work budget for extending an existing preparation cohort. */
|
|
313
|
+
prefillBatchTokenLimit?: number;
|
|
269
314
|
runtime?: RuntimeConfig;
|
|
270
315
|
maxQueued?: number;
|
|
271
316
|
stateCodecs?: import("../../kv-store").CacheCodecProvider;
|
|
@@ -290,6 +335,8 @@ export interface MlxBatchExecutionGroupOptions {
|
|
|
290
335
|
* the same chunk boundaries as the serial path, so a row's quantized bytes
|
|
291
336
|
* preserve the L2 composition. Unsupported schemes fail at construction. */
|
|
292
337
|
kvScheme?: KvScheme;
|
|
338
|
+
/** Model binding owns support for precision transitions. */
|
|
339
|
+
kvBatchCapabilities?: { delayedAffine?: boolean };
|
|
293
340
|
/** Prompt-cache hook (Phase 3.2): admission take()s the longest usable
|
|
294
341
|
* prefix into the joiner's solo caches (suffix-only prefill — the
|
|
295
342
|
* multi-turn chat TTFT path); rows that finish never-merged put() their
|
|
@@ -311,6 +358,7 @@ type LayerInner =
|
|
|
311
358
|
type Row1 = { keys: MlxArray; values: MlxArray };
|
|
312
359
|
|
|
313
360
|
export class MlxBatchExecutionGroup {
|
|
361
|
+
readonly #tasks = new ExecutionTasks();
|
|
314
362
|
readonly #runtime: RuntimeConfig;
|
|
315
363
|
readonly #noPipeline: boolean;
|
|
316
364
|
readonly #stepTrace: boolean;
|
|
@@ -320,7 +368,8 @@ export class MlxBatchExecutionGroup {
|
|
|
320
368
|
#inners: LayerInner[] | null = null; // per-layer batched KV; null when empty
|
|
321
369
|
#fullLeftPad: number[] = []; // per-row padding for FULL layers (rot self-tracks)
|
|
322
370
|
#pending: Row[] = [];
|
|
323
|
-
#prefill:
|
|
371
|
+
#prefill: MlxGroupPreparation | null = null; // the (single) joiner mid-prefill
|
|
372
|
+
#preparationPublishedOutput = false;
|
|
324
373
|
/** Sampled-but-unread token array [B], aligned with #running — the decode
|
|
325
374
|
* pipeline register. Filtered/disposed alongside the batched KV. */
|
|
326
375
|
#pendingToks: MlxArray | null = null;
|
|
@@ -329,7 +378,11 @@ export class MlxBatchExecutionGroup {
|
|
|
329
378
|
* accounting must skip it — see Row.fed/fedTainted). Filtered/cleared in
|
|
330
379
|
* lockstep with the register. */
|
|
331
380
|
#pendingReal: boolean[] | null = null;
|
|
381
|
+
#method: MlxGroupedMethod | undefined;
|
|
382
|
+
#methodKey: string | undefined;
|
|
383
|
+
#decodeStateKey: string | undefined;
|
|
332
384
|
#steps = 0; // decode-step counter (clearCache cadence)
|
|
385
|
+
#cacheMaintenanceSteps = 0;
|
|
333
386
|
#looping = false;
|
|
334
387
|
#closed = false;
|
|
335
388
|
#driver: Promise<void> | null = null;
|
|
@@ -338,24 +391,30 @@ export class MlxBatchExecutionGroup {
|
|
|
338
391
|
readonly #lock: ExclusiveLock | undefined;
|
|
339
392
|
readonly #admissionHeld: (() => boolean) | undefined;
|
|
340
393
|
readonly #prefillChunkSize: number;
|
|
394
|
+
readonly #prefillPolicy: PrefillPolicy;
|
|
395
|
+
readonly #prefillBatchTokenLimit: number;
|
|
341
396
|
readonly #prefillTailSplit: boolean;
|
|
342
397
|
readonly #kvBudgetBytes: number | undefined;
|
|
343
|
-
readonly #
|
|
344
|
-
|
|
398
|
+
readonly #defaultPromptCache: RowPromptCache | undefined;
|
|
399
|
+
#statePolicy?: MlxRequestStatePolicy;
|
|
400
|
+
get #promptCache(): RowPromptCache | undefined {
|
|
401
|
+
return this.#statePolicy ? this.#statePolicy.promptCache : this.#defaultPromptCache;
|
|
402
|
+
}
|
|
345
403
|
/** Retain hook of the currently-ADOPTED row's cache entry (at most one:
|
|
346
404
|
* only a lone adopted row holds un-copied entry caches). Runs after the
|
|
347
405
|
* adopted caches are disposed, or transfers back on put(). */
|
|
348
406
|
#adoptedRetain: (() => void) | null = null;
|
|
407
|
+
#contextKey: string | undefined;
|
|
408
|
+
#releaseContext: (() => void) | undefined;
|
|
349
409
|
readonly #kinds: ("full" | "rot" | "ssm" | "owned-batch")[];
|
|
350
410
|
readonly #rotMaxSize: number[]; // per-layer sliding window (rot layers only)
|
|
351
411
|
readonly #compressedProjectors: Array<(tokens: number) => number> | null;
|
|
352
412
|
readonly #batchCacheMaxTokens: number | null;
|
|
353
413
|
readonly #kvScheme: KvScheme | undefined;
|
|
354
|
-
|
|
355
|
-
readonly #kvByLayer: Map<number, KvQuantSpec> | null;
|
|
414
|
+
readonly #maintainKv: KvMaintenance | null;
|
|
356
415
|
/** Compiled decode runner for the B=1 serial-class case (Phase 3.2) —
|
|
357
416
|
* same eligibility gate as generate.ts (gemma dense, kill switch
|
|
358
|
-
* MLX_BUN_COMPILED_DECODE);
|
|
417
|
+
* MLX_BUN_COMPILED_DECODE); adapter requests disable replay in their plan. Set
|
|
359
418
|
* to null permanently on a failed step (serial disables per
|
|
360
419
|
* generation; the scheduler is one long-lived "generation"). */
|
|
361
420
|
#compiled: CompiledDecode | null;
|
|
@@ -370,17 +429,19 @@ export class MlxBatchExecutionGroup {
|
|
|
370
429
|
this.#maxBatch = Math.max(1, Math.floor(opts.maxBatch));
|
|
371
430
|
this.#lock = opts.lock;
|
|
372
431
|
this.#admissionHeld = opts.admissionHeld;
|
|
373
|
-
this.#
|
|
432
|
+
this.#prefillPolicy = resolveMlxPrefillPolicy(model.config, this.#runtime, opts.prefillChunkSize);
|
|
433
|
+
this.#prefillChunkSize = this.#prefillPolicy.chunkSize(0);
|
|
434
|
+
this.#prefillBatchTokenLimit = opts.prefillBatchTokenLimit ?? 2048;
|
|
374
435
|
this.#prefillTailSplit = this.#runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true);
|
|
375
436
|
this.#kvBudgetBytes = opts.kvBudgetBytes;
|
|
376
|
-
this.#
|
|
377
|
-
this.#promptCache = opts.promptCache;
|
|
437
|
+
this.#defaultPromptCache = opts.promptCache;
|
|
378
438
|
this.#kvScheme = opts.kvScheme;
|
|
379
439
|
const proto = withRuntimeConfig(this.#runtime, () => model.makeCache()); // fresh caches hold no buffers
|
|
380
440
|
if (this.#kvScheme && !this.#kvScheme.batchable(
|
|
381
441
|
model.config,
|
|
382
442
|
(layerIdx) =>
|
|
383
443
|
isPlainKvCache(proto[layerIdx]) || isRotatingPlainCache(proto[layerIdx]),
|
|
444
|
+
proto.length, opts.kvBatchCapabilities,
|
|
384
445
|
)) {
|
|
385
446
|
for (const cache of proto) cache.dispose();
|
|
386
447
|
throw new Error(`unsupported KV scheme for batch scheduler: ${this.#kvScheme.kind}`);
|
|
@@ -398,11 +459,10 @@ export class MlxBatchExecutionGroup {
|
|
|
398
459
|
? proto.map((cache) => (tokens: number) => cache.projectedBytes(tokens))
|
|
399
460
|
: null;
|
|
400
461
|
this.#batchCacheMaxTokens = proto.every(isBatchableCache)
|
|
401
|
-
? Math.min(...proto.map((cache) => cache.maxTokens ??
|
|
462
|
+
? Math.min(...proto.map((cache) => cache.maxTokens ?? Infinity))
|
|
402
463
|
: null;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
? new Map(kvConfig.map((entry) => [entry.layerIdx, entry]))
|
|
464
|
+
this.#maintainKv = this.#kvScheme?.quantized
|
|
465
|
+
? createKvMaintenance(this.#kvScheme.options)
|
|
406
466
|
: null;
|
|
407
467
|
this.#rotMaxSize = proto.map((c) => (isRotatingPlainCache(c) ? c.maxSize : 0));
|
|
408
468
|
for (const c of proto) c.dispose();
|
|
@@ -418,7 +478,7 @@ export class MlxBatchExecutionGroup {
|
|
|
418
478
|
}
|
|
419
479
|
|
|
420
480
|
get pendingRows(): number {
|
|
421
|
-
return this.#pending.length + (this.#prefill
|
|
481
|
+
return this.#pending.length + (this.#prefill?.rows.length ?? 0);
|
|
422
482
|
}
|
|
423
483
|
|
|
424
484
|
/** Projected KV bytes of one row at its worst case (full prompt + full
|
|
@@ -437,14 +497,6 @@ export class MlxBatchExecutionGroup {
|
|
|
437
497
|
);
|
|
438
498
|
}
|
|
439
499
|
|
|
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
500
|
async #forwardHidden(ids: MlxArray, cache: Cache[]): Promise<MlxArray> {
|
|
449
501
|
const asyncModel = this.model as RuntimeModel & {
|
|
450
502
|
forwardHiddenAsync?: (ids: MlxArray, cache: Cache[]) => Promise<MlxArray>;
|
|
@@ -457,7 +509,7 @@ export class MlxBatchExecutionGroup {
|
|
|
457
509
|
/** Projected aggregate KV of everything admitted (running + mid-prefill). */
|
|
458
510
|
get projectedKvBytes(): number {
|
|
459
511
|
let total = this.#running.reduce((a, r) => a + this.#rowKvBytes(r), 0);
|
|
460
|
-
if (this.#prefill) total += this.#rowKvBytes(
|
|
512
|
+
if (this.#prefill) for (const row of this.#prefill.rows) total += this.#rowKvBytes(row);
|
|
461
513
|
return total;
|
|
462
514
|
}
|
|
463
515
|
|
|
@@ -503,6 +555,7 @@ export class MlxBatchExecutionGroup {
|
|
|
503
555
|
if (this.#closed) return Promise.reject(new Error("scheduler closed"));
|
|
504
556
|
if (req.signal?.aborted) return Promise.reject(req.signal.reason);
|
|
505
557
|
if (this.#pending.length >= this.#maxQueued) return Promise.reject(new AdmissionRejected());
|
|
558
|
+
req = { ...req, prefillChunkSize: req.prefillChunkSize ?? this.#prefillPolicy.chunkSize(req.promptIds.length) };
|
|
506
559
|
return new Promise<BatchStats>((resolve, reject) => {
|
|
507
560
|
let abortListener: (() => void) | null = null;
|
|
508
561
|
const cleanup = () => {
|
|
@@ -530,6 +583,21 @@ export class MlxBatchExecutionGroup {
|
|
|
530
583
|
this.#ensureLoop();
|
|
531
584
|
}
|
|
532
585
|
|
|
586
|
+
/** Read-only native preparation borrows a boundary of the live execution
|
|
587
|
+
* group. Model mutation still requires the gateway's exclusive drain. */
|
|
588
|
+
runPreparation<T>(work: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
589
|
+
if (this.#closed) return Promise.reject(new Error("scheduler closed"));
|
|
590
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
591
|
+
const cancellation = signal ? new CancellationSource() : undefined;
|
|
592
|
+
const abort = () => cancellation?.cancel("requested");
|
|
593
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
594
|
+
const result = this.#tasks.enqueue(work, cancellation).catch(error => {
|
|
595
|
+
throw error instanceof GenerationCancelled && signal?.aborted ? signal.reason : error;
|
|
596
|
+
}).finally(() => signal?.removeEventListener("abort", abort));
|
|
597
|
+
this.#ensureLoop();
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
600
|
+
|
|
533
601
|
#ensureLoop(): void {
|
|
534
602
|
if (this.#wake) { this.#wake(); return; }
|
|
535
603
|
if (this.#looping || this.#closed) return;
|
|
@@ -548,9 +616,19 @@ export class MlxBatchExecutionGroup {
|
|
|
548
616
|
async #drive(): Promise<void> {
|
|
549
617
|
const scheduler = this;
|
|
550
618
|
const group: ExecutionGroup = {
|
|
619
|
+
get pendingTasks() { return scheduler.#tasks.pending; },
|
|
620
|
+
advanceTask: () => this.#tasks.advance(),
|
|
551
621
|
get active() { return scheduler.#running.length; },
|
|
552
622
|
get queued() { return scheduler.#pending.length; },
|
|
553
623
|
get preparing() { return scheduler.#prefill !== null; },
|
|
624
|
+
get preparingRows() { return scheduler.#prefill?.rows.length ?? 0; },
|
|
625
|
+
get canPrepareMore() { return scheduler.#prefill?.canAdmit === true; },
|
|
626
|
+
get preparingTokens() { return scheduler.#prefill?.tokenWeight ?? 0; },
|
|
627
|
+
// Cache restoration belongs to preparation. A queued request's prompt
|
|
628
|
+
// length is an upper bound until its cache owner resolves the prefix.
|
|
629
|
+
get nextPreparationTokens() { return scheduler.#pending[0]?.promptTokens ?? 0; },
|
|
630
|
+
get maxPreparationTokens() { return scheduler.#prefillBatchTokenLimit; },
|
|
631
|
+
get preparationPublishedOutput() { return scheduler.#preparationPublishedOutput; },
|
|
554
632
|
get maxActive() { return scheduler.#maxBatch; },
|
|
555
633
|
get admissionHeld() { return scheduler.#admissionHeld?.() === true; },
|
|
556
634
|
get closed() { return scheduler.#closed; },
|
|
@@ -563,18 +641,60 @@ export class MlxBatchExecutionGroup {
|
|
|
563
641
|
}
|
|
564
642
|
},
|
|
565
643
|
admitNext: () => this.#admitNext(),
|
|
566
|
-
canBurst: () => this.#
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
644
|
+
canBurst: () => this.#contextCompatible(this.#pending[0]!) &&
|
|
645
|
+
(this.#kvBudgetBytes === undefined ||
|
|
646
|
+
this.projectedKvBytes + this.#rowKvBytes(this.#pending[0]!) <= this.#kvBudgetBytes),
|
|
647
|
+
get mixedPreparation() {
|
|
648
|
+
return scheduler.#decodeStateKey === undefined && scheduler.#prefill?.supportsMixedWork !== false && scheduler.#runtime.flag("MLX_BUN_MIXED_PREFILL", false) &&
|
|
649
|
+
(!scheduler.#method || scheduler.#method.runningTokens !== undefined) &&
|
|
650
|
+
typeof (scheduler.model as RuntimeModel & Partial<MixedTokenModel>).forwardHiddenMixed === "function"
|
|
651
|
+
? { runningTokens: scheduler.#method?.runningTokens ?? scheduler.#running.length,
|
|
652
|
+
minimumPreparationTokens: scheduler.#prefill?.rows.length ?? 0 }
|
|
653
|
+
: undefined;
|
|
654
|
+
},
|
|
655
|
+
get maxIterationTokens() { return scheduler.#runtime.number("MLX_BUN_MIXED_TOKEN_BUDGET", 256); },
|
|
656
|
+
advanceMixed: async tokenBudget => {
|
|
657
|
+
this.#promptCache?.reclaim?.();
|
|
658
|
+
const advanced = await runMixedTokenIteration({
|
|
659
|
+
prepare: forward => this.#advancePreparation({ forward,
|
|
660
|
+
maxTokens: tokenBudget - (this.#method?.runningTokens ?? this.#running.length) }),
|
|
661
|
+
decode: forward => this.#method ? this.#method.advance(forward) : this.#step(forward),
|
|
662
|
+
forward: async (ids, cache, options) => options?.captureLayer
|
|
663
|
+
? (this.model as RuntimeModel & MixedTokenModel).forwardHiddenMixed([{ ids, cache, ...options }])[0]!
|
|
664
|
+
: this.#forwardHidden(ids, cache),
|
|
665
|
+
mixed: groups => {
|
|
666
|
+
const workId = `mixed:${++nextMixedWorkId}`;
|
|
667
|
+
const tokens = groups.map(group => group.ids.shape[0]! * group.ids.shape[1]!);
|
|
668
|
+
const closes = [...this.#running, ...(this.#prefill?.rows ?? [])].map(row =>
|
|
669
|
+
row.req.trace?.begin("engine.mixed_forward", { workId,
|
|
670
|
+
decodeTokens: tokens[0]!, prefillTokens: tokens[1]!, packedTokens: tokens[0]! + tokens[1]! }));
|
|
671
|
+
try { return (this.model as RuntimeModel & MixedTokenModel).forwardHiddenMixed(groups); }
|
|
672
|
+
finally { for (const close of closes) close?.(); }
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
if (!advanced && this.#running.length) {
|
|
676
|
+
if (this.#method) await this.#method.advance(); else await this.#step();
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
advancePreparation: () => {
|
|
680
|
+
this.#promptCache?.reclaim?.();
|
|
681
|
+
return this.#advancePreparation();
|
|
682
|
+
},
|
|
683
|
+
advance: () => {
|
|
684
|
+
if (this.#cacheMaintenanceSteps++ % 256 === 0) this.#promptCache?.reclaim?.();
|
|
685
|
+
return this.#method ? this.#method.advance() : this.#step();
|
|
686
|
+
},
|
|
572
687
|
failActive: (error) => {
|
|
573
688
|
for (const row of this.#running) row.reject(error);
|
|
574
689
|
this.#applyFilter([], true); // failed state never enters the prefix store
|
|
575
690
|
},
|
|
576
691
|
failAll: (error) => this.#failAll(error),
|
|
577
|
-
reserveResidency: () =>
|
|
692
|
+
reserveResidency: () => {
|
|
693
|
+
const release = acquireModelWiredLimit(this.model);
|
|
694
|
+
return () => disposeResources([
|
|
695
|
+
{ dispose: () => this.#leaveContext() }, { dispose: release },
|
|
696
|
+
]);
|
|
697
|
+
},
|
|
578
698
|
...(this.#lock ? { acquireExecution: () => this.#lock!.acquire() } : {}),
|
|
579
699
|
waitForWork: async () => {
|
|
580
700
|
await new Promise<void>((resolve) => { this.#wake = resolve; });
|
|
@@ -585,70 +705,112 @@ export class MlxBatchExecutionGroup {
|
|
|
585
705
|
await driveExecutionGroup(group, {
|
|
586
706
|
now: () => performance.now(),
|
|
587
707
|
yield: () => new Promise<void>((resolve) => setImmediate(resolve)),
|
|
588
|
-
}, this.#runtime.
|
|
708
|
+
}, this.#runtime.flag("MLX_BUN_EARLY_FIRST_TOKEN", true));
|
|
589
709
|
} finally { this.#looping = false; }
|
|
590
710
|
}
|
|
591
711
|
|
|
712
|
+
#contextCompatible(row: Row): boolean {
|
|
713
|
+
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 && row.req.promptInput?.decodeState?.key === this.#decodeStateKey);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
#leaveContext(): void {
|
|
717
|
+
const release = this.#releaseContext;
|
|
718
|
+
this.#releaseContext = undefined;
|
|
719
|
+
this.#contextKey = undefined;
|
|
720
|
+
release?.();
|
|
721
|
+
}
|
|
722
|
+
|
|
592
723
|
#admitNext(): boolean {
|
|
593
|
-
if (!this.#kvAdmits(this.#pending[0]!)) return false;
|
|
724
|
+
if (!this.#contextCompatible(this.#pending[0]!) || !this.#kvAdmits(this.#pending[0]!)) return false;
|
|
594
725
|
const row = this.#pending.shift()!;
|
|
595
|
-
let owned: { caches: Cache[]; retain?: () => void } | undefined;
|
|
596
|
-
let closePrefill: (() => void) | undefined;
|
|
597
726
|
try {
|
|
727
|
+
this.#statePolicy = row.req.statePolicy;
|
|
728
|
+
this.#decodeStateKey = row.req.promptInput?.decodeState?.key;
|
|
729
|
+
row.cacheNamespace = typeof row.req.cacheNamespace === "function"
|
|
730
|
+
? row.req.cacheNamespace() : row.req.cacheNamespace;
|
|
731
|
+
const key = row.req.context?.key ?? "";
|
|
732
|
+
if (key !== this.#contextKey) {
|
|
733
|
+
this.#leaveContext();
|
|
734
|
+
this.#releaseContext = row.req.context?.enter();
|
|
735
|
+
this.#contextKey = key;
|
|
736
|
+
}
|
|
598
737
|
row.req.onAdmitted?.();
|
|
599
738
|
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
|
-
|
|
739
|
+
if (row.req.method?.key !== this.#methodKey) {
|
|
740
|
+
this.#method?.dispose(); this.#method = undefined; this.#methodKey = undefined;
|
|
741
|
+
if (row.req.method) {
|
|
742
|
+
const executor = this;
|
|
743
|
+
this.#method = row.req.method.open({
|
|
744
|
+
get rows() { return executor.#running; }, runtime: this.#runtime,
|
|
745
|
+
prefillChunkSize: this.#prefillChunkSize, promptCache: this.#promptCache,
|
|
746
|
+
join: row => { this.#running.push(row); },
|
|
747
|
+
filterRows: keep => this.#applyFilter(keep),
|
|
748
|
+
publish: async (row, token, logprobs) => {
|
|
749
|
+
row.req.signal?.throwIfAborted();
|
|
750
|
+
this.#firstOutput(row);
|
|
751
|
+
return row.req.onToken(token, logprobs);
|
|
752
|
+
},
|
|
753
|
+
finish: (row, reason) => this.#finish(row, reason),
|
|
754
|
+
});
|
|
755
|
+
this.#methodKey = row.req.method.key;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (this.#prefill) {
|
|
759
|
+
this.#prefill.admit!(row);
|
|
760
|
+
} else if (this.#method) {
|
|
761
|
+
this.#prefill = this.#method.prepare(row);
|
|
762
|
+
} else {
|
|
763
|
+
const preparation = new MlxPrefillCohort({
|
|
764
|
+
model: this.model, chunkSize: this.#prefillChunkSize, tailSplit: this.#prefillTailSplit,
|
|
765
|
+
promptCache: this.#promptCache, stateCodecs: this.#stateCodecs, maintain: this.#maintainKv ?? undefined,
|
|
766
|
+
forward: (ids, caches) => this.#forwardHidden(ids, caches),
|
|
767
|
+
project: (hidden, caches, completed) => this.#projectPrefill(hidden, caches, completed),
|
|
768
|
+
complete: (state, logits) => this.#completePrefill(state, logits),
|
|
769
|
+
resume: state => this.#completePrefill(state, null),
|
|
770
|
+
reject: (failed, error) => this.#rejectPreparingRows([failed], error),
|
|
771
|
+
});
|
|
772
|
+
preparation.admit(row); this.#prefill = preparation;
|
|
773
|
+
}
|
|
774
|
+
} catch (error) { row.reject(error); }
|
|
625
775
|
return true;
|
|
626
776
|
}
|
|
627
777
|
|
|
628
|
-
|
|
629
|
-
const
|
|
778
|
+
#rejectPreparingRows(rows: readonly Row[], error: unknown): void {
|
|
779
|
+
const retiring = new Set(rows);
|
|
780
|
+
const keep = this.#running.flatMap((row, index) => retiring.has(row) ? [] : [index]);
|
|
781
|
+
if (keep.length !== this.#running.length) this.#applyFilter(keep, true);
|
|
782
|
+
for (const row of rows) row.reject(error);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async #advancePreparation(work?: MlxPreparationWork): Promise<void> {
|
|
786
|
+
const p = this.#prefill!, rows = p.rows;
|
|
787
|
+
this.#preparationPublishedOutput = false;
|
|
630
788
|
try {
|
|
631
|
-
if (await
|
|
632
|
-
p.closePrefill?.();
|
|
633
|
-
this.#prefill = null;
|
|
634
|
-
}
|
|
789
|
+
if (await p.advance(work)) this.#prefill = null;
|
|
635
790
|
} catch (error) {
|
|
636
791
|
this.#prefill = null;
|
|
637
792
|
let failure = error;
|
|
638
|
-
try {
|
|
793
|
+
try { p.dispose(); }
|
|
639
794
|
catch (cleanupError) { failure = new AggregateError([error, cleanupError], "batch prefill and cleanup failed"); }
|
|
640
|
-
|
|
795
|
+
try { this.#rejectPreparingRows(rows, failure); }
|
|
796
|
+
catch (cleanupError) {
|
|
797
|
+
failure = new AggregateError([failure, cleanupError], "batch admission retirement failed");
|
|
798
|
+
for (const row of rows) row.reject(failure);
|
|
799
|
+
this.#failAll(failure);
|
|
800
|
+
}
|
|
641
801
|
}
|
|
642
802
|
}
|
|
643
803
|
|
|
644
804
|
#failAll(error: unknown): void {
|
|
805
|
+
this.#tasks.rejectAll(error);
|
|
645
806
|
const p = this.#prefill;
|
|
646
|
-
const rows = new Set([...this.#running, ...this.#pending, ...(p
|
|
807
|
+
const rows = new Set([...this.#running, ...this.#pending, ...(p?.rows ?? [])]);
|
|
647
808
|
const retain = this.#adoptedRetain;
|
|
648
|
-
const resources = [
|
|
649
|
-
{ dispose: () =>
|
|
809
|
+
const resources = [p, this.#method, ...(this.#inners ?? []), this.#pendingToks,
|
|
810
|
+
{ dispose: () => retain?.() }]
|
|
650
811
|
.filter((resource): resource is DisposableResource => resource != null);
|
|
651
812
|
this.#pending = []; this.#running = []; this.#prefill = null;
|
|
813
|
+
this.#method = undefined; this.#methodKey = undefined;
|
|
652
814
|
this.#inners = null; this.#pendingToks = null; this.#pendingReal = null;
|
|
653
815
|
this.#fullLeftPad = []; this.#adoptedRetain = null;
|
|
654
816
|
for (const row of rows) row.reject(error);
|
|
@@ -665,110 +827,105 @@ export class MlxBatchExecutionGroup {
|
|
|
665
827
|
* bytes bit-exact vs serial `--kv-quant config`. Gateway placement and the
|
|
666
828
|
* constructor both guarantee every named cache can convert. */
|
|
667
829
|
#quantizeSolo(solo: Cache[], trace?: PromptResponseTrace): void {
|
|
668
|
-
if (!this.#
|
|
830
|
+
if (!this.#maintainKv) return;
|
|
669
831
|
const close = trace?.begin("prefill.kv_maintenance", {
|
|
670
832
|
mechanism: "continuous",
|
|
671
833
|
});
|
|
672
834
|
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
|
-
}
|
|
835
|
+
this.#maintainKv(solo);
|
|
687
836
|
} finally {
|
|
688
837
|
close?.();
|
|
689
838
|
}
|
|
690
839
|
}
|
|
691
840
|
|
|
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;
|
|
841
|
+
/** The target owns projection geometry; samplers receive individual rows. */
|
|
842
|
+
#projectPrefill(hidden: MlxArray, caches: Cache[], completed: readonly PrefillState[]): MlxArray {
|
|
843
|
+
for (const p of completed) {
|
|
844
|
+
p.closePrefill?.(); p.closePrefill = undefined;
|
|
845
|
+
p.row.closeTokenZero = p.row.req.trace?.begin("token_zero.total", { mechanism: "continuous" });
|
|
722
846
|
}
|
|
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();
|
|
847
|
+
const attributed = this.#runtime.value("MLX_BUN_P2R_SYNC") === "1"
|
|
848
|
+
? completed.flatMap(p => p.row.req.trace ? [p.row.req.trace] : []) : [];
|
|
849
|
+
const begin = (phase: "token_zero.forward" | "token_zero.head") => attributed.map(trace => trace.begin(phase, {
|
|
850
|
+
mechanism: "continuous", activeBytes: activeMemory(), cacheBytes: cacheMemory(), peakBytes: peakMemory(),
|
|
851
|
+
}));
|
|
852
|
+
if (attributed.length) {
|
|
853
|
+
const closes = begin("token_zero.forward");
|
|
854
|
+
try { withResource(leaseCacheStates(caches), state => ops.evalAll([hidden, ...state])); }
|
|
855
|
+
finally { for (const close of closes) close(); }
|
|
743
856
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
857
|
+
// Project before selecting rows: M4 quantized projection can use different
|
|
858
|
+
// arithmetic at M=1. Tail-split preparation reaches this with N=1.
|
|
859
|
+
const logits = this.model.logitsFromHidden(hidden);
|
|
860
|
+
try {
|
|
861
|
+
if (attributed.length) {
|
|
862
|
+
const closes = begin("token_zero.head");
|
|
863
|
+
try { ops.evalAll([logits]); } finally { for (const close of closes) close(); }
|
|
864
|
+
}
|
|
865
|
+
return logits;
|
|
866
|
+
} catch (error) { logits.dispose(); throw error; }
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** Sample borrowed logits from the shared target projection. */
|
|
870
|
+
async #completePrefill(p: PrefillState, logits: MlxArray | null): Promise<void> {
|
|
871
|
+
if (p.continuation) {
|
|
872
|
+
const saved = p.continuation, row = p.row;
|
|
873
|
+
row.req.continuation!.resumeSampling(saved);
|
|
874
|
+
const interval = row.req.continuation!.interval;
|
|
875
|
+
this.#checkpointAt.set(row, (Math.floor(saved.generatedTokens / interval) + 1) * interval);
|
|
876
|
+
row.firstTokenAt = performance.now();
|
|
877
|
+
row.closeTokenZero?.(); row.closeTokenZero = undefined;
|
|
878
|
+
this.#preparationPublishedOutput = true;
|
|
879
|
+
for (const token of saved.cacheTokens.slice(row.req.promptIds.length)) {
|
|
880
|
+
row.req.signal?.throwIfAborted();
|
|
881
|
+
if (await row.req.onToken(token) === false)
|
|
882
|
+
throw new Error("saved generation prefix triggered a terminal stop while replaying");
|
|
883
|
+
}
|
|
884
|
+
row.fed = saved.cacheTokens.slice(row.req.promptIds.length);
|
|
885
|
+
row.generated = saved.generatedTokens + 1;
|
|
886
|
+
row.sampled = saved.generatedTokens + 1;
|
|
887
|
+
row.cachedTokens = Math.min(saved.cacheTokens.length, row.promptTokens);
|
|
888
|
+
const stop = await this.#emit(row, saved.pendingToken);
|
|
889
|
+
if (stop !== "continue") {
|
|
890
|
+
this.#putOrDispose(p.solo, saved.cacheTokens, p.retain, row.cacheNamespace, row.req.cacheSessionId);
|
|
891
|
+
p.solo = [];
|
|
892
|
+
this.#finish(row, stop);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
await this.#mergeJoiner(p);
|
|
896
|
+
return;
|
|
758
897
|
}
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
898
|
+
if (!logits) throw new Error("ordinary prefill completed without logits");
|
|
899
|
+
const forceAttribution = !!p.row.req.trace && this.#runtime.value("MLX_BUN_P2R_SYNC") === "1";
|
|
900
|
+
const V = logits.shape[2]!;
|
|
901
|
+
const last2 = ops.reshape(logits, [1, V]);
|
|
762
902
|
const closeSample = forceAttribution
|
|
763
903
|
? p.row.req.trace!.begin("token_zero.sample", { mechanism: "continuous" })
|
|
764
904
|
: undefined;
|
|
765
|
-
|
|
905
|
+
let sampled: MlxArray;
|
|
906
|
+
try { sampled = p.row.req.sample!(last2, 0); } finally { last2.dispose(); }
|
|
907
|
+
// The first admission can seed the existing device-token register. Its
|
|
908
|
+
// first decode forward then overlaps token-zero readback, just like every
|
|
909
|
+
// later pipeline step. Grammar and explicit eager-output modes retain
|
|
910
|
+
// their read-before-advance order.
|
|
911
|
+
if (!this.#inners && !p.row.req.grammar && !this.#noPipeline &&
|
|
912
|
+
!this.#runtime.flag("MLX_BUN_EARLY_FIRST_TOKEN", true) && !forceAttribution) {
|
|
913
|
+
let owned: MlxArray | null = sampled;
|
|
914
|
+
try {
|
|
915
|
+
ops.asyncEvalAll([sampled]);
|
|
916
|
+
p.row.sampled = 1;
|
|
917
|
+
this.#quantizeSolo(p.solo, p.row.req.trace);
|
|
918
|
+
await this.#mergeJoiner(p);
|
|
919
|
+
this.#pendingToks = sampled;
|
|
920
|
+
this.#pendingReal = [true];
|
|
921
|
+
owned = null;
|
|
922
|
+
return;
|
|
923
|
+
} finally { owned?.dispose(); }
|
|
924
|
+
}
|
|
925
|
+
const tok = this.#readToken(sampled);
|
|
766
926
|
closeSample?.();
|
|
767
|
-
closeTokenZero?.();
|
|
768
|
-
last2.dispose();
|
|
769
927
|
p.row.sampled = 1;
|
|
770
928
|
p.row.generated = 1;
|
|
771
|
-
clearCache(); // drop the prefill transients (serial's token-0 clear)
|
|
772
929
|
|
|
773
930
|
// Grammar (B1): the mask0 was applied inside the sample closure (the
|
|
774
931
|
// controller is primed at compile). After reading token 0, advance the
|
|
@@ -782,17 +939,17 @@ export class MlxBatchExecutionGroup {
|
|
|
782
939
|
const stop = await this.#emit(p.row, tok);
|
|
783
940
|
// Token 0 was sampled but never fed — the caches cover exactly the
|
|
784
941
|
// prompt, a clean prompt-only entry (put-or-dispose).
|
|
785
|
-
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain);
|
|
942
|
+
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain, p.row.cacheNamespace, p.row.req.cacheSessionId);
|
|
786
943
|
this.#finish(p.row, stop === "continue" ? "stop" : stop);
|
|
787
|
-
return
|
|
944
|
+
return;
|
|
788
945
|
}
|
|
789
946
|
}
|
|
790
947
|
|
|
791
948
|
const stop = await this.#emit(p.row, tok);
|
|
792
949
|
if (stop !== "continue") {
|
|
793
|
-
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain);
|
|
950
|
+
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain, p.row.cacheNamespace, p.row.req.cacheSessionId);
|
|
794
951
|
this.#finish(p.row, stop);
|
|
795
|
-
return
|
|
952
|
+
return;
|
|
796
953
|
}
|
|
797
954
|
// Default tail-split path: the caches were already converted at the head
|
|
798
955
|
// boundary above (oracle composition: prefill ids[:-1] → convert → L=1
|
|
@@ -803,10 +960,7 @@ export class MlxBatchExecutionGroup {
|
|
|
803
960
|
// (before decode step 1 == before the merge).
|
|
804
961
|
this.#quantizeSolo(p.solo, p.row.req.trace);
|
|
805
962
|
await this.#mergeJoiner(p);
|
|
806
|
-
return
|
|
807
|
-
} finally {
|
|
808
|
-
closeChunk?.();
|
|
809
|
-
}
|
|
963
|
+
return;
|
|
810
964
|
}
|
|
811
965
|
|
|
812
966
|
/** Merge a fully-prefilled joiner with the running batch, layer by layer
|
|
@@ -814,6 +968,7 @@ export class MlxBatchExecutionGroup {
|
|
|
814
968
|
* first so the row set is settled and the next step starts cold. */
|
|
815
969
|
async #mergeJoiner(p: PrefillState): Promise<void> {
|
|
816
970
|
await this.#flushPipeline();
|
|
971
|
+
this.#maintainKv?.prepareBatch?.(p.solo);
|
|
817
972
|
|
|
818
973
|
// ADOPT, don't copy (unified-engine plan Phase 3.2): a row joining an
|
|
819
974
|
// EMPTY batch keeps its solo caches as the batch inners — a pointer
|
|
@@ -826,9 +981,9 @@ export class MlxBatchExecutionGroup {
|
|
|
826
981
|
// take/put become possible for it. The rot branch below knows how to
|
|
827
982
|
// treat an adopted RotatingKVCache as the merge's first row.
|
|
828
983
|
if (!this.#inners) {
|
|
829
|
-
this.#inners = p.solo as LayerInner[];
|
|
984
|
+
this.#inners = p.solo as LayerInner[]; p.solo = [];
|
|
830
985
|
this.#fullLeftPad = [0];
|
|
831
|
-
this.#adoptedRetain = p.retain ?? null;
|
|
986
|
+
this.#adoptedRetain = p.retain ?? null; p.retain = undefined;
|
|
832
987
|
this.#running.push(p.row);
|
|
833
988
|
return;
|
|
834
989
|
}
|
|
@@ -839,17 +994,17 @@ export class MlxBatchExecutionGroup {
|
|
|
839
994
|
const newInners: LayerInner[] = [];
|
|
840
995
|
let newFullPad = this.#fullLeftPad;
|
|
841
996
|
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();
|
|
997
|
+
const solo = p.solo[layer]!;
|
|
998
|
+
const merged = ownedCacheLayout(solo);
|
|
999
|
+
if (merged) {
|
|
847
1000
|
const previous = prev?.[layer];
|
|
848
1001
|
merged.mergeRows(previous ? [previous, solo] : [solo]);
|
|
849
1002
|
newFullPad = [...merged.leftPad];
|
|
850
1003
|
newInners.push(merged);
|
|
851
1004
|
continue;
|
|
852
1005
|
}
|
|
1006
|
+
if (this.#kinds[layer] === "owned-batch")
|
|
1007
|
+
throw new Error(`batch-capable layer ${layer} lost its cache capability`);
|
|
853
1008
|
if (this.#kinds[layer] === "ssm") {
|
|
854
1009
|
// No temporal axis, no left-pad: B-axis concat of the state slots.
|
|
855
1010
|
// mergeRows steals the solo arrays when the batch starts cold, so the
|
|
@@ -1049,8 +1204,9 @@ export class MlxBatchExecutionGroup {
|
|
|
1049
1204
|
// run its retain now. The joiner's likewise after its solo dispose.
|
|
1050
1205
|
this.#adoptedRetain?.();
|
|
1051
1206
|
this.#adoptedRetain = null;
|
|
1052
|
-
|
|
1053
|
-
p.retain
|
|
1207
|
+
const completed = p.solo; p.solo = [];
|
|
1208
|
+
const release = p.retain; p.retain = undefined;
|
|
1209
|
+
disposeResources([...completed, { dispose: () => release?.() }]);
|
|
1054
1210
|
// Every row in a REAL merge has its KV interleaved in batched buffers —
|
|
1055
1211
|
// no longer prompt-cache put() candidates.
|
|
1056
1212
|
for (const r of this.#running) r.merged = true;
|
|
@@ -1069,7 +1225,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1069
1225
|
* Rows that finish get one extra harmless KV write from the already-built
|
|
1070
1226
|
* step; filter drops the row (mlx-lm behaves identically). Length-finished
|
|
1071
1227
|
* rows are known in advance and are NOT sampled (placeholder slot). */
|
|
1072
|
-
async #step(): Promise<void> {
|
|
1228
|
+
async #step(forward?: MlxForwardWork): Promise<void> {
|
|
1073
1229
|
if (this.#stepTrace) {
|
|
1074
1230
|
const now = performance.now();
|
|
1075
1231
|
if (STEP_T.lastEnd) STEP_T.gap += now - STEP_T.lastEnd;
|
|
@@ -1078,6 +1234,11 @@ export class MlxBatchExecutionGroup {
|
|
|
1078
1234
|
const rows = this.#running;
|
|
1079
1235
|
const B = rows.length;
|
|
1080
1236
|
const inners = this.#inners!;
|
|
1237
|
+
const decodeState = rows[0]?.req.promptInput?.decodeState;
|
|
1238
|
+
if (decodeState) {
|
|
1239
|
+
const states = rows.map(row => row.req.promptInput!.decodeState!);
|
|
1240
|
+
forward = async (ids, caches) => decodeState.forward(ids, caches, states);
|
|
1241
|
+
}
|
|
1081
1242
|
|
|
1082
1243
|
// A row is live if it still needs tokens sampled; a row whose pending
|
|
1083
1244
|
// unread token is its last (sampled == maxTokens) only awaits emission.
|
|
@@ -1097,7 +1258,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1097
1258
|
const hasLiveGrammar = anyLive && rows.some(
|
|
1098
1259
|
(r) => r.req.grammar && !r.req.grammar.isTerminated,
|
|
1099
1260
|
);
|
|
1100
|
-
if (hasLiveGrammar) return this.#stepGrammar();
|
|
1261
|
+
if (hasLiveGrammar) return this.#stepGrammar(forward);
|
|
1101
1262
|
|
|
1102
1263
|
let nextToks: MlxArray | null = null;
|
|
1103
1264
|
let nextReal: boolean[] | null = null;
|
|
@@ -1131,7 +1292,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1131
1292
|
let lg: MlxArray | null = null;
|
|
1132
1293
|
let evalWith: MlxArray[] = [];
|
|
1133
1294
|
if (
|
|
1134
|
-
this.#compiled && B === 1 && unpadded && this.#running[0]!.req.compiledDecode !== false &&
|
|
1295
|
+
!forward && this.#compiled && B === 1 && unpadded && this.#running[0]!.req.compiledDecode !== false &&
|
|
1135
1296
|
(!this.#pendingToks || this.#pendingToks.dtype === Dtype.uint32) &&
|
|
1136
1297
|
// A filtered-to-one BATCHED rot-quant cache subclasses the serial
|
|
1137
1298
|
// class (so supports() passes) but carries batched ring state —
|
|
@@ -1173,7 +1334,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1173
1334
|
const ids = this.#pendingToks
|
|
1174
1335
|
? ops.reshape(this.#pendingToks, [B, 1]) // feed the unread tokens
|
|
1175
1336
|
: ops.fromInt32(rows.map((r) => r.current), [B, 1]); // pipeline cold
|
|
1176
|
-
const h = await this.#forwardHidden(ids, fwd);
|
|
1337
|
+
const h = await (forward ? forward(ids, fwd) : this.#forwardHidden(ids, fwd));
|
|
1177
1338
|
ids.dispose();
|
|
1178
1339
|
lg = this.model.logitsFromHidden(h); // [B,1,V]
|
|
1179
1340
|
h.dispose();
|
|
@@ -1197,10 +1358,8 @@ export class MlxBatchExecutionGroup {
|
|
|
1197
1358
|
if (vecOk) {
|
|
1198
1359
|
const flat = ops.reshape(lg, [B, V]);
|
|
1199
1360
|
lg.dispose();
|
|
1200
|
-
|
|
1201
|
-
flat.dispose();
|
|
1202
|
-
nextToks = ops.argmaxAxis(lp, -1); // [B]
|
|
1203
|
-
lp.dispose();
|
|
1361
|
+
try { nextToks = independentGreedySampling.sample(flat); }
|
|
1362
|
+
finally { flat.dispose(); }
|
|
1204
1363
|
for (const row of rows) if (row.sampled < row.req.maxTokens) row.sampled++;
|
|
1205
1364
|
} else {
|
|
1206
1365
|
const sampled: MlxArray[] = [];
|
|
@@ -1215,7 +1374,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1215
1374
|
const rl = lg.slice([b, 0, 0], [b + 1, 1, V]);
|
|
1216
1375
|
const rl2 = ops.reshape(rl, [1, V]);
|
|
1217
1376
|
rl.dispose();
|
|
1218
|
-
sampled.push(row.req.sample(rl2, row.sampled));
|
|
1377
|
+
sampled.push(row.req.sample!(rl2, row.sampled));
|
|
1219
1378
|
row.sampled++;
|
|
1220
1379
|
rl2.dispose();
|
|
1221
1380
|
}
|
|
@@ -1246,6 +1405,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1246
1405
|
// set from process start `prev` is always null, so the flush below IS the
|
|
1247
1406
|
// whole phase 2. Same math either way (pipelining is scheduling).
|
|
1248
1407
|
if (this.#noPipeline) {
|
|
1408
|
+
if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
|
|
1249
1409
|
await this.#flushPipeline();
|
|
1250
1410
|
return;
|
|
1251
1411
|
}
|
|
@@ -1266,9 +1426,37 @@ export class MlxBatchExecutionGroup {
|
|
|
1266
1426
|
await this.#emitRows(toks); // also filters #pendingToks on eviction
|
|
1267
1427
|
if (this.#stepTrace) { STEP_T.emit += performance.now() - tEmit; STEP_T.n++; }
|
|
1268
1428
|
}
|
|
1429
|
+
if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
|
|
1269
1430
|
if (this.#stepTrace) STEP_T.lastEnd = performance.now();
|
|
1270
1431
|
}
|
|
1271
1432
|
|
|
1433
|
+
readonly #checkpointAt = new WeakMap<Row, number>();
|
|
1434
|
+
/** Cache extraction belongs to this existing ordinary numerical driver;
|
|
1435
|
+
* the optional request port owns checkpoint eligibility and persistence. */
|
|
1436
|
+
#captureContinuations(): void {
|
|
1437
|
+
if (!this.#pendingToks || !this.#running.some(row => row.req.continuation)) return;
|
|
1438
|
+
let pending: number[] | undefined;
|
|
1439
|
+
const failed = new Set<number>();
|
|
1440
|
+
for (let index = 0; index < this.#running.length; index++) {
|
|
1441
|
+
const row = this.#running[index]!, policy = row.req.continuation;
|
|
1442
|
+
if (!policy || row.fedTainted || row.fed.length !== row.generated ||
|
|
1443
|
+
row.generated < (this.#checkpointAt.get(row) ?? policy.interval) ||
|
|
1444
|
+
this.#pendingReal?.[index] === false) continue;
|
|
1445
|
+
pending ??= this.#pendingToks.toIntTokens();
|
|
1446
|
+
const tokens = [...row.req.promptIds, ...row.fed];
|
|
1447
|
+
const caches = this.#running.length === 1 && !row.merged
|
|
1448
|
+
? cloneSingleRowState(this.#inners! as Cache[], this.#stateCodecs)
|
|
1449
|
+
: this.#extractRowCaches(index, tokens.length);
|
|
1450
|
+
if (!caches) continue;
|
|
1451
|
+
try {
|
|
1452
|
+
policy.captureOwned({ caches, cacheTokens: tokens, generatedTokens: row.generated,
|
|
1453
|
+
pendingToken: pending[index]! });
|
|
1454
|
+
this.#checkpointAt.set(row, (Math.floor(row.generated / policy.interval) + 1) * policy.interval);
|
|
1455
|
+
} catch (error) { row.reject(error); failed.add(index); }
|
|
1456
|
+
}
|
|
1457
|
+
if (failed.size) this.#applyFilter(this.#running.flatMap((_, index) => failed.has(index) ? [] : [index]), true);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1272
1460
|
/** Read out the pipeline register (if any): emit its tokens and evict
|
|
1273
1461
|
* finished rows, leaving the pipeline cold. Called before a join merges. */
|
|
1274
1462
|
async #flushPipeline(): Promise<void> {
|
|
@@ -1302,7 +1490,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1302
1490
|
* finish("stop") in #emitRows. On a cold start (first step after prefill)
|
|
1303
1491
|
* there is no pending array to read; the prefill already accepted token 0
|
|
1304
1492
|
* and fired the fill, so we just await ready() + sample. */
|
|
1305
|
-
async #stepGrammar(): Promise<void> {
|
|
1493
|
+
async #stepGrammar(forward?: MlxForwardWork): Promise<void> {
|
|
1306
1494
|
const rows = this.#running;
|
|
1307
1495
|
const B = rows.length;
|
|
1308
1496
|
const inners = this.#inners!;
|
|
@@ -1349,7 +1537,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1349
1537
|
const ids = prev
|
|
1350
1538
|
? ops.reshape(prev, [B, 1]) // feed the unread tokens (device array)
|
|
1351
1539
|
: ops.fromInt32(rows.map((r) => r.current), [B, 1]); // pipeline cold
|
|
1352
|
-
const h = await this.#forwardHidden(ids, fwd);
|
|
1540
|
+
const h = await (forward ? forward(ids, fwd) : this.#forwardHidden(ids, fwd));
|
|
1353
1541
|
ids.dispose();
|
|
1354
1542
|
const lg = this.model.logitsFromHidden(h); // [B,1,V]
|
|
1355
1543
|
h.dispose();
|
|
@@ -1382,7 +1570,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1382
1570
|
const rl = lg.slice([b, 0, 0], [b + 1, 1, V]);
|
|
1383
1571
|
const rl2 = ops.reshape(rl, [1, V]);
|
|
1384
1572
|
rl.dispose();
|
|
1385
|
-
sampled.push(row.req.sample(rl2, row.sampled));
|
|
1573
|
+
sampled.push(row.req.sample!(rl2, row.sampled));
|
|
1386
1574
|
row.sampled++;
|
|
1387
1575
|
rl2.dispose();
|
|
1388
1576
|
}
|
|
@@ -1408,6 +1596,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1408
1596
|
// rows finish("stop") via #emit's isTerminated check; the filter evicts.
|
|
1409
1597
|
// Cold start: prevVals empty (prefill emitted tok0) → nothing to emit.
|
|
1410
1598
|
if (prevVals.length) await this.#emitRows(prevVals);
|
|
1599
|
+
if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
|
|
1411
1600
|
}
|
|
1412
1601
|
|
|
1413
1602
|
/** Emit one read-back token per running row; evict finished rows. A row's
|
|
@@ -1420,7 +1609,6 @@ export class MlxBatchExecutionGroup {
|
|
|
1420
1609
|
for (let b = 0; b < B; b++) {
|
|
1421
1610
|
const row = rows[b]!;
|
|
1422
1611
|
row.generated++;
|
|
1423
|
-
if (row.generated === 1) row.firstTokenAt = performance.now();
|
|
1424
1612
|
let disp: "continue" | "stop" | "length";
|
|
1425
1613
|
try {
|
|
1426
1614
|
disp = await this.#emit(row, toks[b]!);
|
|
@@ -1452,10 +1640,12 @@ export class MlxBatchExecutionGroup {
|
|
|
1452
1640
|
/** Account one sampled token for a row. Mirrors generate(): EOS terminates
|
|
1453
1641
|
* WITHOUT an onToken call; otherwise onToken(token) runs and `false` halts;
|
|
1454
1642
|
* reaching maxTokens ends with "length". Advances row.current on continue. */
|
|
1455
|
-
async #emit(row: Row, token: number): Promise<"continue" | "stop" | "length"> {
|
|
1643
|
+
async #emit(row: Row, token: number, logprobs?: import("../../generate").TokenLogprobs): Promise<"continue" | "stop" | "length"> {
|
|
1456
1644
|
row.req.signal?.throwIfAborted();
|
|
1645
|
+
this.#firstOutput(row);
|
|
1646
|
+
if (row.generated === 1) clearCache();
|
|
1457
1647
|
if (row.req.eosTokenIds.includes(token)) return "stop";
|
|
1458
|
-
const cont = await row.req.onToken(token);
|
|
1648
|
+
const cont = await row.req.onToken(token, logprobs);
|
|
1459
1649
|
if (cont === false) return "stop";
|
|
1460
1650
|
// Grammar termination: the matcher is satisfied (e.g. closing `}` accepted).
|
|
1461
1651
|
// The final token has been delivered via onToken above; halt with "stop" so
|
|
@@ -1466,10 +1656,22 @@ export class MlxBatchExecutionGroup {
|
|
|
1466
1656
|
return "continue";
|
|
1467
1657
|
}
|
|
1468
1658
|
|
|
1659
|
+
#firstOutput(row: Row): void {
|
|
1660
|
+
if (row.generated === 1) {
|
|
1661
|
+
if (this.#prefill?.rows.includes(row)) this.#preparationPublishedOutput = true;
|
|
1662
|
+
row.firstTokenAt = performance.now();
|
|
1663
|
+
row.closeTokenZero?.();
|
|
1664
|
+
row.closeTokenZero = undefined;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1469
1668
|
#finish(row: Row, reason: "stop" | "length"): void {
|
|
1669
|
+
row.req.continuation?.complete();
|
|
1470
1670
|
const now = performance.now();
|
|
1471
1671
|
const first = row.firstTokenAt || now;
|
|
1472
1672
|
row.resolve({
|
|
1673
|
+
...(row.spec ? { spec: row.spec } : {}),
|
|
1674
|
+
...(row.fill ? { fill: row.fill } : {}),
|
|
1473
1675
|
promptTokens: row.promptTokens,
|
|
1474
1676
|
generatedTokens: row.generated,
|
|
1475
1677
|
cachedTokens: row.cachedTokens,
|
|
@@ -1484,13 +1686,28 @@ export class MlxBatchExecutionGroup {
|
|
|
1484
1686
|
* offset lines up (defensive — a mismatch means the accounting is wrong
|
|
1485
1687
|
* and the entry would corrupt future hits), else dispose. `retain` rides
|
|
1486
1688
|
* along per the PromptCacheEntry contract (runs after dispose). */
|
|
1487
|
-
#putOrDispose(caches: Cache[], tokens: number[], retain?: () => void): void {
|
|
1689
|
+
#putOrDispose(caches: Cache[], tokens: number[], retain?: () => void, namespace = "", sessionId?: string): void {
|
|
1488
1690
|
if (this.#promptCache) {
|
|
1691
|
+
// A never-merged row can still own a row layout. Persistence receives
|
|
1692
|
+
// its existing serial representation, just like merged-row retirement.
|
|
1693
|
+
const extracted: Cache[] = [], replaced: Cache[] = [];
|
|
1694
|
+
let persistent: Cache[];
|
|
1695
|
+
try {
|
|
1696
|
+
persistent = caches.map(cache => {
|
|
1697
|
+
if (!isBatchableCache(cache) || cache.batchSize !== 1) return cache;
|
|
1698
|
+
const row = cache.extractRow(0);
|
|
1699
|
+
extracted.push(row); replaced.push(cache);
|
|
1700
|
+
return row;
|
|
1701
|
+
});
|
|
1702
|
+
} catch (error) { disposeResources(extracted); throw error; }
|
|
1703
|
+
try { disposeResources(replaced); }
|
|
1704
|
+
catch (error) { disposeResources(extracted); throw error; }
|
|
1705
|
+
caches = persistent;
|
|
1489
1706
|
const withOff = caches.find(
|
|
1490
1707
|
(c) => typeof (c as { offset?: unknown }).offset === "number",
|
|
1491
1708
|
) as { offset: number } | undefined;
|
|
1492
1709
|
if (withOff && withOff.offset === tokens.length) {
|
|
1493
|
-
this.#promptCache.put(tokens, caches,
|
|
1710
|
+
this.#promptCache.put(tokens, caches, namespace, retain, undefined, sessionId);
|
|
1494
1711
|
return;
|
|
1495
1712
|
}
|
|
1496
1713
|
}
|
|
@@ -1501,13 +1718,13 @@ export class MlxBatchExecutionGroup {
|
|
|
1501
1718
|
/** Extract a finishing MERGED row's KV into fresh serial caches and put()
|
|
1502
1719
|
* them keyed by [promptIds + fed] — the batch-lane mirror of mlx-lm
|
|
1503
1720
|
* server.py:872 (extract_cache → prompt_cache.insert_cache). Gates:
|
|
1504
|
-
* promptTokens >= 256
|
|
1505
|
-
*
|
|
1721
|
+
* promptTokens >= 256 or an explicit cache session, plus an exact
|
|
1722
|
+
* coverage key (!fedTainted). Refusal
|
|
1506
1723
|
* (#extractRowCaches null) just disposes-by-omission — the row's KV dies
|
|
1507
1724
|
* with the filter, exactly the pre-extraction behavior. */
|
|
1508
1725
|
#extractAndPut(b: number, row: Row): void {
|
|
1509
1726
|
if (!this.#promptCache || !row.merged || row.fedTainted) return;
|
|
1510
|
-
if (row.promptTokens < 256) return;
|
|
1727
|
+
if (row.promptTokens < 256 && !row.req.cacheSessionId) return;
|
|
1511
1728
|
const tokens = [...row.req.promptIds, ...row.fed];
|
|
1512
1729
|
const caches = this.#extractRowCaches(b, tokens.length);
|
|
1513
1730
|
if (!caches) return;
|
|
@@ -1515,8 +1732,8 @@ export class MlxBatchExecutionGroup {
|
|
|
1515
1732
|
// slices depend on this step's KV writes): async — the batched source
|
|
1516
1733
|
// buffers free once the copies land, instead of being pinned by a lazy
|
|
1517
1734
|
// graph inside an idle cache entry.
|
|
1518
|
-
|
|
1519
|
-
this.#putOrDispose(caches, tokens);
|
|
1735
|
+
withResource(leaseCacheStates(caches), state => ops.asyncEvalAll([...state]));
|
|
1736
|
+
this.#putOrDispose(caches, tokens, undefined, row.cacheNamespace, row.req.cacheSessionId);
|
|
1520
1737
|
}
|
|
1521
1738
|
|
|
1522
1739
|
/** Row `b` of every layer as OWNED serial-class caches, or null when a
|
|
@@ -1562,6 +1779,11 @@ export class MlxBatchExecutionGroup {
|
|
|
1562
1779
|
/** Evict rows not in `keep` (sorted ascending) from the batched KV and the
|
|
1563
1780
|
* pipeline register. */
|
|
1564
1781
|
#applyFilter(keep: number[], dropOnly = false): void {
|
|
1782
|
+
if (this.#method) {
|
|
1783
|
+
this.#method.filterRows(keep, dropOnly);
|
|
1784
|
+
this.#running = keep.map(row => this.#running[row]!);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1565
1787
|
const inners = this.#inners!;
|
|
1566
1788
|
if (keep.length === 0) {
|
|
1567
1789
|
// Prompt-cache put (Phase 3.2): a lone NEVER-MERGED row's inners are
|
|
@@ -1579,6 +1801,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1579
1801
|
inners as Cache[],
|
|
1580
1802
|
[...solo.req.promptIds, ...solo.fed],
|
|
1581
1803
|
this.#adoptedRetain ?? undefined,
|
|
1804
|
+
solo.cacheNamespace, solo.req.cacheSessionId,
|
|
1582
1805
|
);
|
|
1583
1806
|
} else {
|
|
1584
1807
|
for (const c of inners) c.dispose();
|
|
@@ -1593,6 +1816,11 @@ export class MlxBatchExecutionGroup {
|
|
|
1593
1816
|
this.#pendingReal = null;
|
|
1594
1817
|
return;
|
|
1595
1818
|
}
|
|
1819
|
+
const keptFullPad = keep.map((i) => this.#fullLeftPad[i]!);
|
|
1820
|
+
// mlx-lm BatchKVCache.filter removes padding shared by all survivors.
|
|
1821
|
+
// Full-attention width and padding move together; each row's absolute
|
|
1822
|
+
// RoPE position stays unchanged. Rotating caches keep their own layout.
|
|
1823
|
+
const trimFull = this.#kinds.includes("full") ? Math.min(...keptFullPad) : 0;
|
|
1596
1824
|
const out: LayerInner[] = [];
|
|
1597
1825
|
for (const inner of inners) {
|
|
1598
1826
|
if (isBatchableCache(inner)) {
|
|
@@ -1607,18 +1835,18 @@ export class MlxBatchExecutionGroup {
|
|
|
1607
1835
|
throw new Error("applyFilter: adopted serial rotating cache cannot be row-filtered");
|
|
1608
1836
|
} else if (isQuantizedKvCache(inner)) {
|
|
1609
1837
|
const [k0, v0] = inner.temporalView();
|
|
1610
|
-
const f = filterQuantRows(k0, v0, keep);
|
|
1838
|
+
const f = filterQuantRows(k0, v0, keep, trimFull);
|
|
1611
1839
|
for (const t of [k0, v0]) { t.packed.dispose(); t.scales.dispose(); t.biases.dispose(); }
|
|
1612
1840
|
const c = new QuantizedKVCache(inner.groupSize, inner.bits);
|
|
1613
|
-
c.restoreState(f.keys, f.values, inner.offset);
|
|
1841
|
+
c.restoreState(f.keys, f.values, inner.offset - trimFull);
|
|
1614
1842
|
out.push(c);
|
|
1615
1843
|
inner.dispose();
|
|
1616
1844
|
} else if (isPlainKvCache(inner)) {
|
|
1617
1845
|
const [k0, v0] = inner.temporalView();
|
|
1618
|
-
const f = filterKVRows(k0, v0, keep);
|
|
1846
|
+
const f = filterKVRows(k0, v0, keep, trimFull);
|
|
1619
1847
|
k0.dispose(); v0.dispose();
|
|
1620
1848
|
const c = new KVCache();
|
|
1621
|
-
c.restoreState(f.keys, f.values, inner.offset);
|
|
1849
|
+
c.restoreState(f.keys, f.values, inner.offset - trimFull);
|
|
1622
1850
|
out.push(c);
|
|
1623
1851
|
inner.dispose();
|
|
1624
1852
|
} else {
|
|
@@ -1626,7 +1854,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1626
1854
|
}
|
|
1627
1855
|
}
|
|
1628
1856
|
this.#inners = out;
|
|
1629
|
-
this.#fullLeftPad =
|
|
1857
|
+
this.#fullLeftPad = keptFullPad.map((padding) => padding - trimFull);
|
|
1630
1858
|
this.#running = keep.map((i) => this.#running[i]!);
|
|
1631
1859
|
if (this.#pendingToks) {
|
|
1632
1860
|
const idx = ops.fromInt32(keep, [keep.length]);
|