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
|
@@ -10,12 +10,16 @@ import type { SpeculativeTransaction } from "../../inference/rollback";
|
|
|
10
10
|
import { bindCacheRollback } from "./rollback";
|
|
11
11
|
import type { GraphDescriptor } from "../../inference/graph";
|
|
12
12
|
import { bindLegacyDraftTarget } from "./draft-target";
|
|
13
|
+
import type { MlxForwardWork } from "./mixed-iteration";
|
|
14
|
+
import type { PrefillPolicy } from "../../inference/prefill";
|
|
15
|
+
import { resolveMlxPrefillPolicy } from "./prefill-policy";
|
|
13
16
|
|
|
14
17
|
/** Bound target operations and draft construction for one speculative run.
|
|
15
18
|
* A replacement graph supplies this entire port, including any hidden taps.
|
|
16
19
|
* The verifier never needs a RuntimeModel or an artifact-family check. */
|
|
17
20
|
export interface MlxSpeculativeBinding {
|
|
18
21
|
readonly runtime?: RuntimeConfig;
|
|
22
|
+
readonly prefillPolicy?: PrefillPolicy;
|
|
19
23
|
readonly memory?: MlxModelMemory;
|
|
20
24
|
readonly descriptor: GraphDescriptor;
|
|
21
25
|
readonly eosTokenIds: readonly number[];
|
|
@@ -23,27 +27,29 @@ export interface MlxSpeculativeBinding {
|
|
|
23
27
|
makeCache(): Cache[];
|
|
24
28
|
openDraft(sampler: Sampler, caches: Cache[]): DraftSource;
|
|
25
29
|
bindRollback(caches: Cache[]): SpeculativeTransaction;
|
|
26
|
-
forward(ids: MlxArray, caches: Cache[], tapLayers?: number[]):
|
|
30
|
+
forward(ids: MlxArray, caches: Cache[], tapLayers?: number[], work?: MlxForwardWork):
|
|
27
31
|
Promise<{ hidden: MlxArray; ctxML: MlxArray | null }>;
|
|
28
32
|
projectLogits(hidden: MlxArray): MlxArray;
|
|
29
33
|
/** Establish the implementation's verify kernel context; restore on close. */
|
|
30
34
|
pinVerify?(): { close(): void };
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
export type MlxSpeculativeTargetBinding = Omit<MlxSpeculativeBinding, "openDraft">;
|
|
38
|
+
|
|
33
39
|
/** Legacy mutable tap/kernel fields require the gateway's exclusive lease. */
|
|
34
|
-
export function
|
|
40
|
+
export function bindSpeculativeTargetModel(model: RuntimeModel): MlxSpeculativeTargetBinding {
|
|
35
41
|
const runtime = runtimeConfig();
|
|
36
42
|
return {
|
|
37
43
|
runtime,
|
|
44
|
+
prefillPolicy: resolveMlxPrefillPolicy(model.config, runtime),
|
|
38
45
|
memory: model,
|
|
39
46
|
descriptor: Object.freeze({ id: `legacy-spec:${model.config.modelType}`, backend: "mlx",
|
|
40
47
|
graphAbi: "mlx-hidden-bsh-v1", stateAbi: "legacy-cache-array-v1", artifact: "legacy-resident-model" }),
|
|
41
48
|
eosTokenIds: model.config.eosTokenIds,
|
|
42
49
|
prefillTailSplit: runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true),
|
|
43
50
|
makeCache: model.makeCache.bind(model),
|
|
44
|
-
openDraft: (sampler, caches) => provider.open({ sampler, target: bindLegacyDraftTarget(model, caches) }),
|
|
45
51
|
bindRollback: bindCacheRollback,
|
|
46
|
-
forward: (ids, caches, tapLayers) => legacyForwardWithTaps(model, ids, caches, tapLayers),
|
|
52
|
+
forward: (ids, caches, tapLayers, work) => legacyForwardWithTaps(model, ids, caches, tapLayers, work),
|
|
47
53
|
projectLogits: model.logitsFromHidden.bind(model),
|
|
48
54
|
...("setSpecKernelPinned" in model ? {
|
|
49
55
|
pinVerify() {
|
|
@@ -54,6 +60,12 @@ export function bindLegacySpeculativeModel(model: RuntimeModel, provider: DraftP
|
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
/** Serial compatibility binds draft construction separately from target work. */
|
|
64
|
+
export function bindLegacySpeculativeModel(model: RuntimeModel, provider: DraftProvider): MlxSpeculativeBinding {
|
|
65
|
+
return { ...bindSpeculativeTargetModel(model),
|
|
66
|
+
openDraft: (sampler, caches) => provider.open({ sampler, target: bindLegacyDraftTarget(model, caches) }) };
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
export function assertMlxSpeculativeBinding(binding: MlxSpeculativeBinding): void {
|
|
58
70
|
const descriptor = binding.descriptor;
|
|
59
71
|
if (descriptor.backend !== "mlx" || descriptor.graphAbi !== "mlx-hidden-bsh-v1" ||
|
|
@@ -71,8 +83,10 @@ async function legacyForwardWithTaps(
|
|
|
71
83
|
ids: MlxArray,
|
|
72
84
|
caches: Cache[],
|
|
73
85
|
tapLayers: number[] | undefined,
|
|
86
|
+
work?: MlxForwardWork,
|
|
74
87
|
): Promise<{ hidden: MlxArray; ctxML: MlxArray | null }> {
|
|
75
88
|
if (!tapLayers) {
|
|
89
|
+
if (work) return { hidden: await work(ids, caches), ctxML: null };
|
|
76
90
|
const asyncModel = model as RuntimeModel & {
|
|
77
91
|
forwardHiddenAsync?: (
|
|
78
92
|
ids: MlxArray,
|
|
@@ -88,10 +102,13 @@ async function legacyForwardWithTaps(
|
|
|
88
102
|
const m = model;
|
|
89
103
|
const previousTap = m.hiddenTap;
|
|
90
104
|
const cap = new Map<number, MlxArray>();
|
|
91
|
-
|
|
105
|
+
const layers = new Set(tapLayers);
|
|
106
|
+
if (!work) m.hiddenTap = { layers, captured: cap };
|
|
92
107
|
let hidden: MlxArray | null = null;
|
|
93
108
|
try {
|
|
94
|
-
hidden =
|
|
109
|
+
hidden = work ? await work(ids, caches, { captureLayer(layer, h) {
|
|
110
|
+
if (layers.has(layer)) cap.set(layer, ops.contiguous(h));
|
|
111
|
+
} }) : model.forwardHidden(ids, caches);
|
|
95
112
|
const perLayer = tapLayers.map((li) => {
|
|
96
113
|
const a = cap.get(li);
|
|
97
114
|
if (!a) throw new Error(`spec tap: layer ${li} not captured`);
|
|
@@ -109,6 +126,6 @@ async function legacyForwardWithTaps(
|
|
|
109
126
|
// are already gone (cap cleared, hidden nulled).
|
|
110
127
|
hidden?.dispose();
|
|
111
128
|
for (const [, a] of cap) a.dispose();
|
|
112
|
-
m.hiddenTap = previousTap;
|
|
129
|
+
if (!work) m.hiddenTap = previousTap;
|
|
113
130
|
}
|
|
114
131
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { BatchableCache, Cache } from "../../model/gemma4-base";
|
|
2
|
+
import { applyStateChanges, cleanupFailure, disposeResources } from "../../engine/resources";
|
|
3
|
+
import type { PreparedStateChange } from "../../contracts/resources";
|
|
4
|
+
|
|
5
|
+
/** Preserve the graph's layout capabilities when an empty sibling is made. */
|
|
6
|
+
export interface MlxRowLayout<Layout extends BatchableCache> extends BatchableCache {
|
|
7
|
+
makeEmptyBatch(): Layout;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Layer layouts own tensor geometry. This collection only applies the same
|
|
11
|
+
* row membership operation to every layer. It has no scheduling, sampling,
|
|
12
|
+
* checkpoint-key or persistence policy. */
|
|
13
|
+
export class MlxStateRows<Layout extends MlxRowLayout<Layout> = BatchableCache> {
|
|
14
|
+
/** Borrowed by the bound target graph; collection ownership stays here. */
|
|
15
|
+
readonly caches: Layout[];
|
|
16
|
+
#rows = 0;
|
|
17
|
+
|
|
18
|
+
/** Takes ownership of empty, compatible layer layouts. */
|
|
19
|
+
constructor(layouts: readonly Layout[]) { this.caches = [...layouts]; }
|
|
20
|
+
get rowCount(): number { return this.#rows; }
|
|
21
|
+
/** Retain an immutable batch boundary without extracting or materializing
|
|
22
|
+
* individual rows. Layer layouts decide how to share their backing arrays. */
|
|
23
|
+
clone(): MlxStateRows<Layout> {
|
|
24
|
+
const layouts: Layout[] = [];
|
|
25
|
+
try {
|
|
26
|
+
for (const source of this.caches) {
|
|
27
|
+
const layout = source.makeEmptyBatch(); layouts.push(layout);
|
|
28
|
+
layout.mergeRows([source]);
|
|
29
|
+
}
|
|
30
|
+
const result = new MlxStateRows(layouts); result.#rows = this.#rows;
|
|
31
|
+
return result;
|
|
32
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(layouts)); }
|
|
33
|
+
}
|
|
34
|
+
/** Prepare an initial cohort with one merge per layer. */
|
|
35
|
+
mergeRows(rows: readonly (readonly Cache[])[]): void {
|
|
36
|
+
const next: Layout[] = [];
|
|
37
|
+
try {
|
|
38
|
+
for (let layer = 0; layer < this.caches.length; layer++) {
|
|
39
|
+
const layout = this.caches[layer]!.makeEmptyBatch(); next.push(layout);
|
|
40
|
+
layout.mergeRows(rows.map(row => row[layer]!));
|
|
41
|
+
}
|
|
42
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(next)); }
|
|
43
|
+
const previous = this.caches.splice(0, this.caches.length, ...next);
|
|
44
|
+
this.#rows = rows.length; disposeResources(previous);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Borrow a prepared request's state. Build every replacement before
|
|
48
|
+
* releasing the active group, so a failed admission leaves it intact. */
|
|
49
|
+
append(row: readonly Cache[]): void {
|
|
50
|
+
applyStateChanges([() => this.prepareAppend(row)]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Stage membership so a method can compose it with companion-state admission. */
|
|
54
|
+
prepareAppend(row: readonly Cache[]): PreparedStateChange {
|
|
55
|
+
return this.prepareAppendMany([row]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Stage several admissions with one membership operation per layer. */
|
|
59
|
+
prepareAppendMany(rows: readonly (readonly Cache[])[]): PreparedStateChange {
|
|
60
|
+
const next: Layout[] = [];
|
|
61
|
+
try {
|
|
62
|
+
for (let layer = 0; layer < this.caches.length; layer++) {
|
|
63
|
+
const layout = this.caches[layer]!;
|
|
64
|
+
const merged = layout.makeEmptyBatch();
|
|
65
|
+
next.push(merged);
|
|
66
|
+
merged.mergeRows([...(this.#rows ? [layout] : []), ...rows.map(row => row[layer]!)]);
|
|
67
|
+
}
|
|
68
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(next)); }
|
|
69
|
+
return {
|
|
70
|
+
commit: () => {
|
|
71
|
+
const previous = this.caches.splice(0, this.caches.length, ...next.splice(0));
|
|
72
|
+
this.#rows += rows.length;
|
|
73
|
+
next.push(...previous);
|
|
74
|
+
},
|
|
75
|
+
dispose: () => disposeResources(next.splice(0)),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The method calls this only between committed steps. A layer failure
|
|
80
|
+
* invalidates the group; its caller discards the state. */
|
|
81
|
+
filterRows(keep: readonly number[]): void {
|
|
82
|
+
if (!keep.length) { this.clear(); return; }
|
|
83
|
+
for (const layout of this.caches) layout.filterRows(keep);
|
|
84
|
+
this.#rows = keep.length;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Returns independent request state for a cache publisher or a new group. */
|
|
88
|
+
extractRow(row: number): Cache[] {
|
|
89
|
+
const state: Cache[] = [];
|
|
90
|
+
try { for (const layout of this.caches) state.push(layout.extractRow(row)); return state; }
|
|
91
|
+
catch (error) { return cleanupFailure(error, () => disposeResources(state)); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
clear(): void {
|
|
95
|
+
const next: Layout[] = [];
|
|
96
|
+
try { for (const layout of this.caches) next.push(layout.makeEmptyBatch()); }
|
|
97
|
+
catch (error) { return cleanupFailure(error, () => disposeResources(next)); }
|
|
98
|
+
const previous = this.caches.splice(0, this.caches.length, ...next);
|
|
99
|
+
this.#rows = 0;
|
|
100
|
+
disposeResources(previous);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
dispose(): void { this.#rows = 0; disposeResources(this.caches); }
|
|
104
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { StateView } from "../../contracts/resources";
|
|
2
2
|
import { disposeResources, ownResource } from "../../engine/resources";
|
|
3
3
|
import type { MlxArray } from "../../mlx/array";
|
|
4
|
-
import type
|
|
4
|
+
import { isBatchableCache, type Cache } from "../../model/gemma4-base";
|
|
5
|
+
import { cloneKvCaches, type CacheCodecProvider } from "../../kv-store";
|
|
5
6
|
|
|
6
7
|
/** Compatibility boundary for legacy state(). Numerical code consumes a lease,
|
|
7
8
|
* never the legacy ownership marker. No copy, evaluation or fence is added. */
|
|
@@ -24,3 +25,22 @@ export function leaseCacheStates(caches: readonly Cache[]): StateView<MlxArray>
|
|
|
24
25
|
throw error;
|
|
25
26
|
}
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
/** Precision/storage transitions constrain reuse independently of scheduling. */
|
|
30
|
+
export function minimumReusableOffset(caches: readonly Pick<Cache, "minimumReusableOffset">[]): number {
|
|
31
|
+
return Math.max(0, ...caches.map(cache => cache.minimumReusableOffset ?? 0));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Snapshot an adopted single-row state into persistence-compatible caches.
|
|
35
|
+
* Row layouts own compact extraction; ordinary serial caches use their codec. */
|
|
36
|
+
export function cloneSingleRowState(caches: readonly Cache[], codecs?: CacheCodecProvider): Cache[] {
|
|
37
|
+
const owned: Cache[] = [];
|
|
38
|
+
try {
|
|
39
|
+
for (const cache of caches) owned.push(...(isBatchableCache(cache)
|
|
40
|
+
? [cache.extractRow(0)] : cloneKvCaches([cache], codecs)));
|
|
41
|
+
return owned;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
try { disposeResources(owned); } catch (cleanupError) { throw new AggregateError([error, cleanupError], "single-row snapshot failed"); }
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Glm52Cache } from "../../model/glm52-cache";
|
|
2
|
+
import { SpeculativeRotatingKVCache } from "../../model/speculative-rotating-kv";
|
|
3
|
+
import { RotatingAffineLayout, SpeculativeRotatingAffineLayout } from "../../model/rotating-kv-layout";
|
|
4
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
5
|
+
import { KVCache, QuantizedKVCache, TurboQuantKVCache, RotatingKVCache, RotatingQuantizedKVCache } from "../../model/gemma4-base";
|
|
6
|
+
import { DelayedRotatingQuantizedKVCache } from "../../model/delayed-rotating-quantized-kv";
|
|
7
|
+
import { DelayedQuantizedKVCache } from "../../model/delayed-quantized-kv";
|
|
8
|
+
import { DelayedTurboQuantKVCache } from "../../model/delayed-turboquant-kv";
|
|
9
|
+
import { SSMCache } from "../../model/qwen3-delta";
|
|
10
|
+
import { BatchedSSMCache } from "../../model/batched-ssm";
|
|
11
|
+
import { BatchedKVCache } from "../../model/batched-kv";
|
|
12
|
+
import { BatchedQuantizedKVCache } from "../../model/batched-quantized-kv";
|
|
13
|
+
import { BatchedTurboQuantKVCache } from "../../model/batched-turboquant-kv";
|
|
14
|
+
|
|
15
|
+
export type TargetRowLayout = Glm52Cache | DelayedRotatingQuantizedKVCache | RotatingAffineLayout | SpeculativeRotatingKVCache | BatchedKVCache | BatchedQuantizedKVCache | BatchedTurboQuantKVCache |
|
|
16
|
+
BatchedSSMCache | DelayedTurboQuantKVCache | DelayedQuantizedKVCache;
|
|
17
|
+
|
|
18
|
+
/** The storage binding advertises a row transaction factory. The gateway can
|
|
19
|
+
* compose supported model graphs without identifying their model family. */
|
|
20
|
+
export function targetRowLayoutFactory(cache: Cache): (() => TargetRowLayout) | undefined {
|
|
21
|
+
if (cache instanceof Glm52Cache) return () => cache.makeEmptyBatch();
|
|
22
|
+
if (cache instanceof DelayedRotatingQuantizedKVCache)
|
|
23
|
+
return () => new DelayedRotatingQuantizedKVCache(cache.maxSize, cache.groupSize, cache.bits, cache.start, cache.maintain, undefined, true);
|
|
24
|
+
if (cache instanceof RotatingAffineLayout || cache instanceof RotatingQuantizedKVCache)
|
|
25
|
+
return () => new SpeculativeRotatingAffineLayout(cache.maxSize, cache.groupSize, cache.bits);
|
|
26
|
+
if (cache instanceof RotatingKVCache || cache instanceof SpeculativeRotatingKVCache) return () => new SpeculativeRotatingKVCache(cache.maxSize);
|
|
27
|
+
if (cache instanceof DelayedTurboQuantKVCache || cache instanceof DelayedQuantizedKVCache ||
|
|
28
|
+
cache instanceof BatchedTurboQuantKVCache || cache instanceof BatchedKVCache ||
|
|
29
|
+
cache instanceof BatchedQuantizedKVCache || cache instanceof BatchedSSMCache) return () => cache.makeEmptyBatch();
|
|
30
|
+
if (cache instanceof TurboQuantKVCache) return () => new BatchedTurboQuantKVCache(cache.kBits, cache.vBits, cache.fusedDecode);
|
|
31
|
+
if (cache instanceof SSMCache) return () => new BatchedSSMCache();
|
|
32
|
+
if (cache instanceof QuantizedKVCache) return () => new BatchedQuantizedKVCache(cache.groupSize, cache.bits);
|
|
33
|
+
if (cache instanceof KVCache) return () => new BatchedKVCache();
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { MlxArray } from "../../mlx/array";
|
|
2
|
+
import * as ops from "../../mlx/ops";
|
|
3
|
+
import type { BatchableCache, Cache, Mask } from "../../model/gemma4-base";
|
|
4
|
+
import { KvTensorRows } from "./kv-tensor-rows";
|
|
5
|
+
import { leaseCacheStates, minimumReusableOffset } from "./state-views";
|
|
6
|
+
import { withResource, disposeResources } from "../../engine/resources";
|
|
7
|
+
import { cloneKvCaches } from "../../kv-store";
|
|
8
|
+
|
|
9
|
+
/** Encoded layouts retain physical row alignment across precision changes. */
|
|
10
|
+
export interface TransitionedKvLayout extends BatchableCache {
|
|
11
|
+
alignRows?(leftPad: readonly number[]): void;
|
|
12
|
+
}
|
|
13
|
+
export interface SpeculativeTransitionedKvLayout extends TransitionedKvLayout {
|
|
14
|
+
specRoundBegin(): void;
|
|
15
|
+
specRoundCommit(): void;
|
|
16
|
+
specRoundRollback(keep: number | readonly number[]): void;
|
|
17
|
+
}
|
|
18
|
+
export interface KvRowTransition<Layout extends TransitionedKvLayout> {
|
|
19
|
+
readonly signature: string;
|
|
20
|
+
readonly conversionOffset?: number;
|
|
21
|
+
maintain(rows: Cache[]): void;
|
|
22
|
+
converted(row: Cache): boolean;
|
|
23
|
+
makeLayout(): Layout;
|
|
24
|
+
packRows?(layout: Layout, rows: readonly Cache[]): void;
|
|
25
|
+
prepareRows?(rows: Cache[]): Cache[];
|
|
26
|
+
extractRow?(row: Cache): Cache;
|
|
27
|
+
rollbackRow?(row: Cache, before: number, keep: number, preserveColumns: boolean): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export interface TransitioningKvPositions {
|
|
32
|
+
readonly rowOffsets: readonly number[];
|
|
33
|
+
readonly leftPad: readonly number[];
|
|
34
|
+
readonly offset: number;
|
|
35
|
+
readonly batchSize: number | null;
|
|
36
|
+
readonly ropeOffsetArr: MlxArray | undefined;
|
|
37
|
+
sync(rows: readonly Cache[], preserve: boolean): void;
|
|
38
|
+
filterRows(keep: readonly number[]): void;
|
|
39
|
+
makeMask(tokens: number, window: number | null): Mask;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|
|
42
|
+
export class FullKvPositions extends KvTensorRows implements TransitioningKvPositions {
|
|
43
|
+
sync(rows: readonly Cache[], preserve: boolean): void {
|
|
44
|
+
this.mergeRows(preserve
|
|
45
|
+
? [{ planes: [], rowOffsets: rows.map(row => row.offset), leftPad: [...this.leftPad] }]
|
|
46
|
+
: rows.map(row => ({ planes: [], rowOffsets: [row.offset], leftPad: [0] })));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Owns membership, committed positions and precision transitions. Codec and
|
|
51
|
+
* attention adapters own tensor access; scheduling and persistence use the
|
|
52
|
+
* existing batch and checkpoint contracts. Row conversion never examines an
|
|
53
|
+
* uncommitted speculative suffix. */
|
|
54
|
+
export abstract class TransitioningKvRows<Layout extends TransitionedKvLayout> implements BatchableCache {
|
|
55
|
+
protected readonly positions: TransitioningKvPositions;
|
|
56
|
+
protected rows: Cache[] = [];
|
|
57
|
+
protected packed?: Layout;
|
|
58
|
+
#prefilling = false;
|
|
59
|
+
|
|
60
|
+
readonly stateNeedsDispose = true;
|
|
61
|
+
constructor(readonly transition: KvRowTransition<Layout>, row?: Cache, positions: TransitioningKvPositions = new FullKvPositions()) {
|
|
62
|
+
this.positions = positions;
|
|
63
|
+
if (row) { this.rows = transition.prepareRows?.([row]) ?? [row]; this.syncPositions(); }
|
|
64
|
+
}
|
|
65
|
+
signature(): string { return this.transition.signature; }
|
|
66
|
+
get prefillMaintenance() { return this; }
|
|
67
|
+
beginPrefill(): void { this.#prefilling = true; }
|
|
68
|
+
commitPrefill(indices: readonly number[]): void {
|
|
69
|
+
if (this.packed) return;
|
|
70
|
+
for (const index of indices) {
|
|
71
|
+
const row = [this.rows[index]!];
|
|
72
|
+
try { this.transition.maintain(row); }
|
|
73
|
+
finally { this.rows[index] = row[0]!; }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
endPrefill(): void { this.#prefilling = false; }
|
|
77
|
+
get minimumReusableOffset(): number { return this.packed?.minimumReusableOffset ?? minimumReusableOffset(this.rows); }
|
|
78
|
+
get rowOffsets(): readonly number[] { return this.packed?.rowOffsets ?? this.positions.rowOffsets; }
|
|
79
|
+
get leftPad(): readonly number[] { return this.packed?.leftPad ?? this.positions.leftPad; }
|
|
80
|
+
get offset(): number { return this.packed?.offset ?? this.positions.offset; }
|
|
81
|
+
maxAppendTokens(): number {
|
|
82
|
+
if (this.packed) return this.packed.maxAppendTokens?.() ?? Infinity;
|
|
83
|
+
const end = this.transition.conversionOffset;
|
|
84
|
+
if (end === undefined) return Infinity;
|
|
85
|
+
return Math.min(Infinity, ...this.rowOffsets.map((offset, row) =>
|
|
86
|
+
offset < end && !this.transition.converted(this.rows[row]!) ? end - offset : Infinity));
|
|
87
|
+
}
|
|
88
|
+
get batchSize(): number | null { return this.packed?.batchSize ?? this.positions.batchSize; }
|
|
89
|
+
get ropeOffsetArr(): MlxArray | undefined { return this.packed ? this.packed.ropeOffsetArr : this.positions.ropeOffsetArr; }
|
|
90
|
+
bytesPerToken(): number { return this.packed?.bytesPerToken?.() ?? Math.max(0, ...this.rows.map(row => row.bytesPerToken?.() ?? 0)); }
|
|
91
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
92
|
+
makeMask(tokens: number, window: number | null): Mask { return (this.packed ?? this.positions).makeMask(tokens, window); }
|
|
93
|
+
isTrimmable(): boolean { return this.packed?.isTrimmable() ?? this.rows.every(row => row.isTrimmable()); }
|
|
94
|
+
trim(count: number): void {
|
|
95
|
+
if (this.packed) this.packed.trim(count);
|
|
96
|
+
else { for (const row of this.rows) row.trim(count); this.syncPositions(true); }
|
|
97
|
+
}
|
|
98
|
+
protected syncPositions(preserve = false): void { this.positions.sync(this.rows, preserve); }
|
|
99
|
+
protected advance(): void {
|
|
100
|
+
if (this.packed) return;
|
|
101
|
+
if (!this.#prefilling) this.transition.maintain(this.rows);
|
|
102
|
+
if (this.rows.length && this.rows.every(row => this.transition.converted(row))) {
|
|
103
|
+
const packed = this.transition.makeLayout();
|
|
104
|
+
try {
|
|
105
|
+
if (this.transition.packRows) this.transition.packRows(packed, this.rows);
|
|
106
|
+
else packed.mergeRows(this.rows);
|
|
107
|
+
packed.alignRows?.(this.positions.leftPad);
|
|
108
|
+
}
|
|
109
|
+
catch (error) { packed.dispose(); throw error; }
|
|
110
|
+
const previous = this.rows; this.rows = []; this.packed = packed;
|
|
111
|
+
this.positions.dispose(); disposeResources(previous);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
abstract updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray];
|
|
115
|
+
abstract makeEmptyBatch(): TransitioningKvRows<Layout>;
|
|
116
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
117
|
+
const owned: Cache[] = [];
|
|
118
|
+
try {
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
if (row instanceof TransitioningKvRows) {
|
|
121
|
+
for (let index = 0; index < row.batchSize!; index++) owned.push(row.extractRow(index));
|
|
122
|
+
} else owned.push(...cloneKvCaches([row]));
|
|
123
|
+
}
|
|
124
|
+
} catch (error) { disposeResources(owned); throw error; }
|
|
125
|
+
let prepared: Cache[];
|
|
126
|
+
try { prepared = this.transition.prepareRows?.(owned) ?? owned; }
|
|
127
|
+
catch (error) { disposeResources(owned); throw error; }
|
|
128
|
+
this.dispose(); this.rows = prepared; this.syncPositions();
|
|
129
|
+
}
|
|
130
|
+
filterRows(keep: readonly number[]): void {
|
|
131
|
+
if (this.packed) { this.packed.filterRows(keep); return; }
|
|
132
|
+
const previous = this.rows; this.rows = keep.map(index => previous[index]!);
|
|
133
|
+
disposeResources(previous.filter((_, index) => !keep.includes(index))); this.positions.filterRows(keep);
|
|
134
|
+
}
|
|
135
|
+
extractRow(row: number): Cache { return this.packed?.extractRow(row) ?? (this.transition.extractRow?.(this.rows[row]!) ?? cloneKvCaches([this.rows[row]!])[0]!); }
|
|
136
|
+
state(): MlxArray[] {
|
|
137
|
+
return withResource(leaseCacheStates(this.packed ? [this.packed] : this.rows), arrays => {
|
|
138
|
+
const views: MlxArray[] = [];
|
|
139
|
+
try { for (const array of arrays) views.push(ops.contiguous(array)); return views; }
|
|
140
|
+
catch (error) { disposeResources(views); throw error; }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
dispose(): void {
|
|
144
|
+
this.#prefilling = false;
|
|
145
|
+
const previous = this.rows; this.rows = [];
|
|
146
|
+
this.packed?.dispose(); this.packed = undefined; this.positions.dispose(); disposeResources(previous);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Rollback is a method capability, separate from ordinary cache membership. */
|
|
151
|
+
export abstract class SpeculativeTransitioningKvRows<Layout extends SpeculativeTransitionedKvLayout> extends TransitioningKvRows<Layout> {
|
|
152
|
+
#beforeRound?: number[];
|
|
153
|
+
specRoundBegin(): void {
|
|
154
|
+
this.advance(); this.#beforeRound = [...this.rowOffsets]; this.packed?.specRoundBegin();
|
|
155
|
+
}
|
|
156
|
+
specRoundCommit(): void {
|
|
157
|
+
this.packed?.specRoundCommit(); this.#beforeRound = undefined; this.advance();
|
|
158
|
+
}
|
|
159
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
160
|
+
if (this.packed) this.packed.specRoundRollback(keep);
|
|
161
|
+
else {
|
|
162
|
+
for (const [index, row] of this.rows.entries()) {
|
|
163
|
+
const count = typeof keep === "number" ? keep : keep[index]!;
|
|
164
|
+
const before = this.#beforeRound![index]!;
|
|
165
|
+
if (this.transition.rollbackRow) this.transition.rollbackRow(row, before, count, this.rows.length > 1);
|
|
166
|
+
else row.trim(row.offset - before - count);
|
|
167
|
+
}
|
|
168
|
+
this.syncPositions(true);
|
|
169
|
+
}
|
|
170
|
+
this.#beforeRound = undefined; this.advance();
|
|
171
|
+
}
|
|
172
|
+
override dispose(): void { this.#beforeRound = undefined; super.dispose(); }
|
|
173
|
+
}
|