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
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
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { MlxArray } from "../mlx/array";
|
|
|
3
3
|
import { KvTensorRows, type KvTensorRowView } from "../backends/mlx/kv-tensor-rows";
|
|
4
4
|
import { disposeResources } from "../engine/resources";
|
|
5
5
|
import { TurboQuantKVCache, type BatchableCache, type Cache, type Mask, type RotatedValueAttentionState, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
6
|
-
import { TurboQuantCodec, disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
6
|
+
import { TurboQuantCodec, turboQuantFusedDecode, disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
7
7
|
|
|
8
8
|
const fields = ["kIdx", "kScales", "kZeros", "vPacked", "vScales"] as const;
|
|
9
9
|
function encoded(planes: readonly MlxArray[]): TurboQuantTensor {
|
|
@@ -17,8 +17,9 @@ export class BatchedTurboQuantKVCache implements BatchableCache, PaddedPrefillCa
|
|
|
17
17
|
readonly #codec: TurboQuantCodec;
|
|
18
18
|
#reuseOffsets: number[] = [];
|
|
19
19
|
#headDim: number | null = null;
|
|
20
|
-
constructor(readonly kBits: number, readonly vBits: number
|
|
21
|
-
|
|
20
|
+
constructor(readonly kBits: number, readonly vBits: number,
|
|
21
|
+
readonly fusedDecode = turboQuantFusedDecode()) {
|
|
22
|
+
this.#codec = new TurboQuantCodec(kBits, vBits, fusedDecode);
|
|
22
23
|
}
|
|
23
24
|
restorePrefillEnds(ends: readonly number[] | undefined): void { this.#storage.restorePrefillEnds(ends); }
|
|
24
25
|
preparePrefill(padding: PrefillPadding): void {
|
|
@@ -43,7 +44,7 @@ export class BatchedTurboQuantKVCache implements BatchableCache, PaddedPrefillCa
|
|
|
43
44
|
get batchSize(): number | null { return this.#storage.batchSize; }
|
|
44
45
|
get offset(): number { return this.#storage.offset; }
|
|
45
46
|
get ropeOffsetArr(): MlxArray | undefined { return this.#storage.ropeOffsetArr; }
|
|
46
|
-
makeEmptyBatch(): BatchedTurboQuantKVCache { return new BatchedTurboQuantKVCache(this.kBits, this.vBits); }
|
|
47
|
+
makeEmptyBatch(): BatchedTurboQuantKVCache { return new BatchedTurboQuantKVCache(this.kBits, this.vBits, this.fusedDecode); }
|
|
47
48
|
bytesPerToken(): number { return this.#storage.bytesPerToken(); }
|
|
48
49
|
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
49
50
|
makeMask(tokens: number, window: number | null): Mask { return this.#storage.makeMask(tokens, window); }
|
|
@@ -84,7 +85,7 @@ export class BatchedTurboQuantKVCache implements BatchableCache, PaddedPrefillCa
|
|
|
84
85
|
}
|
|
85
86
|
state(): MlxArray[] { return this.#storage.planes; }
|
|
86
87
|
extractRow(row: number): TurboQuantKVCache {
|
|
87
|
-
const result = new TurboQuantKVCache(this.kBits, this.vBits), count = this.rowOffsets[row]!;
|
|
88
|
+
const result = new TurboQuantKVCache(this.kBits, this.vBits, this.fusedDecode), count = this.rowOffsets[row]!;
|
|
88
89
|
if (count) result.restoreState(encoded(this.#storage.extractRow(row)), count, this.#headDim!);
|
|
89
90
|
result.minimumReusableOffset = this.#reuseOffsets[row] ?? 0;
|
|
90
91
|
return result;
|
|
@@ -14,7 +14,7 @@ import { QuantizedKVCache, type Cache, type Mask, type KvAttentionState, type Kv
|
|
|
14
14
|
export class DelayedQuantizedKVCache extends FullTransitioningKvRows<BatchedQuantizedKVCache> implements KvAttentionState {
|
|
15
15
|
constructor(readonly groupSize: number, readonly bits: number, readonly start: number,
|
|
16
16
|
readonly maintain: (rows: Cache[]) => void, row?: Cache) {
|
|
17
|
-
super({ signature: `kv:delayed-quant:${bits}:${groupSize}:${start}`, maintain,
|
|
17
|
+
super({ signature: `kv:delayed-quant:${bits}:${groupSize}:${start}`, conversionOffset: start, maintain,
|
|
18
18
|
converted: row => row instanceof QuantizedKVCache,
|
|
19
19
|
makeLayout: () => new BatchedQuantizedKVCache(groupSize, bits) }, row);
|
|
20
20
|
}
|
|
@@ -44,7 +44,7 @@ export class DelayedQuantizedKVCache extends FullTransitioningKvRows<BatchedQuan
|
|
|
44
44
|
this.syncPositions(true);
|
|
45
45
|
} catch (error) { for (const view of views) view.dispose(); throw error; }
|
|
46
46
|
return {
|
|
47
|
-
attend(q, scale, mask) {
|
|
47
|
+
attend(q, scale, mask, independentPositions) {
|
|
48
48
|
const outputs: MlxArray[] = [];
|
|
49
49
|
try {
|
|
50
50
|
for (const [row, view] of views.entries()) {
|
|
@@ -57,7 +57,7 @@ export class DelayedQuantizedKVCache extends FullTransitioningKvRows<BatchedQuan
|
|
|
57
57
|
end[end.length - 1] = pads[row]! + lengths[row]!;
|
|
58
58
|
}
|
|
59
59
|
using arr = mask.arr ? mask.arr.slice(start!, end!) : null;
|
|
60
|
-
outputs.push(view.attend(qr, scale, { mode: mask.mode, arr }));
|
|
60
|
+
outputs.push(view.attend(qr, scale, { mode: mask.mode, arr }, independentPositions));
|
|
61
61
|
}
|
|
62
62
|
return outputs.length === 1 ? ops.contiguous(outputs[0]!) : ops.concatAxis(outputs, 0);
|
|
63
63
|
} finally { for (const output of outputs) output.dispose(); }
|
|
@@ -11,7 +11,7 @@ import { captureKvAttention, combineKvDonorAttention } from "./kv-attention-view
|
|
|
11
11
|
export class DelayedRotatingQuantizedKVCache extends SpeculativeTransitioningKvRows<RotatingAffineLayout> implements KvAttentionState, PaddedPrefillCache {
|
|
12
12
|
constructor(readonly maxSize: number, readonly groupSize: number, readonly bits: number, readonly start: number,
|
|
13
13
|
readonly maintain: (rows: Cache[]) => void, row?: Cache, readonly speculative = false) {
|
|
14
|
-
super({ signature: `kv:delayed-rotating-quant:${maxSize}:${bits}:${groupSize}:${start}`, maintain,
|
|
14
|
+
super({ signature: `kv:delayed-rotating-quant:${maxSize}:${bits}:${groupSize}:${start}`, conversionOffset: start, maintain,
|
|
15
15
|
converted: row => row instanceof AlignedRotatingCache && row.inner instanceof BatchedRotatingQuantCache,
|
|
16
16
|
makeLayout: () => speculative ? new SpeculativeRotatingAffineLayout(maxSize, groupSize, bits) : new RotatingAffineLayout(maxSize, groupSize, bits), prepareRows: alignRotatingRows,
|
|
17
17
|
packRows: (layout, rows) => layout.adoptAlignedRows(rows as readonly AlignedRotatingCache[]),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { turboQuantFusedDecode } from "./turboquant-codec";
|
|
1
2
|
import { captureFullKvDonorRows } from "../backends/mlx/full-kv-row-donor";
|
|
2
3
|
import { decodedKvDonorAttention } from "./decoded-kv-donor";
|
|
3
4
|
import { appendFullKvRows } from "../backends/mlx/full-kv-row-append";
|
|
@@ -14,10 +15,11 @@ import { FullTransitioningKvRows } from "../backends/mlx/full-transitioning-kv-r
|
|
|
14
15
|
export class DelayedTurboQuantKVCache extends FullTransitioningKvRows<BatchedTurboQuantKVCache> implements RotatedValueAttentionState {
|
|
15
16
|
#rotated: boolean[] = [];
|
|
16
17
|
constructor(readonly kBits: number, readonly vBits: number, readonly start: number,
|
|
17
|
-
readonly maintain: (rows: Cache[]) => void, row?: Cache
|
|
18
|
-
|
|
18
|
+
readonly maintain: (rows: Cache[]) => void, row?: Cache,
|
|
19
|
+
readonly fusedDecode = turboQuantFusedDecode()) {
|
|
20
|
+
super({ signature: `kv:delayed-turboquant:${kBits}:${vBits}:${start}`, conversionOffset: start, maintain,
|
|
19
21
|
converted: row => row instanceof TurboQuantKVCache,
|
|
20
|
-
makeLayout: () => new BatchedTurboQuantKVCache(kBits, vBits) }, row);
|
|
22
|
+
makeLayout: () => new BatchedTurboQuantKVCache(kBits, vBits, fusedDecode) }, row);
|
|
21
23
|
}
|
|
22
24
|
captureDonorRows(): import("./gemma4-base").KvDonorRows {
|
|
23
25
|
return this.packed?.captureDonorRows() ?? captureFullKvDonorRows(this.rows, this.leftPad, this.offset);
|
|
@@ -51,5 +53,5 @@ export class DelayedTurboQuantKVCache extends FullTransitioningKvRows<BatchedTur
|
|
|
51
53
|
return ops.where(mask, restored, output);
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
|
-
makeEmptyBatch(): DelayedTurboQuantKVCache { return new DelayedTurboQuantKVCache(this.kBits, this.vBits, this.start, this.maintain); }
|
|
56
|
+
makeEmptyBatch(): DelayedTurboQuantKVCache { return new DelayedTurboQuantKVCache(this.kBits, this.vBits, this.start, this.maintain, undefined, this.fusedDecode); }
|
|
55
57
|
}
|
package/src/model/factory.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type Glm52MemoryPlan,
|
|
24
24
|
} from "./glm52-memory";
|
|
25
25
|
import { UniversalDenseModel } from "./universal/dense";
|
|
26
|
+
import { WhisperModel } from "./whisper";
|
|
26
27
|
import { genericArgsFor } from "./universal/archs";
|
|
27
28
|
import {
|
|
28
29
|
ModelImplementationRegistry,
|
|
@@ -134,6 +135,26 @@ export async function openGlm52RuntimeModel(
|
|
|
134
135
|
return { model, plan };
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
/** Open a Whisper checkpoint (encoder-decoder speech model) through the same
|
|
139
|
+
* profile resolution as text models. Whisper never enters the chat loop, so
|
|
140
|
+
* it is not a RuntimeModel: the transcription engine (src/audio/whisper-*)
|
|
141
|
+
* owns it. */
|
|
142
|
+
export async function openWhisperModel(
|
|
143
|
+
modelDir: string, options: { readonly profiles?: ResolveModelProfileOptions } = {},
|
|
144
|
+
): Promise<{ model: WhisperModel; profile: ResolvedModelProfile; config: ModelConfig }> {
|
|
145
|
+
const config = await loadModelConfig(modelDir);
|
|
146
|
+
const profile = resolveModelProfile(config, options.profiles);
|
|
147
|
+
if (profile.profile.execution.graph !== "whisper")
|
|
148
|
+
throw new Error(`${modelDir} is not a Whisper checkpoint (profile ${profile.profile.id})`);
|
|
149
|
+
const weights = await Weights.open(modelDir);
|
|
150
|
+
try {
|
|
151
|
+
return { model: new WhisperModel(weights, config), profile, config };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
weights.dispose();
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
/**
|
|
138
159
|
* Artifact-aware construction. GLM-5.2's Colibri snapshot has no ordinary
|
|
139
160
|
* model.safetensors.index.json, so it must bypass Weights.open and use its
|
|
@@ -155,6 +176,11 @@ export async function openModel<Model>(
|
|
|
155
176
|
throw new Error("custom Colibri implementations require a streamed loader binding; refusing to fall back");
|
|
156
177
|
return (await openGlm52RuntimeModel(modelDir, options)).model;
|
|
157
178
|
}
|
|
179
|
+
if (profile.profile.execution.loop === "encoder-decoder")
|
|
180
|
+
throw new Error(
|
|
181
|
+
`${modelDir} is a speech model (${profile.profile.id}); open it with openWhisperModel ` +
|
|
182
|
+
"or serve it as a transcription model — it has no chat/completion loop",
|
|
183
|
+
);
|
|
158
184
|
// Resolve before opening weights; missing or incompatible code allocates nothing.
|
|
159
185
|
const implementation = (options.implementations ?? MLX_MODEL_IMPLEMENTATIONS).select(config, profile);
|
|
160
186
|
const weights = await Weights.open(modelDir);
|
package/src/model/fingerprint.ts
CHANGED
|
@@ -31,3 +31,15 @@ export function configFingerprint(config: ModelConfig): string {
|
|
|
31
31
|
h.update(payload);
|
|
32
32
|
return h.digest("hex").slice(0, 16);
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/** Exact immutable model-component identity, calculated once while its owner
|
|
36
|
+
* holds the native execution lease. Names/shapes/dtypes disambiguate layouts. */
|
|
37
|
+
export function tensorFingerprint(entries: Iterable<readonly [string,
|
|
38
|
+
Pick<import("../mlx/array").MlxArray, "dtype" | "shape" | "rawBytesView">]>): string {
|
|
39
|
+
const hash = new Bun.CryptoHasher("sha256");
|
|
40
|
+
for (const [name, tensor] of [...entries].sort(([a], [b]) => a.localeCompare(b))) {
|
|
41
|
+
hash.update(JSON.stringify([name, tensor.dtype, tensor.shape]));
|
|
42
|
+
hash.update(tensor.rawBytesView());
|
|
43
|
+
}
|
|
44
|
+
return hash.digest("hex");
|
|
45
|
+
}
|
|
@@ -65,7 +65,7 @@ export class FullPrefillRow implements Cache {
|
|
|
65
65
|
{ packed: planes[3]!, scales: planes[4]!, biases: planes[5]! }, offset);
|
|
66
66
|
return result;
|
|
67
67
|
}
|
|
68
|
-
const result = new TurboQuantKVCache(source.kBits, source.vBits);
|
|
68
|
+
const result = new TurboQuantKVCache(source.kBits, source.vBits, source.fusedDecode);
|
|
69
69
|
if (planes.length) result.restoreState({ kIdx: planes[0]!, kScales: planes[1]!, kZeros: planes[2]!,
|
|
70
70
|
vPacked: planes[3]!, vScales: planes[4]! }, offset, source.headDim!);
|
|
71
71
|
return result;
|
package/src/model/gemma4-base.ts
CHANGED
|
@@ -12,13 +12,13 @@ import type { ModelConfig } from "../config";
|
|
|
12
12
|
import { quantFor } from "../config";
|
|
13
13
|
import type { Weights } from "../weights";
|
|
14
14
|
import { MlxArray } from "../mlx/array";
|
|
15
|
-
import { Dtype } from "../mlx/ffi";
|
|
15
|
+
import { Dtype, deviceArchitecture } from "../mlx/ffi";
|
|
16
16
|
import * as ops from "../mlx/ops";
|
|
17
17
|
import { quantizedMatmulRows } from "../mlx/quantized-rows";
|
|
18
18
|
import { expertOffloadArray } from "../expert-offload";
|
|
19
19
|
import * as tq from "../mlx/turboquant-ops";
|
|
20
20
|
import type { KvCodec } from "../backends/mlx/kv-codec";
|
|
21
|
-
import { TurboQuantCodec, disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
21
|
+
import { TurboQuantCodec, turboQuantFusedDecode, disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
22
22
|
export { disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
23
23
|
import { runtimeValue } from "../runtime-config";
|
|
24
24
|
|
|
@@ -297,7 +297,8 @@ export interface RotatedValueAttentionState {
|
|
|
297
297
|
* again. It owns its tensor handles independently of later cache membership or
|
|
298
298
|
* precision changes. Query/mask inputs are borrowed; outputs are owned. */
|
|
299
299
|
export interface KvAttentionView {
|
|
300
|
-
|
|
300
|
+
/** Qualified committed spans retain the single-position reduction order. */
|
|
301
|
+
attend(q: MlxArray, scale: number, mask: Mask, independentPositions?: boolean): MlxArray;
|
|
301
302
|
dispose(): void;
|
|
302
303
|
}
|
|
303
304
|
|
|
@@ -325,6 +326,8 @@ export interface KvDonorAttention extends KvAttentionView {
|
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
export interface Cache {
|
|
329
|
+
/** Maximum committed positions before this state changes precision. */
|
|
330
|
+
maxAppendTokens?(): number;
|
|
328
331
|
captureDonorRows?(): KvDonorRows;
|
|
329
332
|
captureDonorAttention?(): KvDonorAttention;
|
|
330
333
|
/** A preparation method commits each request's own precision boundary.
|
|
@@ -1671,8 +1674,9 @@ export class TurboQuantKVCache implements Cache {
|
|
|
1671
1674
|
/** Experimental operation selection, captured once for this cache. */
|
|
1672
1675
|
readonly #codec: KvCodec<TurboQuantTensor>;
|
|
1673
1676
|
|
|
1674
|
-
constructor(readonly kBits: number, readonly vBits: number
|
|
1675
|
-
|
|
1677
|
+
constructor(readonly kBits: number, readonly vBits: number,
|
|
1678
|
+
readonly fusedDecode = turboQuantFusedDecode()) {
|
|
1679
|
+
this.#codec = new TurboQuantCodec(kBits, vBits, fusedDecode);
|
|
1676
1680
|
}
|
|
1677
1681
|
|
|
1678
1682
|
captureDonorRows(): KvDonorRows {
|
|
@@ -1940,8 +1944,23 @@ export function quantizedSdpaUnfused(
|
|
|
1940
1944
|
vT = expand(vq);
|
|
1941
1945
|
}
|
|
1942
1946
|
|
|
1943
|
-
|
|
1947
|
+
// M4 Pro's native key matvec retains exact arithmetic when three GQA
|
|
1948
|
+
// heads share a batch. Restore score geometry before softmax/value work.
|
|
1949
|
+
const groupHeads = B <= 2 && H === 24 && KV === 4 && L === 3 && D === 256 &&
|
|
1950
|
+
N >= 8192 && groupSize === 64 && bits === 4 &&
|
|
1951
|
+
(q.dtype === Dtype.bfloat16 || q.dtype === Dtype.float32) &&
|
|
1952
|
+
deviceArchitecture() === "applegpu_g16s";
|
|
1953
|
+
let keyQueries = queries;
|
|
1954
|
+
if (groupHeads) {
|
|
1955
|
+
keyQueries = ops.reshape(queries, [B, KV, 2, 9, D]);
|
|
1956
|
+
owned.push(keyQueries);
|
|
1957
|
+
}
|
|
1958
|
+
let scores = ops.quantizedMatmulQT(keyQueries, kT, true, groupSize, bits);
|
|
1944
1959
|
owned.push(scores);
|
|
1960
|
+
if (groupHeads) {
|
|
1961
|
+
scores = ops.reshape(scores, [B, KV, nRep, L, N]);
|
|
1962
|
+
owned.push(scores);
|
|
1963
|
+
}
|
|
1945
1964
|
|
|
1946
1965
|
let maskArr: MlxArray | null = null;
|
|
1947
1966
|
let ownsMask = false;
|
package/src/model/gemma4.ts
CHANGED
|
@@ -52,11 +52,12 @@ import {
|
|
|
52
52
|
captureRopeOffsets,
|
|
53
53
|
type LoraWeights,
|
|
54
54
|
} from "./gemma4-base";
|
|
55
|
+
import { mapPackedTokens, type TokenGroup } from "./token-groups";
|
|
55
56
|
import { Checkpoint } from "../mlx/checkpoint";
|
|
56
57
|
import { flashAttention, getTrainingAttn, flashSupported } from "./flash-attention";
|
|
57
58
|
import { unrotateValues as tqUnrotateValues } from "../mlx/turboquant-ops";
|
|
58
59
|
import { CompiledFunction } from "../mlx/compile";
|
|
59
|
-
import {
|
|
60
|
+
import { runtimeFlag } from "../runtime-config";
|
|
60
61
|
|
|
61
62
|
/** Verbatim port of mlx_lm/models/gemma4_text.py:
|
|
62
63
|
* `@partial(mx.compile, shapeless=True) def geglu(gate, x): return nn.gelu_approx(gate) * x`
|
|
@@ -77,7 +78,7 @@ import { flagOn } from "../runtime-config";
|
|
|
77
78
|
* ~9-op path. `MLX_BUN_COMPILED_GEGLU=0` (--compiled-activations off) selects the
|
|
78
79
|
* uncompiled composition — same L1 parity, slower (the A/B opt-out). */
|
|
79
80
|
export function compiledGegluActive(): boolean {
|
|
80
|
-
return
|
|
81
|
+
return runtimeFlag("MLX_BUN_COMPILED_GEGLU", true);
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
let _gegluClosure: CompiledFunction | null = null;
|
|
@@ -1020,6 +1021,85 @@ export class Gemma4Model {
|
|
|
1020
1021
|
return h;
|
|
1021
1022
|
}
|
|
1022
1023
|
|
|
1024
|
+
/** Mixed token execution: attention retains each group's cache, positions and
|
|
1025
|
+
* SDPA shape; feed-forward work packs only real tokens. Kernel selection for
|
|
1026
|
+
* packed matmuls can differ from a solo GEMV, so this is an explicit Lab port
|
|
1027
|
+
* until its matching packed oracle and serving comparison are accepted. */
|
|
1028
|
+
forwardHiddenMixed(work: readonly TokenGroup[]): MlxArray[] {
|
|
1029
|
+
if (work.length === 1 && !work[0]!.captureLayer) return [this.forwardHidden(work[0]!.ids, work[0]!.cache)];
|
|
1030
|
+
const groups: Array<{ ids: MlxArray; cache: Cache[]; h: MlxArray;
|
|
1031
|
+
masks: Map<string, Mask>; perLayer: MlxArray | null; intermediates: (SharedKv | null)[] }> = [];
|
|
1032
|
+
const results: MlxArray[] = [];
|
|
1033
|
+
const packedMlp = runtimeFlag("MLX_BUN_MIXED_PACKED_MLP", true) && !work.some(group => group.preserveTokenGeometry);
|
|
1034
|
+
try {
|
|
1035
|
+
for (const { ids, cache } of work) {
|
|
1036
|
+
const group = { ids, cache, h: this.embed.encode(ids), masks: new Map<string, Mask>(),
|
|
1037
|
+
perLayer: null as MlxArray | null, intermediates: Array<SharedKv | null>(this.layers.length).fill(null) };
|
|
1038
|
+
groups.push(group);
|
|
1039
|
+
group.h = disposing(group.h, ops.mulScalar(group.h, this.embedScale));
|
|
1040
|
+
for (let i = 0; i < this.numDonors; i++) {
|
|
1041
|
+
const type = this.layers[i]!.layerType;
|
|
1042
|
+
if (!group.masks.has(type)) group.masks.set(type,
|
|
1043
|
+
cache[i]!.makeMask(ids.shape[1]!, type === "sliding_attention" ? this.windowSize : null));
|
|
1044
|
+
}
|
|
1045
|
+
if (this.perLayerWidth > 0) group.perLayer = this.computePerLayerInputs(ids, group.h);
|
|
1046
|
+
}
|
|
1047
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
1048
|
+
const layer = this.layers[i]!, ci = this.cacheIndex[i]!;
|
|
1049
|
+
const mids: MlxArray[] = [], inputs: MlxArray[] = [];
|
|
1050
|
+
try {
|
|
1051
|
+
for (const group of groups) {
|
|
1052
|
+
const sharedIn = ci === -1 ? group.intermediates[this.previousKvs[i]!]! : null;
|
|
1053
|
+
const { h, shared } = layer.forwardAttn(group.h, group.masks.get(layer.layerType)!,
|
|
1054
|
+
ci === -1 ? null : group.cache[ci]!, sharedIn);
|
|
1055
|
+
mids.push(h); group.intermediates[i] = shared;
|
|
1056
|
+
if (group.perLayer) {
|
|
1057
|
+
const [B, L] = group.ids.shape;
|
|
1058
|
+
using part = group.perLayer.slice([0, 0, i, 0], [B!, L!, i + 1, this.perLayerWidth]);
|
|
1059
|
+
inputs.push(ops.reshape(part, [B!, L!, this.perLayerWidth]));
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
// Per-layer inputs use the same token order as the hidden states.
|
|
1063
|
+
let packedInput: MlxArray | null = null;
|
|
1064
|
+
if (packedMlp && inputs.length) {
|
|
1065
|
+
const flat = inputs.map(input => ops.reshape(input, [1, -1, this.perLayerWidth]));
|
|
1066
|
+
try { packedInput = ops.concatAxis(flat, 1); }
|
|
1067
|
+
finally { for (const input of flat) input.dispose(); }
|
|
1068
|
+
}
|
|
1069
|
+
using pli = packedInput;
|
|
1070
|
+
const outputs: MlxArray[] = [];
|
|
1071
|
+
try {
|
|
1072
|
+
if (packedMlp) outputs.push(...mapPackedTokens(mids, packed => layer.forwardMlp(packed, pli)));
|
|
1073
|
+
else for (const [row, mid] of mids.entries()) outputs.push(layer.forwardMlp(mid, inputs[row] ?? null));
|
|
1074
|
+
} catch (error) { for (const output of outputs) output.dispose(); throw error; }
|
|
1075
|
+
for (const [index, group] of groups.entries()) {
|
|
1076
|
+
group.h.dispose(); group.h = outputs[index]!;
|
|
1077
|
+
}
|
|
1078
|
+
for (const [index, group] of groups.entries()) work[index]!.captureLayer?.(i, group.h);
|
|
1079
|
+
} finally {
|
|
1080
|
+
for (const mid of mids) mid.dispose();
|
|
1081
|
+
for (const input of inputs) input.dispose();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
for (const [index, group] of groups.entries()) {
|
|
1085
|
+
const hidden = this.finalNorm.forward(group.h); results.push(hidden);
|
|
1086
|
+
work[index]!.captureLayer?.(this.layers.length, hidden);
|
|
1087
|
+
}
|
|
1088
|
+
return results;
|
|
1089
|
+
} catch (error) { for (const result of results) result.dispose(); throw error; }
|
|
1090
|
+
finally {
|
|
1091
|
+
for (const group of groups) {
|
|
1092
|
+
group.h.dispose(); group.perLayer?.dispose();
|
|
1093
|
+
for (let i = 0; i < this.numDonors; i++) {
|
|
1094
|
+
const shared = group.intermediates[i];
|
|
1095
|
+
if (shared) Gemma4Model.disposeSharedKv(shared);
|
|
1096
|
+
}
|
|
1097
|
+
for (const mask of group.masks.values()) mask.arr?.dispose();
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
|
|
1023
1103
|
// --- Segmented-backward support (docs/design/orpo-training.md
|
|
1024
1104
|
// §4 Phase B). These are ADDITIVE — forwardLayers is untouched. The segmented
|
|
1025
1105
|
// driver builds masks + per-layer inputs ONCE, then drives runLayerRange per
|
|
@@ -2,6 +2,7 @@ import type { MlxArray } from "../mlx/array";
|
|
|
2
2
|
import * as ops from "../mlx/ops";
|
|
3
3
|
import type { Cache, KvAttentionView, KvDonorAttention } from "./gemma4-base";
|
|
4
4
|
import { quantizedSdpa, disposeTriple } from "./gemma4-base";
|
|
5
|
+
import { quantizedAppendAttention } from "./quantized-append-attention";
|
|
5
6
|
|
|
6
7
|
export function captureKvDonorAttention(cache: Cache): KvDonorAttention {
|
|
7
8
|
if (cache.captureDonorAttention) return cache.captureDonorAttention();
|
|
@@ -31,7 +32,9 @@ export function captureKvAttention(cache: Cache, k: MlxArray, v: MlxArray): KvAt
|
|
|
31
32
|
const { groupSize, bits } = quantized;
|
|
32
33
|
const [keys, values] = quantized.updateAndFetchQuantized(k, v);
|
|
33
34
|
return {
|
|
34
|
-
attend: (q, scale, mask) =>
|
|
35
|
+
attend: (q, scale, mask, independentPositions) => independentPositions && q.shape[2]! > 1
|
|
36
|
+
? quantizedAppendAttention(q, keys, values, scale, groupSize, bits)
|
|
37
|
+
: quantizedSdpa(q, keys, values, scale, mask, groupSize, bits),
|
|
35
38
|
dispose() { disposeTriple(keys); disposeTriple(values); },
|
|
36
39
|
};
|
|
37
40
|
}
|
package/src/model/profile.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isQwen35Config,
|
|
8
8
|
isQwen3Config,
|
|
9
9
|
isQwen3MoeConfig,
|
|
10
|
+
isWhisperConfig,
|
|
10
11
|
} from "./support";
|
|
11
12
|
import { GENERATED } from "./generated";
|
|
12
13
|
import { GENERIC_MODEL_TYPES, genericArgsFor, remapModelType } from "./universal/archs";
|
|
@@ -16,6 +17,7 @@ export const ENGINE_CAPABILITIES = Object.freeze([
|
|
|
16
17
|
"colibri-container",
|
|
17
18
|
"diffusion",
|
|
18
19
|
"diffusion-gemma-graph",
|
|
20
|
+
"encoder-decoder",
|
|
19
21
|
"gemma4-graph",
|
|
20
22
|
"generated-graph",
|
|
21
23
|
"glm5.2-graph",
|
|
@@ -30,6 +32,7 @@ export const ENGINE_CAPABILITIES = Object.freeze([
|
|
|
30
32
|
"streamed-experts",
|
|
31
33
|
"universal-dense-graph",
|
|
32
34
|
"vision-sidecar",
|
|
35
|
+
"whisper-graph",
|
|
33
36
|
] as const);
|
|
34
37
|
|
|
35
38
|
export type EngineCapability = typeof ENGINE_CAPABILITIES[number];
|
|
@@ -37,6 +40,7 @@ export type FidelityTier = "l1" | "l2" | "l3";
|
|
|
37
40
|
|
|
38
41
|
export type FidelityTarget =
|
|
39
42
|
| Readonly<{ tier: "l1"; oracle: "mlx-lm"; claim: "bit-exact" }>
|
|
43
|
+
| Readonly<{ tier: "l1"; oracle: "mlx-whisper"; claim: "bit-exact" }>
|
|
40
44
|
| Readonly<{ tier: "l2"; oracle: "mlx-optiq"; claim: "bit-exact" }>
|
|
41
45
|
| Readonly<{ tier: "l3"; oracle: null; claim: "measured" }>;
|
|
42
46
|
|
|
@@ -49,8 +53,9 @@ export type ModelGraph =
|
|
|
49
53
|
| "qwen3-moe"
|
|
50
54
|
| "diffusion-gemma"
|
|
51
55
|
| "glm5.2"
|
|
52
|
-
| "universal-dense"
|
|
53
|
-
|
|
56
|
+
| "universal-dense"
|
|
57
|
+
| "whisper";
|
|
58
|
+
export type GenerationLoop = "autoregressive" | "diffusion" | "encoder-decoder";
|
|
54
59
|
export type ModelSpecialization = "artifact" | "dedicated" | "generated" | "generic";
|
|
55
60
|
|
|
56
61
|
export interface ModelExecutionComposition {
|
|
@@ -105,6 +110,11 @@ export interface ResolveModelProfileOptions {
|
|
|
105
110
|
const L1: FidelityTarget = Object.freeze({
|
|
106
111
|
tier: "l1", oracle: "mlx-lm", claim: "bit-exact",
|
|
107
112
|
});
|
|
113
|
+
/** L1 for speech: the capability's own reference implementation (mlx-whisper)
|
|
114
|
+
* plays the mlx-lm role — no mlx-lm arm exists for Whisper. */
|
|
115
|
+
const L1_WHISPER: FidelityTarget = Object.freeze({
|
|
116
|
+
tier: "l1", oracle: "mlx-whisper", claim: "bit-exact",
|
|
117
|
+
});
|
|
108
118
|
const L2: FidelityTarget = Object.freeze({
|
|
109
119
|
tier: "l2", oracle: "mlx-optiq", claim: "bit-exact",
|
|
110
120
|
});
|
|
@@ -231,6 +241,14 @@ const FAMILY_PROFILES = {
|
|
|
231
241
|
loader: "colibri", graph: "glm5.2", loop: "autoregressive", specialization: "dedicated",
|
|
232
242
|
},
|
|
233
243
|
}),
|
|
244
|
+
whisper: freezeProfile({
|
|
245
|
+
id: "whisper-dedicated",
|
|
246
|
+
fidelity: L1_WHISPER,
|
|
247
|
+
requiredCapabilities: ["safetensors", "encoder-decoder", "whisper-graph"],
|
|
248
|
+
execution: {
|
|
249
|
+
loader: "safetensors", graph: "whisper", loop: "encoder-decoder", specialization: "dedicated",
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
234
252
|
universal: freezeProfile({
|
|
235
253
|
id: "universal-dense",
|
|
236
254
|
fidelity: L1,
|
|
@@ -263,6 +281,7 @@ function familyProfile(config: ModelConfig, fingerprint: string): ModelProfile {
|
|
|
263
281
|
if (isQwen35Config(config)) return FAMILY_PROFILES.qwen35;
|
|
264
282
|
if (isQwen3MoeConfig(config)) return FAMILY_PROFILES.qwen3Moe;
|
|
265
283
|
if (isQwen3Config(config)) return FAMILY_PROFILES.qwen3;
|
|
284
|
+
if (isWhisperConfig(config)) return FAMILY_PROFILES.whisper;
|
|
266
285
|
if (config.modelType.startsWith("gemma4"))
|
|
267
286
|
return GENERATED.has(fingerprint) ? FAMILY_PROFILES.gemma4Generated : FAMILY_PROFILES.gemma4;
|
|
268
287
|
if (genericArgsFor(config)) return FAMILY_PROFILES.universal;
|
|
@@ -271,7 +290,7 @@ function familyProfile(config: ModelConfig, fingerprint: string): ModelProfile {
|
|
|
271
290
|
throw new Error(
|
|
272
291
|
`unsupported model_type "${config.modelType}"` +
|
|
273
292
|
(arch !== config.modelType ? ` (mlx-lm remaps it to "${arch}")` : "") +
|
|
274
|
-
` — targeted: gemma4*, diffusion_gemma, glm_moe_dsa, qwen3_5, qwen3, qwen3_moe, MiniCPM5;` +
|
|
293
|
+
` — targeted: gemma4*, diffusion_gemma, glm_moe_dsa, qwen3_5, qwen3, qwen3_moe, whisper, MiniCPM5;` +
|
|
275
294
|
` generic (Tier-0): ${[...GENERIC_MODEL_TYPES].sort().join(", ")}`,
|
|
276
295
|
);
|
|
277
296
|
}
|
|
@@ -301,6 +320,7 @@ const GRAPH_METADATA: Readonly<Record<ModelGraph, GraphMetadata>> = Object.freez
|
|
|
301
320
|
accepts: isGlm52Config,
|
|
302
321
|
capabilities: ["glm5.2-graph", "streamed-experts"],
|
|
303
322
|
},
|
|
323
|
+
"whisper": { accepts: isWhisperConfig, capabilities: ["whisper-graph"] },
|
|
304
324
|
"universal-dense": {
|
|
305
325
|
accepts: (config) => {
|
|
306
326
|
try { return genericArgsFor(config) !== null; } catch { return false; }
|
|
@@ -318,6 +338,7 @@ function executionCapabilities(profile: ModelProfile): EngineCapability[] {
|
|
|
318
338
|
? "colibri-container"
|
|
319
339
|
: "safetensors"];
|
|
320
340
|
if (profile.execution.loop === "diffusion") required.push("diffusion");
|
|
341
|
+
else if (profile.execution.loop === "encoder-decoder") required.push("encoder-decoder");
|
|
321
342
|
else required.push("autoregressive");
|
|
322
343
|
required.push(...GRAPH_METADATA[profile.execution.graph].capabilities);
|
|
323
344
|
if (profile.execution.specialization === "generated") required.push("generated-graph");
|
|
@@ -337,6 +358,8 @@ function validateProfile(profile: ModelProfile): void {
|
|
|
337
358
|
const fidelityOk =
|
|
338
359
|
(profile.fidelity.tier === "l1" && profile.fidelity.oracle === "mlx-lm" &&
|
|
339
360
|
profile.fidelity.claim === "bit-exact") ||
|
|
361
|
+
(profile.fidelity.tier === "l1" && profile.fidelity.oracle === "mlx-whisper" &&
|
|
362
|
+
profile.fidelity.claim === "bit-exact" && profile.execution.graph === "whisper") ||
|
|
340
363
|
(profile.fidelity.tier === "l2" && profile.fidelity.oracle === "mlx-optiq" &&
|
|
341
364
|
profile.fidelity.claim === "bit-exact") ||
|
|
342
365
|
(profile.fidelity.tier === "l3" && profile.fidelity.oracle === null &&
|
|
@@ -345,7 +368,9 @@ function validateProfile(profile: ModelProfile): void {
|
|
|
345
368
|
const expectedLoader = profile.execution.graph === "glm5.2" ? "colibri" : "safetensors";
|
|
346
369
|
const expectedLoop = profile.execution.graph === "diffusion-gemma"
|
|
347
370
|
? "diffusion"
|
|
348
|
-
: "
|
|
371
|
+
: profile.execution.graph === "whisper"
|
|
372
|
+
? "encoder-decoder"
|
|
373
|
+
: "autoregressive";
|
|
349
374
|
if (profile.execution.loader !== expectedLoader || profile.execution.loop !== expectedLoop)
|
|
350
375
|
throw new Error(
|
|
351
376
|
`model profile ${profile.id} has an invalid execution composition for ` +
|