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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { BatchableCache, Cache, Mask, KvAttentionView } from "../../model/gemma4-base";
|
|
2
|
+
import { buildBatchedDecodeMask, mergeKVRows } from "../../model/batched-mask";
|
|
3
|
+
import * as ops from "../../mlx/ops";
|
|
4
|
+
import { runtimeFlag } from "../../runtime-config";
|
|
5
|
+
import { MlxArray } from "../../mlx/array";
|
|
6
|
+
import { disposeResources } from "../../engine/resources";
|
|
7
|
+
import { PagedKVCache, poolBlocksFor, type PagedQuantization } from "./paged-kv";
|
|
8
|
+
|
|
9
|
+
/** Block storage owns row membership; attention consumes the same gathered
|
|
10
|
+
* K/V representation as the existing paged cache. No scheduler policy here. */
|
|
11
|
+
export class PagedKvRows implements BatchableCache {
|
|
12
|
+
#rows: PagedKVCache[] = [];
|
|
13
|
+
#rope?: MlxArray;
|
|
14
|
+
#ropeOffset = -1;
|
|
15
|
+
|
|
16
|
+
constructor(readonly capacityTokens: number, readonly blockSize: number, readonly direct = runtimeFlag("MLX_BUN_PAGED_ATTN", false), readonly quantization?: PagedQuantization) {}
|
|
17
|
+
signature(): string { return "kv:paged-rows"; }
|
|
18
|
+
get batchSize(): number { return this.#rows.length; }
|
|
19
|
+
get offset(): number { return Math.max(0, ...this.#rows.map(row => row.offset)); }
|
|
20
|
+
get rowOffsets(): number[] { return this.#rows.map(row => row.offset); }
|
|
21
|
+
get leftPad(): number[] { const width = this.offset; return this.#rows.map(row => width - row.offset); }
|
|
22
|
+
get ropeOffsetArr(): MlxArray | undefined {
|
|
23
|
+
if (this.#rows.length < 2) return undefined;
|
|
24
|
+
if (!this.#rope || this.#ropeOffset !== this.offset) {
|
|
25
|
+
this.releaseRopeArr();
|
|
26
|
+
this.#rope = MlxArray.fromInt32(Int32Array.from(this.rowOffsets), [this.batchSize]);
|
|
27
|
+
this.#ropeOffset = this.offset;
|
|
28
|
+
}
|
|
29
|
+
return this.#rope;
|
|
30
|
+
}
|
|
31
|
+
releaseRopeArr(): void { this.#rope?.dispose(); this.#rope = undefined; this.#ropeOffset = -1; }
|
|
32
|
+
makeEmptyBatch(): PagedKvRows { return new PagedKvRows(this.capacityTokens, this.blockSize, this.direct, this.quantization); }
|
|
33
|
+
|
|
34
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
35
|
+
const next: PagedKVCache[] = [];
|
|
36
|
+
try {
|
|
37
|
+
for (const source of rows) {
|
|
38
|
+
if (source instanceof PagedKvRows) {
|
|
39
|
+
for (const row of source.#rows) next.push(row.clone());
|
|
40
|
+
} else if (source instanceof PagedKVCache) next.push(source.clone());
|
|
41
|
+
else throw new Error(`Paged rows cannot merge ${source.signature()}`);
|
|
42
|
+
}
|
|
43
|
+
} catch (error) { disposeResources(next); throw error; }
|
|
44
|
+
const previous = this.#rows; this.#rows = next;
|
|
45
|
+
this.releaseRopeArr(); disposeResources(previous);
|
|
46
|
+
}
|
|
47
|
+
extractRow(row: number): PagedKVCache { return this.#rows[row]!.clone(); }
|
|
48
|
+
filterRows(keep: readonly number[]): void {
|
|
49
|
+
const previous = this.#rows;
|
|
50
|
+
this.#rows = keep.map(row => previous[row]!);
|
|
51
|
+
const retained = new Set(keep);
|
|
52
|
+
this.releaseRopeArr();
|
|
53
|
+
disposeResources(previous.filter((_, row) => !retained.has(row)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get attentionState(): this | undefined { return this.direct || this.quantization ? this : undefined; }
|
|
57
|
+
appendAndFetch(keys: MlxArray, values: MlxArray): KvAttentionView {
|
|
58
|
+
const views: KvAttentionView[] = [];
|
|
59
|
+
try {
|
|
60
|
+
for (const [row, storage] of this.#rows.entries()) {
|
|
61
|
+
using k = keys.slice([row, 0, 0, 0], [row + 1, ...keys.shape.slice(1)]);
|
|
62
|
+
using v = values.slice([row, 0, 0, 0], [row + 1, ...values.shape.slice(1)]);
|
|
63
|
+
views.push(storage.appendAndFetch(k, v));
|
|
64
|
+
}
|
|
65
|
+
} catch (error) { disposeResources(views); throw error; }
|
|
66
|
+
return {
|
|
67
|
+
attend(q, scale, mask) {
|
|
68
|
+
const outputs: MlxArray[] = [];
|
|
69
|
+
try {
|
|
70
|
+
for (const [row, view] of views.entries()) {
|
|
71
|
+
using query = q.slice([row, 0, 0, 0], [row + 1, ...q.shape.slice(1)]);
|
|
72
|
+
outputs.push(view.attend(query, scale, mask));
|
|
73
|
+
}
|
|
74
|
+
return ops.concatAxis(outputs, 0);
|
|
75
|
+
} finally { disposeResources(outputs); }
|
|
76
|
+
},
|
|
77
|
+
dispose() { disposeResources(views); },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
updateAndFetch(keys: MlxArray, values: MlxArray): [MlxArray, MlxArray] {
|
|
82
|
+
if (this.#rows.length === 1) return this.#rows[0]!.updateAndFetch(keys, values);
|
|
83
|
+
const fetched: Array<{ keys: MlxArray; values: MlxArray }> = [];
|
|
84
|
+
try {
|
|
85
|
+
for (const [row, storage] of this.#rows.entries()) {
|
|
86
|
+
using k = keys.slice([row, 0, 0, 0], [row + 1, ...keys.shape.slice(1)]);
|
|
87
|
+
using v = values.slice([row, 0, 0, 0], [row + 1, ...values.shape.slice(1)]);
|
|
88
|
+
const [nextK, nextV] = storage.updateAndFetch(k, v);
|
|
89
|
+
fetched.push({ keys: nextK, values: nextV });
|
|
90
|
+
}
|
|
91
|
+
const result = mergeKVRows(fetched);
|
|
92
|
+
return [result.keys, result.values];
|
|
93
|
+
} finally { disposeResources(fetched.flatMap(row => [row.keys, row.values])); }
|
|
94
|
+
}
|
|
95
|
+
makeMask(tokens: number, window: number | null): Mask {
|
|
96
|
+
if (this.attentionState && window === null) return { mode: tokens === 1 ? "" : "causal", arr: null };
|
|
97
|
+
if (this.#rows.length === 1) return this.#rows[0]!.makeMask(tokens, window);
|
|
98
|
+
return { mode: "array", arr: buildBatchedDecodeMask(this.batchSize, tokens,
|
|
99
|
+
this.offset + tokens, this.leftPad, window) };
|
|
100
|
+
}
|
|
101
|
+
state(): MlxArray[] { return this.#rows.flatMap(row => row.state()); }
|
|
102
|
+
isTrimmable(): boolean { return true; }
|
|
103
|
+
trim(tokens: number): void { for (const row of this.#rows) row.trim(tokens); this.releaseRopeArr(); }
|
|
104
|
+
projectedBytes(tokens: number): number {
|
|
105
|
+
const pool = this.#rows.find(row => row.pool)?.pool;
|
|
106
|
+
if (!pool) return 0;
|
|
107
|
+
const bytesPerBlock = pool.arrays().reduce((bytes, array) => bytes + array.nbytes, 0) / pool.numBlocks;
|
|
108
|
+
return poolBlocksFor(tokens, this.blockSize) * bytesPerBlock;
|
|
109
|
+
}
|
|
110
|
+
dispose(): void { const rows = this.#rows; this.#rows = []; this.releaseRopeArr(); disposeResources(rows); }
|
|
111
|
+
}
|
|
@@ -1,30 +1,16 @@
|
|
|
1
|
-
// Paged KV
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// Storage: K/V live in fixed-size per-layer pool tensors
|
|
7
|
-
// [numBlocks, H_kv, blockSize, headDim]; a host-side block table maps the
|
|
8
|
-
// sequence's logical block order to physical pool slots. Writes go through
|
|
9
|
-
// ops.sliceUpdate into the tail block; reads gather the occupied blocks
|
|
10
|
-
// back into ONE contiguous [1, H, S, D] pair (ops.takeAxis over the pool's
|
|
11
|
-
// block axis + transpose + reshape) and hand that to the unchanged
|
|
12
|
-
// ops.sdpa call site — identical bytes to what a plain KVCache would have
|
|
13
|
-
// fetched, so the paged path is gated bit-exact, not KL-tolerated
|
|
14
|
-
// (tests/parity/paged-kv-parity.test.ts). The gather IS the cost: a full K/V copy
|
|
15
|
-
// per step, pure bandwidth tax at batch=1 — v1 ships the abstraction for
|
|
16
|
-
// the batched/CoW follow-ups, not a speed win (see the design doc's
|
|
17
|
-
// Motivation, which says this honestly).
|
|
18
|
-
//
|
|
19
|
-
// Deliberately NOT a KVCache subclass (the TurboQuantKVCache reasoning):
|
|
20
|
-
// every instanceof gate in the tree — CompiledDecode.supports, the batch
|
|
21
|
-
// gateway's #modelCachesBatchable, generated forwards' #matches() — must
|
|
22
|
-
// EXCLUDE paged caches and fall back to the monolith/uncompiled path.
|
|
1
|
+
// Paged KV storage behind the common cache and attention interfaces.
|
|
2
|
+
// Full-attention pools support bf16 and affine KV4/KV8. The established
|
|
3
|
+
// gather + SDPA arm is the numerical control; direct Metal reads are Lab.
|
|
4
|
+
// Published views and SSD snapshots own immutable buffers independently of
|
|
5
|
+
// appends, row membership and RAM eviction.
|
|
23
6
|
|
|
7
|
+
import { disposeResources } from "../../engine/resources";
|
|
8
|
+
import { pagedAttentionView } from "./paged-attention";
|
|
9
|
+
import { runtimeFlag } from "../../runtime-config";
|
|
24
10
|
import { MlxArray } from "../../mlx/array";
|
|
25
11
|
import { Dtype } from "../../mlx/ffi";
|
|
26
12
|
import * as ops from "../../mlx/ops";
|
|
27
|
-
import { createCausalMask, type Cache, type Mask } from "../../model/gemma4-base";
|
|
13
|
+
import { createCausalMask, type Cache, type Mask, type KvAttentionView } from "../../model/gemma4-base";
|
|
28
14
|
|
|
29
15
|
/** Typed pool-exhaustion error: a generation outgrew its pool. Sizing from
|
|
30
16
|
* prompt+maxTokens at construction makes this unreachable in practice;
|
|
@@ -42,10 +28,18 @@ export class PagedPoolExhausted extends Error {
|
|
|
42
28
|
* (ops.zeros — no host-pointer alignment/dtor hazards), shaped
|
|
43
29
|
* [numBlocks, H, blockSize, D]. alloc()/free() manage physical slot
|
|
44
30
|
* indices; the owning PagedKVCache writes/gathers through the pool. */
|
|
31
|
+
export interface PagedQuantization { bits: 4 | 8; groupSize: number }
|
|
32
|
+
|
|
45
33
|
export class BlockPool {
|
|
34
|
+
keyScales?: MlxArray; keyBiases?: MlxArray;
|
|
35
|
+
valueScales?: MlxArray; valueBiases?: MlxArray;
|
|
36
|
+
readonly headDim: number;
|
|
37
|
+
readonly vHeadDim: number;
|
|
38
|
+
readonly dtype: Dtype;
|
|
39
|
+
readonly quantization?: PagedQuantization;
|
|
46
40
|
keys: MlxArray;
|
|
47
41
|
values: MlxArray;
|
|
48
|
-
|
|
42
|
+
numBlocks: number;
|
|
49
43
|
readonly blockSize: number;
|
|
50
44
|
#free: number[];
|
|
51
45
|
|
|
@@ -58,16 +52,60 @@ export class BlockPool {
|
|
|
58
52
|
* reach v1's gemma4 scope, but the pool stays shape-honest). */
|
|
59
53
|
vHeadDim?: number;
|
|
60
54
|
dtype: Dtype;
|
|
61
|
-
|
|
55
|
+
quantization?: PagedQuantization;
|
|
56
|
+
}, source?: BlockPool) {
|
|
57
|
+
this.headDim = opts.headDim; this.vHeadDim = opts.vHeadDim ?? opts.headDim;
|
|
58
|
+
this.dtype = opts.dtype; this.quantization = opts.quantization;
|
|
62
59
|
this.numBlocks = opts.numBlocks;
|
|
63
60
|
this.blockSize = opts.blockSize;
|
|
64
|
-
this.keys = ops.zeros(
|
|
61
|
+
this.keys = source ? ops.contiguous(source.keys) : ops.zeros(
|
|
65
62
|
[opts.numBlocks, opts.numKvHeads, opts.blockSize, opts.headDim], opts.dtype);
|
|
66
|
-
this.values = ops.zeros(
|
|
63
|
+
this.values = source ? ops.contiguous(source.values) : ops.zeros(
|
|
67
64
|
[opts.numBlocks, opts.numKvHeads, opts.blockSize, opts.vHeadDim ?? opts.headDim],
|
|
68
65
|
opts.dtype);
|
|
66
|
+
if (source?.quantization) {
|
|
67
|
+
this.keyScales = ops.contiguous(source.keyScales!); this.keyBiases = ops.contiguous(source.keyBiases!);
|
|
68
|
+
this.valueScales = ops.contiguous(source.valueScales!); this.valueBiases = ops.contiguous(source.valueBiases!);
|
|
69
|
+
} else if (this.quantization) {
|
|
70
|
+
const { bits, groupSize } = this.quantization;
|
|
71
|
+
const k = ops.quantize(this.keys, groupSize, bits), v = ops.quantize(this.values, groupSize, bits);
|
|
72
|
+
this.keys.dispose(); this.values.dispose();
|
|
73
|
+
this.keys = k.packed; this.keyScales = k.scales; this.keyBiases = k.biases;
|
|
74
|
+
this.values = v.packed; this.valueScales = v.scales; this.valueBiases = v.biases;
|
|
75
|
+
}
|
|
69
76
|
// LIFO free list, low indices first — deterministic layout for tests.
|
|
70
|
-
this.#free = Array.from({ length: opts.numBlocks }, (_, i) => opts.numBlocks - 1 - i);
|
|
77
|
+
this.#free = source ? [...source.#free] : Array.from({ length: opts.numBlocks }, (_, i) => opts.numBlocks - 1 - i);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Independent ownership and allocation metadata over immutable MLX buffers.
|
|
81
|
+
* Functional writes detach storage while another snapshot retains it. */
|
|
82
|
+
clone(): BlockPool {
|
|
83
|
+
return new BlockPool({ numBlocks: this.numBlocks, blockSize: this.blockSize,
|
|
84
|
+
numKvHeads: this.keys.shape[1]!, headDim: this.headDim,
|
|
85
|
+
vHeadDim: this.vHeadDim, dtype: this.dtype, quantization: this.quantization }, this);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
restoreArrays(arrays: MlxArray[], occupied: readonly number[]): void {
|
|
89
|
+
disposeResources(this.arrays());
|
|
90
|
+
[this.keys, this.values] = [arrays[0]!, arrays[1]!];
|
|
91
|
+
[this.keyScales, this.keyBiases, this.valueScales, this.valueBiases] = arrays.slice(2);
|
|
92
|
+
const used = new Set(occupied);
|
|
93
|
+
this.#free = Array.from({ length: this.numBlocks }, (_, i) => this.numBlocks - 1 - i).filter(i => !used.has(i));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
grow(blocks: number): void {
|
|
97
|
+
if (blocks <= this.numBlocks) return;
|
|
98
|
+
const grow = (array: MlxArray) => {
|
|
99
|
+
using zeros = ops.zeros([blocks - this.numBlocks, ...array.shape.slice(1)], array.dtype);
|
|
100
|
+
const next = ops.concatAxis([array, zeros], 0); array.dispose(); return next;
|
|
101
|
+
};
|
|
102
|
+
this.keys = grow(this.keys); this.values = grow(this.values);
|
|
103
|
+
if (this.quantization) {
|
|
104
|
+
this.keyScales = grow(this.keyScales!); this.keyBiases = grow(this.keyBiases!);
|
|
105
|
+
this.valueScales = grow(this.valueScales!); this.valueBiases = grow(this.valueBiases!);
|
|
106
|
+
}
|
|
107
|
+
for (let i = blocks - 1; i >= this.numBlocks; i--) this.#free.push(i);
|
|
108
|
+
this.numBlocks = blocks;
|
|
71
109
|
}
|
|
72
110
|
|
|
73
111
|
get freeBlocks(): number {
|
|
@@ -89,6 +127,20 @@ export class BlockPool {
|
|
|
89
127
|
* the pool tensors are single-referenced here, so mlx donates the
|
|
90
128
|
* buffer and the write is in-place in the steady state. */
|
|
91
129
|
writeBlock(physIdx: number, k: MlxArray, v: MlxArray, within: number): void {
|
|
130
|
+
if (this.quantization) {
|
|
131
|
+
const { bits, groupSize } = this.quantization;
|
|
132
|
+
const kq = ops.quantize(k, groupSize, bits), vq = ops.quantize(v, groupSize, bits);
|
|
133
|
+
const replace = (target: MlxArray, input: MlxArray): MlxArray => {
|
|
134
|
+
const result = ops.sliceUpdate(target, input, [physIdx, 0, within, 0],
|
|
135
|
+
[physIdx + 1, input.shape[1]!, within + input.shape[2]!, input.shape[3]!]);
|
|
136
|
+
target.dispose(); return result;
|
|
137
|
+
};
|
|
138
|
+
try {
|
|
139
|
+
this.keys = replace(this.keys, kq.packed); this.keyScales = replace(this.keyScales!, kq.scales); this.keyBiases = replace(this.keyBiases!, kq.biases);
|
|
140
|
+
this.values = replace(this.values, vq.packed); this.valueScales = replace(this.valueScales!, vq.scales); this.valueBiases = replace(this.valueBiases!, vq.biases);
|
|
141
|
+
} finally { disposeResources([kq.packed, kq.scales, kq.biases, vq.packed, vq.scales, vq.biases]); }
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
92
144
|
const l = k.shape[2]!;
|
|
93
145
|
const [, H, , kD] = this.keys.shape as [number, number, number, number];
|
|
94
146
|
const vD = this.values.shape[3]!;
|
|
@@ -115,13 +167,23 @@ export class BlockPool {
|
|
|
115
167
|
t.dispose();
|
|
116
168
|
return out;
|
|
117
169
|
};
|
|
118
|
-
const
|
|
119
|
-
|
|
170
|
+
const read = (data: MlxArray, scales?: MlxArray, biases?: MlxArray) => {
|
|
171
|
+
const packed = pick(data);
|
|
172
|
+
if (!this.quantization) return packed;
|
|
173
|
+
using owned = packed; using s = pick(scales!), b = pick(biases!);
|
|
174
|
+
return ops.dequantize(owned, s, b, { ...this.quantization, mode: "affine" });
|
|
175
|
+
};
|
|
176
|
+
const k = read(this.keys, this.keyScales, this.keyBiases);
|
|
177
|
+
const v = read(this.values, this.valueScales, this.valueBiases);
|
|
120
178
|
idx.dispose();
|
|
121
179
|
return [k, v];
|
|
122
180
|
}
|
|
123
181
|
|
|
182
|
+
arrays(): MlxArray[] {
|
|
183
|
+
return [this.keys, this.values, this.keyScales, this.keyBiases, this.valueScales, this.valueBiases].filter((a): a is MlxArray => !!a);
|
|
184
|
+
}
|
|
124
185
|
dispose(): void {
|
|
186
|
+
disposeResources([this.keyScales, this.keyBiases, this.valueScales, this.valueBiases].filter((a): a is MlxArray => !!a));
|
|
125
187
|
this.keys.dispose();
|
|
126
188
|
this.values.dispose();
|
|
127
189
|
this.#free = [];
|
|
@@ -134,8 +196,7 @@ export class BlockPool {
|
|
|
134
196
|
* shape): head count / head dims / dtype come from the first k/v pair,
|
|
135
197
|
* so the wiring (maybePageKv) needs no per-model shape plumbing. */
|
|
136
198
|
export class PagedKVCache implements Cache {
|
|
137
|
-
/** Distinct kind
|
|
138
|
-
* batch, so no capability guard should ever match it as plain. */
|
|
199
|
+
/** Distinct storage kind; the row adapter preserves its block pools. */
|
|
139
200
|
signature(): string { return "kv:paged"; }
|
|
140
201
|
/** Matches KVCache.STEP: v1's growth granularity is a permutation of
|
|
141
202
|
* today's 256-token step into fixed reusable slots, not a new tuning
|
|
@@ -153,8 +214,18 @@ export class PagedKVCache implements Cache {
|
|
|
153
214
|
/** Tokens this cache must be able to hold (prompt + maxTokens). */
|
|
154
215
|
readonly capacityTokens: number,
|
|
155
216
|
readonly blockSize: number,
|
|
217
|
+
readonly direct = runtimeFlag("MLX_BUN_PAGED_ATTN", false),
|
|
218
|
+
readonly quantization?: PagedQuantization,
|
|
156
219
|
) {}
|
|
157
220
|
|
|
221
|
+
clone(): PagedKVCache {
|
|
222
|
+
const copy = new PagedKVCache(this.capacityTokens, this.blockSize, this.direct, this.quantization);
|
|
223
|
+
copy.offset = this.offset;
|
|
224
|
+
copy.blockTable = [...this.blockTable];
|
|
225
|
+
copy.pool = this.pool?.clone() ?? null;
|
|
226
|
+
return copy;
|
|
227
|
+
}
|
|
228
|
+
|
|
158
229
|
get #blockSize(): number {
|
|
159
230
|
return this.blockSize;
|
|
160
231
|
}
|
|
@@ -164,7 +235,23 @@ export class PagedKVCache implements Cache {
|
|
|
164
235
|
return Math.ceil(n / this.#blockSize);
|
|
165
236
|
}
|
|
166
237
|
|
|
238
|
+
get attentionState(): this | undefined { return this.direct || this.quantization ? this : undefined; }
|
|
239
|
+
appendAndFetch(k: MlxArray, v: MlxArray): KvAttentionView {
|
|
240
|
+
this.append(k, v);
|
|
241
|
+
return pagedAttentionView(this.pool!, [...this.blockTable], this.offset, this.direct);
|
|
242
|
+
}
|
|
243
|
+
|
|
167
244
|
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
245
|
+
this.append(k, v);
|
|
246
|
+
const H = this.pool!.keys.shape[1]!, kD = this.pool!.headDim, vD = this.pool!.vHeadDim;
|
|
247
|
+
const [gk, gv] = this.pool!.gather(this.blockTable);
|
|
248
|
+
const keys = gk.slice([0, 0, 0, 0], [1, H, this.offset, kD]);
|
|
249
|
+
const values = gv.slice([0, 0, 0, 0], [1, H, this.offset, vD]);
|
|
250
|
+
gk.dispose(); gv.dispose();
|
|
251
|
+
return [keys, values];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
append(k: MlxArray, v: MlxArray): void {
|
|
168
255
|
const L = k.shape[2]!;
|
|
169
256
|
const [, H, , kD] = k.shape as [number, number, number, number];
|
|
170
257
|
const vD = v.shape[3]!;
|
|
@@ -175,7 +262,7 @@ export class PagedKVCache implements Cache {
|
|
|
175
262
|
numKvHeads: H,
|
|
176
263
|
headDim: kD,
|
|
177
264
|
vHeadDim: vD,
|
|
178
|
-
dtype: k.dtype,
|
|
265
|
+
dtype: k.dtype, quantization: this.quantization,
|
|
179
266
|
});
|
|
180
267
|
// Write, splitting the incoming L along block boundaries. Blocks past
|
|
181
268
|
// the current tail allocate from the free list as they're reached.
|
|
@@ -184,7 +271,10 @@ export class PagedKVCache implements Cache {
|
|
|
184
271
|
const pos = this.offset + written;
|
|
185
272
|
const bi = Math.floor(pos / this.#blockSize);
|
|
186
273
|
const within = pos % this.#blockSize;
|
|
187
|
-
while (this.blockTable.length <= bi)
|
|
274
|
+
while (this.blockTable.length <= bi) {
|
|
275
|
+
if (!this.pool.freeBlocks) this.pool.grow(Math.max(this.pool.numBlocks + 1, Math.ceil(this.pool.numBlocks * 1.5)));
|
|
276
|
+
this.blockTable.push(this.pool.alloc());
|
|
277
|
+
}
|
|
188
278
|
const l = Math.min(this.#blockSize - within, L - written);
|
|
189
279
|
// Full-range pieces skip the slice (fresh-view slice would be a
|
|
190
280
|
// gratuitous op); partial pieces slice the [written, written+l) rows.
|
|
@@ -201,14 +291,6 @@ export class PagedKVCache implements Cache {
|
|
|
201
291
|
}
|
|
202
292
|
this.offset += L;
|
|
203
293
|
|
|
204
|
-
// Fetch: gather occupied blocks contiguous, slice to the live prefix.
|
|
205
|
-
const nb = this.#blocksFor(this.offset);
|
|
206
|
-
const [gk, gv] = this.pool.gather(this.blockTable.slice(0, nb));
|
|
207
|
-
const keys = gk.slice([0, 0, 0, 0], [1, H, this.offset, kD]);
|
|
208
|
-
const values = gv.slice([0, 0, 0, 0], [1, H, this.offset, vD]);
|
|
209
|
-
gk.dispose();
|
|
210
|
-
gv.dispose();
|
|
211
|
-
return [keys, values];
|
|
212
294
|
}
|
|
213
295
|
|
|
214
296
|
/** Same mask policy as KVCache.makeMask (it reads only offset). */
|
|
@@ -219,11 +301,9 @@ export class PagedKVCache implements Cache {
|
|
|
219
301
|
return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
|
|
220
302
|
}
|
|
221
303
|
|
|
222
|
-
/**
|
|
223
|
-
* NOT prompt-cache-compatible in v1 (cloneKvCaches never sees paged
|
|
224
|
-
* caches; the serve lane bypasses take/put for paged requests). */
|
|
304
|
+
/** Owned planes materialized at prefill and immutable publication boundaries. */
|
|
225
305
|
state(): MlxArray[] {
|
|
226
|
-
return this.pool ?
|
|
306
|
+
return this.pool ? this.pool.arrays() : [];
|
|
227
307
|
}
|
|
228
308
|
|
|
229
309
|
isTrimmable(): boolean {
|
package/src/md-modules.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ declare module "*.md" {
|
|
|
6
6
|
export default content;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
declare module "*.worker.js" {
|
|
10
|
+
const content: string;
|
|
11
|
+
export default content;
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
// Same deal for vendored text-imported assets under src/web/vendor/ (see
|
|
10
15
|
// that dir's README) — served as raw strings via `with { type: "text" }`,
|
|
11
16
|
// but bun-types has no ambient declaration for .js/.css read as text.
|
package/src/mlx/array.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Explicit .dispose() is the contract; a FinalizationRegistry backstop
|
|
3
3
|
// frees leaked handles on GC (verified in lab/spikes/phase0-memory.ts).
|
|
4
4
|
|
|
5
|
+
import { hostBufferDestructor, type HostBuffer } from "../storage/host-buffer";
|
|
5
6
|
import { dlopen, ptr, toArrayBuffer } from "bun:ffi";
|
|
6
7
|
import { C, Dtype, DTYPE_NAMES, type MlxHandle, optInt, outArray, takeMlxError } from "./ffi";
|
|
7
8
|
import type { SafetensorsDtype } from "../safetensors";
|
|
@@ -130,6 +131,18 @@ export class MlxArray {
|
|
|
130
131
|
return new MlxArray(handle);
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
/** Adopt a page-aligned CPU restore allocation without a host copy.
|
|
135
|
+
* The native destructor owns it after this call, including GPU references. */
|
|
136
|
+
static adoptHostBuffer(buffer: HostBuffer, shape: number[], dtype: Dtype): MlxArray {
|
|
137
|
+
const sb = shapeBuf(shape);
|
|
138
|
+
const handle = C.mlx_array_new_data_managed_payload(
|
|
139
|
+
buffer.pointer, ptr(sb), shape.length, dtype, buffer.pointer, hostBufferDestructor,
|
|
140
|
+
);
|
|
141
|
+
const array = new MlxArray(handle);
|
|
142
|
+
buffer.transfer();
|
|
143
|
+
return array;
|
|
144
|
+
}
|
|
145
|
+
|
|
133
146
|
/** Copying constructor for small host data. */
|
|
134
147
|
static fromFloat32(data: Float32Array, shape: number[]): MlxArray {
|
|
135
148
|
const sb = shapeBuf(shape);
|
|
@@ -232,11 +245,35 @@ export class MlxArray {
|
|
|
232
245
|
);
|
|
233
246
|
}
|
|
234
247
|
|
|
235
|
-
/** Raw bytes of
|
|
248
|
+
/** Raw bytes of a supported floating-point or integer array (copy). */
|
|
236
249
|
rawBytes(): Uint8Array {
|
|
237
250
|
return this.rawBytesView().slice();
|
|
238
251
|
}
|
|
239
252
|
|
|
253
|
+
/** Publish an evaluated immutable buffer to a CPU reader. Strides are in
|
|
254
|
+
* elements, so padding, transposes and slices need no GPU packing copy.
|
|
255
|
+
* The caller retains this array until the reader reports completion. */
|
|
256
|
+
storageView(): import("../storage/kv-writer").StoredTensorView {
|
|
257
|
+
this.eval();
|
|
258
|
+
const shape = this.shape;
|
|
259
|
+
const stridePtr = C.mlx_array_strides(this.handle);
|
|
260
|
+
const strides = shape.length
|
|
261
|
+
? [...new BigInt64Array(toArrayBuffer(stridePtr!, 0, shape.length * 8))].map(Number)
|
|
262
|
+
: [];
|
|
263
|
+
const dt = this.dtype;
|
|
264
|
+
const pointer =
|
|
265
|
+
dt === Dtype.float32 ? C.mlx_array_data_float32(this.handle)
|
|
266
|
+
: dt === Dtype.float16 ? C.mlx_array_data_float16(this.handle)
|
|
267
|
+
: dt === Dtype.bfloat16 ? C.mlx_array_data_bfloat16(this.handle)
|
|
268
|
+
: dt === Dtype.uint32 ? C.mlx_array_data_uint32(this.handle)
|
|
269
|
+
: dt === Dtype.int32 ? C.mlx_array_data_int32(this.handle)
|
|
270
|
+
: dt === Dtype.uint8 ? C.mlx_array_data_uint8(this.handle)
|
|
271
|
+
: dt === Dtype.int8 ? C.mlx_array_data_int8(this.handle)
|
|
272
|
+
: null;
|
|
273
|
+
if (pointer === null && this.size) throw new Error(`storageView: unsupported or unavailable ${this.dtypeName}`);
|
|
274
|
+
return { pointer: Number(pointer), shape, strides, itemSize: Number(C.mlx_array_itemsize(this.handle)) };
|
|
275
|
+
}
|
|
276
|
+
|
|
240
277
|
/** ZERO-COPY view of the evaluated array's bytes — aliases the mlx
|
|
241
278
|
* buffer directly, no JS-heap copy. Valid only while THIS array is
|
|
242
279
|
* alive and unmutated: do not retain past dispose(). Built for
|
|
@@ -253,6 +290,7 @@ export class MlxArray {
|
|
|
253
290
|
: dt === Dtype.float16 ? C.mlx_array_data_float16(this.handle)
|
|
254
291
|
: dt === Dtype.bfloat16 ? C.mlx_array_data_bfloat16(this.handle)
|
|
255
292
|
: dt === Dtype.uint32 ? C.mlx_array_data_uint32(this.handle)
|
|
293
|
+
: dt === Dtype.int32 ? C.mlx_array_data_int32(this.handle)
|
|
256
294
|
: dt === Dtype.uint8 ? C.mlx_array_data_uint8(this.handle)
|
|
257
295
|
: dt === Dtype.int8 ? C.mlx_array_data_int8(this.handle)
|
|
258
296
|
: null;
|
package/src/mlx/ffi.ts
CHANGED
|
@@ -53,6 +53,8 @@ export const C = dlopen(LIBMLXC_PATH, {
|
|
|
53
53
|
// array metadata
|
|
54
54
|
mlx_array_ndim: { args: [u64], returns: u64 },
|
|
55
55
|
mlx_array_shape: { args: [u64], returns: P },
|
|
56
|
+
mlx_array_strides: { args: [u64], returns: P },
|
|
57
|
+
mlx_array_itemsize: { args: [u64], returns: u64 },
|
|
56
58
|
mlx_array_dim: { args: [u64, i32], returns: i32 },
|
|
57
59
|
mlx_array_dtype: { args: [u64], returns: i32 },
|
|
58
60
|
mlx_array_size: { args: [u64], returns: u64 },
|
|
@@ -181,6 +183,23 @@ export const C = dlopen(LIBMLXC_PATH, {
|
|
|
181
183
|
// (res, input, weight, stride, padding, dilation, groups, stream) — depthwise
|
|
182
184
|
// causal conv1d for Qwen3.5 gated-DeltaNet (groups == channels, padding 0).
|
|
183
185
|
mlx_conv1d: { args: [P, u64, u64, i32, i32, i32, i32, u64], returns: i32 },
|
|
186
|
+
// --- Whisper (src/model/whisper.ts, src/audio/whisper-mel.ts) -----------
|
|
187
|
+
// (res, a, n, axis, norm, stream) — mlx_fft_norm enum: 0 backward (numpy default).
|
|
188
|
+
mlx_fft_rfft: { args: [P, u64, i32, i32, i32, u64], returns: i32 },
|
|
189
|
+
// (res, a, shape*, shape_num, strides*(int64), strides_num, offset, stream)
|
|
190
|
+
mlx_as_strided: { args: [P, u64, P, u64, P, u64, u64, u64], returns: i32 },
|
|
191
|
+
// (res, a, axes*, n, low*, n, high*, n, pad_value, mode, stream)
|
|
192
|
+
mlx_pad: { args: [P, u64, P, u64, P, u64, P, u64, u64, cstring, u64], returns: i32 },
|
|
193
|
+
mlx_log10: { args: [P, u64, u64], returns: i32 },
|
|
194
|
+
// full reduce: (res, a, keepdims, stream)
|
|
195
|
+
mlx_max: { args: [P, u64, FFIType.bool, u64], returns: i32 },
|
|
196
|
+
mlx_broadcast_to: { args: [P, u64, P, u64, u64], returns: i32 },
|
|
197
|
+
mlx_topk_axis: { args: [P, u64, i32, i32, u64], returns: i32 },
|
|
198
|
+
mlx_var_axis: { args: [P, u64, i32, FFIType.bool, i32, u64], returns: i32 },
|
|
199
|
+
mlx_stack_axis: { args: [P, u64, i32, u64], returns: i32 },
|
|
200
|
+
mlx_minimum: { args: [P, u64, u64, u64], returns: i32 },
|
|
201
|
+
// flat take: (res, a, indices, stream)
|
|
202
|
+
mlx_take: { args: [P, u64, u64, u64], returns: i32 },
|
|
184
203
|
// Natural mlx-c signatures. Bun 1.4 fixed the macOS arm64 stack packing
|
|
185
204
|
// bug recorded in lab/repro/bun-ffi-stack-args, so these must match the
|
|
186
205
|
// header exactly. mlx-bun now requires Bun 1.4 or newer.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Greedy selection preserves dtype-rounded normalized scores, including ties.
|
|
2
|
+
// Native logsumexp retains the reference reduction. Tiled selection avoids
|
|
3
|
+
// writing the vocabulary-sized normalized array. Inputs remain borrowed.
|
|
4
|
+
import { MlxArray } from "./array";
|
|
5
|
+
import { Dtype } from "./ffi";
|
|
6
|
+
import * as ops from "./ops";
|
|
7
|
+
import { CompiledFunction, isShapelessTracing } from "./compile";
|
|
8
|
+
import { MetalKernel } from "./metal-kernel";
|
|
9
|
+
|
|
10
|
+
const header = `
|
|
11
|
+
inline bool better(float x, uint xi, float y, uint yi) {
|
|
12
|
+
return x > y || (x == y && xi < yi);
|
|
13
|
+
}`;
|
|
14
|
+
|
|
15
|
+
// Four simdgroups reduce score/index pairs. Lowest token ID wins every tie.
|
|
16
|
+
// The caller finishes the simd==0 block by writing its output from lane zero.
|
|
17
|
+
const reduction = `
|
|
18
|
+
for (uint distance = 16; distance > 0; distance >>= 1) {
|
|
19
|
+
float other = simd_shuffle_down(best, distance);
|
|
20
|
+
uint oi = simd_shuffle_down(bestId, distance);
|
|
21
|
+
if (lane + distance < 32 && better(other, oi, best, bestId)) {
|
|
22
|
+
best = other; bestId = oi;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
threadgroup float scores[4];
|
|
26
|
+
threadgroup uint ids[4];
|
|
27
|
+
if (lane == 0) { scores[simd] = best; ids[simd] = bestId; }
|
|
28
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
29
|
+
if (simd == 0) {
|
|
30
|
+
best = lane < 4 ? scores[lane] : -INFINITY;
|
|
31
|
+
bestId = lane < 4 ? ids[lane] : 0xffffffffu;
|
|
32
|
+
for (uint distance = 2; distance > 0; distance >>= 1) {
|
|
33
|
+
float other = simd_shuffle_down(best, distance);
|
|
34
|
+
uint oi = simd_shuffle_down(bestId, distance);
|
|
35
|
+
if (lane + distance < 4 && better(other, oi, best, bestId)) {
|
|
36
|
+
best = other; bestId = oi;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const partialSource = `
|
|
42
|
+
const uint tid = thread_position_in_threadgroup.x;
|
|
43
|
+
const uint lane = tid % 32, simd = tid / 32;
|
|
44
|
+
const uint row = threadgroup_position_in_grid.y;
|
|
45
|
+
const uint tile = threadgroup_position_in_grid.x;
|
|
46
|
+
float best = -INFINITY;
|
|
47
|
+
uint bestId = 0xffffffffu;
|
|
48
|
+
for (uint i = tile * 1024 + tid; i < min(uint(V), (tile + 1) * 1024); i += 128) {
|
|
49
|
+
float value = float(T(logits[row * V + i] - lse[row]));
|
|
50
|
+
// A nonfinite logsumexp produces only NaN/-inf normalized scores.
|
|
51
|
+
// Native argmax selects token zero for this degenerate row.
|
|
52
|
+
value = isnan(value) ? -INFINITY : value;
|
|
53
|
+
if (better(value, i, best, bestId)) { best = value; bestId = i; }
|
|
54
|
+
}
|
|
55
|
+
${reduction}
|
|
56
|
+
if (lane == 0) { values[row * P + tile] = best; indices[row * P + tile] = bestId; }
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const finishSource = `
|
|
61
|
+
const uint tid = thread_position_in_threadgroup.x;
|
|
62
|
+
const uint lane = tid % 32, simd = tid / 32;
|
|
63
|
+
const uint row = threadgroup_position_in_grid.y;
|
|
64
|
+
float best = -INFINITY;
|
|
65
|
+
uint bestId = 0xffffffffu;
|
|
66
|
+
for (uint i = tid; i < P; i += 128) {
|
|
67
|
+
float value = values[row * P + i];
|
|
68
|
+
uint id = indices[row * P + i];
|
|
69
|
+
if (better(value, id, best, bestId)) { best = value; bestId = id; }
|
|
70
|
+
}
|
|
71
|
+
${reduction}
|
|
72
|
+
if (lane == 0) out[row] = bestId;
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
let partial: MetalKernel | undefined;
|
|
77
|
+
let finish: MetalKernel | undefined;
|
|
78
|
+
let compiled: CompiledFunction | undefined;
|
|
79
|
+
|
|
80
|
+
function kernelGraph(logits: MlxArray): MlxArray {
|
|
81
|
+
const shape = logits.shape;
|
|
82
|
+
const vocab = shape.at(-1)!, rows = logits.size / vocab, tiles = Math.ceil(vocab / 1024);
|
|
83
|
+
partial ??= new MetalKernel({ name: "normalized_argmax_partial", header,
|
|
84
|
+
inputNames: ["logits", "lse"], outputNames: ["values", "indices"], source: partialSource });
|
|
85
|
+
finish ??= new MetalKernel({ name: "normalized_argmax_finish", header,
|
|
86
|
+
inputNames: ["values", "indices"], outputNames: ["out"], source: finishSource });
|
|
87
|
+
using lse = ops.logsumexpAxis(logits, -1, true);
|
|
88
|
+
const [values, indices] = partial.apply([logits, lse], {
|
|
89
|
+
outputs: [{ shape: [rows, tiles], dtype: Dtype.float32 }, { shape: [rows, tiles], dtype: Dtype.uint32 }],
|
|
90
|
+
grid: [tiles * 128, rows, 1], threadGroup: [128, 1, 1],
|
|
91
|
+
templateInts: { V: vocab, P: tiles }, templateDtypes: { T: logits.dtype },
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
using tokens = finish.apply([values!, indices!], {
|
|
95
|
+
outputs: [{ shape: [rows], dtype: Dtype.uint32 }],
|
|
96
|
+
grid: [128, rows, 1], threadGroup: [128, 1, 1], templateInts: { P: tiles },
|
|
97
|
+
})[0]!;
|
|
98
|
+
return ops.reshape(tokens, shape.slice(0, -1));
|
|
99
|
+
} finally { values!.dispose(); indices!.dispose(); }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Return owned greedy IDs with the same shape/rounding as argmax(logprobs).
|
|
103
|
+
* Fixed-shape compilation caches graph/FFI work for repeated row geometries.
|
|
104
|
+
* Shapeless enclosing graphs retain native operations, which support symbolic
|
|
105
|
+
* sizes; integer/empty inputs also retain the original operation semantics. */
|
|
106
|
+
export function normalizedArgmax(logits: MlxArray): MlxArray {
|
|
107
|
+
const dtype = logits.dtype;
|
|
108
|
+
if (isShapelessTracing() || logits.size === 0 ||
|
|
109
|
+
(dtype !== Dtype.float32 && dtype !== Dtype.float16 && dtype !== Dtype.bfloat16)) {
|
|
110
|
+
using lse = ops.logsumexpAxis(logits, -1, true);
|
|
111
|
+
using scores = ops.sub(logits, lse);
|
|
112
|
+
return ops.argmaxAxis(scores, -1);
|
|
113
|
+
}
|
|
114
|
+
compiled ??= new CompiledFunction(([input]) => [kernelGraph(input!)], false);
|
|
115
|
+
return compiled.apply([logits])[0]!;
|
|
116
|
+
}
|