mlx-bun 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -32
- package/docs/README.md +55 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +220 -7
- package/docs/design/decode-speed-program.md +121 -66
- package/docs/design/generic-model-support.md +116 -0
- package/docs/design/kv-cache.md +380 -211
- package/docs/design/speculative-decoding.md +165 -26
- package/docs/design/turboquant.md +20 -7
- package/docs/design/unified-engine-frontier-plan.md +120 -13
- package/docs/planning/PRODUCT_ROADMAP.md +5 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3463 -3489
- package/docs/reference/cli.md +62 -1
- package/docs/reference/distribution.md +29 -4
- package/docs/reference/environment.md +104 -8
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +17 -5
- package/docs/reference/server-api.md +181 -11
- package/docs/reference/server-config.md +232 -105
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +111 -27
- package/src/backends/mlx/cache-layout.ts +3 -3
- package/src/backends/mlx/continuation-persistence.ts +5 -4
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/gateway-binding.ts +56 -12
- package/src/backends/mlx/kv-maintenance.ts +30 -2
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +4 -3
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +15 -4
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +57 -13
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +18 -5
- package/src/backends/mlx/rollback.ts +6 -3
- package/src/backends/mlx/serial-executor.ts +16 -7
- package/src/backends/mlx/speculative-group.ts +105 -26
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/target-layout-capability.ts +1 -1
- package/src/backends/mlx/transitioning-kv-rows.ts +8 -0
- package/src/cli.ts +322 -0
- package/src/contracts/execution.ts +10 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +13 -3
- package/src/contracts/scheduling.ts +9 -0
- package/src/engine/execution-plan.ts +23 -12
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/scheduler.ts +22 -2
- package/src/fill/fill-session.ts +14 -3
- package/src/generate.ts +28 -22
- package/src/grammar.ts +13 -0
- package/src/inference/prefill.ts +6 -0
- package/src/kv-scheme.ts +22 -0
- package/src/kv-store.ts +115 -79
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +33 -5
- package/src/lab/paged-kv/paged-kv.ts +108 -42
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +37 -0
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/batched-turboquant-kv.ts +6 -5
- package/src/model/delayed-quantized-kv.ts +3 -3
- package/src/model/delayed-rotating-quantized-kv.ts +1 -1
- package/src/model/delayed-turboquant-kv.ts +6 -4
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-row.ts +1 -1
- package/src/model/gemma4-base.ts +25 -6
- package/src/model/gemma4.ts +82 -2
- package/src/model/kv-attention-view.ts +4 -1
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +99 -37
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +6 -0
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/prompt-cache.ts +265 -43
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +5 -4
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +17 -12
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +52 -11
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +2 -0
- package/src/serve/request-prep.ts +38 -21
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +132 -164
- package/src/spec/assistant-rows.ts +1 -0
- package/src/spec/deepspec-rows.ts +1 -1
- package/src/spec/dflash-source.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +1 -0
- package/src/spec/ngram-source.ts +27 -5
- package/src/spec/projected-draft-rows.ts +2 -2
- package/src/spec/qwen-mtp-rows.ts +11 -0
- package/src/spec/qwen-mtp-source.ts +3 -0
- package/src/spec/serve-loop.ts +5 -3
- package/src/spec/source.ts +14 -1
- package/src/spec/two-model-rows.ts +1 -0
- package/src/spec/two-model.ts +2 -2
- package/src/ssd-cache.ts +76 -17
- package/src/ssd-durability.ts +20 -22
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +160 -0
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- /package/docs/{planning → archive/planning}/release-notes-v0.4.0.md +0 -0
|
@@ -5,6 +5,9 @@ import type { RuntimeModel } from "../../model/factory";
|
|
|
5
5
|
import { CompiledDecode } from "../../model/compiled-decode";
|
|
6
6
|
import { runtimeConfig, type RuntimeConfig } from "../../runtime-config";
|
|
7
7
|
import { bindMlxGraph } from "./graph";
|
|
8
|
+
import type { PrefillPolicy } from "../../inference/prefill";
|
|
9
|
+
import { resolveMlxPrefillPolicy } from "./prefill-policy";
|
|
10
|
+
import type { KvSchemeOptions } from "../../kv-scheme";
|
|
8
11
|
|
|
9
12
|
/** One declaration shared by planning and both native execution lanes. */
|
|
10
13
|
export function legacyCompiledDecodeAvailable(model: RuntimeModel): boolean {
|
|
@@ -33,16 +36,33 @@ export interface MlxDecodeStep {
|
|
|
33
36
|
/** Model-owned execution for committed tokens. Recheck the chunk limit after
|
|
34
37
|
* each forward: native arithmetic can change at a cache-length boundary. */
|
|
35
38
|
export interface MlxTokenAppend {
|
|
36
|
-
|
|
39
|
+
/** Affine formats whose committed append retains one-token arithmetic. */
|
|
40
|
+
readonly affineKvBits?: readonly number[];
|
|
41
|
+
readonly turboQuantFormats?: readonly { readonly kBits: number; readonly vBits: number }[];
|
|
42
|
+
/** Maximum positions per row at this cohort size; omitted rows means B=1. */
|
|
43
|
+
maxChunkSize(state: readonly Cache[], rows?: number): number;
|
|
37
44
|
forwardHidden(ids: MlxArray, state: Cache[]): MlxArray | Promise<MlxArray>;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
/** The model declares which codecs retain its committed-span arithmetic. */
|
|
48
|
+
export function supportsCommittedAppendCache(
|
|
49
|
+
append: Pick<MlxTokenAppend, "affineKvBits" | "turboQuantFormats"> | null | undefined,
|
|
50
|
+
options: Pick<KvSchemeOptions, "kvBits" | "kvConfig" | "turboQuant">,
|
|
51
|
+
): boolean {
|
|
52
|
+
if (options.turboQuant) return !!append?.turboQuantFormats?.some(format =>
|
|
53
|
+
format.kBits === options.turboQuant!.kBits && format.vBits === options.turboQuant!.vBits);
|
|
54
|
+
return options.kvConfig?.length
|
|
55
|
+
? options.kvConfig.every(layer => append?.affineKvBits?.includes(layer.bits))
|
|
56
|
+
: !options.kvBits || !!append?.affineKvBits?.includes(options.kvBits);
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
/** One replaceable binding owns all graph-specific operations used by the AR
|
|
41
60
|
* loop. The current method requires MLX tensors and the legacy Cache[] ABI.
|
|
42
61
|
* Weights are borrowed for the binding's lifetime; generated caches are owned
|
|
43
62
|
* by the run. Caller-provided caches and media remain borrowed. */
|
|
44
63
|
export interface MlxAutoregressiveBinding {
|
|
45
64
|
readonly runtime?: RuntimeConfig;
|
|
65
|
+
readonly prefillPolicy?: PrefillPolicy;
|
|
46
66
|
readonly graph: AutoregressiveGraph<MlxArray, Cache[], MlxArray>;
|
|
47
67
|
readonly eosTokenIds: readonly number[];
|
|
48
68
|
readonly memory: MlxModelMemory;
|
|
@@ -63,6 +83,7 @@ export function bindLegacyAutoregressiveModel(model: RuntimeModel): MlxAutoregre
|
|
|
63
83
|
const runtime = runtimeConfig();
|
|
64
84
|
return {
|
|
65
85
|
runtime,
|
|
86
|
+
prefillPolicy: resolveMlxPrefillPolicy(model.config, runtime),
|
|
66
87
|
graph: bindMlxGraph<Cache[]>(model, {
|
|
67
88
|
id: `legacy:${model.config.modelType}`, artifact: "legacy-resident-model",
|
|
68
89
|
stateAbi: "legacy-cache-array-v1", // not a persistence identity
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { runMixedTokenIteration, type MlxForwardWork, type MlxPreparationWork } from "./mixed-iteration";
|
|
2
|
+
import type { MixedTokenModel } from "../../model/token-groups";
|
|
1
3
|
import type { OrdinaryContinuation } from "./continuation";
|
|
2
4
|
import type { MlxRequestStatePolicy } from "./request-state-policy";
|
|
3
5
|
import { AdmissionRejected } from "../../engine/admission";
|
|
@@ -63,6 +65,8 @@ import { MlxPrefillCohort, type PrefillState } from "./prefill-cohort";
|
|
|
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";
|
|
@@ -96,6 +100,8 @@ import { SSMCache } from "../../model/qwen3-delta";
|
|
|
96
100
|
import type { RuntimeModel } from "../../model/factory";
|
|
97
101
|
import type { GrammarController } from "../../grammar";
|
|
98
102
|
import { independentGreedySampling } from "../../sampler";
|
|
103
|
+
import { ExecutionTasks } from "../../engine/execution-tasks";
|
|
104
|
+
import { CancellationSource, GenerationCancelled } from "../../engine/cancellation";
|
|
99
105
|
import { acquireModelWiredLimit } from "../../generate";
|
|
100
106
|
import { batchRowKvBytes } from "../../serve/kv-budget";
|
|
101
107
|
import type { PromptResponseTrace } from "../../serve/prompt-response-trace";
|
|
@@ -138,8 +144,9 @@ export interface MlxGroupPreparation {
|
|
|
138
144
|
readonly rows: readonly Row[];
|
|
139
145
|
readonly tokenWeight?: number;
|
|
140
146
|
readonly canAdmit?: boolean;
|
|
147
|
+
readonly supportsMixedWork?: boolean;
|
|
141
148
|
admit?(row: Row): void;
|
|
142
|
-
advance(): Promise<boolean>;
|
|
149
|
+
advance(work?: MlxPreparationWork): Promise<boolean>;
|
|
143
150
|
dispose(): void;
|
|
144
151
|
}
|
|
145
152
|
export interface MlxGroupMethodHost {
|
|
@@ -153,8 +160,10 @@ export interface MlxGroupMethodHost {
|
|
|
153
160
|
finish(row: Row, reason: "stop" | "length"): void;
|
|
154
161
|
}
|
|
155
162
|
export interface MlxGroupedMethod {
|
|
163
|
+
/** Maximum target tokens in the next iteration, including candidates. */
|
|
164
|
+
readonly runningTokens?: number;
|
|
156
165
|
prepare(row: Row): MlxGroupPreparation;
|
|
157
|
-
advance(): Promise<void>;
|
|
166
|
+
advance(work?: MlxForwardWork): Promise<void>;
|
|
158
167
|
filterRows(keep: readonly number[], discard: boolean): void;
|
|
159
168
|
dispose(): void;
|
|
160
169
|
}
|
|
@@ -171,6 +180,9 @@ export type BatchRequest = BatchRequestFields & (
|
|
|
171
180
|
);
|
|
172
181
|
|
|
173
182
|
interface BatchRequestFields {
|
|
183
|
+
/** Borrowed prepared input; its owner retains it through preparation. */
|
|
184
|
+
promptInput?: import("./prompt-input").MlxPromptInput;
|
|
185
|
+
cacheSessionId?: string;
|
|
174
186
|
/** Optional ordinary continuation policy; owns persistence and sampler recovery. */
|
|
175
187
|
continuation?: OrdinaryContinuation;
|
|
176
188
|
statePolicy?: MlxRequestStatePolicy;
|
|
@@ -225,6 +237,7 @@ interface BatchRequestFields {
|
|
|
225
237
|
|
|
226
238
|
export interface BatchStats {
|
|
227
239
|
spec?: import("../../generate").GenerateStats["spec"];
|
|
240
|
+
fill?: import("../../generate").GenerateStats["fill"];
|
|
228
241
|
promptTokens: number;
|
|
229
242
|
generatedTokens: number;
|
|
230
243
|
/** Prompt tokens served from the prompt cache (Phase 3.2): a joiner's solo
|
|
@@ -239,6 +252,8 @@ export interface BatchStats {
|
|
|
239
252
|
decodeMs: number;
|
|
240
253
|
}
|
|
241
254
|
|
|
255
|
+
let nextMixedWorkId = 0;
|
|
256
|
+
|
|
242
257
|
/** The slice of PromptCache the scheduler drives (structural — the server's
|
|
243
258
|
* PromptCache satisfies it). take() on admission (any joiner: a restored
|
|
244
259
|
* prefix + suffix prefill is byte-safe whether the row later merges or
|
|
@@ -251,6 +266,7 @@ export type RowPromptCache = import("./checkpoint-state").MlxPrefixCache;
|
|
|
251
266
|
|
|
252
267
|
export interface Row {
|
|
253
268
|
spec?: import("../../generate").GenerateStats["spec"];
|
|
269
|
+
fill?: import("../../generate").GenerateStats["fill"];
|
|
254
270
|
req: BatchRequest;
|
|
255
271
|
resolve: (s: BatchStats) => void;
|
|
256
272
|
reject: (e: unknown) => void;
|
|
@@ -342,6 +358,7 @@ type LayerInner =
|
|
|
342
358
|
type Row1 = { keys: MlxArray; values: MlxArray };
|
|
343
359
|
|
|
344
360
|
export class MlxBatchExecutionGroup {
|
|
361
|
+
readonly #tasks = new ExecutionTasks();
|
|
345
362
|
readonly #runtime: RuntimeConfig;
|
|
346
363
|
readonly #noPipeline: boolean;
|
|
347
364
|
readonly #stepTrace: boolean;
|
|
@@ -363,7 +380,9 @@ export class MlxBatchExecutionGroup {
|
|
|
363
380
|
#pendingReal: boolean[] | null = null;
|
|
364
381
|
#method: MlxGroupedMethod | undefined;
|
|
365
382
|
#methodKey: string | undefined;
|
|
383
|
+
#decodeStateKey: string | undefined;
|
|
366
384
|
#steps = 0; // decode-step counter (clearCache cadence)
|
|
385
|
+
#cacheMaintenanceSteps = 0;
|
|
367
386
|
#looping = false;
|
|
368
387
|
#closed = false;
|
|
369
388
|
#driver: Promise<void> | null = null;
|
|
@@ -372,6 +391,7 @@ export class MlxBatchExecutionGroup {
|
|
|
372
391
|
readonly #lock: ExclusiveLock | undefined;
|
|
373
392
|
readonly #admissionHeld: (() => boolean) | undefined;
|
|
374
393
|
readonly #prefillChunkSize: number;
|
|
394
|
+
readonly #prefillPolicy: PrefillPolicy;
|
|
375
395
|
readonly #prefillBatchTokenLimit: number;
|
|
376
396
|
readonly #prefillTailSplit: boolean;
|
|
377
397
|
readonly #kvBudgetBytes: number | undefined;
|
|
@@ -409,8 +429,8 @@ export class MlxBatchExecutionGroup {
|
|
|
409
429
|
this.#maxBatch = Math.max(1, Math.floor(opts.maxBatch));
|
|
410
430
|
this.#lock = opts.lock;
|
|
411
431
|
this.#admissionHeld = opts.admissionHeld;
|
|
412
|
-
this.#
|
|
413
|
-
|
|
432
|
+
this.#prefillPolicy = resolveMlxPrefillPolicy(model.config, this.#runtime, opts.prefillChunkSize);
|
|
433
|
+
this.#prefillChunkSize = this.#prefillPolicy.chunkSize(0);
|
|
414
434
|
this.#prefillBatchTokenLimit = opts.prefillBatchTokenLimit ?? 2048;
|
|
415
435
|
this.#prefillTailSplit = this.#runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true);
|
|
416
436
|
this.#kvBudgetBytes = opts.kvBudgetBytes;
|
|
@@ -535,6 +555,7 @@ export class MlxBatchExecutionGroup {
|
|
|
535
555
|
if (this.#closed) return Promise.reject(new Error("scheduler closed"));
|
|
536
556
|
if (req.signal?.aborted) return Promise.reject(req.signal.reason);
|
|
537
557
|
if (this.#pending.length >= this.#maxQueued) return Promise.reject(new AdmissionRejected());
|
|
558
|
+
req = { ...req, prefillChunkSize: req.prefillChunkSize ?? this.#prefillPolicy.chunkSize(req.promptIds.length) };
|
|
538
559
|
return new Promise<BatchStats>((resolve, reject) => {
|
|
539
560
|
let abortListener: (() => void) | null = null;
|
|
540
561
|
const cleanup = () => {
|
|
@@ -562,6 +583,21 @@ export class MlxBatchExecutionGroup {
|
|
|
562
583
|
this.#ensureLoop();
|
|
563
584
|
}
|
|
564
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
|
+
|
|
565
601
|
#ensureLoop(): void {
|
|
566
602
|
if (this.#wake) { this.#wake(); return; }
|
|
567
603
|
if (this.#looping || this.#closed) return;
|
|
@@ -580,6 +616,8 @@ export class MlxBatchExecutionGroup {
|
|
|
580
616
|
async #drive(): Promise<void> {
|
|
581
617
|
const scheduler = this;
|
|
582
618
|
const group: ExecutionGroup = {
|
|
619
|
+
get pendingTasks() { return scheduler.#tasks.pending; },
|
|
620
|
+
advanceTask: () => this.#tasks.advance(),
|
|
583
621
|
get active() { return scheduler.#running.length; },
|
|
584
622
|
get queued() { return scheduler.#pending.length; },
|
|
585
623
|
get preparing() { return scheduler.#prefill !== null; },
|
|
@@ -606,8 +644,46 @@ export class MlxBatchExecutionGroup {
|
|
|
606
644
|
canBurst: () => this.#contextCompatible(this.#pending[0]!) &&
|
|
607
645
|
(this.#kvBudgetBytes === undefined ||
|
|
608
646
|
this.projectedKvBytes + this.#rowKvBytes(this.#pending[0]!) <= this.#kvBudgetBytes),
|
|
609
|
-
|
|
610
|
-
|
|
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
|
+
},
|
|
611
687
|
failActive: (error) => {
|
|
612
688
|
for (const row of this.#running) row.reject(error);
|
|
613
689
|
this.#applyFilter([], true); // failed state never enters the prefix store
|
|
@@ -629,12 +705,12 @@ export class MlxBatchExecutionGroup {
|
|
|
629
705
|
await driveExecutionGroup(group, {
|
|
630
706
|
now: () => performance.now(),
|
|
631
707
|
yield: () => new Promise<void>((resolve) => setImmediate(resolve)),
|
|
632
|
-
}, this.#runtime.
|
|
708
|
+
}, this.#runtime.flag("MLX_BUN_EARLY_FIRST_TOKEN", true));
|
|
633
709
|
} finally { this.#looping = false; }
|
|
634
710
|
}
|
|
635
711
|
|
|
636
712
|
#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);
|
|
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);
|
|
638
714
|
}
|
|
639
715
|
|
|
640
716
|
#leaveContext(): void {
|
|
@@ -649,6 +725,7 @@ export class MlxBatchExecutionGroup {
|
|
|
649
725
|
const row = this.#pending.shift()!;
|
|
650
726
|
try {
|
|
651
727
|
this.#statePolicy = row.req.statePolicy;
|
|
728
|
+
this.#decodeStateKey = row.req.promptInput?.decodeState?.key;
|
|
652
729
|
row.cacheNamespace = typeof row.req.cacheNamespace === "function"
|
|
653
730
|
? row.req.cacheNamespace() : row.req.cacheNamespace;
|
|
654
731
|
const key = row.req.context?.key ?? "";
|
|
@@ -705,11 +782,11 @@ export class MlxBatchExecutionGroup {
|
|
|
705
782
|
for (const row of rows) row.reject(error);
|
|
706
783
|
}
|
|
707
784
|
|
|
708
|
-
async #advancePreparation(): Promise<void> {
|
|
785
|
+
async #advancePreparation(work?: MlxPreparationWork): Promise<void> {
|
|
709
786
|
const p = this.#prefill!, rows = p.rows;
|
|
710
787
|
this.#preparationPublishedOutput = false;
|
|
711
788
|
try {
|
|
712
|
-
if (await p.advance()) this.#prefill = null;
|
|
789
|
+
if (await p.advance(work)) this.#prefill = null;
|
|
713
790
|
} catch (error) {
|
|
714
791
|
this.#prefill = null;
|
|
715
792
|
let failure = error;
|
|
@@ -725,6 +802,7 @@ export class MlxBatchExecutionGroup {
|
|
|
725
802
|
}
|
|
726
803
|
|
|
727
804
|
#failAll(error: unknown): void {
|
|
805
|
+
this.#tasks.rejectAll(error);
|
|
728
806
|
const p = this.#prefill;
|
|
729
807
|
const rows = new Set([...this.#running, ...this.#pending, ...(p?.rows ?? [])]);
|
|
730
808
|
const retain = this.#adoptedRetain;
|
|
@@ -809,7 +887,7 @@ export class MlxBatchExecutionGroup {
|
|
|
809
887
|
row.cachedTokens = Math.min(saved.cacheTokens.length, row.promptTokens);
|
|
810
888
|
const stop = await this.#emit(row, saved.pendingToken);
|
|
811
889
|
if (stop !== "continue") {
|
|
812
|
-
this.#putOrDispose(p.solo, saved.cacheTokens, p.retain, row.cacheNamespace);
|
|
890
|
+
this.#putOrDispose(p.solo, saved.cacheTokens, p.retain, row.cacheNamespace, row.req.cacheSessionId);
|
|
813
891
|
p.solo = [];
|
|
814
892
|
this.#finish(row, stop);
|
|
815
893
|
return;
|
|
@@ -831,7 +909,7 @@ export class MlxBatchExecutionGroup {
|
|
|
831
909
|
// later pipeline step. Grammar and explicit eager-output modes retain
|
|
832
910
|
// their read-before-advance order.
|
|
833
911
|
if (!this.#inners && !p.row.req.grammar && !this.#noPipeline &&
|
|
834
|
-
this.#runtime.
|
|
912
|
+
!this.#runtime.flag("MLX_BUN_EARLY_FIRST_TOKEN", true) && !forceAttribution) {
|
|
835
913
|
let owned: MlxArray | null = sampled;
|
|
836
914
|
try {
|
|
837
915
|
ops.asyncEvalAll([sampled]);
|
|
@@ -861,7 +939,7 @@ export class MlxBatchExecutionGroup {
|
|
|
861
939
|
const stop = await this.#emit(p.row, tok);
|
|
862
940
|
// Token 0 was sampled but never fed — the caches cover exactly the
|
|
863
941
|
// prompt, a clean prompt-only entry (put-or-dispose).
|
|
864
|
-
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain, p.row.cacheNamespace);
|
|
942
|
+
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain, p.row.cacheNamespace, p.row.req.cacheSessionId);
|
|
865
943
|
this.#finish(p.row, stop === "continue" ? "stop" : stop);
|
|
866
944
|
return;
|
|
867
945
|
}
|
|
@@ -869,7 +947,7 @@ export class MlxBatchExecutionGroup {
|
|
|
869
947
|
|
|
870
948
|
const stop = await this.#emit(p.row, tok);
|
|
871
949
|
if (stop !== "continue") {
|
|
872
|
-
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain, p.row.cacheNamespace);
|
|
950
|
+
this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain, p.row.cacheNamespace, p.row.req.cacheSessionId);
|
|
873
951
|
this.#finish(p.row, stop);
|
|
874
952
|
return;
|
|
875
953
|
}
|
|
@@ -1147,7 +1225,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1147
1225
|
* Rows that finish get one extra harmless KV write from the already-built
|
|
1148
1226
|
* step; filter drops the row (mlx-lm behaves identically). Length-finished
|
|
1149
1227
|
* rows are known in advance and are NOT sampled (placeholder slot). */
|
|
1150
|
-
async #step(): Promise<void> {
|
|
1228
|
+
async #step(forward?: MlxForwardWork): Promise<void> {
|
|
1151
1229
|
if (this.#stepTrace) {
|
|
1152
1230
|
const now = performance.now();
|
|
1153
1231
|
if (STEP_T.lastEnd) STEP_T.gap += now - STEP_T.lastEnd;
|
|
@@ -1156,6 +1234,11 @@ export class MlxBatchExecutionGroup {
|
|
|
1156
1234
|
const rows = this.#running;
|
|
1157
1235
|
const B = rows.length;
|
|
1158
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
|
+
}
|
|
1159
1242
|
|
|
1160
1243
|
// A row is live if it still needs tokens sampled; a row whose pending
|
|
1161
1244
|
// unread token is its last (sampled == maxTokens) only awaits emission.
|
|
@@ -1175,7 +1258,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1175
1258
|
const hasLiveGrammar = anyLive && rows.some(
|
|
1176
1259
|
(r) => r.req.grammar && !r.req.grammar.isTerminated,
|
|
1177
1260
|
);
|
|
1178
|
-
if (hasLiveGrammar) return this.#stepGrammar();
|
|
1261
|
+
if (hasLiveGrammar) return this.#stepGrammar(forward);
|
|
1179
1262
|
|
|
1180
1263
|
let nextToks: MlxArray | null = null;
|
|
1181
1264
|
let nextReal: boolean[] | null = null;
|
|
@@ -1209,7 +1292,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1209
1292
|
let lg: MlxArray | null = null;
|
|
1210
1293
|
let evalWith: MlxArray[] = [];
|
|
1211
1294
|
if (
|
|
1212
|
-
this.#compiled && B === 1 && unpadded && this.#running[0]!.req.compiledDecode !== false &&
|
|
1295
|
+
!forward && this.#compiled && B === 1 && unpadded && this.#running[0]!.req.compiledDecode !== false &&
|
|
1213
1296
|
(!this.#pendingToks || this.#pendingToks.dtype === Dtype.uint32) &&
|
|
1214
1297
|
// A filtered-to-one BATCHED rot-quant cache subclasses the serial
|
|
1215
1298
|
// class (so supports() passes) but carries batched ring state —
|
|
@@ -1251,7 +1334,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1251
1334
|
const ids = this.#pendingToks
|
|
1252
1335
|
? ops.reshape(this.#pendingToks, [B, 1]) // feed the unread tokens
|
|
1253
1336
|
: ops.fromInt32(rows.map((r) => r.current), [B, 1]); // pipeline cold
|
|
1254
|
-
const h = await this.#forwardHidden(ids, fwd);
|
|
1337
|
+
const h = await (forward ? forward(ids, fwd) : this.#forwardHidden(ids, fwd));
|
|
1255
1338
|
ids.dispose();
|
|
1256
1339
|
lg = this.model.logitsFromHidden(h); // [B,1,V]
|
|
1257
1340
|
h.dispose();
|
|
@@ -1407,7 +1490,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1407
1490
|
* finish("stop") in #emitRows. On a cold start (first step after prefill)
|
|
1408
1491
|
* there is no pending array to read; the prefill already accepted token 0
|
|
1409
1492
|
* and fired the fill, so we just await ready() + sample. */
|
|
1410
|
-
async #stepGrammar(): Promise<void> {
|
|
1493
|
+
async #stepGrammar(forward?: MlxForwardWork): Promise<void> {
|
|
1411
1494
|
const rows = this.#running;
|
|
1412
1495
|
const B = rows.length;
|
|
1413
1496
|
const inners = this.#inners!;
|
|
@@ -1454,7 +1537,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1454
1537
|
const ids = prev
|
|
1455
1538
|
? ops.reshape(prev, [B, 1]) // feed the unread tokens (device array)
|
|
1456
1539
|
: ops.fromInt32(rows.map((r) => r.current), [B, 1]); // pipeline cold
|
|
1457
|
-
const h = await this.#forwardHidden(ids, fwd);
|
|
1540
|
+
const h = await (forward ? forward(ids, fwd) : this.#forwardHidden(ids, fwd));
|
|
1458
1541
|
ids.dispose();
|
|
1459
1542
|
const lg = this.model.logitsFromHidden(h); // [B,1,V]
|
|
1460
1543
|
h.dispose();
|
|
@@ -1588,6 +1671,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1588
1671
|
const first = row.firstTokenAt || now;
|
|
1589
1672
|
row.resolve({
|
|
1590
1673
|
...(row.spec ? { spec: row.spec } : {}),
|
|
1674
|
+
...(row.fill ? { fill: row.fill } : {}),
|
|
1591
1675
|
promptTokens: row.promptTokens,
|
|
1592
1676
|
generatedTokens: row.generated,
|
|
1593
1677
|
cachedTokens: row.cachedTokens,
|
|
@@ -1602,7 +1686,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1602
1686
|
* offset lines up (defensive — a mismatch means the accounting is wrong
|
|
1603
1687
|
* and the entry would corrupt future hits), else dispose. `retain` rides
|
|
1604
1688
|
* along per the PromptCacheEntry contract (runs after dispose). */
|
|
1605
|
-
#putOrDispose(caches: Cache[], tokens: number[], retain?: () => void, namespace = ""): void {
|
|
1689
|
+
#putOrDispose(caches: Cache[], tokens: number[], retain?: () => void, namespace = "", sessionId?: string): void {
|
|
1606
1690
|
if (this.#promptCache) {
|
|
1607
1691
|
// A never-merged row can still own a row layout. Persistence receives
|
|
1608
1692
|
// its existing serial representation, just like merged-row retirement.
|
|
@@ -1623,7 +1707,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1623
1707
|
(c) => typeof (c as { offset?: unknown }).offset === "number",
|
|
1624
1708
|
) as { offset: number } | undefined;
|
|
1625
1709
|
if (withOff && withOff.offset === tokens.length) {
|
|
1626
|
-
this.#promptCache.put(tokens, caches, namespace, retain);
|
|
1710
|
+
this.#promptCache.put(tokens, caches, namespace, retain, undefined, sessionId);
|
|
1627
1711
|
return;
|
|
1628
1712
|
}
|
|
1629
1713
|
}
|
|
@@ -1634,13 +1718,13 @@ export class MlxBatchExecutionGroup {
|
|
|
1634
1718
|
/** Extract a finishing MERGED row's KV into fresh serial caches and put()
|
|
1635
1719
|
* them keyed by [promptIds + fed] — the batch-lane mirror of mlx-lm
|
|
1636
1720
|
* server.py:872 (extract_cache → prompt_cache.insert_cache). Gates:
|
|
1637
|
-
* promptTokens >= 256
|
|
1638
|
-
*
|
|
1721
|
+
* promptTokens >= 256 or an explicit cache session, plus an exact
|
|
1722
|
+
* coverage key (!fedTainted). Refusal
|
|
1639
1723
|
* (#extractRowCaches null) just disposes-by-omission — the row's KV dies
|
|
1640
1724
|
* with the filter, exactly the pre-extraction behavior. */
|
|
1641
1725
|
#extractAndPut(b: number, row: Row): void {
|
|
1642
1726
|
if (!this.#promptCache || !row.merged || row.fedTainted) return;
|
|
1643
|
-
if (row.promptTokens < 256) return;
|
|
1727
|
+
if (row.promptTokens < 256 && !row.req.cacheSessionId) return;
|
|
1644
1728
|
const tokens = [...row.req.promptIds, ...row.fed];
|
|
1645
1729
|
const caches = this.#extractRowCaches(b, tokens.length);
|
|
1646
1730
|
if (!caches) return;
|
|
@@ -1649,7 +1733,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1649
1733
|
// buffers free once the copies land, instead of being pinned by a lazy
|
|
1650
1734
|
// graph inside an idle cache entry.
|
|
1651
1735
|
withResource(leaseCacheStates(caches), state => ops.asyncEvalAll([...state]));
|
|
1652
|
-
this.#putOrDispose(caches, tokens, undefined, row.cacheNamespace);
|
|
1736
|
+
this.#putOrDispose(caches, tokens, undefined, row.cacheNamespace, row.req.cacheSessionId);
|
|
1653
1737
|
}
|
|
1654
1738
|
|
|
1655
1739
|
/** Row `b` of every layer as OWNED serial-class caches, or null when a
|
|
@@ -1717,7 +1801,7 @@ export class MlxBatchExecutionGroup {
|
|
|
1717
1801
|
inners as Cache[],
|
|
1718
1802
|
[...solo.req.promptIds, ...solo.fed],
|
|
1719
1803
|
this.#adoptedRetain ?? undefined,
|
|
1720
|
-
solo.cacheNamespace,
|
|
1804
|
+
solo.cacheNamespace, solo.req.cacheSessionId,
|
|
1721
1805
|
);
|
|
1722
1806
|
} else {
|
|
1723
1807
|
for (const c of inners) c.dispose();
|
|
@@ -8,15 +8,15 @@ import { PagedKvRows } from "../../lab/paged-kv/paged-kv-rows";
|
|
|
8
8
|
|
|
9
9
|
/** Bind encoded state to its row layout. Execution owners only move rows. */
|
|
10
10
|
export function ownedCacheLayout(cache: Cache): BatchableCache | undefined {
|
|
11
|
-
if (cache instanceof PagedKVCache) return new PagedKvRows(cache.capacityTokens, cache.blockSize);
|
|
11
|
+
if (cache instanceof PagedKVCache) return new PagedKvRows(cache.capacityTokens, cache.blockSize, cache.direct, cache.quantization);
|
|
12
12
|
if (isBatchableCache(cache)) return cache.makeEmptyBatch();
|
|
13
|
-
if (cache instanceof TurboQuantKVCache) return new BatchedTurboQuantKVCache(cache.kBits, cache.vBits);
|
|
13
|
+
if (cache instanceof TurboQuantKVCache) return new BatchedTurboQuantKVCache(cache.kBits, cache.vBits, cache.fusedDecode);
|
|
14
14
|
return undefined;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/** Prefill reuses decode's layouts; shape selection belongs to storage. */
|
|
18
18
|
export function prefillCacheLayout(cache: Cache): BatchableCache {
|
|
19
|
-
if (cache instanceof PagedKVCache) return new PagedKvRows(cache.capacityTokens, cache.blockSize);
|
|
19
|
+
if (cache instanceof PagedKVCache) return new PagedKvRows(cache.capacityTokens, cache.blockSize, cache.direct, cache.quantization);
|
|
20
20
|
if (isBatchableCache(cache)) return cache.makeEmptyBatch();
|
|
21
21
|
if (cache instanceof RotatingKVCache) return new DelayedRotatingQuantizedKVCache(cache.maxSize, 64, 4, Infinity, () => {});
|
|
22
22
|
if (cache instanceof RotatingQuantizedKVCache) return new RotatingAffineLayout(cache.maxSize, cache.groupSize, cache.bits);
|
|
@@ -19,13 +19,14 @@ export class ContinuationPersistence {
|
|
|
19
19
|
#nextBarrier = 0;
|
|
20
20
|
constructor(readonly store: ContinuationStore, options: {
|
|
21
21
|
maxBytes: number;
|
|
22
|
-
/**
|
|
23
|
-
runStep
|
|
22
|
+
/** Optional owner-thread preparation hook; disk writes use the CPU worker. */
|
|
23
|
+
runStep?: <T>(step: () => T) => Promise<T>;
|
|
24
24
|
}) {
|
|
25
|
+
const runStep = options.runStep ?? (async <T>(step: () => T): Promise<T> => step());
|
|
25
26
|
this.#queue = new SpillQueue(options.maxBytes, cacheBytes, async item => {
|
|
26
27
|
const job = this.#jobs.get(item.caches)!;
|
|
27
28
|
if (job.cleanup) {
|
|
28
|
-
await
|
|
29
|
+
await runStep(() => store.removeGenerationCheckpoints(job.attempt.key));
|
|
29
30
|
return true;
|
|
30
31
|
}
|
|
31
32
|
const current = () => this.#attempts.get(job.attempt.key) === job.attempt && !job.attempt.completed;
|
|
@@ -33,7 +34,7 @@ export class ContinuationPersistence {
|
|
|
33
34
|
const stored = await store.storeGenerationCheckpoint(item.tokens, item.caches, job.metadata!, options.runStep);
|
|
34
35
|
// No newer queued write can run until this callback returns. An old
|
|
35
36
|
// in-flight rename can therefore be removed without deleting new work.
|
|
36
|
-
if (!current()) await
|
|
37
|
+
if (!current()) await runStep(() => store.removeGenerationCheckpoints(job.attempt.key));
|
|
37
38
|
return stored;
|
|
38
39
|
}, disposeResources, item => !this.#jobs.get(item.caches)?.cleanup);
|
|
39
40
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ObjectCache } from "../../contracts/object-cache";
|
|
2
|
+
import { cloneAttachments, disposeAttachments, type CheckpointAttachment } from "./checkpoint-state";
|
|
3
|
+
import type { MlxArray } from "../../mlx/array";
|
|
4
|
+
|
|
5
|
+
/** Encoder-owned tensor schema over the cache's exact-object interface. */
|
|
6
|
+
export class EncoderCache {
|
|
7
|
+
constructor(private readonly objects: ObjectCache<CheckpointAttachment[]>,
|
|
8
|
+
readonly identity: string) {}
|
|
9
|
+
|
|
10
|
+
async take(key: string): Promise<MlxArray | null> {
|
|
11
|
+
const lease = await this.objects.take(`encoder-v1:${this.identity}:${key}`);
|
|
12
|
+
if (!lease) return null;
|
|
13
|
+
try {
|
|
14
|
+
const tensor = lease.value[0]!.tensors[0]!;
|
|
15
|
+
return tensor.slice(tensor.shape.map(() => 0), [...tensor.shape]);
|
|
16
|
+
} finally { lease.dispose(); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Borrow the result; storage retains its own immutable native view. */
|
|
20
|
+
put(key: string, features: MlxArray): void {
|
|
21
|
+
const owned = cloneAttachments([{ schema: "encoder-features-v1", metadata: {}, tensors: [features] }]);
|
|
22
|
+
try { this.objects.put(`encoder-v1:${this.identity}:${key}`, owned); }
|
|
23
|
+
catch (error) { disposeAttachments(owned); throw error; }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -6,8 +6,8 @@ import { leaseCacheStates } from "./state-views";
|
|
|
6
6
|
|
|
7
7
|
/** Append an already accepted span without intermediate vocabulary heads.
|
|
8
8
|
* Chunking changes the graph shape, so callers must qualify its numerics for
|
|
9
|
-
* their model.
|
|
10
|
-
* one forward per round.
|
|
9
|
+
* their model. Verification uses its separate round operation because
|
|
10
|
+
* recurrent rollback records one forward per round. Append precedes emission/cancellation
|
|
11
11
|
* boundaries, preserving the cache-covered prefix on a consumer break.
|
|
12
12
|
*/
|
|
13
13
|
export async function appendFillHidden(
|
|
@@ -17,15 +17,29 @@ export async function appendFillHidden(
|
|
|
17
17
|
chunkSize: (cache: readonly Cache[]) => number,
|
|
18
18
|
): Promise<MlxArray> {
|
|
19
19
|
const input = ops.fromInt32(ids, [1, ids.length]);
|
|
20
|
+
try { return await appendHiddenRows(forward, cache, input, chunkSize); }
|
|
21
|
+
finally { input.dispose(); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Append equal-length committed token rows [B,L]. The caller owns the input;
|
|
25
|
+
* the returned hidden rows belong to the caller. Model policy receives B so
|
|
26
|
+
* a larger cohort cannot silently cross its qualified projection geometry. */
|
|
27
|
+
export async function appendHiddenRows(
|
|
28
|
+
forward: (ids: MlxArray, cache: Cache[]) => MlxArray | Promise<MlxArray>,
|
|
29
|
+
cache: Cache[],
|
|
30
|
+
input: MlxArray,
|
|
31
|
+
chunkSize: (cache: readonly Cache[], rows: number) => number,
|
|
32
|
+
): Promise<MlxArray> {
|
|
33
|
+
const [rows, length] = input.shape as [number, number];
|
|
20
34
|
const hidden: MlxArray[] = [];
|
|
21
35
|
try {
|
|
22
|
-
for (let start = 0; start <
|
|
23
|
-
const limit = chunkSize(cache);
|
|
36
|
+
for (let start = 0; start < length;) {
|
|
37
|
+
const limit = chunkSize(cache, rows);
|
|
24
38
|
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
25
39
|
throw new Error("Model append chunk limit must be a positive integer");
|
|
26
|
-
const end = Math.min(
|
|
27
|
-
if (start === 0 && end ===
|
|
28
|
-
const part = input.slice([0, start], [
|
|
40
|
+
const end = Math.min(length, start + limit);
|
|
41
|
+
if (start === 0 && end === length) return await forward(input, cache);
|
|
42
|
+
const part = input.slice([0, start], [rows, end]);
|
|
29
43
|
try {
|
|
30
44
|
const h = await forward(part, cache);
|
|
31
45
|
hidden.push(h);
|
|
@@ -40,6 +54,5 @@ export async function appendFillHidden(
|
|
|
40
54
|
return ops.concatAxis(hidden, 1);
|
|
41
55
|
} finally {
|
|
42
56
|
for (const h of hidden) h.dispose();
|
|
43
|
-
input.dispose();
|
|
44
57
|
}
|
|
45
58
|
}
|