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.
Files changed (231) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +58 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  5. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  6. package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
  7. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  8. package/docs/design/batching.md +1535 -50
  9. package/docs/design/decode-speed-program.md +394 -1171
  10. package/docs/design/generic-model-support.md +195 -3
  11. package/docs/design/kv-cache.md +538 -225
  12. package/docs/design/speculative-decoding.md +552 -50
  13. package/docs/design/turboquant.md +140 -17
  14. package/docs/design/unified-engine-frontier-plan.md +259 -150
  15. package/docs/planning/PRODUCT_ROADMAP.md +21 -6
  16. package/docs/planning/public-readiness-handoff.md +244 -0
  17. package/docs/planning/public-readiness.md +149 -0
  18. package/docs/reference/benchmarks.md +3537 -1112
  19. package/docs/reference/cli.md +102 -2
  20. package/docs/reference/distribution.md +45 -13
  21. package/docs/reference/environment.md +159 -13
  22. package/docs/reference/library-api.md +18 -4
  23. package/docs/reference/memory.md +1 -1
  24. package/docs/reference/models.md +28 -7
  25. package/docs/reference/server-api.md +257 -33
  26. package/docs/reference/server-config.md +372 -151
  27. package/docs/reference/troubleshooting.md +13 -21
  28. package/package.json +2 -2
  29. package/src/audio/audiotoolbox.ts +112 -0
  30. package/src/audio/conformer.ts +8 -0
  31. package/src/audio/mic-capture.ts +98 -0
  32. package/src/audio/silero-vad.ts +379 -0
  33. package/src/audio/whisper-decode.ts +774 -0
  34. package/src/audio/whisper-fast.ts +732 -0
  35. package/src/audio/whisper-format.ts +73 -0
  36. package/src/audio/whisper-mel.ts +185 -0
  37. package/src/audio/whisper-timing.ts +274 -0
  38. package/src/audio/whisper-tokenizer.ts +239 -0
  39. package/src/audio/whisper-transcribe.ts +410 -0
  40. package/src/backends/mlx/assistant-target.ts +46 -0
  41. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  42. package/src/backends/mlx/autoregressive.ts +22 -1
  43. package/src/backends/mlx/batch-group.ts +464 -236
  44. package/src/backends/mlx/cache-attachment.ts +67 -0
  45. package/src/backends/mlx/cache-layout.ts +31 -0
  46. package/src/backends/mlx/checkpoint-state.ts +53 -0
  47. package/src/backends/mlx/continuation-persistence.ts +107 -0
  48. package/src/backends/mlx/continuation-request.ts +43 -0
  49. package/src/backends/mlx/continuation.ts +77 -0
  50. package/src/backends/mlx/draft-target.ts +7 -22
  51. package/src/backends/mlx/encoder-cache.ts +25 -0
  52. package/src/backends/mlx/fill-append.ts +21 -8
  53. package/src/backends/mlx/fill-group.ts +497 -0
  54. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  55. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  56. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  57. package/src/backends/mlx/gateway-binding.ts +122 -19
  58. package/src/backends/mlx/kv-codec.ts +9 -0
  59. package/src/backends/mlx/kv-maintenance.ts +161 -0
  60. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  61. package/src/backends/mlx/mixed-iteration.ts +62 -0
  62. package/src/backends/mlx/model-prompt.ts +46 -32
  63. package/src/backends/mlx/model-serving.ts +5 -4
  64. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  65. package/src/backends/mlx/prefill-cohort.ts +100 -0
  66. package/src/backends/mlx/prefill-policy.ts +26 -0
  67. package/src/backends/mlx/prefill-rows.ts +244 -0
  68. package/src/backends/mlx/prefill.ts +1 -1
  69. package/src/backends/mlx/projected-context-rows.ts +72 -0
  70. package/src/backends/mlx/prompt-input.ts +29 -0
  71. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  72. package/src/backends/mlx/request-state-policy.ts +54 -0
  73. package/src/backends/mlx/rollback.ts +45 -4
  74. package/src/backends/mlx/row-sampling.ts +49 -0
  75. package/src/backends/mlx/serial-executor.ts +51 -73
  76. package/src/backends/mlx/speculative-group.ts +438 -0
  77. package/src/backends/mlx/speculative-round.ts +161 -0
  78. package/src/backends/mlx/speculative.ts +24 -7
  79. package/src/backends/mlx/state-rows.ts +104 -0
  80. package/src/backends/mlx/state-views.ts +21 -1
  81. package/src/backends/mlx/target-layout-capability.ts +35 -0
  82. package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
  83. package/src/cli.ts +333 -36
  84. package/src/contracts/execution.ts +21 -0
  85. package/src/contracts/object-cache.ts +8 -0
  86. package/src/contracts/pi-provider.ts +1 -1
  87. package/src/contracts/pi.ts +3 -1
  88. package/src/contracts/prefix-cache.ts +33 -0
  89. package/src/contracts/resources.ts +7 -0
  90. package/src/contracts/sampling.ts +23 -0
  91. package/src/contracts/scheduling.ts +27 -0
  92. package/src/engine/execution-plan.ts +38 -25
  93. package/src/engine/execution-tasks.ts +44 -0
  94. package/src/engine/namespaced-cache.ts +13 -0
  95. package/src/engine/resources.ts +12 -1
  96. package/src/engine/scheduler.ts +32 -7
  97. package/src/fill/fill-session.ts +14 -3
  98. package/src/fit.ts +29 -15
  99. package/src/generate.ts +105 -195
  100. package/src/grammar.ts +13 -0
  101. package/src/inference/draft-acceptance.ts +41 -0
  102. package/src/inference/draft-output.ts +49 -0
  103. package/src/inference/prefill.ts +6 -0
  104. package/src/inference/rollback.ts +2 -2
  105. package/src/kv-scheme.ts +92 -12
  106. package/src/kv-store.ts +183 -93
  107. package/src/lab/README.md +5 -1
  108. package/src/lab/paged-kv/paged-attention.ts +117 -0
  109. package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
  110. package/src/lab/paged-kv/paged-kv.ts +126 -46
  111. package/src/md-modules.d.ts +5 -0
  112. package/src/mlx/array.ts +39 -1
  113. package/src/mlx/ffi.ts +19 -0
  114. package/src/mlx/normalized-argmax.ts +116 -0
  115. package/src/mlx/ops.ts +97 -10
  116. package/src/model/artifact-identity.ts +73 -0
  117. package/src/model/batched-kv.ts +235 -0
  118. package/src/model/batched-mask.ts +12 -4
  119. package/src/model/batched-quant.ts +8 -2
  120. package/src/model/batched-quantized-kv.ts +144 -0
  121. package/src/model/batched-rotating-quant.ts +76 -15
  122. package/src/model/batched-rotating-state.ts +76 -0
  123. package/src/model/batched-rotating.ts +64 -22
  124. package/src/model/batched-row-storage.ts +52 -0
  125. package/src/model/batched-ssm.ts +52 -0
  126. package/src/model/batched-turboquant-kv.ts +94 -0
  127. package/src/model/compiled-decode.ts +7 -1
  128. package/src/model/decoded-kv-donor.ts +12 -0
  129. package/src/model/delayed-quantized-kv.ts +72 -0
  130. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  131. package/src/model/delayed-turboquant-kv.ts +57 -0
  132. package/src/model/factory.ts +26 -0
  133. package/src/model/fingerprint.ts +12 -0
  134. package/src/model/full-prefill-padding.ts +56 -0
  135. package/src/model/full-prefill-row.ts +105 -0
  136. package/src/model/gemma4-base.ts +183 -82
  137. package/src/model/gemma4.ts +116 -28
  138. package/src/model/generated/gemma4-12b.ts +4 -1
  139. package/src/model/generated/gemma4-26b.ts +4 -1
  140. package/src/model/generated/gemma4-e4b.ts +4 -1
  141. package/src/model/glm52-cache.ts +80 -4
  142. package/src/model/glm52-mla.ts +12 -6
  143. package/src/model/glm52.ts +19 -40
  144. package/src/model/kv-attention-view.ts +70 -0
  145. package/src/model/minicpm5.ts +11 -7
  146. package/src/model/profile.ts +29 -4
  147. package/src/model/quantized-append-attention.ts +31 -0
  148. package/src/model/qwen3-delta-state.ts +74 -0
  149. package/src/model/qwen3-delta.ts +59 -36
  150. package/src/model/qwen3-mrope.ts +7 -9
  151. package/src/model/qwen3_5.ts +177 -75
  152. package/src/model/rotating-kv-donor.ts +35 -0
  153. package/src/model/rotating-kv-layout.ts +262 -0
  154. package/src/model/rotating-row-transaction.ts +35 -0
  155. package/src/model/speculative-rotating-kv.ts +101 -0
  156. package/src/model/ssm-prefill-padding.ts +39 -0
  157. package/src/model/support.ts +8 -0
  158. package/src/model/token-groups.ts +66 -0
  159. package/src/model/trellis-linear.ts +4 -4
  160. package/src/model/turboquant-codec.ts +89 -0
  161. package/src/model/universal/dense.ts +14 -13
  162. package/src/model/whisper.ts +435 -0
  163. package/src/native/mic_capture.swift +92 -0
  164. package/src/native-pack.ts +6 -5
  165. package/src/pi-web.ts +9 -1
  166. package/src/preflight.ts +31 -15
  167. package/src/prompt-cache.ts +306 -86
  168. package/src/runtime-config.ts +1 -5
  169. package/src/safetensors.ts +9 -2
  170. package/src/sampler.ts +37 -25
  171. package/src/serve/audio-routes.ts +323 -0
  172. package/src/serve/chat-request.ts +2 -0
  173. package/src/serve/chat-stage.ts +19 -14
  174. package/src/serve/completion-executor.ts +9 -1
  175. package/src/serve/discovery-routes.ts +17 -1
  176. package/src/serve/generated-token-history.ts +57 -0
  177. package/src/serve/generation-gateway.ts +88 -38
  178. package/src/serve/model-binding.ts +7 -1
  179. package/src/serve/model-host.ts +3 -4
  180. package/src/serve/preparation.ts +3 -3
  181. package/src/serve/prompt-response-trace.ts +14 -0
  182. package/src/serve/request-plan.ts +6 -3
  183. package/src/serve/request-prep.ts +40 -25
  184. package/src/serve/text-completion-stage.ts +2 -2
  185. package/src/serve/transcription-server.ts +64 -0
  186. package/src/serve/transcription-service.ts +502 -0
  187. package/src/server.ts +185 -261
  188. package/src/spec/assistant-rows.ts +93 -0
  189. package/src/spec/assistant-source.ts +28 -18
  190. package/src/spec/cache-identity.ts +22 -0
  191. package/src/spec/deepspec-rows.ts +29 -0
  192. package/src/spec/deepspec-source.ts +12 -10
  193. package/src/spec/dflash-source.ts +28 -4
  194. package/src/spec/drafter.ts +153 -142
  195. package/src/spec/dspark/deepspec-module.ts +95 -120
  196. package/src/spec/dspark/module-dflash.ts +126 -170
  197. package/src/spec/glm52-mtp-graph.ts +29 -0
  198. package/src/spec/glm52-mtp-rows.ts +132 -0
  199. package/src/spec/glm52-mtp-source.ts +54 -136
  200. package/src/spec/glm52-mtp-state.ts +33 -0
  201. package/src/spec/ngram-source.ts +115 -21
  202. package/src/spec/projected-draft-rows.ts +73 -0
  203. package/src/spec/qwen-mtp-module.ts +171 -0
  204. package/src/spec/qwen-mtp-rows.ts +230 -0
  205. package/src/spec/qwen-mtp-source.ts +84 -226
  206. package/src/spec/qwen-mtp-state.ts +30 -0
  207. package/src/spec/serve-loop.ts +73 -69
  208. package/src/spec/source.ts +93 -15
  209. package/src/spec/two-model-rows.ts +62 -0
  210. package/src/spec/two-model.ts +45 -103
  211. package/src/ssd-cache.ts +107 -36
  212. package/src/ssd-durability.ts +29 -27
  213. package/src/storage/host-buffer.ts +24 -0
  214. package/src/storage/kv-writer.ts +83 -0
  215. package/src/storage/kv-writer.worker.js +264 -0
  216. package/src/storage/retention-policy.ts +37 -0
  217. package/src/tiered-prompt-cache.ts +45 -0
  218. package/src/vision/embedder.ts +8 -0
  219. package/src/vision/prompt.ts +101 -65
  220. package/src/vision/qwen3vl-prompt.ts +101 -64
  221. package/src/vision/qwen3vl-tower.ts +11 -0
  222. package/src/vision/siglip.ts +8 -0
  223. package/src/web/app.html +12 -0
  224. package/src/web/app.js +217 -57
  225. package/src/web/src/chat.ts +4 -1
  226. package/src/web/src/composer.ts +3 -0
  227. package/src/web/src/voice.ts +141 -0
  228. package/src/web/sw.js +1 -1
  229. package/src/backends/mlx/memory-guard.ts +0 -40
  230. package/src/spec/prefix-state.ts +0 -45
  231. /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 cache — vLLM-style block-pool storage behind the standard Cache
2
- // interface (docs/design/kv-cache.md). OPTIONAL and default-off
3
- // (`--paged-kv` / GenerateOptions.pagedKv); v1 scope is serial batch=1
4
- // decode on Gemma4-family plain full-attention layers, bf16 only.
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
- readonly numBlocks: number;
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 k = pick(this.keys);
119
- const v = pick(this.values);
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: paged layout is serial-only and never merges into a
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) this.blockTable.push(this.pool.alloc());
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
- /** Pool tensors — what a prefill-chunk boundary must materialize.
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 ? [this.pool.keys, this.pool.values] : [];
306
+ return this.pool ? this.pool.arrays() : [];
227
307
  }
228
308
 
229
309
  isTrimmable(): boolean {
@@ -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 the evaluated array (copy). bf16/f16/f32 only. */
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
+ }