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
package/src/mlx/ops.ts
CHANGED
|
@@ -472,14 +472,12 @@ export function where(cond: MlxArray, x: MlxArray, y: MlxArray, s: S = gpuStream
|
|
|
472
472
|
);
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
// across attention layers and sampling, but bound their retained storage:
|
|
482
|
-
// causal masks request a new range on every round once context exceeds 64K.
|
|
475
|
+
// The old native arange workaround concerned stale typed-array out-pointer
|
|
476
|
+
// reads after Bun JIT tier-up, not f64 argument marshaling. outArray uses
|
|
477
|
+
// native pointer reads now. Large nonnegative unit-step int32 ranges avoid
|
|
478
|
+
// host construction; small/general integer ranges retain JS conversion,
|
|
479
|
+
// including its wrapping behavior outside int32. Cache storage stays bounded
|
|
480
|
+
// as causal masks request new lengths on successive long-context rounds.
|
|
483
481
|
const arangeCache = new Map<string, { array: MlxArray; bytes: number }>();
|
|
484
482
|
const ARANGE_CACHE_MIN = 65536;
|
|
485
483
|
const ARANGE_CACHE_BYTES = 8 * 1024 * 1024;
|
|
@@ -498,8 +496,10 @@ export function arange(start: number, stop: number, step: number, dtype: Dtype,
|
|
|
498
496
|
}
|
|
499
497
|
|
|
500
498
|
let arr: MlxArray;
|
|
501
|
-
|
|
502
|
-
|
|
499
|
+
const nativePositions = n === 0 ||
|
|
500
|
+
(n >= ARANGE_CACHE_MIN && step === 1 && start >= 0 && stop <= 2147483647);
|
|
501
|
+
if (dtype === Dtype.int32 && !nativePositions) {
|
|
502
|
+
// Preserve JS conversion for small/general integer ranges.
|
|
503
503
|
const data = new Int32Array(n);
|
|
504
504
|
for (let i = 0; i < n; i++) data[i] = start + i * step;
|
|
505
505
|
arr = MlxArray.fromInt32(data, [n]);
|
|
@@ -1019,3 +1019,90 @@ export function leftShift(a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray
|
|
|
1019
1019
|
export function rightShift(a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray {
|
|
1020
1020
|
return new MlxArray(outArray("right_shift", (o) => C.mlx_right_shift(o, a.handle, b.handle, s)));
|
|
1021
1021
|
}
|
|
1022
|
+
|
|
1023
|
+
// --- Whisper additions (fft / strided framing / pad / reductions) ----------
|
|
1024
|
+
|
|
1025
|
+
/** mx.fft.rfft(a, n, axis) with numpy "backward" normalization. Real input
|
|
1026
|
+
* → complex64 output of length n/2+1 along `axis`. */
|
|
1027
|
+
export function rfft(a: MlxArray, n: number, axis = -1, s: S = gpuStream): MlxArray {
|
|
1028
|
+
return new MlxArray(outArray("fft_rfft", (o) => C.mlx_fft_rfft(o, a.handle, n, axis, 0, s)));
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** mx.as_strided(a, shape, strides, offset) — element strides. */
|
|
1032
|
+
export function asStrided(
|
|
1033
|
+
a: MlxArray, shape: number[], strides: number[], offset = 0, s: S = gpuStream,
|
|
1034
|
+
): MlxArray {
|
|
1035
|
+
const shp = new Int32Array(shape);
|
|
1036
|
+
const str = new BigInt64Array(strides.map((v) => BigInt(v)));
|
|
1037
|
+
return new MlxArray(
|
|
1038
|
+
outArray("as_strided", (o) =>
|
|
1039
|
+
C.mlx_as_strided(o, a.handle, ptr(shp), BigInt(shape.length), ptr(str), BigInt(strides.length), BigInt(offset), s),
|
|
1040
|
+
),
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/** mx.pad(a, [(lo, hi), …]) with constant zero fill (all axes given). */
|
|
1045
|
+
export function pad(a: MlxArray, widths: [number, number][], s: S = gpuStream): MlxArray {
|
|
1046
|
+
const axes = new Int32Array(widths.map((_, i) => i));
|
|
1047
|
+
const lo = new Int32Array(widths.map((w) => w[0]));
|
|
1048
|
+
const hi = new Int32Array(widths.map((w) => w[1]));
|
|
1049
|
+
const zero = scalarLike(0, a);
|
|
1050
|
+
try {
|
|
1051
|
+
return new MlxArray(
|
|
1052
|
+
outArray("pad", (o) =>
|
|
1053
|
+
C.mlx_pad(
|
|
1054
|
+
o, a.handle, ptr(axes), BigInt(widths.length), ptr(lo), BigInt(widths.length),
|
|
1055
|
+
ptr(hi), BigInt(widths.length), zero.handle, ptr(cstr("constant")), s,
|
|
1056
|
+
),
|
|
1057
|
+
),
|
|
1058
|
+
);
|
|
1059
|
+
} finally {
|
|
1060
|
+
zero.dispose();
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
export function log10(a: MlxArray, s: S = gpuStream): MlxArray {
|
|
1065
|
+
return new MlxArray(outArray("log10", (o) => C.mlx_log10(o, a.handle, s)));
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** Reduce-max over all elements → 0-d array (keepdims=false). */
|
|
1069
|
+
export function maxAll(a: MlxArray, s: S = gpuStream): MlxArray {
|
|
1070
|
+
return new MlxArray(outArray("max", (o) => C.mlx_max(o, a.handle, false, s)));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
export function broadcastTo(a: MlxArray, shape: number[], s: S = gpuStream): MlxArray {
|
|
1074
|
+
const buf = new Int32Array(shape);
|
|
1075
|
+
return new MlxArray(
|
|
1076
|
+
outArray("broadcast_to", (o) => C.mlx_broadcast_to(o, a.handle, ptr(buf), BigInt(shape.length), s)),
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** mx.topk(a, k, axis) — the k largest values along axis (unsorted). */
|
|
1081
|
+
export function topkAxis(a: MlxArray, k: number, axis: number, s: S = gpuStream): MlxArray {
|
|
1082
|
+
return new MlxArray(outArray("topk_axis", (o) => C.mlx_topk_axis(o, a.handle, k, axis, s)));
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
export function varAxis(a: MlxArray, axis: number, keepdims: boolean, ddof = 0, s: S = gpuStream): MlxArray {
|
|
1086
|
+
return new MlxArray(
|
|
1087
|
+
outArray("var_axis", (o) => C.mlx_var_axis(o, a.handle, axis, keepdims, ddof, s)),
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
export function stackAxis(arrays: MlxArray[], axis: number, s: S = gpuStream): MlxArray {
|
|
1092
|
+
const handles = new BigUint64Array(arrays.map((a) => a.handle));
|
|
1093
|
+
const vec = C.mlx_vector_array_new_data(ptr(handles), BigInt(arrays.length));
|
|
1094
|
+
try {
|
|
1095
|
+
return new MlxArray(outArray("stack_axis", (o) => C.mlx_stack_axis(o, vec, axis, s)));
|
|
1096
|
+
} finally {
|
|
1097
|
+
C.mlx_vector_array_free(vec);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
export function minimum(a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray {
|
|
1102
|
+
return new MlxArray(outArray("minimum", (o) => C.mlx_minimum(o, a.handle, b.handle, s)));
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/** Flat mx.take(a, indices) — gathers from the flattened array. */
|
|
1106
|
+
export function takeFlat(a: MlxArray, indices: MlxArray, s: S = gpuStream): MlxArray {
|
|
1107
|
+
return new MlxArray(outArray("take", (o) => C.mlx_take(o, a.handle, indices.handle, s)));
|
|
1108
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
/** Identity calculation owns bytes; this interface owns memo persistence. */
|
|
7
|
+
export interface ArtifactIdentityStore {
|
|
8
|
+
get(key: string): Promise<string | undefined>;
|
|
9
|
+
put(key: string, digest: string): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class FileArtifactIdentityStore implements ArtifactIdentityStore {
|
|
13
|
+
readonly #memory = new Map<string, string>();
|
|
14
|
+
readonly #pending = new Set<Promise<void>>();
|
|
15
|
+
constructor(readonly directory: string) {}
|
|
16
|
+
|
|
17
|
+
async get(key: string): Promise<string | undefined> {
|
|
18
|
+
const memory = this.#memory.get(key);
|
|
19
|
+
if (memory) return memory;
|
|
20
|
+
try {
|
|
21
|
+
const digest = await readFile(join(this.directory, key), "utf8");
|
|
22
|
+
if (!/^[0-9a-f]{64}$/.test(digest)) return undefined;
|
|
23
|
+
this.#memory.set(key, digest);
|
|
24
|
+
return digest;
|
|
25
|
+
} catch { return undefined; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
put(key: string, digest: string): void {
|
|
29
|
+
this.#memory.set(key, digest);
|
|
30
|
+
const temporary = join(this.directory, `${key}.${process.pid}.${randomUUID()}.tmp`);
|
|
31
|
+
const write = async () => {
|
|
32
|
+
try {
|
|
33
|
+
await mkdir(this.directory, { recursive: true });
|
|
34
|
+
await writeFile(temporary, digest);
|
|
35
|
+
await rename(temporary, join(this.directory, key));
|
|
36
|
+
} catch { await rm(temporary, { force: true }).catch(() => {}); }
|
|
37
|
+
};
|
|
38
|
+
const pending = write().finally(() => this.#pending.delete(pending));
|
|
39
|
+
this.#pending.add(pending);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async flush(): Promise<void> { await Promise.all(this.#pending); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const defaultStore = new FileArtifactIdentityStore(join(homedir(), ".cache", "mlx-bun", "artifact-identities"));
|
|
46
|
+
export interface ArtifactIdentityFile { name: string; path: string; }
|
|
47
|
+
|
|
48
|
+
async function revision(files: readonly ArtifactIdentityFile[]): Promise<string> {
|
|
49
|
+
return JSON.stringify(await Promise.all(files.map(async file => {
|
|
50
|
+
const path = await realpath(file.path), info = await stat(path, { bigint: true });
|
|
51
|
+
return [file.name, path, info.dev, info.ino, info.size, info.mtimeNs, info.ctimeNs].map(String);
|
|
52
|
+
})));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Preserve the existing seed/name/byte digest, reusing it while file revisions
|
|
56
|
+
* are unchanged. ctime catches same-size edits even when mtime is restored.
|
|
57
|
+
* A memo miss computes the full identity; persistence never delays its caller. */
|
|
58
|
+
export async function artifactIdentity(seed: string, input: readonly ArtifactIdentityFile[],
|
|
59
|
+
store: ArtifactIdentityStore = defaultStore): Promise<string> {
|
|
60
|
+
const files = [...input].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
61
|
+
const before = await revision(files);
|
|
62
|
+
const key = createHash("sha256").update(JSON.stringify([seed, before])).digest("hex");
|
|
63
|
+
const cached = await store.get(key);
|
|
64
|
+
if (cached) return cached;
|
|
65
|
+
const digest = createHash("sha256").update(seed);
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
digest.update(file.name);
|
|
68
|
+
for await (const chunk of Bun.file(file.path).stream()) digest.update(chunk);
|
|
69
|
+
}
|
|
70
|
+
const value = digest.digest("hex");
|
|
71
|
+
if (before === await revision(files)) store.put(key, value);
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { MlxArray } from "../mlx/array";
|
|
2
|
+
import { Dtype } from "../mlx/ffi";
|
|
3
|
+
import * as ops from "../mlx/ops";
|
|
4
|
+
import { materializeCopy } from "../mlx/materialize";
|
|
5
|
+
import { KVCache, isPlainKvCache, type BatchableCache, type Cache, type Mask, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
6
|
+
import { FullPrefillPadding } from "./full-prefill-padding";
|
|
7
|
+
|
|
8
|
+
/** Full-attention storage with independently advancing row positions.
|
|
9
|
+
* Speculative rollback changes coverage, without moving the retained KV.
|
|
10
|
+
* Subsequent appends overwrite each row's rejected suffix in place when MLX
|
|
11
|
+
* can donate the buffer. Queue membership is outside this layout. */
|
|
12
|
+
export class BatchedKVCache implements BatchableCache, PaddedPrefillCache {
|
|
13
|
+
keys: MlxArray | null = null;
|
|
14
|
+
values: MlxArray | null = null;
|
|
15
|
+
rowOffsets: number[] = [];
|
|
16
|
+
leftPad: number[] = [];
|
|
17
|
+
#rope: MlxArray | undefined;
|
|
18
|
+
#beforeRound: number[] | undefined;
|
|
19
|
+
readonly #padding = new FullPrefillPadding();
|
|
20
|
+
|
|
21
|
+
restorePrefillEnds(ends: readonly number[] | undefined): void { this.#padding.restoreEnds(ends); }
|
|
22
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
23
|
+
this.#padding.prepare(this, padding); this.#positionsChanged();
|
|
24
|
+
}
|
|
25
|
+
finalizePrefill(): void {
|
|
26
|
+
const next = this.#padding.finalize(this.state(), this);
|
|
27
|
+
if (!next) return;
|
|
28
|
+
this.keys?.dispose(); this.values?.dispose();
|
|
29
|
+
this.keys = next[0] ?? null; this.values = next[1] ?? null;
|
|
30
|
+
this.#positionsChanged();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
captureDonorRows(): import("./gemma4-base").KvDonorRows {
|
|
34
|
+
const B = this.rowOffsets.length, width = this.offset;
|
|
35
|
+
const keys = this.keys!.slice([0,0,0,0], [B,this.keys!.shape[1]!,width,this.keys!.shape[3]!]);
|
|
36
|
+
let values: MlxArray;
|
|
37
|
+
try { values = this.values!.slice([0,0,0,0], [B,this.values!.shape[1]!,width,this.values!.shape[3]!]); }
|
|
38
|
+
catch (error) { keys.dispose(); throw error; }
|
|
39
|
+
return { keys, values, offsets: [...this.rowOffsets], starts: [...this.leftPad], ends: this.#ends() };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
signature(): string { return "kv:batched-positions"; }
|
|
43
|
+
get batchSize(): number | null { return this.rowOffsets.length || null; }
|
|
44
|
+
get offset(): number { return Math.max(0, ...this.#ends()); }
|
|
45
|
+
#ends(): number[] { return this.rowOffsets.map((offset, row) => offset + this.leftPad[row]!); }
|
|
46
|
+
get ropeOffsetArr(): MlxArray | undefined {
|
|
47
|
+
// A common unpadded position uses the existing scalar RoPE operation.
|
|
48
|
+
if (this.leftPad.every(pad => pad === 0) && this.rowOffsets.every(offset => offset === this.rowOffsets[0])) return undefined;
|
|
49
|
+
return this.#rope ??= ops.fromInt32(this.rowOffsets, [this.rowOffsets.length]);
|
|
50
|
+
}
|
|
51
|
+
#positionsChanged(): void { this.#rope?.dispose(); this.#rope = undefined; }
|
|
52
|
+
makeEmptyBatch(): BatchedKVCache { return new BatchedKVCache(); }
|
|
53
|
+
bytesPerToken(): number {
|
|
54
|
+
if (!this.keys || !this.values) return 0;
|
|
55
|
+
return (this.keys.nbytes + this.values.nbytes) / (this.rowOffsets.length * this.keys.shape[2]!);
|
|
56
|
+
}
|
|
57
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
58
|
+
state(): MlxArray[] { return this.keys && this.values ? [this.keys, this.values] : []; }
|
|
59
|
+
isTrimmable(): boolean { return true; }
|
|
60
|
+
trim(count: number): void {
|
|
61
|
+
this.rowOffsets = this.rowOffsets.map(offset => offset - count);
|
|
62
|
+
this.#positionsChanged();
|
|
63
|
+
}
|
|
64
|
+
specRoundBegin(): void { this.#beforeRound = [...this.rowOffsets]; }
|
|
65
|
+
specRoundCommit(): void { this.#beforeRound = undefined; }
|
|
66
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
67
|
+
this.rowOffsets = this.#beforeRound!.map((offset, row) => offset + (typeof keep === "number" ? keep : keep[row]!));
|
|
68
|
+
this.#beforeRound = undefined;
|
|
69
|
+
this.#positionsChanged();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
makeMask(N: number, window: number | null): Mask {
|
|
73
|
+
const B = this.rowOffsets.length, ends = this.#ends(), S = this.offset + N;
|
|
74
|
+
if (window === null && this.leftPad.every(pad => pad === 0) && ends.every(end => end === ends[0]))
|
|
75
|
+
return { mode: N === 1 ? "" : "causal", arr: null };
|
|
76
|
+
using starts = ops.fromInt32(ends, [B, 1, 1, 1]);
|
|
77
|
+
using relative = ops.arange(0, N, 1, Dtype.int32);
|
|
78
|
+
using queries = ops.reshape(relative, [1, 1, N, 1]);
|
|
79
|
+
using position = ops.add(starts, queries);
|
|
80
|
+
using indices = ops.arange(0, S, 1, Dtype.int32);
|
|
81
|
+
using keys = ops.reshape(indices, [1, 1, 1, S]);
|
|
82
|
+
using pads = ops.fromInt32(this.leftPad, [B, 1, 1, 1]);
|
|
83
|
+
using causal = ops.lessEqual(keys, position);
|
|
84
|
+
using valid = ops.greaterEqual(keys, pads);
|
|
85
|
+
const mask = ops.logicalAnd(causal, valid);
|
|
86
|
+
if (window === null) return { mode: "array", arr: mask };
|
|
87
|
+
using windowSize = ops.fromInt32([window], []);
|
|
88
|
+
using limit = ops.add(keys, windowSize);
|
|
89
|
+
using within = ops.less(position, limit);
|
|
90
|
+
try { return { mode: "array", arr: ops.logicalAnd(mask, within) }; }
|
|
91
|
+
finally { mask.dispose(); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
95
|
+
const B = this.rowOffsets.length, N = k.shape[2]!, ends = this.#ends();
|
|
96
|
+
const needed = Math.max(...ends) + N;
|
|
97
|
+
const grow = (array: MlxArray) => {
|
|
98
|
+
const [rows, heads, capacity, width] = array.shape as [number, number, number, number];
|
|
99
|
+
if (capacity >= needed) return array;
|
|
100
|
+
using padding = ops.zeros([rows, heads, Math.ceil(needed / 256) * 256 - capacity, width], array.dtype);
|
|
101
|
+
const grown = ops.concatAxis([array, padding], 2);
|
|
102
|
+
array.dispose();
|
|
103
|
+
return grown;
|
|
104
|
+
};
|
|
105
|
+
const allocate = (input: MlxArray) => ops.zeros(
|
|
106
|
+
[B, input.shape[1]!, Math.ceil(needed / 256) * 256, input.shape[3]!], input.dtype);
|
|
107
|
+
this.keys = this.keys ? grow(this.keys) : allocate(k);
|
|
108
|
+
this.values = this.values ? grow(this.values) : allocate(v);
|
|
109
|
+
const common = ends.every(end => end === ends[0]);
|
|
110
|
+
using positions = common ? null : ops.fromInt32(ends.flatMap(end =>
|
|
111
|
+
Array.from({ length: N }, (_, position) => end + position)), [B, 1, N, 1]);
|
|
112
|
+
const append = (array: MlxArray, values: MlxArray) => common
|
|
113
|
+
? ops.sliceUpdate(array, values, [0, 0, ends[0]!, 0], [B, array.shape[1]!, ends[0]! + N, array.shape[3]!])
|
|
114
|
+
: ops.putAlongAxis(array, positions!, values, 2);
|
|
115
|
+
const keys = append(this.keys, k);
|
|
116
|
+
let values: MlxArray;
|
|
117
|
+
try { values = append(this.values, v); }
|
|
118
|
+
catch (error) { keys.dispose(); throw error; }
|
|
119
|
+
this.keys.dispose(); this.values.dispose();
|
|
120
|
+
this.keys = keys; this.values = values;
|
|
121
|
+
this.rowOffsets = this.rowOffsets.map(offset => offset + N);
|
|
122
|
+
this.#positionsChanged();
|
|
123
|
+
return [keys.slice([0, 0, 0, 0], [B, keys.shape[1]!, needed, keys.shape[3]!]),
|
|
124
|
+
values.slice([0, 0, 0, 0], [B, values.shape[1]!, needed, values.shape[3]!])];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
128
|
+
const only = rows.length === 1 ? rows[0] : undefined;
|
|
129
|
+
if (only && (only instanceof BatchedKVCache || isPlainKvCache(only))) {
|
|
130
|
+
// Preserve allocated capacity when a request starts alone. Immutable
|
|
131
|
+
// aliases transfer ownership without copying its prefill storage.
|
|
132
|
+
const keys = only.keys ? ops.copyOf(only.keys) : null;
|
|
133
|
+
let values: MlxArray | null;
|
|
134
|
+
try { values = only.values ? ops.copyOf(only.values) : null; }
|
|
135
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
136
|
+
const offsets = only instanceof BatchedKVCache ? [...only.rowOffsets] : [only.offset];
|
|
137
|
+
const pads = only instanceof BatchedKVCache ? [...only.leftPad] : [0];
|
|
138
|
+
this.dispose(); this.keys = keys; this.values = values;
|
|
139
|
+
this.rowOffsets = offsets; this.leftPad = pads;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const chunks: Array<{ keys: MlxArray | null; values: MlxArray | null; offsets: number[]; pads: number[]; width: number }> = [];
|
|
143
|
+
const held: MlxArray[] = [];
|
|
144
|
+
try {
|
|
145
|
+
for (const cache of rows) {
|
|
146
|
+
let keys: MlxArray | null, values: MlxArray | null, offsets: number[], pads: number[];
|
|
147
|
+
if (cache instanceof BatchedKVCache && cache.offset === 0) {
|
|
148
|
+
keys = values = null; offsets = [...cache.rowOffsets]; pads = [...cache.leftPad];
|
|
149
|
+
} else if (cache instanceof BatchedKVCache) {
|
|
150
|
+
const B = cache.rowOffsets.length, width = cache.offset;
|
|
151
|
+
keys = cache.keys!.slice([0, 0, 0, 0], [B, cache.keys!.shape[1]!, width, cache.keys!.shape[3]!]);
|
|
152
|
+
held.push(keys);
|
|
153
|
+
values = cache.values!.slice([0, 0, 0, 0], [B, cache.values!.shape[1]!, width, cache.values!.shape[3]!]);
|
|
154
|
+
held.push(values);
|
|
155
|
+
offsets = [...cache.rowOffsets]; pads = [...cache.leftPad];
|
|
156
|
+
} else if (isPlainKvCache(cache)) {
|
|
157
|
+
if (cache.offset === 0) keys = values = null;
|
|
158
|
+
else { [keys, values] = cache.temporalView(); held.push(keys, values); }
|
|
159
|
+
offsets = [cache.offset]; pads = [0];
|
|
160
|
+
} else throw new Error(`full-attention batch cannot merge ${cache.signature()}`);
|
|
161
|
+
chunks.push({ keys, values, offsets, pads, width: keys?.shape[2] ?? 0 });
|
|
162
|
+
}
|
|
163
|
+
const width = Math.max(0, ...chunks.map(chunk => chunk.width));
|
|
164
|
+
const padded = (array: MlxArray, count: number) => {
|
|
165
|
+
if (!count) return array;
|
|
166
|
+
using padding = ops.zeros([array.shape[0]!, array.shape[1]!, count, array.shape[3]!], array.dtype);
|
|
167
|
+
const result = ops.concatAxis([padding, array], 2); held.push(result); return result;
|
|
168
|
+
};
|
|
169
|
+
const merge = (field: "keys" | "values") => {
|
|
170
|
+
if (!width) return null; // Geometry arrives with the first append.
|
|
171
|
+
const prototype = chunks.find(chunk => chunk[field])![field]!;
|
|
172
|
+
return ops.concatAxis(chunks.map(chunk => {
|
|
173
|
+
const array = chunk[field];
|
|
174
|
+
if (array) return padded(array, width - chunk.width);
|
|
175
|
+
const empty = ops.zeros([chunk.offsets.length, prototype.shape[1]!, width, prototype.shape[3]!], prototype.dtype);
|
|
176
|
+
held.push(empty); return empty;
|
|
177
|
+
}), 0);
|
|
178
|
+
};
|
|
179
|
+
const keys = merge("keys");
|
|
180
|
+
let values: MlxArray | null;
|
|
181
|
+
try { values = merge("values"); }
|
|
182
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
183
|
+
this.dispose();
|
|
184
|
+
this.keys = keys; this.values = values;
|
|
185
|
+
this.rowOffsets = chunks.flatMap(chunk => chunk.offsets);
|
|
186
|
+
this.leftPad = chunks.flatMap(chunk => chunk.pads.map(pad => pad + width - chunk.width));
|
|
187
|
+
} finally { for (const array of held) array.dispose(); }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
filterRows(rows: readonly number[]): void {
|
|
191
|
+
if (!rows.length) { this.dispose(); return; }
|
|
192
|
+
const pads = rows.map(row => this.leftPad[row]!);
|
|
193
|
+
const sharedPadding = Math.min(...pads);
|
|
194
|
+
using indices = this.keys ? ops.fromInt32([...rows], [rows.length]) : null;
|
|
195
|
+
const select = (array: MlxArray | null): MlxArray | null => {
|
|
196
|
+
if (!array) return null;
|
|
197
|
+
const selected = ops.takeAxis(array, indices!, 0);
|
|
198
|
+
if (!sharedPadding) return selected;
|
|
199
|
+
// mlx-lm BatchKVCache.filter removes padding common to all survivors.
|
|
200
|
+
// Keeping it changes attention reduction geometry after retirement.
|
|
201
|
+
try { return selected.slice([0, 0, sharedPadding, 0], [...selected.shape]); }
|
|
202
|
+
finally { selected.dispose(); }
|
|
203
|
+
};
|
|
204
|
+
const keys = select(this.keys);
|
|
205
|
+
let values: MlxArray | null;
|
|
206
|
+
try { values = select(this.values); }
|
|
207
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
208
|
+
this.keys?.dispose(); this.values?.dispose();
|
|
209
|
+
this.keys = keys; this.values = values;
|
|
210
|
+
this.rowOffsets = rows.map(row => this.rowOffsets[row]!);
|
|
211
|
+
this.leftPad = pads.map(pad => pad - sharedPadding);
|
|
212
|
+
this.#padding.filter(rows);
|
|
213
|
+
this.#positionsChanged();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
extractRow(row: number): KVCache {
|
|
217
|
+
const start = this.leftPad[row]!, count = this.rowOffsets[row]!;
|
|
218
|
+
if (count === 0) return new KVCache();
|
|
219
|
+
const copy = (array: MlxArray) => {
|
|
220
|
+
using view = array.slice([row, 0, start, 0], [row + 1, array.shape[1]!, start + count, array.shape[3]!]);
|
|
221
|
+
return materializeCopy(view);
|
|
222
|
+
};
|
|
223
|
+
const keys = copy(this.keys!);
|
|
224
|
+
let values: MlxArray;
|
|
225
|
+
try { values = copy(this.values!); }
|
|
226
|
+
catch (error) { keys.dispose(); throw error; }
|
|
227
|
+
const cache = new KVCache(); cache.restoreState(keys, values, count); return cache;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
dispose(): void {
|
|
231
|
+
this.keys?.dispose(); this.values?.dispose(); this.#positionsChanged();
|
|
232
|
+
this.keys = this.values = null;
|
|
233
|
+
this.rowOffsets = []; this.leftPad = []; this.#beforeRound = undefined; this.#padding.clear();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -259,13 +259,21 @@ export function extractKVRow(cache: KVCache, leftPad: number, i: number): KVCach
|
|
|
259
259
|
|
|
260
260
|
/** mlx-lm `filter`: keep only `keep` (sorted row indices) along the batch
|
|
261
261
|
* axis — eviction of finished sequences from a batched [B,H,S,D] buffer.
|
|
262
|
-
*
|
|
262
|
+
* `trimLeft` removes padding shared by all survivors (the caller updates
|
|
263
|
+
* offsets and per-row padding). Caller owns the result; inputs stay owned. */
|
|
263
264
|
export function filterKVRows(
|
|
264
|
-
keys: MlxArray, values: MlxArray, keep: number[],
|
|
265
|
+
keys: MlxArray, values: MlxArray, keep: number[], trimLeft = 0,
|
|
265
266
|
): { keys: MlxArray; values: MlxArray } {
|
|
266
267
|
const idx = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
|
|
267
|
-
const
|
|
268
|
-
|
|
268
|
+
const take = (a: MlxArray) => {
|
|
269
|
+
const rows = ops.takeAxis(a, idx, 0);
|
|
270
|
+
if (trimLeft === 0) return rows;
|
|
271
|
+
const trimmed = rows.slice([0, 0, trimLeft, 0], rows.shape);
|
|
272
|
+
rows.dispose();
|
|
273
|
+
return trimmed;
|
|
274
|
+
};
|
|
275
|
+
const k = take(keys);
|
|
276
|
+
const v = take(values);
|
|
269
277
|
idx.dispose();
|
|
270
278
|
return { keys: k, values: v };
|
|
271
279
|
}
|
|
@@ -134,11 +134,17 @@ export function extractQuantRow(
|
|
|
134
134
|
/** Quantized twin of filterKVRows: keep `keep` rows along the batch axis.
|
|
135
135
|
* Caller owns the result; inputs are not disposed. */
|
|
136
136
|
export function filterQuantRows(
|
|
137
|
-
keys: ops.QuantizedTensor, values: ops.QuantizedTensor, keep: number[],
|
|
137
|
+
keys: ops.QuantizedTensor, values: ops.QuantizedTensor, keep: number[], trimLeft = 0,
|
|
138
138
|
): { keys: ops.QuantizedTensor; values: ops.QuantizedTensor } {
|
|
139
139
|
const idx = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
|
|
140
140
|
const take = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
141
|
-
tripleMap(t, (a) =>
|
|
141
|
+
tripleMap(t, (a) => {
|
|
142
|
+
const rows = ops.takeAxis(a, idx, 0);
|
|
143
|
+
if (trimLeft === 0) return rows;
|
|
144
|
+
const trimmed = rows.slice([0, 0, trimLeft, 0], rows.shape);
|
|
145
|
+
rows.dispose();
|
|
146
|
+
return trimmed;
|
|
147
|
+
});
|
|
142
148
|
const k = take(keys);
|
|
143
149
|
const v = take(values);
|
|
144
150
|
idx.dispose();
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import { cleanupFailure, disposeResources } from "../engine/resources";
|
|
4
|
+
import { BatchedKVCache } from "./batched-kv";
|
|
5
|
+
import { KVCache, QuantizedKVCache, isQuantizedKvCache,
|
|
6
|
+
type BatchableCache, type Cache, type Mask, type QuantizedAttentionState, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
7
|
+
|
|
8
|
+
import { KvTensorRows } from "../backends/mlx/kv-tensor-rows";
|
|
9
|
+
import { quantizedDonorAttention } from "./kv-attention-view";
|
|
10
|
+
|
|
11
|
+
const FIELDS = ["packed", "scales", "biases"] as const;
|
|
12
|
+
|
|
13
|
+
/** Affine KV uses the same row-position storage operations for its packed
|
|
14
|
+
* values, scales and biases. No dequantization or requantization is needed
|
|
15
|
+
* for merge, rollback, retirement or extraction. Inputs share this codec. */
|
|
16
|
+
export class BatchedQuantizedKVCache implements BatchableCache, QuantizedAttentionState, PaddedPrefillCache {
|
|
17
|
+
#planes = FIELDS.map(() => new BatchedKVCache());
|
|
18
|
+
#minimum: number[] = [];
|
|
19
|
+
get minimumReusableOffset(): number { return Math.max(0, ...this.#minimum); }
|
|
20
|
+
constructor(readonly groupSize: number, readonly bits: number) {}
|
|
21
|
+
restorePrefillEnds(ends: readonly number[] | undefined): void { for (const plane of this.#planes) plane.restorePrefillEnds(ends); }
|
|
22
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
23
|
+
for (const plane of this.#planes) plane.preparePrefill(padding);
|
|
24
|
+
if (!this.#minimum.length) this.#minimum = padding.lengths.map(() => 0);
|
|
25
|
+
}
|
|
26
|
+
finalizePrefill(): void { for (const plane of this.#planes) plane.finalizePrefill(); }
|
|
27
|
+
get quantizedAttention(): QuantizedAttentionState { return this; }
|
|
28
|
+
captureDonorAttention(): import("./gemma4-base").KvDonorAttention {
|
|
29
|
+
const views: import("./gemma4-base").KvDonorRows[] = [];
|
|
30
|
+
try {
|
|
31
|
+
for (const plane of this.#planes) views.push(plane.captureDonorRows());
|
|
32
|
+
const field = (name: "keys" | "values") => ({
|
|
33
|
+
packed: views[0]![name], scales: views[1]![name], biases: views[2]![name],
|
|
34
|
+
});
|
|
35
|
+
const { offsets, starts, ends } = views[0]!;
|
|
36
|
+
return quantizedDonorAttention(field("keys"), field("values"), { offsets, starts, ends }, this.groupSize, this.bits);
|
|
37
|
+
} catch (error) { disposeResources(views.flatMap(view => [view.keys, view.values])); throw error; }
|
|
38
|
+
}
|
|
39
|
+
signature(): string { return `kv:batched-quant:${this.bits}:${this.groupSize}`; }
|
|
40
|
+
get rowOffsets(): readonly number[] { return this.#planes[0]!.rowOffsets; }
|
|
41
|
+
get leftPad(): readonly number[] { return this.#planes[0]!.leftPad; }
|
|
42
|
+
get batchSize(): number | null { return this.#planes[0]!.batchSize; }
|
|
43
|
+
get offset(): number { return this.#planes[0]!.offset; }
|
|
44
|
+
get ropeOffsetArr(): MlxArray | undefined { return this.#planes[0]!.ropeOffsetArr; }
|
|
45
|
+
makeEmptyBatch(): BatchedQuantizedKVCache { return new BatchedQuantizedKVCache(this.groupSize, this.bits); }
|
|
46
|
+
bytesPerToken(): number { return this.#planes.reduce((n, plane) => n + plane.bytesPerToken(), 0); }
|
|
47
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
48
|
+
makeMask(tokens: number, window: number | null): Mask { return this.#planes[0]!.makeMask(tokens, window); }
|
|
49
|
+
isTrimmable(): boolean { return true; }
|
|
50
|
+
trim(count: number): void { for (const plane of this.#planes) plane.trim(count); }
|
|
51
|
+
specRoundBegin(): void { for (const plane of this.#planes) plane.specRoundBegin(); }
|
|
52
|
+
specRoundCommit(): void { for (const plane of this.#planes) plane.specRoundCommit(); }
|
|
53
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
54
|
+
for (const plane of this.#planes) plane.specRoundRollback(keep);
|
|
55
|
+
}
|
|
56
|
+
updateAndFetch(): [MlxArray, MlxArray] { throw new Error("quantized attention uses updateAndFetchQuantized"); }
|
|
57
|
+
|
|
58
|
+
updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
59
|
+
using inputs = new DisposableStack();
|
|
60
|
+
const key = ops.quantize(k, this.groupSize, this.bits);
|
|
61
|
+
for (const field of FIELDS) inputs.use(key[field]);
|
|
62
|
+
const value = ops.quantize(v, this.groupSize, this.bits);
|
|
63
|
+
for (const field of FIELDS) inputs.use(value[field]);
|
|
64
|
+
const held: MlxArray[] = [], keys: Partial<ops.QuantizedTensor> = {}, values: Partial<ops.QuantizedTensor> = {};
|
|
65
|
+
try {
|
|
66
|
+
for (const [index, field] of FIELDS.entries()) {
|
|
67
|
+
const [kk, vv] = this.#planes[index]!.updateAndFetch(key[field], value[field]);
|
|
68
|
+
held.push(kk, vv); keys[field] = kk; values[field] = vv;
|
|
69
|
+
}
|
|
70
|
+
return [keys as ops.QuantizedTensor, values as ops.QuantizedTensor];
|
|
71
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(held)); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Borrow prepared request state. All planes stage replacements before
|
|
75
|
+
* releasing the current group, preserving donor ownership on failure. */
|
|
76
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
77
|
+
const next: BatchedKVCache[] = [];
|
|
78
|
+
try {
|
|
79
|
+
for (const [index, field] of FIELDS.entries()) {
|
|
80
|
+
const held: KVCache[] = [], inputs: Cache[] = [];
|
|
81
|
+
try {
|
|
82
|
+
for (const row of rows) {
|
|
83
|
+
if (row instanceof BatchedQuantizedKVCache) inputs.push(row.#planes[index]!);
|
|
84
|
+
else if (isQuantizedKvCache(row)) {
|
|
85
|
+
const view = new KVCache(); held.push(view);
|
|
86
|
+
if (row.offset > 0) {
|
|
87
|
+
const key = ops.copyOf(row.keys![field]);
|
|
88
|
+
let value: MlxArray;
|
|
89
|
+
try { value = ops.copyOf(row.values![field]); }
|
|
90
|
+
catch (error) { key.dispose(); throw error; }
|
|
91
|
+
view.restoreState(key, value, row.offset);
|
|
92
|
+
}
|
|
93
|
+
inputs.push(view);
|
|
94
|
+
} else throw new Error(`quantized row layout cannot merge ${row.signature()}`);
|
|
95
|
+
}
|
|
96
|
+
const plane = new BatchedKVCache(); next.push(plane); plane.mergeRows(inputs);
|
|
97
|
+
} finally { disposeResources(held); }
|
|
98
|
+
}
|
|
99
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(next)); }
|
|
100
|
+
const previous = this.#planes; this.#planes = next; disposeResources(previous);
|
|
101
|
+
this.#minimum = rows.flatMap(row => row instanceof BatchedQuantizedKVCache ? row.#minimum : [row.minimumReusableOffset ?? 0]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
alignRows(leftPad: readonly number[]): void {
|
|
105
|
+
if (leftPad.every((pad, row) => pad === this.leftPad[row])) return;
|
|
106
|
+
const aligned = new KvTensorRows();
|
|
107
|
+
try {
|
|
108
|
+
aligned.mergeRows([{ planes: this.#planes.flatMap(plane => plane.keys ? [plane.keys, plane.values!] : []),
|
|
109
|
+
rowOffsets: this.rowOffsets, leftPad: this.leftPad }]);
|
|
110
|
+
aligned.alignRows(leftPad);
|
|
111
|
+
for (const plane of this.#planes) {
|
|
112
|
+
plane.keys?.dispose(); plane.values?.dispose();
|
|
113
|
+
plane.keys = aligned.planes.shift() ?? null; plane.values = aligned.planes.shift() ?? null;
|
|
114
|
+
plane.leftPad = [...leftPad];
|
|
115
|
+
}
|
|
116
|
+
} finally { aligned.dispose(); }
|
|
117
|
+
}
|
|
118
|
+
filterRows(keep: readonly number[]): void {
|
|
119
|
+
for (const plane of this.#planes) plane.filterRows(keep);
|
|
120
|
+
this.#minimum = keep.map(row => this.#minimum[row]!);
|
|
121
|
+
}
|
|
122
|
+
state(): MlxArray[] {
|
|
123
|
+
if (!this.#planes[0]!.keys) return [];
|
|
124
|
+
return [...this.#planes.map(plane => plane.keys!), ...this.#planes.map(plane => plane.values!)];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Return the existing persisted representation, owning compact arrays. */
|
|
128
|
+
extractRow(row: number): QuantizedKVCache {
|
|
129
|
+
const offset = this.rowOffsets[row]!, result = new QuantizedKVCache(this.groupSize, this.bits);
|
|
130
|
+
if (this.#minimum[row]) result.minimumReusableOffset = this.#minimum[row];
|
|
131
|
+
if (offset === 0) return result;
|
|
132
|
+
const held: KVCache[] = [];
|
|
133
|
+
try {
|
|
134
|
+
for (const plane of this.#planes) held.push(plane.extractRow(row));
|
|
135
|
+
result.restoreState(
|
|
136
|
+
{ packed: held[0]!.keys!, scales: held[1]!.keys!, biases: held[2]!.keys! },
|
|
137
|
+
{ packed: held[0]!.values!, scales: held[1]!.values!, biases: held[2]!.values! }, offset);
|
|
138
|
+
for (const cache of held) cache.keys = cache.values = null;
|
|
139
|
+
return result;
|
|
140
|
+
} finally { disposeResources(held); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
dispose(): void { disposeResources(this.#planes); this.#minimum = []; }
|
|
144
|
+
}
|