mlx-bun 0.0.13 → 0.2.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 (224) hide show
  1. package/README.md +31 -21
  2. package/bin/mlx-bun.mjs +1 -1
  3. package/docs/{design → archive}/hlg-sampling.md +3 -3
  4. package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
  5. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
  6. package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
  7. package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
  8. package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
  9. package/docs/design/colibri-glm52-port.md +1 -1
  10. package/docs/design/compat-cli-surface-design.md +1 -1
  11. package/docs/design/decode-speed-program.md +1 -1
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +1 -1
  14. package/docs/design/dspark-speculative-decoding.md +53 -3
  15. package/docs/design/grammar-spec-batching-integration.md +1 -1
  16. package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
  17. package/docs/design/orpo-dynamic-lambda.md +1 -1
  18. package/docs/design/orpo-future-enhancements.md +1 -1
  19. package/docs/design/orpo-training.md +2 -2
  20. package/docs/design/parity-tier-dag.md +1 -1
  21. package/docs/design/repo-cleanup-plan.md +46 -4
  22. package/docs/design/segmented-backward-training.md +2 -2
  23. package/docs/design/ssd-kv-cold-tier.md +127 -0
  24. package/docs/design/turboquant-weights.md +467 -0
  25. package/docs/design/w4a16-compute-precision-spike.md +1 -1
  26. package/docs/design/web-chat-redesign.md +5 -5
  27. package/docs/planning/chunk-task-roadmap.md +1 -1
  28. package/docs/planning/curve-sampler-research-plan.md +4 -4
  29. package/docs/planning/memory-docs-and-dag-plan.md +1 -1
  30. package/docs/planning/release-notes-v0.2.0.md +49 -0
  31. package/docs/planning/video-series.md +421 -0
  32. package/docs/reference/cli.md +15 -4
  33. package/docs/reference/distribution.md +1 -0
  34. package/docs/reference/features-matrix.md +3 -2
  35. package/docs/reference/library-api.md +41 -1
  36. package/docs/reference/models.md +33 -0
  37. package/docs/reference/server-api.md +69 -14
  38. package/docs/reference/server-config.md +57 -33
  39. package/package.json +2 -2
  40. package/src/anthropic.ts +92 -3
  41. package/src/cli.ts +131 -36
  42. package/src/eval/kl.ts +27 -20
  43. package/src/eval/runner.ts +23 -4
  44. package/src/eval/tasks/bfcl.ts +2 -1
  45. package/src/eval/tasks/gsm8k.ts +2 -1
  46. package/src/eval/tasks/hashhop.ts +2 -1
  47. package/src/eval/tasks/humaneval.ts +5 -4
  48. package/src/eval/tasks/ifeval.ts +2 -1
  49. package/src/eval/tasks/mmlu.ts +2 -1
  50. package/src/expert-io.ts +3 -1
  51. package/src/expert-offload.ts +3 -1
  52. package/src/expert-trace.ts +3 -1
  53. package/src/fit.ts +24 -43
  54. package/src/flags.ts +7 -9
  55. package/src/generate.ts +107 -92
  56. package/src/grammar.ts +2 -1
  57. package/src/harness-pi.ts +1 -1
  58. package/src/index.ts +20 -0
  59. package/src/jobs/job-entry.ts +3 -2
  60. package/src/kv-scheme.ts +199 -0
  61. package/src/kv-store.ts +370 -205
  62. package/src/media-fetch.ts +19 -3
  63. package/src/memory/model.ts +2 -1
  64. package/src/memory/vault.ts +2 -1
  65. package/src/memory-plan.ts +18 -0
  66. package/src/mlx/ffi.ts +8 -11
  67. package/src/mlx/ops.ts +31 -6
  68. package/src/model/batched-mask.ts +1 -1
  69. package/src/model/batched-quant.ts +1 -1
  70. package/src/model/batched-rotating-quant.ts +43 -97
  71. package/src/model/batched-rotating-state.ts +73 -0
  72. package/src/model/batched-rotating.ts +56 -105
  73. package/src/model/batched-row-storage.ts +127 -0
  74. package/src/model/compiled-decode.ts +3 -2
  75. package/src/model/factory.ts +44 -49
  76. package/src/model/gemma4-base.ts +163 -10
  77. package/src/model/generated/gemma4-12b.ts +50 -146
  78. package/src/model/generated/gemma4-26b.ts +32 -92
  79. package/src/model/generated/gemma4-e4b.ts +26 -74
  80. package/src/model/glm52-cache.ts +19 -9
  81. package/src/model/glm52-memory.ts +28 -1
  82. package/src/model/minicpm5.ts +2 -1
  83. package/src/model/profile.ts +433 -0
  84. package/src/model/qwen3-delta.ts +98 -2
  85. package/src/model/qwen3-mrope.ts +252 -0
  86. package/src/model/qwen3_5.ts +217 -25
  87. package/src/native/frame_extract.swift +140 -0
  88. package/src/native-pack.ts +13 -6
  89. package/src/pi-web.ts +6 -5
  90. package/src/prompt-cache.ts +2 -2
  91. package/src/quantize/atomic-output.ts +38 -0
  92. package/src/quantize/calibration.ts +2 -1
  93. package/src/quantize/config-writer.ts +6 -0
  94. package/src/quantize/index.ts +28 -1
  95. package/src/quantize/job.ts +11 -0
  96. package/src/quantize/quantizer.ts +265 -100
  97. package/src/quantize/rotate.ts +603 -0
  98. package/src/quantize/safetensors-writer.ts +79 -1
  99. package/src/quantize/sensitivity.ts +41 -59
  100. package/src/quantize/weight-transform.ts +292 -0
  101. package/src/responses.ts +102 -2
  102. package/src/runtime-config.ts +67 -0
  103. package/src/sampler.ts +272 -0
  104. package/src/serve/admin-routes.ts +184 -0
  105. package/src/serve/aux-routes.ts +132 -0
  106. package/src/serve/batch-scheduler.ts +200 -87
  107. package/src/serve/completion-executor.ts +346 -0
  108. package/src/serve/completion-sink.ts +157 -0
  109. package/src/serve/discovery-routes.ts +205 -0
  110. package/src/serve/generation-gateway.ts +116 -109
  111. package/src/serve/kv-budget.ts +14 -0
  112. package/src/serve/lab-routes.ts +338 -0
  113. package/src/serve/model-admin-routes.ts +153 -0
  114. package/src/serve/prompt-response-trace.ts +147 -0
  115. package/src/serve/request-plan.ts +158 -0
  116. package/src/serve/static-routes.ts +101 -0
  117. package/src/server.ts +704 -1400
  118. package/src/spec/dflash-source.ts +2 -1
  119. package/src/spec/dspark/module-dflash.ts +1 -1
  120. package/src/spec/glm52-mtp-source.ts +8 -0
  121. package/src/spec/qwen-mtp-source.ts +431 -0
  122. package/src/spec/serve-loop.ts +36 -68
  123. package/src/ssd-cache.ts +17 -14
  124. package/src/ssd-durability.ts +195 -0
  125. package/src/train/flash-cce.ts +12 -11
  126. package/src/train/forward.ts +32 -1
  127. package/src/train/loss.ts +2 -1
  128. package/src/train/segmented.ts +4 -3
  129. package/src/train/trainer.ts +3 -2
  130. package/src/vision/preprocess.ts +1 -1
  131. package/src/vision/prompt.ts +47 -1
  132. package/src/vision/qwen3vl-preprocess.ts +409 -0
  133. package/src/vision/qwen3vl-prompt.ts +196 -0
  134. package/src/vision/qwen3vl-tower.ts +513 -0
  135. package/src/vision/video-frames.ts +148 -0
  136. package/src/web/app.js +10 -3
  137. package/src/web/src/chat.ts +12 -5
  138. package/src/weights.ts +81 -26
  139. /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
  140. /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
  141. /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
  142. /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
  143. /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
  144. /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
  145. /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
  146. /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
  147. /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
  148. /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
  149. /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
  150. /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
  151. /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
  152. /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
  153. /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
  154. /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
  155. /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
  156. /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
  157. /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
  158. /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
  159. /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
  160. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
  161. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
  162. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
  163. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
  164. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
  165. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
  166. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
  167. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
  168. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
  169. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
  170. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
  171. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
  172. /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
  173. /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
  174. /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
  175. /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
  176. /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
  177. /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
  178. /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
  179. /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
  180. /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
  181. /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
  182. /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
  183. /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
  184. /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
  185. /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
  186. /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
  187. /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
  188. /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
  189. /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
  190. /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
  191. /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
  192. /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
  193. /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
  194. /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
  195. /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
  196. /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
  197. /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
  198. /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
  199. /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
  200. /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
  201. /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
  202. /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
  203. /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
  204. /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
  205. /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
  206. /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
  207. /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
  208. /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
  209. /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
  210. /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
  211. /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
  212. /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
  213. /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
  214. /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
  215. /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
  216. /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
  217. /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
  218. /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
  219. /package/docs/{planning → archive/planning}/journal.md +0 -0
  220. /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
  221. /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
  222. /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
  223. /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
  224. /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
package/src/kv-store.ts CHANGED
@@ -46,7 +46,7 @@ import { MlxArray } from "./mlx/array";
46
46
  import type { Dtype } from "./mlx/ffi";
47
47
  import * as ops from "./mlx/ops";
48
48
  import {
49
- type Cache, KVCache, RotatingKVCache,
49
+ type Cache, cacheSignature, KVCache, RotatingKVCache,
50
50
  QuantizedKVCache, RotatingQuantizedKVCache,
51
51
  TurboQuantKVCache, type TurboQuantTensor,
52
52
  } from "./model/gemma4-base";
@@ -142,6 +142,331 @@ const alignUp = (n: number) => Math.ceil(n / ALIGN) * ALIGN;
142
142
  * as-laid-out with ringIdx. Slice handles are lazy graph nodes (no GPU
143
143
  * materialization until rawBytes at write time). */
144
144
  interface TensorSource { arr: MlxArray; disposeAfter: boolean }
145
+
146
+ interface SnapshotContext {
147
+ slots: TensorSlot[];
148
+ push(a: MlxArray, disposeAfter: boolean): void;
149
+ liveSlice(a: MlxArray, upTo: number): MlxArray;
150
+ liveMlaSlice(a: MlxArray, upTo: number): MlxArray;
151
+ pushTriple(t: ops.QuantizedTensor, upTo: number | null): void;
152
+ }
153
+
154
+ interface CloneContext {
155
+ view(a: MlxArray): MlxArray;
156
+ liveView(a: MlxArray, upTo: number): MlxArray;
157
+ mlaView(a: MlxArray, upTo: number): MlxArray;
158
+ tripleView(t: ops.QuantizedTensor, upTo: number | null): ops.QuantizedTensor;
159
+ }
160
+
161
+ interface LoadContext {
162
+ path: string;
163
+ arr(slot: TensorSlot): MlxArray;
164
+ grownArr(slot: TensorSlot, offset: number): MlxArray;
165
+ triple(slots: TensorSlot[], at: number): ops.QuantizedTensor;
166
+ }
167
+
168
+ interface CacheCodec {
169
+ matches(cache: Cache): boolean;
170
+ snapshot(cache: Cache, context: SnapshotContext): CacheHeaderEntry;
171
+ clone(cache: Cache, context: CloneContext): Cache;
172
+ load(entry: CacheHeaderEntry, context: LoadContext): Cache;
173
+ headerTrimmable(entry: CacheHeaderEntry): boolean;
174
+ }
175
+
176
+ const requireMlaState = (cache: Glm52Cache, operation: string): void => {
177
+ if (!cache.latent || !cache.rope || cache.batchSize === null || cache.offset === 0)
178
+ throw new Error(`cannot ${operation} an empty GLM compressed cache`);
179
+ if (cache.dsa && (!cache.dsa.data || cache.dsa.offset !== cache.offset))
180
+ throw new Error(`cannot ${operation} misaligned GLM DSA state`);
181
+ };
182
+
183
+ const snapshotMla = (
184
+ kind: Extract<CacheKind, "mla" | "mla-dsa" | "mtp-mla">,
185
+ cache: Cache,
186
+ context: SnapshotContext,
187
+ ): CacheHeaderEntry => {
188
+ const c = cache as Glm52Cache;
189
+ requireMlaState(c, "persist");
190
+ context.push(context.liveMlaSlice(c.latent!, c.offset), true);
191
+ context.push(context.liveMlaSlice(c.rope!, c.offset), true);
192
+ if (c.dsa) context.push(context.liveMlaSlice(c.dsa.data!, c.offset), true);
193
+ return {
194
+ kind,
195
+ offset: c.offset,
196
+ kvLoraRank: c.kvLoraRank,
197
+ ropeHeadDim: c.ropeHeadDim,
198
+ ...(c.dsa ? { dsaHeadDim: c.dsa.headDim } : {}),
199
+ maxTokens: c.maxTokens,
200
+ tensors: context.slots,
201
+ };
202
+ };
203
+
204
+ const cloneMla = (cache: Cache, context: CloneContext): Cache => {
205
+ const c = cache as Glm52Cache;
206
+ requireMlaState(c, "clone");
207
+ const clone = new MLACache({
208
+ kvLoraRank: c.kvLoraRank,
209
+ ropeHeadDim: c.ropeHeadDim,
210
+ ...(c.dsa ? { dsa: { headDim: c.dsa.headDim } } : {}),
211
+ maxTokens: c.maxTokens,
212
+ role: c.role,
213
+ });
214
+ clone.restoreCompressedState(
215
+ context.mlaView(c.latent!, c.offset),
216
+ context.mlaView(c.rope!, c.offset),
217
+ c.dsa ? context.mlaView(c.dsa.data!, c.offset) : null,
218
+ c.offset,
219
+ );
220
+ return clone;
221
+ };
222
+
223
+ const loadMla = (entry: CacheHeaderEntry, context: LoadContext): Cache => {
224
+ const hasDsa = entry.kind === "mla-dsa";
225
+ if (entry.kind === "mtp-mla" && entry.dsaHeadDim !== undefined)
226
+ throw new Error(`${context.path}: native MTP cache cannot contain DSA state`);
227
+ const cache = new MLACache({
228
+ kvLoraRank: entry.kvLoraRank!,
229
+ ropeHeadDim: entry.ropeHeadDim!,
230
+ ...(hasDsa ? { dsa: { headDim: entry.dsaHeadDim! } } : {}),
231
+ maxTokens: entry.maxTokens,
232
+ role: entry.kind === "mtp-mla" ? "mtp" : "target",
233
+ });
234
+ cache.restoreCompressedState(
235
+ context.arr(entry.tensors[0]!),
236
+ context.arr(entry.tensors[1]!),
237
+ hasDsa ? context.arr(entry.tensors[2]!) : null,
238
+ entry.offset,
239
+ );
240
+ return cache;
241
+ };
242
+
243
+ /** One authoritative persistence algebra. Adding a cache storage kind must
244
+ * define all four operations here; save/clone/load and SSD eligibility
245
+ * therefore cannot grow independent instanceof/switch chains. */
246
+ const CACHE_CODECS = {
247
+ kv: {
248
+ matches: (cache) => cacheSignature(cache) === "kv:plain",
249
+ snapshot: (cache, context) => {
250
+ const c = cache as KVCache;
251
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
252
+ context.push(context.liveSlice(c.keys, c.offset), true);
253
+ context.push(context.liveSlice(c.values, c.offset), true);
254
+ return { kind: "kv", offset: c.offset, tensors: context.slots };
255
+ },
256
+ clone: (cache, context) => {
257
+ const c = cache as KVCache;
258
+ if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
259
+ const clone = new KVCache();
260
+ clone.restoreState(
261
+ context.liveView(c.keys, c.offset),
262
+ context.liveView(c.values, c.offset),
263
+ c.offset,
264
+ );
265
+ return clone;
266
+ },
267
+ load: (entry, context) => {
268
+ const cache = new KVCache();
269
+ cache.restoreState(
270
+ context.grownArr(entry.tensors[0]!, entry.offset),
271
+ context.grownArr(entry.tensors[1]!, entry.offset),
272
+ entry.offset,
273
+ );
274
+ return cache;
275
+ },
276
+ headerTrimmable: () => true,
277
+ },
278
+ rotating: {
279
+ matches: (cache) => cacheSignature(cache) === "kv:rotating-plain",
280
+ snapshot: (cache, context) => {
281
+ const c = cache as RotatingKVCache;
282
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
283
+ context.push(c.keys, false);
284
+ context.push(c.values, false);
285
+ return { kind: "rotating", offset: c.offset, idx: c.ringIdx,
286
+ maxSize: c.maxSize, tensors: context.slots };
287
+ },
288
+ clone: (cache, context) => {
289
+ const c = cache as RotatingKVCache;
290
+ if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
291
+ const clone = new RotatingKVCache(c.maxSize);
292
+ clone.restoreState(context.view(c.keys), context.view(c.values), c.offset, c.ringIdx);
293
+ return clone;
294
+ },
295
+ load: (entry, context) => {
296
+ const cache = new RotatingKVCache(entry.maxSize!);
297
+ cache.restoreState(
298
+ context.arr(entry.tensors[0]!), context.arr(entry.tensors[1]!),
299
+ entry.offset, entry.idx!,
300
+ );
301
+ return cache;
302
+ },
303
+ headerTrimmable: (entry) => entry.offset < (entry.maxSize ?? 0),
304
+ },
305
+ qkv: {
306
+ matches: (cache) => cacheSignature(cache).startsWith("kv:quant:"),
307
+ snapshot: (cache, context) => {
308
+ const c = cache as QuantizedKVCache;
309
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
310
+ context.pushTriple(c.keys, c.offset);
311
+ context.pushTriple(c.values, c.offset);
312
+ return { kind: "qkv", offset: c.offset, groupSize: c.groupSize,
313
+ bits: c.bits, tensors: context.slots };
314
+ },
315
+ clone: (cache, context) => {
316
+ const c = cache as QuantizedKVCache;
317
+ if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
318
+ const clone = new QuantizedKVCache(c.groupSize, c.bits);
319
+ clone.restoreState(
320
+ context.tripleView(c.keys, c.offset),
321
+ context.tripleView(c.values, c.offset),
322
+ c.offset,
323
+ );
324
+ return clone;
325
+ },
326
+ load: (entry, context) => {
327
+ const cache = new QuantizedKVCache(entry.groupSize!, entry.bits!);
328
+ cache.restoreState(
329
+ context.triple(entry.tensors, 0), context.triple(entry.tensors, 3), entry.offset,
330
+ );
331
+ return cache;
332
+ },
333
+ headerTrimmable: () => true,
334
+ },
335
+ "rotating-qkv": {
336
+ matches: (cache) => cacheSignature(cache).startsWith("kv:rotating-quant:"),
337
+ snapshot: (cache, context) => {
338
+ const c = cache as RotatingQuantizedKVCache;
339
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
340
+ context.pushTriple(c.keys, null);
341
+ context.pushTriple(c.values, null);
342
+ return { kind: "rotating-qkv", offset: c.offset, idx: c.ringIdx,
343
+ maxSize: c.maxSize, groupSize: c.groupSize, bits: c.bits,
344
+ tensors: context.slots };
345
+ },
346
+ clone: (cache, context) => {
347
+ const c = cache as RotatingQuantizedKVCache;
348
+ if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
349
+ const clone = new RotatingQuantizedKVCache(c.maxSize, c.groupSize, c.bits);
350
+ clone.restoreState(
351
+ context.tripleView(c.keys, null), context.tripleView(c.values, null),
352
+ c.offset, c.ringIdx,
353
+ );
354
+ return clone;
355
+ },
356
+ load: (entry, context) => {
357
+ const cache = new RotatingQuantizedKVCache(
358
+ entry.maxSize!, entry.groupSize!, entry.bits!,
359
+ );
360
+ cache.restoreState(
361
+ context.triple(entry.tensors, 0), context.triple(entry.tensors, 3),
362
+ entry.offset, entry.idx!,
363
+ );
364
+ return cache;
365
+ },
366
+ headerTrimmable: (entry) => entry.offset < (entry.maxSize ?? 0),
367
+ },
368
+ ssm: {
369
+ matches: (cache) => cacheSignature(cache) === "ssm",
370
+ snapshot: (cache, context) => {
371
+ const c = cache as SSMCache;
372
+ if (!c.conv || !c.recurrent) throw new Error("cannot persist an empty cache");
373
+ context.push(c.conv, false);
374
+ context.push(c.recurrent, false);
375
+ return { kind: "ssm", offset: c.offset, tensors: context.slots };
376
+ },
377
+ clone: (cache, context) => {
378
+ const c = cache as SSMCache;
379
+ if (!c.conv || !c.recurrent) throw new Error("cannot clone an empty cache");
380
+ const clone = new SSMCache();
381
+ clone.conv = context.view(c.conv);
382
+ clone.recurrent = context.view(c.recurrent);
383
+ clone.offset = c.offset;
384
+ return clone;
385
+ },
386
+ load: (entry, context) => {
387
+ const cache = new SSMCache();
388
+ cache.conv = context.arr(entry.tensors[0]!);
389
+ cache.recurrent = context.arr(entry.tensors[1]!);
390
+ cache.offset = entry.offset;
391
+ return cache;
392
+ },
393
+ headerTrimmable: () => false,
394
+ },
395
+ turboquant: {
396
+ matches: (cache) => cacheSignature(cache).startsWith("kv:turboquant:"),
397
+ snapshot: (cache, context) => {
398
+ const c = cache as TurboQuantKVCache;
399
+ const state = c.state();
400
+ if (state.length === 0) throw new Error("cannot persist an empty cache");
401
+ for (const array of state) context.push(array, true);
402
+ return { kind: "turboquant", offset: c.offset, kBits: c.kBits,
403
+ vBits: c.vBits, headDim: c.headDim ?? 0, tensors: context.slots };
404
+ },
405
+ clone: (cache, context) => {
406
+ const c = cache as TurboQuantKVCache;
407
+ const state = c.state();
408
+ if (state.length === 0) throw new Error("cannot clone an empty cache");
409
+ try {
410
+ const [kIdx, kScales, kZeros, vPacked, vScales] = state;
411
+ const clone = new TurboQuantKVCache(c.kBits, c.vBits);
412
+ clone.restoreState({
413
+ kIdx: context.view(kIdx!),
414
+ kScales: context.view(kScales!),
415
+ kZeros: context.view(kZeros!),
416
+ vPacked: context.view(vPacked!),
417
+ vScales: context.view(vScales!),
418
+ }, c.offset, c.headDim!);
419
+ return clone;
420
+ } finally {
421
+ for (const array of state) array.dispose();
422
+ }
423
+ },
424
+ load: (entry, context) => {
425
+ const cache = new TurboQuantKVCache(entry.kBits!, entry.vBits!);
426
+ const tensors: TurboQuantTensor = {
427
+ kIdx: context.arr(entry.tensors[0]!),
428
+ kScales: context.arr(entry.tensors[1]!),
429
+ kZeros: context.arr(entry.tensors[2]!),
430
+ vPacked: context.arr(entry.tensors[3]!),
431
+ vScales: context.arr(entry.tensors[4]!),
432
+ };
433
+ cache.restoreState(tensors, entry.offset, entry.headDim!);
434
+ return cache;
435
+ },
436
+ headerTrimmable: () => true,
437
+ },
438
+ mla: {
439
+ matches: (cache) => cacheSignature(cache) === "kv:mla:target",
440
+ snapshot: (cache, context) => snapshotMla("mla", cache, context),
441
+ clone: cloneMla,
442
+ load: loadMla,
443
+ headerTrimmable: () => true,
444
+ },
445
+ "mla-dsa": {
446
+ matches: (cache) => cacheSignature(cache) === "kv:mla:target:dsa",
447
+ snapshot: (cache, context) => snapshotMla("mla-dsa", cache, context),
448
+ clone: cloneMla,
449
+ load: loadMla,
450
+ headerTrimmable: () => true,
451
+ },
452
+ "mtp-mla": {
453
+ matches: (cache) => cacheSignature(cache) === "kv:mla:mtp",
454
+ snapshot: (cache, context) => snapshotMla("mtp-mla", cache, context),
455
+ clone: cloneMla,
456
+ load: loadMla,
457
+ headerTrimmable: () => true,
458
+ },
459
+ } satisfies Record<CacheKind, CacheCodec>;
460
+
461
+ const cacheCodec = (cache: Cache): CacheCodec => {
462
+ const codec = Object.values(CACHE_CODECS).find((candidate) => candidate.matches(cache));
463
+ if (!codec) throw new Error(`unsupported cache signature ${cacheSignature(cache)}`);
464
+ return codec;
465
+ };
466
+
467
+ export const cacheHeadersTrimmable = (caches: readonly CacheHeaderEntry[]): boolean =>
468
+ caches.every((entry) => CACHE_CODECS[entry.kind].headerTrimmable(entry));
469
+
145
470
  function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry; sources: TensorSource[]; next: number } {
146
471
  const slots: TensorSlot[] = [];
147
472
  const sources: TensorSource[] = [];
@@ -168,64 +493,9 @@ function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry;
168
493
  }
169
494
  };
170
495
 
171
- let entry: CacheHeaderEntry;
172
- if (c instanceof Glm52Cache) {
173
- if (!c.latent || !c.rope || c.batchSize === null || c.offset === 0)
174
- throw new Error("cannot persist an empty GLM compressed cache");
175
- push(liveMlaSlice(c.latent, c.offset), true);
176
- push(liveMlaSlice(c.rope, c.offset), true);
177
- if (c.dsa) {
178
- if (!c.dsa.data || c.dsa.offset !== c.offset)
179
- throw new Error("cannot persist misaligned GLM DSA state");
180
- push(liveMlaSlice(c.dsa.data, c.offset), true);
181
- }
182
- entry = {
183
- kind: c.role === "mtp" ? "mtp-mla" : c.dsa ? "mla-dsa" : "mla",
184
- offset: c.offset,
185
- kvLoraRank: c.kvLoraRank,
186
- ropeHeadDim: c.ropeHeadDim,
187
- ...(c.dsa ? { dsaHeadDim: c.dsa.headDim } : {}),
188
- maxTokens: c.maxTokens,
189
- tensors: slots,
190
- };
191
- } else if (c instanceof TurboQuantKVCache) {
192
- const t = c.state();
193
- if (t.length === 0) throw new Error("cannot persist an empty cache");
194
- const [kIdx, kScales, kZeros, vPacked, vScales] = t as [MlxArray, MlxArray, MlxArray, MlxArray, MlxArray];
195
- // state() already trims to offset — dispose these lazy slice views
196
- // once written (same disposeAfter contract as liveSlice elsewhere).
197
- for (const a of [kIdx, kScales, kZeros, vPacked, vScales]) push(a, true);
198
- entry = { kind: "turboquant", offset: c.offset, kBits: c.kBits, vBits: c.vBits,
199
- headDim: c.headDim ?? 0, tensors: slots };
200
- } else if (c instanceof RotatingQuantizedKVCache) {
201
- if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
202
- pushTriple(c.keys, null);
203
- pushTriple(c.values, null);
204
- entry = { kind: "rotating-qkv", offset: c.offset, idx: c.ringIdx, maxSize: c.maxSize,
205
- groupSize: c.groupSize, bits: c.bits, tensors: slots };
206
- } else if (c instanceof QuantizedKVCache) {
207
- if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
208
- pushTriple(c.keys, c.offset);
209
- pushTriple(c.values, c.offset);
210
- entry = { kind: "qkv", offset: c.offset, groupSize: c.groupSize, bits: c.bits, tensors: slots };
211
- } else if (c instanceof RotatingKVCache) {
212
- if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
213
- push(c.keys, false);
214
- push(c.values, false);
215
- entry = { kind: "rotating", offset: c.offset, idx: c.ringIdx, maxSize: c.maxSize, tensors: slots };
216
- } else if (c instanceof SSMCache) {
217
- if (!c.conv || !c.recurrent) throw new Error("cannot persist an empty cache");
218
- push(c.conv, false);
219
- push(c.recurrent, false);
220
- entry = { kind: "ssm", offset: c.offset, tensors: slots };
221
- } else if (c instanceof KVCache) {
222
- if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
223
- push(liveSlice(c.keys, c.offset), true);
224
- push(liveSlice(c.values, c.offset), true);
225
- entry = { kind: "kv", offset: c.offset, tensors: slots };
226
- } else {
227
- throw new Error("unknown cache type");
228
- }
496
+ const entry = cacheCodec(c).snapshot(c, {
497
+ slots, push, liveSlice, liveMlaSlice, pushTriple,
498
+ });
229
499
  return { entry, sources, next: cursor };
230
500
  }
231
501
 
@@ -261,65 +531,7 @@ export function cloneKvCaches(caches: Cache[]): Cache[] {
261
531
  const out: Cache[] = [];
262
532
  try {
263
533
  for (const c of caches) {
264
- if (c instanceof Glm52Cache) {
265
- if (!c.latent || !c.rope || c.batchSize === null || c.offset === 0)
266
- throw new Error("cannot clone an empty GLM compressed cache");
267
- const n = new MLACache({
268
- kvLoraRank: c.kvLoraRank,
269
- ropeHeadDim: c.ropeHeadDim,
270
- ...(c.dsa ? { dsa: { headDim: c.dsa.headDim } } : {}),
271
- maxTokens: c.maxTokens,
272
- role: c.role,
273
- });
274
- n.restoreCompressedState(
275
- mlaView(c.latent, c.offset),
276
- mlaView(c.rope, c.offset),
277
- c.dsa ? mlaView(c.dsa.data!, c.offset) : null,
278
- c.offset,
279
- );
280
- out.push(n);
281
- } else if (c instanceof TurboQuantKVCache) {
282
- const state = c.state();
283
- if (state.length === 0) throw new Error("cannot clone an empty cache");
284
- const [kIdx, kScales, kZeros, vPacked, vScales] = state;
285
- const n = new TurboQuantKVCache(c.kBits, c.vBits);
286
- n.restoreState(
287
- { kIdx: view(kIdx!), kScales: view(kScales!), kZeros: view(kZeros!),
288
- vPacked: view(vPacked!), vScales: view(vScales!) },
289
- c.offset, c.headDim!,
290
- );
291
- for (const a of state) a.dispose();
292
- out.push(n);
293
- } else if (c instanceof RotatingQuantizedKVCache) {
294
- if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
295
- const n = new RotatingQuantizedKVCache(c.maxSize, c.groupSize, c.bits);
296
- n.restoreState(tripleView(c.keys, null), tripleView(c.values, null), c.offset, c.ringIdx);
297
- out.push(n);
298
- } else if (c instanceof QuantizedKVCache) {
299
- if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
300
- const n = new QuantizedKVCache(c.groupSize, c.bits);
301
- n.restoreState(tripleView(c.keys, c.offset), tripleView(c.values, c.offset), c.offset);
302
- out.push(n);
303
- } else if (c instanceof RotatingKVCache) {
304
- if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
305
- const n = new RotatingKVCache(c.maxSize);
306
- n.restoreState(view(c.keys), view(c.values), c.offset, c.ringIdx);
307
- out.push(n);
308
- } else if (c instanceof SSMCache) {
309
- if (!c.conv || !c.recurrent) throw new Error("cannot clone an empty cache");
310
- const n = new SSMCache();
311
- n.conv = view(c.conv);
312
- n.recurrent = view(c.recurrent);
313
- n.offset = c.offset;
314
- out.push(n);
315
- } else if (c instanceof KVCache) {
316
- if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
317
- const n = new KVCache();
318
- n.restoreState(liveView(c.keys, c.offset), liveView(c.values, c.offset), c.offset);
319
- out.push(n);
320
- } else {
321
- throw new Error("cloneKvCaches: unknown cache type");
322
- }
534
+ out.push(cacheCodec(c).clone(c, { view, liveView, mlaView, tripleView }));
323
535
  }
324
536
  } catch (err) {
325
537
  for (const c of out) c.dispose();
@@ -468,12 +680,17 @@ export interface SpillItem {
468
680
  * Items run strictly serially in enqueue order via an internal chain;
469
681
  * store failures are swallowed (cold tier is best-effort) but clones
470
682
  * are disposed on every settle path. */
471
- type SpillRec = SpillItem & { bytes: number; dropped: boolean };
683
+ type SpillRec = SpillItem & {
684
+ bytes: number;
685
+ dropped: boolean;
686
+ settle: (stored: boolean) => void;
687
+ };
472
688
 
473
689
  export class SpillQueue {
474
690
  #queue: SpillRec[] = [];
475
691
  #bytes = 0;
476
692
  #dropped = 0;
693
+ #failed = 0;
477
694
  #inFlight: object | null = null;
478
695
  #chain: Promise<void> = Promise.resolve();
479
696
 
@@ -492,9 +709,18 @@ export class SpillQueue {
492
709
  get pendingCount(): number { return this.#queue.length; }
493
710
  /** Spills dropped by the cap since start (each = one future cache miss). */
494
711
  get droppedCount(): number { return this.#dropped; }
495
-
496
- enqueue(item: SpillItem): void {
497
- const rec = { ...item, bytes: this.bytesOf(item.caches), dropped: false };
712
+ /** Store calls that threw or returned false. */
713
+ get failedCount(): number { return this.#failed; }
714
+
715
+ enqueue(item: SpillItem): Promise<boolean> {
716
+ let settle!: (stored: boolean) => void;
717
+ const result = new Promise<boolean>((resolve) => { settle = resolve; });
718
+ const rec: SpillRec = {
719
+ ...item,
720
+ bytes: this.bytesOf(item.caches),
721
+ dropped: false,
722
+ settle,
723
+ };
498
724
  this.#queue.push(rec);
499
725
  this.#bytes += rec.bytes;
500
726
  while (this.#bytes > this.capBytes) {
@@ -508,24 +734,30 @@ export class SpillQueue {
508
734
  if (rec.dropped) return; // clones already disposed at drop time
509
735
  this.#inFlight = rec;
510
736
  try {
511
- await this.store(rec);
737
+ const stored = await this.store(rec);
738
+ if (stored === false) this.#failed++;
739
+ rec.settle(stored !== false);
512
740
  } catch {
513
741
  // best-effort tier — the entry simply won't restore
742
+ this.#failed++;
743
+ rec.settle(false);
514
744
  } finally {
515
745
  this.#inFlight = null;
516
746
  this.#remove(rec);
517
747
  this.disposeClones(rec.caches);
518
748
  }
519
749
  });
750
+ return result;
520
751
  }
521
752
 
522
753
  /** Settles when everything enqueued so far has flushed or dropped
523
- * (tests; a future shutdown hook would await this). */
754
+ * (tests and graceful shutdown). */
524
755
  drain(): Promise<void> { return this.#chain; }
525
756
 
526
757
  #drop(rec: SpillRec): void {
527
758
  rec.dropped = true;
528
759
  this.#dropped++;
760
+ rec.settle(false);
529
761
  this.#remove(rec);
530
762
  this.disposeClones(rec.caches);
531
763
  }
@@ -592,24 +824,26 @@ function validateGlm52Prototype(
592
824
  ): void {
593
825
  const glmKind = entry.kind === "mla" || entry.kind === "mla-dsa" ||
594
826
  entry.kind === "mtp-mla";
595
- if (!(prototype instanceof Glm52Cache)) {
827
+ const prototypeSignature = cacheSignature(prototype);
828
+ if (!prototypeSignature.startsWith("kv:mla:")) {
596
829
  if (glmKind)
597
830
  throw new Error(`${path}: GLM cache kind ${entry.kind} does not match model cache`);
598
831
  return;
599
832
  }
600
- const expectedKind: CacheKind = prototype.role === "mtp"
833
+ const glmPrototype = prototype as Glm52Cache;
834
+ const expectedKind: CacheKind = glmPrototype.role === "mtp"
601
835
  ? "mtp-mla"
602
- : prototype.dsa ? "mla-dsa" : "mla";
836
+ : glmPrototype.dsa ? "mla-dsa" : "mla";
603
837
  if (entry.kind !== expectedKind) {
604
838
  throw new Error(
605
839
  `${path}: cache kind ${entry.kind} != model cache kind ${expectedKind}`,
606
840
  );
607
841
  }
608
842
  for (const [field, actual, expected] of [
609
- ["kvLoraRank", entry.kvLoraRank, prototype.kvLoraRank],
610
- ["ropeHeadDim", entry.ropeHeadDim, prototype.ropeHeadDim],
611
- ["dsaHeadDim", entry.dsaHeadDim, prototype.dsa?.headDim],
612
- ["maxTokens", entry.maxTokens, prototype.maxTokens],
843
+ ["kvLoraRank", entry.kvLoraRank, glmPrototype.kvLoraRank],
844
+ ["ropeHeadDim", entry.ropeHeadDim, glmPrototype.ropeHeadDim],
845
+ ["dsaHeadDim", entry.dsaHeadDim, glmPrototype.dsa?.headDim],
846
+ ["maxTokens", entry.maxTokens, glmPrototype.maxTokens],
613
847
  ] as const) {
614
848
  if (actual !== expected) {
615
849
  throw new Error(
@@ -617,7 +851,7 @@ function validateGlm52Prototype(
617
851
  );
618
852
  }
619
853
  }
620
- const expectedTensors = prototype.dsa ? 3 : 2;
854
+ const expectedTensors = glmPrototype.dsa ? 3 : 2;
621
855
  if (entry.tensors.length !== expectedTensors) {
622
856
  throw new Error(
623
857
  `${path}: ${entry.kind} has ${entry.tensors.length} tensors, ` +
@@ -732,79 +966,10 @@ export function loadKvCache(
732
966
  const caches: Cache[] = [];
733
967
  try {
734
968
  for (const e of header.caches) {
735
- const t = e.tensors;
736
- switch (e.kind) {
737
- case "kv": {
738
- const c = new KVCache();
739
- c.restoreState(
740
- grownArr(t[0]!, e.offset),
741
- grownArr(t[1]!, e.offset),
742
- e.offset,
743
- );
744
- caches.push(c);
745
- break;
746
- }
747
- case "rotating": {
748
- const c = new RotatingKVCache(e.maxSize!);
749
- c.restoreState(arr(t[0]!), arr(t[1]!), e.offset, e.idx!);
750
- caches.push(c);
751
- break;
752
- }
753
- case "qkv": {
754
- const c = new QuantizedKVCache(e.groupSize!, e.bits!);
755
- c.restoreState(triple(t, 0), triple(t, 3), e.offset);
756
- caches.push(c);
757
- break;
758
- }
759
- case "rotating-qkv": {
760
- const c = new RotatingQuantizedKVCache(e.maxSize!, e.groupSize!, e.bits!);
761
- c.restoreState(triple(t, 0), triple(t, 3), e.offset, e.idx!);
762
- caches.push(c);
763
- break;
764
- }
765
- case "ssm": {
766
- const c = new SSMCache();
767
- c.conv = arr(t[0]!);
768
- c.recurrent = arr(t[1]!);
769
- c.offset = e.offset;
770
- caches.push(c);
771
- break;
772
- }
773
- case "turboquant": {
774
- const c = new TurboQuantKVCache(e.kBits!, e.vBits!);
775
- const kv: TurboQuantTensor = {
776
- kIdx: arr(t[0]!), kScales: arr(t[1]!), kZeros: arr(t[2]!),
777
- vPacked: arr(t[3]!), vScales: arr(t[4]!),
778
- };
779
- c.restoreState(kv, e.offset, e.headDim!);
780
- caches.push(c);
781
- break;
782
- }
783
- case "mla":
784
- case "mla-dsa":
785
- case "mtp-mla": {
786
- const hasDsa = e.kind === "mla-dsa";
787
- if (e.kind === "mtp-mla" && e.dsaHeadDim !== undefined)
788
- throw new Error(`${path}: native MTP cache cannot contain DSA state`);
789
- const c = new MLACache({
790
- kvLoraRank: e.kvLoraRank!,
791
- ropeHeadDim: e.ropeHeadDim!,
792
- ...(hasDsa ? { dsa: { headDim: e.dsaHeadDim! } } : {}),
793
- maxTokens: e.maxTokens,
794
- role: e.kind === "mtp-mla" ? "mtp" : "target",
795
- });
796
- c.restoreCompressedState(
797
- arr(t[0]!),
798
- arr(t[1]!),
799
- hasDsa ? arr(t[2]!) : null,
800
- e.offset,
801
- );
802
- caches.push(c);
803
- break;
804
- }
805
- default:
806
- throw new Error(`${path}: unknown cache kind ${(e as { kind: string }).kind}`);
807
- }
969
+ const codec = CACHE_CODECS[e.kind];
970
+ if (!codec)
971
+ throw new Error(`${path}: unknown cache kind ${(e as { kind: string }).kind}`);
972
+ caches.push(codec.load(e, { path, arr, grownArr, triple }));
808
973
  // This entry's tensors are now owned by its cache — stop tracking them
809
974
  // (the catch must not double-free through both pending AND caches).
810
975
  pending.length = 0;