mlx-bun 0.4.0 → 1.0.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 (601) hide show
  1. package/README.md +1453 -88
  2. package/THIRD_PARTY_NOTICES.md +882 -0
  3. package/bin/mlx-bun.convert.mjs +4 -0
  4. package/bin/mlx-bun.fuse.mjs +4 -0
  5. package/bin/mlx-bun.generate.mjs +4 -0
  6. package/bin/mlx-bun.lora.mjs +4 -0
  7. package/bin/mlx-bun.mjs +13 -21
  8. package/bin/mlx-bun.server.mjs +4 -0
  9. package/bin/mlx-bun.upload.mjs +4 -0
  10. package/dist/web/app.js +9584 -0
  11. package/package.json +50 -53
  12. package/src/cli/ane-install.ts +76 -0
  13. package/src/cli/args.ts +179 -0
  14. package/src/cli/engine-entry.ts +68 -0
  15. package/src/cli/inference.ts +214 -0
  16. package/src/cli/job-entry.ts +97 -0
  17. package/src/cli/library-host.ts +117 -0
  18. package/src/cli/main.ts +57 -0
  19. package/src/cli/memory-engine.ts +163 -0
  20. package/src/cli/memory.ts +36 -0
  21. package/src/cli/mlx-lm-aliases.ts +309 -0
  22. package/src/cli/model-choice.ts +33 -0
  23. package/src/cli/model-selection.ts +84 -0
  24. package/src/cli/module-host.ts +11 -0
  25. package/src/cli/module-verbs.ts +50 -0
  26. package/src/cli/numerical-policy.ts +12 -0
  27. package/src/cli/perplexity.ts +89 -0
  28. package/src/cli/serve-host.ts +423 -0
  29. package/src/cli/serve-isolated.ts +217 -0
  30. package/src/cli/serve-options.ts +106 -0
  31. package/src/cli/serve-state.ts +205 -0
  32. package/src/cli/serve.ts +500 -0
  33. package/src/cli/served-model-host.ts +102 -0
  34. package/src/cli/server-entry.ts +148 -0
  35. package/src/cli/serving-unit.ts +177 -0
  36. package/src/{tui.ts → cli/terminal.ts} +13 -8
  37. package/src/cli/worker-adapters.ts +34 -0
  38. package/src/cli/worker-entry.ts +272 -0
  39. package/src/cli/worker-unit.ts +179 -0
  40. package/src/engine/adapter-operation.ts +26 -0
  41. package/src/engine/cache-services.ts +114 -0
  42. package/src/engine/completion.ts +39 -0
  43. package/src/engine/generation-gateway.ts +358 -0
  44. package/src/engine/index.ts +59 -0
  45. package/src/engine/kv-budget.ts +120 -0
  46. package/src/engine/media-preparation.ts +232 -0
  47. package/src/engine/model-binding.ts +24 -0
  48. package/src/engine/model-host.ts +513 -0
  49. package/src/engine/model-serving.ts +89 -0
  50. package/src/{serve → engine}/preparation.ts +10 -8
  51. package/src/{serve → engine}/session-completion-engine.ts +6 -6
  52. package/src/engine/telemetry.ts +109 -0
  53. package/src/hub/downloads.ts +116 -0
  54. package/src/jobs/db.ts +16 -7
  55. package/src/jobs/events.ts +29 -0
  56. package/src/jobs/executable.ts +5 -0
  57. package/src/jobs/host.ts +51 -0
  58. package/src/jobs/in-process.ts +55 -0
  59. package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
  60. package/src/jobs/runner.ts +150 -231
  61. package/src/jobs/service.ts +75 -0
  62. package/src/jobs/sse.ts +59 -23
  63. package/src/jobs/worker-memory.ts +46 -0
  64. package/src/jobs/worker-process.ts +163 -0
  65. package/src/jobs/worker-supervisor.ts +258 -0
  66. package/src/modules.ts +58 -0
  67. package/src/publishing/catalog-hub.ts +18 -0
  68. package/src/publishing/credentials.ts +31 -0
  69. package/src/publishing/upload.ts +37 -0
  70. package/src/residency/leased-adapters.ts +21 -0
  71. package/src/residency/local-records.ts +16 -0
  72. package/src/residency/model-residency.ts +405 -0
  73. package/src/residency/record-index.ts +28 -0
  74. package/src/residency/resident-estimate.ts +50 -0
  75. package/src/{anthropic.ts → server/anthropic.ts} +3 -81
  76. package/src/server/cache-routes.ts +36 -0
  77. package/src/{serve → server}/chat-request.ts +12 -10
  78. package/src/{serve → server}/chat-stage.ts +27 -22
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +42 -42
  81. package/src/server/decision-body.ts +55 -0
  82. package/src/server/decision-routes.ts +134 -0
  83. package/src/server/decision-wire.ts +228 -0
  84. package/src/{serve → server}/discovery-routes.ts +42 -83
  85. package/src/server/generated-token-history.ts +57 -0
  86. package/src/{serve → server}/http.ts +25 -13
  87. package/src/{serve → server}/inference-request.ts +5 -5
  88. package/src/server/job-routes.ts +20 -0
  89. package/src/server/media-prompt.ts +63 -0
  90. package/src/server/memory-completion-client.ts +46 -0
  91. package/src/server/model-routes.ts +192 -0
  92. package/src/{serve → server}/openai-wire.ts +3 -2
  93. package/src/{serve → server}/pipeline.ts +1 -1
  94. package/src/server/prompt-builder.ts +13 -0
  95. package/src/server/prompt-contracts.ts +17 -0
  96. package/src/server/proxy-routes.ts +187 -0
  97. package/src/server/publishing-routes.ts +41 -0
  98. package/src/{serve → server}/request-plan.ts +8 -8
  99. package/src/{serve → server}/request-prep.ts +82 -68
  100. package/src/{serve → server}/responses-client.ts +30 -13
  101. package/src/{responses.ts → server/responses.ts} +12 -93
  102. package/src/server/routes.ts +205 -0
  103. package/src/server/start.ts +72 -0
  104. package/src/server/status-routes.ts +108 -0
  105. package/src/{serve → server}/text-completion-stage.ts +25 -4
  106. package/src/server/text-prompt.ts +12 -0
  107. package/src/{serve → server}/token-streams.ts +36 -38
  108. package/src/server/worker-routes.ts +311 -0
  109. package/src/storage/paths.ts +61 -0
  110. package/src/web/assets.ts +37 -0
  111. package/src/web/{src → browser}/api.ts +5 -23
  112. package/src/web/browser/chat-host.ts +17 -0
  113. package/src/web/browser/installed-panels.ts +6 -0
  114. package/src/web/browser/main.ts +55 -0
  115. package/src/web/browser/markdown.ts +279 -0
  116. package/src/web/browser/memory-panel.ts +9 -0
  117. package/src/web/browser/palette.ts +64 -0
  118. package/src/web/browser/protocol.ts +13 -0
  119. package/src/web/browser/shell.ts +467 -0
  120. package/src/web/{src → browser}/status.ts +32 -36
  121. package/src/web/build.ts +51 -0
  122. package/src/web/public/app.html +1054 -0
  123. package/src/web/{sw.js → public/sw.js} +4 -3
  124. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  125. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  126. package/THIRD_PARTY_LICENSES.md +0 -51
  127. package/docs/README.md +0 -94
  128. package/docs/archive/hlg-sampling.md +0 -350
  129. package/docs/archive/investigations/audio-input-plan.md +0 -356
  130. package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
  131. package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
  132. package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
  133. package/docs/archive/investigations/curve-bisector-routing.md +0 -123
  134. package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
  135. package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
  136. package/docs/archive/investigations/dspark-handoff.md +0 -224
  137. package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
  138. package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
  139. package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
  140. package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
  141. package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
  142. package/docs/archive/investigations/lab-build-journal.md +0 -157
  143. package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
  144. package/docs/archive/investigations/openjarvis-fit.md +0 -198
  145. package/docs/archive/investigations/openwiki-evaluation.md +0 -249
  146. package/docs/archive/investigations/optimization_plan.md +0 -346
  147. package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
  148. package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
  149. package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
  150. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
  151. package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
  152. package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
  153. package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
  154. package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
  155. package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
  156. package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
  157. package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
  158. package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
  159. package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
  160. package/docs/archive/planning/chat-ui-vision.md +0 -50
  161. package/docs/archive/planning/chunk-task-roadmap.md +0 -77
  162. package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
  163. package/docs/archive/planning/journal.md +0 -316
  164. package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
  165. package/docs/archive/planning/pi-integration-review.md +0 -178
  166. package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
  167. package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
  168. package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
  169. package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
  170. package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
  171. package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
  172. package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
  173. package/docs/archive/planning/web-ui-pass-plan.md +0 -213
  174. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  175. package/docs/design/batching.md +0 -1841
  176. package/docs/design/decode-speed-program.md +0 -2349
  177. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  178. package/docs/design/generic-model-support.md +0 -705
  179. package/docs/design/kv-cache.md +0 -824
  180. package/docs/design/orpo-training.md +0 -680
  181. package/docs/design/speculative-decoding.md +0 -1990
  182. package/docs/design/turboquant.md +0 -2212
  183. package/docs/design/unified-engine-frontier-plan.md +0 -1880
  184. package/docs/design/web-chat-redesign.md +0 -695
  185. package/docs/planning/PRODUCT_ROADMAP.md +0 -594
  186. package/docs/planning/ResearchTopics.md +0 -119
  187. package/docs/planning/release-notes-v0.4.0.md +0 -45
  188. package/docs/planning/video-series.md +0 -421
  189. package/docs/reference/benchmarks.md +0 -3613
  190. package/docs/reference/cli.md +0 -585
  191. package/docs/reference/distribution.md +0 -355
  192. package/docs/reference/environment.md +0 -398
  193. package/docs/reference/glossary.md +0 -39
  194. package/docs/reference/library-api.md +0 -674
  195. package/docs/reference/memory.md +0 -260
  196. package/docs/reference/models.md +0 -334
  197. package/docs/reference/server-api.md +0 -1775
  198. package/docs/reference/server-config.md +0 -846
  199. package/docs/reference/training.md +0 -462
  200. package/docs/reference/troubleshooting.md +0 -158
  201. package/src/admission-plan.ts +0 -18
  202. package/src/audio/conformer.ts +0 -647
  203. package/src/audio/decode.ts +0 -133
  204. package/src/audio/features.ts +0 -202
  205. package/src/audio/transcode.ts +0 -67
  206. package/src/backends/mlx/assistant-target.ts +0 -46
  207. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  208. package/src/backends/mlx/autoregressive.ts +0 -108
  209. package/src/backends/mlx/batch-group.ts +0 -1790
  210. package/src/backends/mlx/cache-attachment.ts +0 -67
  211. package/src/backends/mlx/cache-layout.ts +0 -31
  212. package/src/backends/mlx/checkpoint-state.ts +0 -53
  213. package/src/backends/mlx/continuation-persistence.ts +0 -106
  214. package/src/backends/mlx/continuation-request.ts +0 -43
  215. package/src/backends/mlx/continuation.ts +0 -77
  216. package/src/backends/mlx/diffusion.ts +0 -39
  217. package/src/backends/mlx/draft-target.ts +0 -32
  218. package/src/backends/mlx/evaluation.ts +0 -105
  219. package/src/backends/mlx/fill-append.ts +0 -45
  220. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  221. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  222. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  223. package/src/backends/mlx/gateway-binding.ts +0 -130
  224. package/src/backends/mlx/graph.ts +0 -38
  225. package/src/backends/mlx/kv-codec.ts +0 -9
  226. package/src/backends/mlx/kv-maintenance.ts +0 -133
  227. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  228. package/src/backends/mlx/memory-client.ts +0 -189
  229. package/src/backends/mlx/methods.ts +0 -90
  230. package/src/backends/mlx/model-prompt.ts +0 -164
  231. package/src/backends/mlx/model-serving.ts +0 -79
  232. package/src/backends/mlx/prefill-cohort.ts +0 -89
  233. package/src/backends/mlx/prefill-rows.ts +0 -200
  234. package/src/backends/mlx/prefill.ts +0 -33
  235. package/src/backends/mlx/projected-context-rows.ts +0 -72
  236. package/src/backends/mlx/request-policy.ts +0 -20
  237. package/src/backends/mlx/request-state-policy.ts +0 -41
  238. package/src/backends/mlx/rollback.ts +0 -89
  239. package/src/backends/mlx/row-sampling.ts +0 -49
  240. package/src/backends/mlx/serial-executor.ts +0 -242
  241. package/src/backends/mlx/speculative-group.ts +0 -359
  242. package/src/backends/mlx/speculative-round.ts +0 -161
  243. package/src/backends/mlx/speculative.ts +0 -114
  244. package/src/backends/mlx/state-rows.ts +0 -104
  245. package/src/backends/mlx/state-views.ts +0 -46
  246. package/src/backends/mlx/target-layout-capability.ts +0 -35
  247. package/src/backends/mlx/text-engine.ts +0 -73
  248. package/src/backends/mlx/transitioning-kv-rows.ts +0 -165
  249. package/src/chat-template.ts +0 -382
  250. package/src/cli.ts +0 -3168
  251. package/src/config.ts +0 -399
  252. package/src/contracts/completion.ts +0 -16
  253. package/src/contracts/execution.ts +0 -60
  254. package/src/contracts/generation.ts +0 -103
  255. package/src/contracts/host.ts +0 -7
  256. package/src/contracts/pi-provider.ts +0 -30
  257. package/src/contracts/pi.ts +0 -259
  258. package/src/contracts/prefix-cache.ts +0 -23
  259. package/src/contracts/resources.ts +0 -25
  260. package/src/contracts/sampling.ts +0 -23
  261. package/src/contracts/scheduling.ts +0 -45
  262. package/src/contracts/tsconfig.json +0 -14
  263. package/src/dataset/generators.ts +0 -1087
  264. package/src/dataset/index.ts +0 -12
  265. package/src/dataset/job.ts +0 -81
  266. package/src/dataset/llm.ts +0 -71
  267. package/src/dataset/registry.ts +0 -528
  268. package/src/diffusion/diffusion-generate.ts +0 -507
  269. package/src/download.ts +0 -505
  270. package/src/embed.ts +0 -77
  271. package/src/engine/admission.ts +0 -59
  272. package/src/engine/cancellation.ts +0 -38
  273. package/src/engine/engine.ts +0 -56
  274. package/src/engine/execution-coordinator.ts +0 -70
  275. package/src/engine/execution-plan.ts +0 -53
  276. package/src/engine/resources.ts +0 -53
  277. package/src/engine/scheduler.ts +0 -72
  278. package/src/engine/session.ts +0 -247
  279. package/src/eval/README.md +0 -130
  280. package/src/eval/ifeval.ts +0 -81
  281. package/src/eval/kl-prompts.ts +0 -29
  282. package/src/eval/kl.ts +0 -590
  283. package/src/eval/perplexity.ts +0 -164
  284. package/src/eval/quality-db.ts +0 -92
  285. package/src/eval/reference-logits.ts +0 -207
  286. package/src/eval/runner.ts +0 -124
  287. package/src/eval/score.ts +0 -49
  288. package/src/eval/tasks/bfcl.ts +0 -535
  289. package/src/eval/tasks/gsm8k.ts +0 -98
  290. package/src/eval/tasks/hashhop.ts +0 -139
  291. package/src/eval/tasks/humaneval.ts +0 -275
  292. package/src/eval/tasks/ifeval.ts +0 -525
  293. package/src/eval/tasks/mmlu.ts +0 -157
  294. package/src/eval/ultrafeedback.ts +0 -60
  295. package/src/evaldb.ts +0 -199
  296. package/src/expert-io.ts +0 -393
  297. package/src/expert-offload-build.ts +0 -77
  298. package/src/expert-offload.ts +0 -76
  299. package/src/expert-residency.ts +0 -1134
  300. package/src/expert-usage.ts +0 -415
  301. package/src/fill/echo-index.ts +0 -188
  302. package/src/fill/fill-session.ts +0 -452
  303. package/src/fill/proposal.ts +0 -68
  304. package/src/fill/schema-rows.ts +0 -506
  305. package/src/fill/tool-boundary.ts +0 -103
  306. package/src/fit.ts +0 -249
  307. package/src/generate.ts +0 -1346
  308. package/src/grammar.ts +0 -591
  309. package/src/harness-pi.ts +0 -184
  310. package/src/hf-push.ts +0 -628
  311. package/src/hub-rest.ts +0 -242
  312. package/src/index.ts +0 -63
  313. package/src/inference/denoising.ts +0 -17
  314. package/src/inference/draft-acceptance.ts +0 -41
  315. package/src/inference/draft-output.ts +0 -49
  316. package/src/inference/graph.ts +0 -33
  317. package/src/inference/prefill.ts +0 -39
  318. package/src/inference/rollback.ts +0 -7
  319. package/src/jobs/index.ts +0 -31
  320. package/src/jobs/job-entry.ts +0 -107
  321. package/src/jobs/task-client.ts +0 -19
  322. package/src/jobs/types.ts +0 -2
  323. package/src/kv-scheme.ts +0 -257
  324. package/src/kv-store.ts +0 -1099
  325. package/src/lab/README.md +0 -18
  326. package/src/lab/curve/curve-designer.html +0 -303
  327. package/src/lab/curve/curve-sampler.ts +0 -122
  328. package/src/lab/expert-trace/expert-trace.ts +0 -69
  329. package/src/lab/paged-kv/paged-kv-rows.ts +0 -83
  330. package/src/lab/paged-kv/paged-kv.ts +0 -270
  331. package/src/library.ts +0 -40
  332. package/src/lora.ts +0 -408
  333. package/src/md-modules.d.ts +0 -49
  334. package/src/media-fetch.ts +0 -442
  335. package/src/memory/article.ts +0 -558
  336. package/src/memory/chunk-validation.ts +0 -150
  337. package/src/memory/chunk.ts +0 -318
  338. package/src/memory/cluster.ts +0 -351
  339. package/src/memory/coverage.ts +0 -134
  340. package/src/memory/crosslink.ts +0 -0
  341. package/src/memory/db.ts +0 -253
  342. package/src/memory/entity.ts +0 -203
  343. package/src/memory/gate.ts +0 -187
  344. package/src/memory/history.ts +0 -152
  345. package/src/memory/ingest.ts +0 -335
  346. package/src/memory/model.ts +0 -183
  347. package/src/memory/normalize.ts +0 -671
  348. package/src/memory/parse.ts +0 -22
  349. package/src/memory/pipeline.ts +0 -275
  350. package/src/memory/prompts.ts +0 -65
  351. package/src/memory/query.ts +0 -334
  352. package/src/memory/reconcile.ts +0 -734
  353. package/src/memory/redirect.ts +0 -108
  354. package/src/memory/reindex.ts +0 -325
  355. package/src/memory/resolve.ts +0 -307
  356. package/src/memory/rest.ts +0 -245
  357. package/src/memory/route.ts +0 -272
  358. package/src/memory/schedule.ts +0 -165
  359. package/src/memory/stages.ts +0 -777
  360. package/src/memory/synthesize.ts +0 -1638
  361. package/src/memory/tools.ts +0 -749
  362. package/src/memory/vault.ts +0 -1194
  363. package/src/memory/wikify.ts +0 -1064
  364. package/src/mlx/array.ts +0 -388
  365. package/src/mlx/autograd.ts +0 -304
  366. package/src/mlx/checkpoint.ts +0 -149
  367. package/src/mlx/compile.ts +0 -168
  368. package/src/mlx/custom-vjp.ts +0 -188
  369. package/src/mlx/ffi.ts +0 -512
  370. package/src/mlx/materialize.ts +0 -23
  371. package/src/mlx/metal-kernel.ts +0 -159
  372. package/src/mlx/ops.ts +0 -1021
  373. package/src/mlx/quantized-rows.ts +0 -36
  374. package/src/mlx/token-bitmask.ts +0 -52
  375. package/src/mlx/turboquant-kv-decode.ts +0 -82
  376. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  377. package/src/mlx/turboquant-ops.ts +0 -861
  378. package/src/mlx/turboquant-tables.ts +0 -237
  379. package/src/mmap.ts +0 -90
  380. package/src/model/artifact-identity.ts +0 -73
  381. package/src/model/batched-kv.ts +0 -235
  382. package/src/model/batched-mask.ts +0 -279
  383. package/src/model/batched-quant.ts +0 -212
  384. package/src/model/batched-quantized-kv.ts +0 -144
  385. package/src/model/batched-rotating-quant.ts +0 -353
  386. package/src/model/batched-rotating-state.ts +0 -149
  387. package/src/model/batched-rotating.ts +0 -338
  388. package/src/model/batched-row-storage.ts +0 -179
  389. package/src/model/batched-ssm.ts +0 -52
  390. package/src/model/batched-turboquant-kv.ts +0 -93
  391. package/src/model/compiled-decode.ts +0 -712
  392. package/src/model/decoded-kv-donor.ts +0 -12
  393. package/src/model/delayed-quantized-kv.ts +0 -72
  394. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  395. package/src/model/delayed-turboquant-kv.ts +0 -55
  396. package/src/model/diffusion-gemma.ts +0 -1026
  397. package/src/model/factory.ts +0 -189
  398. package/src/model/fingerprint.ts +0 -33
  399. package/src/model/flash-attention.ts +0 -694
  400. package/src/model/full-prefill-padding.ts +0 -56
  401. package/src/model/full-prefill-row.ts +0 -105
  402. package/src/model/gemma4-base.ts +0 -2351
  403. package/src/model/gemma4.ts +0 -1315
  404. package/src/model/generated/gemma4-12b.ts +0 -429
  405. package/src/model/generated/gemma4-26b.ts +0 -448
  406. package/src/model/generated/gemma4-e4b.ts +0 -717
  407. package/src/model/generated/index.ts +0 -18
  408. package/src/model/glm52-atlas.ts +0 -511
  409. package/src/model/glm52-cache.ts +0 -792
  410. package/src/model/glm52-config.ts +0 -262
  411. package/src/model/glm52-container.ts +0 -305
  412. package/src/model/glm52-coupling.ts +0 -388
  413. package/src/model/glm52-dsa.ts +0 -606
  414. package/src/model/glm52-expert-layout.ts +0 -253
  415. package/src/model/glm52-layout.ts +0 -152
  416. package/src/model/glm52-memory.ts +0 -500
  417. package/src/model/glm52-mla.ts +0 -792
  418. package/src/model/glm52-moe.ts +0 -223
  419. package/src/model/glm52-pilot.ts +0 -537
  420. package/src/model/glm52-reference.ts +0 -445
  421. package/src/model/glm52-residency.ts +0 -557
  422. package/src/model/glm52-resident-weights.ts +0 -135
  423. package/src/model/glm52-streamed-experts.ts +0 -453
  424. package/src/model/glm52-streamed-metal.ts +0 -610
  425. package/src/model/glm52-weights.ts +0 -503
  426. package/src/model/glm52.ts +0 -1132
  427. package/src/model/implementation.ts +0 -69
  428. package/src/model/kv-attention-view.ts +0 -67
  429. package/src/model/minicpm5.ts +0 -376
  430. package/src/model/profile.ts +0 -443
  431. package/src/model/qwen-append.ts +0 -12
  432. package/src/model/qwen-conv.ts +0 -52
  433. package/src/model/qwen3-delta-state.ts +0 -74
  434. package/src/model/qwen3-delta.ts +0 -451
  435. package/src/model/qwen3-moe.ts +0 -404
  436. package/src/model/qwen3-mrope.ts +0 -252
  437. package/src/model/qwen3.ts +0 -277
  438. package/src/model/qwen3_5-checkpoint.ts +0 -164
  439. package/src/model/qwen3_5.ts +0 -819
  440. package/src/model/rotating-kv-donor.ts +0 -35
  441. package/src/model/rotating-kv-layout.ts +0 -262
  442. package/src/model/rotating-row-transaction.ts +0 -35
  443. package/src/model/speculative-rotating-kv.ts +0 -101
  444. package/src/model/ssm-prefill-padding.ts +0 -39
  445. package/src/model/support.ts +0 -102
  446. package/src/model/trellis-balanced-scatter.ts +0 -51
  447. package/src/model/trellis-linear.ts +0 -672
  448. package/src/model/trellis-shared-m.ts +0 -151
  449. package/src/model/trellis-shared-scatter.ts +0 -144
  450. package/src/model/trellis-splitk-prefill.ts +0 -158
  451. package/src/model/trellis-tiled-prefill.ts +0 -136
  452. package/src/model/trellis-vector-expand.ts +0 -62
  453. package/src/model/trellis-wide-prefill.ts +0 -94
  454. package/src/model/turboquant-codec.ts +0 -83
  455. package/src/model/universal/archs.ts +0 -332
  456. package/src/model/universal/dense.ts +0 -612
  457. package/src/model/universal/modules.ts +0 -170
  458. package/src/model/universal/rope.ts +0 -381
  459. package/src/native/expert_io.c +0 -523
  460. package/src/native/frame_extract.swift +0 -140
  461. package/src/native-pack.ts +0 -162
  462. package/src/pi-provider.ts +0 -53
  463. package/src/pi-session.ts +0 -81
  464. package/src/pi-terminal.ts +0 -274
  465. package/src/pi-web.ts +0 -1932
  466. package/src/preflight.ts +0 -158
  467. package/src/prompt-cache.ts +0 -460
  468. package/src/quantize/allocator.ts +0 -627
  469. package/src/quantize/atomic-output.ts +0 -38
  470. package/src/quantize/calibration.ts +0 -181
  471. package/src/quantize/config-writer.ts +0 -145
  472. package/src/quantize/index.ts +0 -84
  473. package/src/quantize/job.ts +0 -162
  474. package/src/quantize/quantizer.ts +0 -666
  475. package/src/quantize/rotate.ts +0 -603
  476. package/src/quantize/safetensors-writer.ts +0 -249
  477. package/src/quantize/sensitivity.ts +0 -216
  478. package/src/quantize/trellis.ts +0 -586
  479. package/src/quantize/weight-transform.ts +0 -292
  480. package/src/registry.ts +0 -602
  481. package/src/runtime-config.ts +0 -81
  482. package/src/safetensors.ts +0 -158
  483. package/src/sampler.ts +0 -1011
  484. package/src/serve/admin-routes.ts +0 -184
  485. package/src/serve/assets/curve-terrain.html +0 -105
  486. package/src/serve/assets/training-inference-map.html +0 -575
  487. package/src/serve/aux-routes.ts +0 -132
  488. package/src/serve/batch-scheduler.ts +0 -8
  489. package/src/serve/checkpoint-identity.ts +0 -64
  490. package/src/serve/generation-gateway.ts +0 -496
  491. package/src/serve/isolate.ts +0 -609
  492. package/src/serve/kv-budget.ts +0 -14
  493. package/src/serve/lab-routes.ts +0 -341
  494. package/src/serve/lane-registry.ts +0 -45
  495. package/src/serve/model-admin-routes.ts +0 -155
  496. package/src/serve/model-binding.ts +0 -43
  497. package/src/serve/model-host.ts +0 -511
  498. package/src/serve/parent-application.ts +0 -56
  499. package/src/serve/prompt-response-trace.ts +0 -147
  500. package/src/serve/response-lease.ts +0 -24
  501. package/src/serve/session-search.ts +0 -326
  502. package/src/serve/static-routes.ts +0 -101
  503. package/src/server.ts +0 -1314
  504. package/src/spec/assistant-rows.ts +0 -92
  505. package/src/spec/assistant-source.ts +0 -126
  506. package/src/spec/cache-identity.ts +0 -22
  507. package/src/spec/deepspec-rows.ts +0 -29
  508. package/src/spec/deepspec-source.ts +0 -145
  509. package/src/spec/dflash-source.ts +0 -149
  510. package/src/spec/drafter.ts +0 -304
  511. package/src/spec/dspark/ab-stats.ts +0 -136
  512. package/src/spec/dspark/calibration.ts +0 -112
  513. package/src/spec/dspark/data-dflash.ts +0 -156
  514. package/src/spec/dspark/data.ts +0 -201
  515. package/src/spec/dspark/deepspec-module.ts +0 -776
  516. package/src/spec/dspark/generate-dflash.ts +0 -245
  517. package/src/spec/dspark/generate.ts +0 -290
  518. package/src/spec/dspark/loader.ts +0 -23
  519. package/src/spec/dspark/loss.ts +0 -213
  520. package/src/spec/dspark/module-dflash.ts +0 -560
  521. package/src/spec/dspark/module.ts +0 -540
  522. package/src/spec/dspark/quantize-drafter.ts +0 -91
  523. package/src/spec/dspark/sample.ts +0 -112
  524. package/src/spec/generate.ts +0 -276
  525. package/src/spec/glm52-mtp-graph.ts +0 -29
  526. package/src/spec/glm52-mtp-rows.ts +0 -132
  527. package/src/spec/glm52-mtp-source.ts +0 -317
  528. package/src/spec/glm52-mtp-state.ts +0 -33
  529. package/src/spec/ngram-source.ts +0 -204
  530. package/src/spec/projected-draft-rows.ts +0 -73
  531. package/src/spec/qwen-mtp-module.ts +0 -171
  532. package/src/spec/qwen-mtp-rows.ts +0 -219
  533. package/src/spec/qwen-mtp-source.ts +0 -383
  534. package/src/spec/qwen-mtp-state.ts +0 -30
  535. package/src/spec/serve-loop.ts +0 -585
  536. package/src/spec/source.ts +0 -215
  537. package/src/spec/two-model-rows.ts +0 -61
  538. package/src/spec/two-model.ts +0 -113
  539. package/src/ssd-cache.ts +0 -372
  540. package/src/ssd-durability.ts +0 -211
  541. package/src/tokenizer.ts +0 -335
  542. package/src/tool-approvals.ts +0 -131
  543. package/src/tool-call.ts +0 -695
  544. package/src/train/dataset.ts +0 -484
  545. package/src/train/diffusion-lora.ts +0 -216
  546. package/src/train/export.ts +0 -32
  547. package/src/train/flash-cce.ts +0 -1234
  548. package/src/train/forward.ts +0 -266
  549. package/src/train/fuse.ts +0 -243
  550. package/src/train/index.ts +0 -46
  551. package/src/train/job.ts +0 -205
  552. package/src/train/lora-params.ts +0 -254
  553. package/src/train/loss.ts +0 -1591
  554. package/src/train/merge.ts +0 -195
  555. package/src/train/optimizer.ts +0 -183
  556. package/src/train/prefix-shared.ts +0 -490
  557. package/src/train/rank.ts +0 -137
  558. package/src/train/segmented.ts +0 -1828
  559. package/src/train/steel-qmm-header.ts +0 -1461
  560. package/src/train/trainer.ts +0 -1221
  561. package/src/train/watch.ts +0 -542
  562. package/src/vision/diffusion-vision.ts +0 -620
  563. package/src/vision/embedder.ts +0 -168
  564. package/src/vision/preprocess.ts +0 -247
  565. package/src/vision/prompt.ts +0 -373
  566. package/src/vision/qwen3vl-preprocess.ts +0 -409
  567. package/src/vision/qwen3vl-prompt.ts +0 -196
  568. package/src/vision/qwen3vl-tower.ts +0 -555
  569. package/src/vision/siglip.ts +0 -599
  570. package/src/vision/video-frames.ts +0 -148
  571. package/src/web/app.html +0 -2143
  572. package/src/web/app.js +0 -5703
  573. package/src/web/skills/memory/SKILL.md +0 -120
  574. package/src/web/skills/web-research/SKILL.md +0 -46
  575. package/src/web/skills.ts +0 -97
  576. package/src/web/src/adapters-panel.ts +0 -288
  577. package/src/web/src/assistant.ts +0 -451
  578. package/src/web/src/chat.ts +0 -1057
  579. package/src/web/src/composer.ts +0 -959
  580. package/src/web/src/dataset.ts +0 -132
  581. package/src/web/src/finetune.ts +0 -267
  582. package/src/web/src/hub.ts +0 -428
  583. package/src/web/src/main.ts +0 -98
  584. package/src/web/src/markdown.ts +0 -0
  585. package/src/web/src/memory-panel.ts +0 -572
  586. package/src/web/src/model-picker.ts +0 -159
  587. package/src/web/src/palette.ts +0 -346
  588. package/src/web/src/protocol.ts +0 -26
  589. package/src/web/src/quantize.ts +0 -167
  590. package/src/web/src/rag.ts +0 -274
  591. package/src/web/src/sessions.ts +0 -469
  592. package/src/web/src/shell.ts +0 -804
  593. package/src/web/src/ui-catalog.ts +0 -78
  594. package/src/web/vendor/README.md +0 -61
  595. package/src/web-assets.ts +0 -47
  596. package/src/web-tools.ts +0 -478
  597. package/src/weights.ts +0 -255
  598. /package/src/{serve → server}/completion-sink.ts +0 -0
  599. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  600. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  601. /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
@@ -1,2351 +0,0 @@
1
- // gemma4-base — the config-INDEPENDENT machinery shared by every
2
- // architecture variant (docs/archive/investigations/optimization_plan.md Phase B: pure code
3
- // movement out of the gemma4.ts monolith; every definition here is
4
- // verbatim from it). Cache classes, quantized-KV SDPA, masks, the
5
- // quantized primitives + LoRA machinery, and small graph helpers live
6
- // here; everything that branches on architecture (Attention, MLP, MoE,
7
- // DecoderLayer, Gemma4Model) stays in gemma4.ts and becomes the
8
- // per-model generated code in Phase C.
9
-
10
- import { decodedKvDonorAttention } from "./decoded-kv-donor";
11
- import type { ModelConfig } from "../config";
12
- import { quantFor } from "../config";
13
- import type { Weights } from "../weights";
14
- import { MlxArray } from "../mlx/array";
15
- import { Dtype } from "../mlx/ffi";
16
- import * as ops from "../mlx/ops";
17
- import { quantizedMatmulRows } from "../mlx/quantized-rows";
18
- import { expertOffloadArray } from "../expert-offload";
19
- import * as tq from "../mlx/turboquant-ops";
20
- import type { KvCodec } from "../backends/mlx/kv-codec";
21
- import { TurboQuantCodec, disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
22
- export { disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
23
- import { runtimeValue } from "../runtime-config";
24
-
25
- export type MaskMode = "" | "causal";
26
- export interface Mask {
27
- mode: MaskMode | "array";
28
- arr: MlxArray | null;
29
- /** True when `arr` is exactly the bottom-right-aligned causal matrix
30
- * (offset continuation, no window) — the case where mlx-lm would
31
- * have handed the string "causal" instead. Only these array masks
32
- * are eligible for the fused tiled SDPA: the optiq wrapper falls
33
- * back to unfused on every array mask, and window/bidir masks must
34
- * match that to stay scenario-bit-exact (Phase 9 finding). */
35
- causalEquivalent?: boolean;
36
- }
37
-
38
- /** Per-adapter LoRA weights for one linear (mlx-lm LoRALinear shapes:
39
- * a [in_features, rank], b [rank, out_features], typically f32). */
40
- export interface LoraWeights {
41
- a: MlxArray;
42
- b: MlxArray;
43
- scale: number;
44
- rank: number;
45
- }
46
-
47
- /** Active-adapter state, shared by every mounted linear of one model.
48
- * A plain field, NOT a ContextVar port: our generation queue is
49
- * serialized, so exactly one request's adapters are active at a time
50
- * (PLAN Phase 8 decision). Set/restored by generate(). */
51
- export class LoraState {
52
- active: string[] = [];
53
- /** Training-only LoRA-input dropout. `rate` is the drop probability; `seed` is
54
- * set per micro-step by the trainer and is CONSTANT across that step's forward
55
- * and any recompute (segmented / gradient-checkpoint), so each layer's mask —
56
- * keyed by (seed, the linear's dropoutId) — is reproduced exactly in the
57
- * backward. `seed === null` (the default, and for inference) disables it. */
58
- dropoutRate = 0;
59
- dropoutSeed: number | null = null;
60
- }
61
-
62
- /** Replaceable quantized payload. Inference sees this as read-only through
63
- * QuantizedLinear getters; sensitivity analysis exchanges it atomically. */
64
- export interface QuantizedLinearState {
65
- w: MlxArray;
66
- scales: MlxArray;
67
- biases: MlxArray | null;
68
- spec: ops.QuantSpec;
69
- }
70
-
71
- /** Inverted LoRA-input dropout, keyed by (seed, id) so it is deterministic —
72
- * the same (seed, id, shape) reproduces the mask, which is what makes the
73
- * segmented / gradient-checkpoint recompute correct. kept ⇒ x/(1-p),
74
- * dropped ⇒ 0 (preserves the expectation). Caller owns the result. */
75
- export function loraInputDropout(x: MlxArray, p: number, seed: number, id: number): MlxArray {
76
- const key = ops.randomKey(BigInt(seed) * 100003n + BigInt(id));
77
- const u = ops.randomUniform(x.shape, Dtype.float32, 0, 1, key); // [shape] in [0,1)
78
- key.dispose();
79
- const pArr = ops.scalarLike(p, u);
80
- const keep = ops.less(pArr, u); // u > p ⇒ keep (bool [shape])
81
- const scaleX = ops.scalarLike(1 / (1 - p), x);
82
- const zeroX = ops.scalarLike(0, x);
83
- const mask = ops.where(keep, scaleX, zeroX); // x.dtype
84
- const xd = ops.mul(x, mask);
85
- for (const a of [u, pArr, keep, scaleX, zeroX, mask]) a.dispose();
86
- return xd;
87
- }
88
-
89
- export class QuantizedLinear {
90
- /** Mounted adapters keyed by id (null until first mount — fast path). */
91
- adapters: Map<string, LoraWeights> | null = null;
92
- /** Shared per-model active state (wired by AdapterManager.mount). */
93
- loraState: LoraState | null = null;
94
- /** Stable per-target index for keying training-only LoRA dropout (set by
95
- * attachForTraining). Gives each adapted linear an independent dropout mask. */
96
- dropoutId = 0;
97
- private _w: MlxArray;
98
- private _scales: MlxArray;
99
- private _biases: MlxArray | null;
100
- private _spec: ops.QuantSpec;
101
-
102
- constructor(
103
- w: MlxArray,
104
- scales: MlxArray,
105
- biases: MlxArray | null,
106
- spec: ops.QuantSpec,
107
- /** ADDITIVE bias term (`.bias`, mlx nn.QuantizedLinear's optional bias —
108
- * qwen2 qkv, starcoder2, …). Distinct from `biases`, the quantization
109
- * zero-points. Applied after the matmul, before any LoRA residual,
110
- * exactly like mlx's QuantizedLinear.__call__. */
111
- readonly bias: MlxArray | null = null,
112
- ) {
113
- this._w = w;
114
- this._scales = scales;
115
- this._biases = biases;
116
- this._spec = spec;
117
- }
118
-
119
- get w(): MlxArray { return this._w; }
120
- get scales(): MlxArray { return this._scales; }
121
- get biases(): MlxArray | null { return this._biases; }
122
- get spec(): ops.QuantSpec { return this._spec; }
123
-
124
- /** Atomically install a quantized payload and return the previous one.
125
- * This is the sole mutation capability used by sensitivity sweeps. */
126
- exchangeQuantizedState(next: QuantizedLinearState): QuantizedLinearState {
127
- const previous = {
128
- w: this._w,
129
- scales: this._scales,
130
- biases: this._biases,
131
- spec: this._spec,
132
- };
133
- this._w = next.w;
134
- this._scales = next.scales;
135
- this._biases = next.biases;
136
- this._spec = next.spec;
137
- return previous;
138
- }
139
-
140
- static load(weights: Weights, path: string, config: ModelConfig): QuantizedLinear {
141
- if (!weights.has(`${path}.scales`))
142
- throw new Error(`${path}: expected quantized linear (no .scales tensor)`);
143
- const spec = quantFor(config.quantization, path);
144
- if (!spec) throw new Error(`${path}: no quant spec`);
145
- return new QuantizedLinear(
146
- weights.tensor(`${path}.weight`),
147
- weights.tensor(`${path}.scales`),
148
- weights.has(`${path}.biases`) ? weights.tensor(`${path}.biases`) : null,
149
- spec,
150
- weights.has(`${path}.bias`) ? weights.tensor(`${path}.bias`) : null,
151
- );
152
- }
153
-
154
- /** (in_features, out_features) from the quantized tensors (reference
155
- * _infer_linear_shape: scales are [out, in/group_size]). */
156
- get inFeatures(): number {
157
- return this._scales.shape[1]! * this._spec.groupSize;
158
- }
159
- get outFeatures(): number {
160
- return this._scales.shape[0]!;
161
- }
162
-
163
- forward(x: MlxArray, independentRows = false): MlxArray {
164
- let out = independentRows ? quantizedMatmulRows(
165
- x, this._w, this._scales, this._biases, this._spec,
166
- ) : ops.quantizedMatmul(
167
- x,
168
- this._w,
169
- this._scales,
170
- this._biases,
171
- this._spec,
172
- true,
173
- );
174
- // Additive bias (mlx QuantizedLinear: `x = x + self["bias"]`), before
175
- // the LoRA residual (LoRALinear calls the base linear bias-inclusive).
176
- if (this.bias) out = disposing(out, ops.add(out, this.bias));
177
- // LoRA residual — composition is mlx-lm LoRALinear / optiq apply.py:
178
- // y + (scale · ((x @ A) @ B)).astype(x.dtype)
179
- // (optiq mount.py omits the astype, leaking the f32 residual into the
180
- // bf16 stream — divergence documented in PLAN Phase 8 findings; the
181
- // cast form is what the adapters were trained behind.)
182
- const st = this.loraState;
183
- if (st && st.active.length > 0 && this.adapters && this.adapters.size > 0) {
184
- // Training-only LoRA-input dropout (PEFT applies dropout to x before A;
185
- // the base quantized path is untouched). Keyed by (seed, dropoutId) so the
186
- // backward recompute reproduces the exact mask.
187
- let xLora = x;
188
- let xDrop: MlxArray | null = null;
189
- if (st.dropoutSeed !== null && st.dropoutRate > 0) {
190
- xDrop = loraInputDropout(x, st.dropoutRate, st.dropoutSeed, this.dropoutId);
191
- xLora = xDrop;
192
- }
193
- for (const id of st.active) {
194
- const lw = this.adapters.get(id);
195
- if (!lw) continue;
196
- const xa = ops.matmul(xLora, lw.a);
197
- const z = ops.matmul(xa, lw.b);
198
- xa.dispose();
199
- const zs = ops.mulScalar(z, lw.scale);
200
- z.dispose();
201
- const zc = zs.astype(x.dtype);
202
- zs.dispose();
203
- out = disposing(out, ops.add(out, zc));
204
- zc.dispose();
205
- }
206
- xDrop?.dispose();
207
- }
208
- return out;
209
- }
210
- }
211
-
212
- export class RMSNorm {
213
- constructor(readonly weight: MlxArray | null, readonly eps: number) {}
214
- forward(x: MlxArray): MlxArray {
215
- return ops.rmsNorm(x, this.weight, this.eps);
216
- }
217
- }
218
-
219
- export class QuantizedEmbedding {
220
- constructor(
221
- readonly w: MlxArray,
222
- readonly scales: MlxArray,
223
- readonly biases: MlxArray | null,
224
- readonly spec: ops.QuantSpec,
225
- ) {}
226
-
227
- static load(weights: Weights, path: string, config: ModelConfig): QuantizedEmbedding {
228
- const spec = quantFor(config.quantization, path)!;
229
- return new QuantizedEmbedding(
230
- weights.tensor(`${path}.weight`),
231
- weights.tensor(`${path}.scales`),
232
- weights.has(`${path}.biases`) ? weights.tensor(`${path}.biases`) : null,
233
- spec,
234
- );
235
- }
236
-
237
- /** ids [1, L] (int/uint) → embeddings [1, L, hidden]. */
238
- encode(ids: MlxArray): MlxArray {
239
- const rows = ops.takeAxis(this.w, ids, 0);
240
- const scaleRows = ops.takeAxis(this.scales, ids, 0);
241
- const biasRows = this.biases ? ops.takeAxis(this.biases, ids, 0) : null;
242
- const out = ops.dequantize(rows, scaleRows, biasRows, this.spec);
243
- for (const a of [rows, scaleRows]) a.dispose();
244
- biasRows?.dispose();
245
- return out;
246
- }
247
-
248
- /** Tied output head: h [1, L, hidden] → logits [1, L, vocab]. */
249
- asLinear(h: MlxArray): MlxArray {
250
- return ops.quantizedMatmul(h, this.w, this.scales, this.biases, this.spec, true);
251
- }
252
- }
253
-
254
- /** Fetched KV handed from a donor layer to its sharers (and to the
255
- * speculative drafter). Owned by forwardLayers for the pass.
256
- * `offsetArr` is an owned pre-write position snapshot. Donor and sharing
257
- * attention layers keep it until the fetched KV is released, even if the
258
- * cache advances or replaces its borrowed row-position array. */
259
- export type SharedKv =
260
- | { kind: "plain"; keys: MlxArray; values: MlxArray; offset: number;
261
- offsetArr?: MlxArray;
262
- /** TurboQuant deferred-V: `values` are still in the rotated (FWHT)
263
- * domain — every consumer must un-rotate its attention OUTPUT
264
- * (tq.unrotateValues) after sdpa. Attention is linear in V, so the
265
- * result is the same decode, just transformed once per query row
266
- * instead of once per cached token. */
267
- vRotated?: boolean;
268
- restoreValues?: (output: MlxArray) => MlxArray }
269
- | { kind: "quant"; keys: ops.QuantizedTensor; values: ops.QuantizedTensor;
270
- offset: number; groupSize: number; bits: number; offsetArr?: MlxArray }
271
- | { kind: "view"; attention: KvAttentionView; offset: number; offsetArr?: MlxArray };
272
-
273
- /** Retain an independent array handle for positions used across cache writes.
274
- * The contiguous view preserves the integer values and shares their storage;
275
- * callers release it with the fetched attention state. */
276
- export function captureRopeOffsets(cache: Pick<Cache, "ropeOffsetArr">): MlxArray | undefined {
277
- const offsets = cache.ropeOffsetArr;
278
- return offsets ? ops.contiguous(offsets) : undefined;
279
- }
280
-
281
- /** Quantized attention consumes a numerical storage port, independent of the
282
- * concrete layout used for row positions, retention and persistence. */
283
- export interface QuantizedAttentionState {
284
- readonly groupSize: number;
285
- readonly bits: number;
286
- updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor];
287
- }
288
-
289
- /** Values may stay in the codec's rotated domain until after attention. */
290
- export interface RotatedValueAttentionState {
291
- /** Capture a fetched value domain independently of later row changes. */
292
- captureValueTransform?(): (output: MlxArray) => MlxArray;
293
- updateAndFetchDeferredV(k: MlxArray, v: MlxArray): [MlxArray, MlxArray];
294
- }
295
-
296
- /** A captured numerical view supports multiple queries without appending KV
297
- * again. It owns its tensor handles independently of later cache membership or
298
- * precision changes. Query/mask inputs are borrowed; outputs are owned. */
299
- export interface KvAttentionView {
300
- attend(q: MlxArray, scale: number, mask: Mask): MlxArray;
301
- dispose(): void;
302
- }
303
-
304
- /** Storage appends once and hands every attention consumer the same view. */
305
- export interface KvAttentionState {
306
- appendAndFetch(k: MlxArray, v: MlxArray): KvAttentionView;
307
- }
308
-
309
- /** Owned read-only planes with validity separate from physical padding. */
310
- export interface KvDonorRows {
311
- readonly keys: MlxArray;
312
- readonly values: MlxArray;
313
- readonly offsets: readonly number[];
314
- readonly starts: readonly number[];
315
- readonly ends: readonly number[];
316
- }
317
-
318
- /** Captured Q-only attention hides plain or encoded donor storage. */
319
- export interface KvDonorAttention extends KvAttentionView {
320
- readonly width: number;
321
- readonly dtype: Dtype;
322
- readonly offsets: readonly number[];
323
- readonly starts: readonly number[];
324
- readonly ends: readonly number[];
325
- }
326
-
327
- export interface Cache {
328
- captureDonorRows?(): KvDonorRows;
329
- captureDonorAttention?(): KvDonorAttention;
330
- /** A preparation method commits each request's own precision boundary.
331
- * Splitting a forward for a sibling must not move that boundary. */
332
- readonly prefillMaintenance?: {
333
- beginPrefill(): void;
334
- commitPrefill(rows: readonly number[]): void;
335
- endPrefill(): void;
336
- };
337
- /** Optional representation-owned affine conversion. Logical position can
338
- * differ from the physical write head in a padded rotating layout. */
339
- readonly affineConversion?: { readonly offset: number; toQuantized(groupSize: number, bits: number): Cache };
340
- readonly turboConversion?: { readonly offset: number; toTurboQuantized(kBits: number, vBits: number): Cache };
341
- readonly attentionState?: KvAttentionState;
342
- /** Earliest prefix whose retained representation can resume generation.
343
- * Irreversible transitions may preserve bytes while invalidating older
344
- * precision boundaries. Reuse policy must not trim below this offset. */
345
- minimumReusableOffset?: number;
346
- readonly rotatedValueAttention?: RotatedValueAttentionState;
347
- readonly quantizedAttention?: QuantizedAttentionState;
348
- offset: number;
349
- /** Stable storage identity for compatibility guards and persistence.
350
- * REQUIRED: every capability predicate (isPlainKvCache, isRotating*,
351
- * kv-store codecs) is a string compare on this value, so a missing
352
- * override used to fail OPEN into wrong routing (PRs #42/#43:
353
- * BatchedRotatingCache shipped without one and batched joins dropped
354
- * running rows). Wrappers report their own kind plus the inner kind. */
355
- signature(): string;
356
- /** Physical cache storage consumed by one additional token for one row.
357
- * Recurrent caches return 0 because their state does not grow with the
358
- * sequence. Optional for stateless/training adapters that are never
359
- * admitted or persisted. */
360
- bytesPerToken?(): number;
361
- /** state() returned temporary views that the caller must release. */
362
- readonly stateNeedsDispose?: boolean;
363
- /** Compiled-decode trace adapters expose the RoPE offset as an int32
364
- * array input here; real caches leave it unset (static int path). */
365
- readonly ropeOffsetArr?: MlxArray;
366
- updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray];
367
- /** Mask for an N-token step given this cache's state. */
368
- makeMask(N: number, windowSize: number | null): Mask;
369
- state(): MlxArray[];
370
- /** Can `trim(n)` drop the last n tokens? (Ring caches lose trimability
371
- * once wrapped.) */
372
- isTrimmable(): boolean;
373
- /** Drop the last n tokens (future writes overwrite them). `bypass` (default
374
- * false → unchanged behavior) skips the isTrimmable guard for the
375
- * speculative-decode rollback case (trim the last n ≤ γ tips right after a
376
- * >1 concat write); rotating caches then physically shrink the buffer tail. */
377
- trim(n: number, bypass?: boolean): void;
378
- /** Speculative verify-round support for NON-trimmable recurrent caches
379
- * (SSMCache — gated-DeltaNet conv + recurrent state). A cache exposing
380
- * these is spec-eligible without isTrimmable(): the serve loop arms a
381
- * round before the verify forward (the layer snapshots pre-round state —
382
- * free, MLX arrays are immutable — and records its position-local kernel
383
- * inputs), then resolves it: commit on full accept, or rollback(keep)
384
- * which restores the snapshot and bit-exactly REPLAYS the first `keep`
385
- * window tokens through the recurrence (identical arithmetic prefix ⇒
386
- * identical state). Trimmable caches leave these unset and keep trim(). */
387
- specRoundBegin?(): void;
388
- specRoundCommit?(): void;
389
- specRoundRollback?(keep: number): void;
390
- dispose(): void;
391
- }
392
-
393
- export function cacheSignature(cache: Cache | undefined): string {
394
- return cache ? cache.signature() : "unknown";
395
- }
396
-
397
- export interface RowBatchCache extends Cache {
398
- readonly batchSize: number;
399
- filterRows(keep: readonly number[]): void;
400
- extractRow(row: number): Cache | null;
401
- }
402
-
403
- export function isRowBatchCache(cache: Cache): cache is RowBatchCache {
404
- const candidate = cache as Partial<RowBatchCache>;
405
- return typeof candidate.batchSize === "number" &&
406
- typeof candidate.filterRows === "function" &&
407
- typeof candidate.extractRow === "function";
408
- }
409
-
410
- export function isPlainKvCache(cache: Cache | undefined): cache is KVCache {
411
- return cacheSignature(cache) === "kv:plain";
412
- }
413
-
414
- export function isQuantizedKvCache(cache: Cache | undefined): cache is QuantizedKVCache {
415
- return cacheSignature(cache).startsWith("kv:quant:");
416
- }
417
-
418
- export function isRotatingPlainCache(cache: Cache | undefined): cache is RotatingKVCache {
419
- return cacheSignature(cache) === "kv:rotating-plain";
420
- }
421
-
422
- export function isRotatingQuantizedCache(
423
- cache: Cache | undefined,
424
- ): cache is RotatingQuantizedKVCache {
425
- return cacheSignature(cache).startsWith("kv:rotating-quant:");
426
- }
427
-
428
- /**
429
- * Capability contract for cache layouts that own their dynamic-row batching.
430
- * The scheduler uses this for non-K/V state families (for example GLM's
431
- * compressed MLA/DSA state) instead of teaching the scheduler their tensor
432
- * layout. `mergeRows` writes into an empty cache and does not consume inputs;
433
- * extracted rows are independent owned caches.
434
- */
435
- export interface BatchableCache extends Cache {
436
- readonly batchSize: number | null;
437
- readonly rowOffsets: readonly number[];
438
- readonly leftPad: readonly number[];
439
- readonly maxTokens?: number;
440
- makeEmptyBatch(): BatchableCache;
441
- mergeRows(rows: readonly Cache[]): void;
442
- extractRow(row: number): Cache;
443
- filterRows(keep: readonly number[]): void;
444
- projectedBytes(tokens: number): number;
445
- }
446
-
447
- /** Padding is interpreted by cache geometry, independently of scheduling.
448
- * Left padding initializes an empty group; lengths count input columns before
449
- * trailing padding. Finalize before ordinary decode or publishing checkpoints. */
450
- export interface PrefillPadding {
451
- readonly leftPadding?: readonly number[];
452
- readonly lengths: readonly number[];
453
- readonly rightPadding?: readonly number[];
454
- }
455
- export interface PaddedPrefillCache extends Cache {
456
- preparePrefill(padding: PrefillPadding): void;
457
- finalizePrefill(): void;
458
- }
459
-
460
- export function isBatchableCache(cache: Cache): cache is BatchableCache {
461
- const candidate = cache as Partial<BatchableCache>;
462
- return (
463
- typeof candidate.makeEmptyBatch === "function" &&
464
- typeof candidate.mergeRows === "function" &&
465
- typeof candidate.extractRow === "function" &&
466
- typeof candidate.filterRows === "function" &&
467
- typeof candidate.projectedBytes === "function" &&
468
- Array.isArray(candidate.rowOffsets) &&
469
- Array.isArray(candidate.leftPad)
470
- );
471
- }
472
-
473
- /** How one decode step interacts with a cache under compiled decode
474
- * (returned by prepareDecodeStep, consumed by compiled-decode.ts):
475
- * - "concat": the compiled graph fetches concat(active prefix, new kv)
476
- * — same values as today's write-then-slice — and the WRITE happens
477
- * outside the graph right after (writeDecodeStep), keeping the buffer
478
- * single-referenced at its slice_update so mlx donates it in place.
479
- * - "ring": the write happens IN-graph (slice_update_dynamic at
480
- * writePos) and the fetch is the full updated buffer — the rotating
481
- * steady state, where attention reads the whole ring in ring order
482
- * (a concat would permute KV positions and change summation order).
483
- * The updated buffers come back as closure outputs; adoptDecodeStep
484
- * swaps them in. */
485
- export interface DecodeStepPlan {
486
- fetch: "concat" | "ring";
487
- /** Write position on axis 2 (== ring index for rotating caches). */
488
- writePos: number;
489
- /** Valid prefix length for "concat" fetches (== offset). */
490
- activeLen: number;
491
- }
492
-
493
- /** KV cache — port of mlx-lm cache.py KVCache: preallocated in steps of
494
- * 256 along the sequence axis, updated in place via slice_update. */
495
- export class KVCache implements Cache {
496
- static readonly STEP = 256;
497
- /** Set only by compiled-decode trace adapters (see Cache). */
498
- readonly ropeOffsetArr?: MlxArray;
499
- keys: MlxArray | null = null;
500
- values: MlxArray | null = null;
501
- offset = 0;
502
-
503
- signature(): string { return "kv:plain"; }
504
-
505
- bytesPerToken(): number {
506
- const steps = this.keys?.shape[2] ?? 0;
507
- return steps > 0 ? (this.keys!.nbytes + this.values!.nbytes) / steps : 0;
508
- }
509
-
510
- updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
511
- const prev = this.offset;
512
- const L = k.shape[2]!;
513
- if (!this.keys || prev + L > this.keys.shape[2]!) {
514
- const [B, H, , D] = k.shape as [number, number, number, number];
515
- const vD = v.shape[3]!;
516
- const nSteps = Math.floor((KVCache.STEP + L - 1) / KVCache.STEP);
517
- const newK = ops.zeros([B, H, nSteps * KVCache.STEP, D], k.dtype);
518
- const newV = ops.zeros([B, H, nSteps * KVCache.STEP, vD], v.dtype);
519
- if (this.keys && this.values) {
520
- let oldK = this.keys;
521
- let oldV = this.values;
522
- if (prev % KVCache.STEP !== 0) {
523
- const trimK = oldK.slice([0, 0, 0, 0], [B, H, prev, D]);
524
- const trimV = oldV.slice([0, 0, 0, 0], [B, H, prev, vD]);
525
- oldK.dispose();
526
- oldV.dispose();
527
- oldK = trimK;
528
- oldV = trimV;
529
- }
530
- this.keys = ops.concatAxis([oldK, newK], 2);
531
- this.values = ops.concatAxis([oldV, newV], 2);
532
- for (const a of [oldK, oldV, newK, newV]) a.dispose();
533
- } else {
534
- this.keys = newK;
535
- this.values = newV;
536
- }
537
- }
538
-
539
- this.offset += L;
540
- const [B, H, S, D] = this.keys!.shape as [number, number, number, number];
541
- const vD = this.values!.shape[3]!;
542
- const k2 = ops.sliceUpdate(this.keys!, k, [0, 0, prev, 0], [B, H, this.offset, D]);
543
- const v2 = ops.sliceUpdate(this.values!, v, [0, 0, prev, 0], [B, H, this.offset, vD]);
544
- this.keys!.dispose();
545
- this.values!.dispose();
546
- this.keys = k2;
547
- this.values = v2;
548
- return [
549
- this.keys.slice([0, 0, 0, 0], [B, H, this.offset, D]),
550
- this.values.slice([0, 0, 0, 0], [B, H, this.offset, vD]),
551
- ];
552
- }
553
-
554
- makeMask(N: number, windowSize: number | null): Mask {
555
- if (N === 1) return { mode: "", arr: null };
556
- // Windowless multi-token chunks get the STRING "causal" at ANY offset —
557
- // mlx-lm cache.py:114-125 create_attention_mask does exactly this, and
558
- // mx.fast SDPA aligns the string bottom-right, i.e. the same values as
559
- // our materialized matrix. Shipping the array instead (the old
560
- // offset>0 branch) forced a materialize-and-add mask path: a bool
561
- // [N, offset+N] built PER CHUNK and read by every full-attention layer
562
- // — the e4b 16k prefill gap vs mlx-lm (2026-07-06b, hd=512 layers
563
- // where the fallback dispatch can't block-skip an array mask).
564
- if (windowSize === null) return { mode: "causal", arr: null };
565
- if (this.offset === 0 && N <= windowSize) return { mode: "causal", arr: null };
566
- return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
567
- }
568
-
569
- /** Arrays to eval to materialize cache state (prefill chunk boundary). */
570
- state(): MlxArray[] {
571
- return this.keys && this.values ? [this.keys, this.values] : [];
572
- }
573
-
574
- isTrimmable(): boolean {
575
- return true;
576
- }
577
-
578
- trim(n: number): void {
579
- this.offset = Math.max(0, this.offset - n);
580
- }
581
-
582
- /** Chronological (K, V) view sliced to offset (drafter donor read). */
583
- captureDonorRows(): KvDonorRows {
584
- const [keys, values] = this.temporalView();
585
- const B = keys.shape[0]!, width = keys.shape[2]!;
586
- return { keys, values, offsets: Array(B).fill(this.offset),
587
- starts: Array(B).fill(0), ends: Array(B).fill(width) };
588
- }
589
-
590
- temporalView(): [MlxArray, MlxArray] {
591
- if (!this.keys || !this.values) throw new Error("cache is empty");
592
- const [B, H, , D] = this.keys.shape as [number, number, number, number];
593
- const vD = this.values.shape[3]!;
594
- return [
595
- this.keys.slice([0, 0, 0, 0], [B, H, this.offset, D]),
596
- this.values.slice([0, 0, 0, 0], [B, H, this.offset, vD]),
597
- ];
598
- }
599
-
600
- /** Compiled decode: host-side bookkeeping for a 1-token step — the
601
- * growth/trim half of updateAndFetch (L=1), without the write. */
602
- prepareDecodeStep(): DecodeStepPlan {
603
- const prev = this.offset;
604
- if (!this.keys || !this.values) throw new Error("compiled decode on an empty cache");
605
- if (prev + 1 > this.keys.shape[2]!) {
606
- const [B, H, , D] = this.keys.shape as [number, number, number, number];
607
- const vD = this.values.shape[3]!;
608
- let oldK = this.keys;
609
- let oldV = this.values;
610
- if (prev % KVCache.STEP !== 0) {
611
- const trimK = oldK.slice([0, 0, 0, 0], [B, H, prev, D]);
612
- const trimV = oldV.slice([0, 0, 0, 0], [B, H, prev, vD]);
613
- oldK.dispose();
614
- oldV.dispose();
615
- oldK = trimK;
616
- oldV = trimV;
617
- }
618
- const newK = ops.zeros([B, H, KVCache.STEP, D], oldK.dtype);
619
- const newV = ops.zeros([B, H, KVCache.STEP, vD], oldV.dtype);
620
- this.keys = ops.concatAxis([oldK, newK], 2);
621
- this.values = ops.concatAxis([oldV, newV], 2);
622
- for (const a of [oldK, oldV, newK, newV]) a.dispose();
623
- }
624
- return { fetch: "concat", writePos: prev, activeLen: prev };
625
- }
626
-
627
- /** Compiled decode: the write half (same sliceUpdate as updateAndFetch).
628
- * Takes ownership of kNew/vNew; returns the arrays to async-eval with
629
- * the step (the updated buffers). */
630
- writeDecodeStep(kNew: MlxArray, vNew: MlxArray): MlxArray[] {
631
- const prev = this.offset;
632
- const [B, H, , D] = this.keys!.shape as [number, number, number, number];
633
- const vD = this.values!.shape[3]!;
634
- const k2 = ops.sliceUpdate(this.keys!, kNew, [0, 0, prev, 0], [B, H, prev + 1, D]);
635
- const v2 = ops.sliceUpdate(this.values!, vNew, [0, 0, prev, 0], [B, H, prev + 1, vD]);
636
- this.keys!.dispose();
637
- this.values!.dispose();
638
- kNew.dispose();
639
- vNew.dispose();
640
- this.keys = k2;
641
- this.values = v2;
642
- this.offset = prev + 1;
643
- return [k2, v2];
644
- }
645
-
646
- /** Adopt persisted state (takes ownership of the arrays). */
647
- restoreState(keys: MlxArray, values: MlxArray, offset: number): void {
648
- this.dispose();
649
- this.keys = keys;
650
- this.values = values;
651
- this.offset = offset;
652
- }
653
-
654
- /** Port of mlx-lm KVCache.to_quantized: quantize the whole buffer
655
- * (padding included — it's overwritten before being read). */
656
- toQuantized(groupSize: number, bits: number): QuantizedKVCache {
657
- const q = new QuantizedKVCache(groupSize, bits);
658
- q.offset = this.offset;
659
- if (this.keys && this.values) {
660
- q.keys = ops.quantize(this.keys, groupSize, bits);
661
- q.values = ops.quantize(this.values, groupSize, bits);
662
- }
663
- this.dispose();
664
- return q;
665
- }
666
-
667
- dispose(): void {
668
- this.keys?.dispose();
669
- this.values?.dispose();
670
- this.keys = this.values = null;
671
- this.offset = 0;
672
- }
673
- }
674
-
675
- /** Quantized KV cache — port of mlx-lm QuantizedKVCache: keys/values
676
- * stored as (packed u32, scales, biases) triples, quantized along
677
- * head_dim. Only full-attention layers convert (mlx-lm's rotating-cache
678
- * quantization is NYI upstream; sliding layers are window-capped
679
- * anyway). Attention dispatches to quantizedSdpa for these. */
680
- export class QuantizedKVCache implements Cache {
681
- minimumReusableOffset?: number;
682
- static readonly STEP = 256;
683
- /** Set only by compiled-decode trace adapters (see Cache). */
684
- readonly ropeOffsetArr?: MlxArray;
685
- keys: ops.QuantizedTensor | null = null;
686
- values: ops.QuantizedTensor | null = null;
687
- offset = 0;
688
-
689
- constructor(readonly groupSize: number, readonly bits: number) {}
690
-
691
- get quantizedAttention(): QuantizedAttentionState { return this; }
692
-
693
- signature(): string { return `kv:quant:${this.bits}:${this.groupSize}`; }
694
-
695
- bytesPerToken(): number {
696
- const steps = this.keys?.packed.shape[2] ?? 0;
697
- if (steps === 0 || !this.keys || !this.values) return 0;
698
- const tensors = [
699
- this.keys.packed, this.keys.scales, this.keys.biases,
700
- this.values.packed, this.values.scales, this.values.biases,
701
- ];
702
- return tensors.reduce((bytes, array) => bytes + array.nbytes, 0) / steps;
703
- }
704
-
705
- updateAndFetch(): [MlxArray, MlxArray] {
706
- throw new Error("QuantizedKVCache: use updateAndFetchQuantized");
707
- }
708
-
709
- #grow(triple: ops.QuantizedTensor | null, lastDims: number[], B: number, H: number, steps: number, dtype: Dtype): ops.QuantizedTensor {
710
- const mk = (dim: number, dt: Dtype) => ops.zeros([B, H, steps, dim], dt);
711
- if (!triple)
712
- return { packed: mk(lastDims[0]!, Dtype.uint32), scales: mk(lastDims[1]!, dtype), biases: mk(lastDims[2]!, dtype) };
713
- const ext = (a: MlxArray, dim: number, dt: Dtype): MlxArray => {
714
- const z = mk(dim, dt);
715
- const out = ops.concatAxis([a, z], 2);
716
- a.dispose();
717
- z.dispose();
718
- return out;
719
- };
720
- return {
721
- packed: ext(triple.packed, lastDims[0]!, Dtype.uint32),
722
- scales: ext(triple.scales, lastDims[1]!, dtype),
723
- biases: ext(triple.biases, lastDims[2]!, dtype),
724
- };
725
- }
726
-
727
- /** Quantize incoming k/v and append; returns quantized views to offset. */
728
- updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
729
- const [B, H, L, kD] = k.shape as [number, number, number, number];
730
- const vD = v.shape[3]!;
731
- const prev = this.offset;
732
- const elPerInt = 32 / this.bits;
733
-
734
- if (!this.keys || prev + L > this.keys.packed.shape[2]!) {
735
- const newSteps = Math.floor((QuantizedKVCache.STEP + L - 1) / QuantizedKVCache.STEP) * QuantizedKVCache.STEP;
736
- if (this.keys && prev % QuantizedKVCache.STEP !== 0) {
737
- const trimTo = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
738
- const cut = (a: MlxArray): MlxArray => {
739
- const [b, h, , d] = a.shape as [number, number, number, number];
740
- const s = a.slice([0, 0, 0, 0], [b, h, prev, d]);
741
- a.dispose();
742
- return s;
743
- };
744
- return { packed: cut(t.packed), scales: cut(t.scales), biases: cut(t.biases) };
745
- };
746
- this.keys = trimTo(this.keys);
747
- this.values = trimTo(this.values!);
748
- }
749
- const dtype = k.dtype;
750
- this.keys = this.#grow(this.keys, [kD / elPerInt, kD / this.groupSize, kD / this.groupSize], B, H, newSteps, dtype);
751
- this.values = this.#grow(this.values, [vD / elPerInt, vD / this.groupSize, vD / this.groupSize], B, H, newSteps, dtype);
752
- }
753
-
754
- this.offset += L;
755
- const kq = ops.quantize(k, this.groupSize, this.bits);
756
- const vq = ops.quantize(v, this.groupSize, this.bits);
757
-
758
- const writeAll = (dst: ops.QuantizedTensor, src: ops.QuantizedTensor): ops.QuantizedTensor => {
759
- const w = (d: MlxArray, s: MlxArray): MlxArray => {
760
- const [b, h, , dd] = d.shape as [number, number, number, number];
761
- const u = ops.sliceUpdate(d, s, [0, 0, prev, 0], [b, h, this.offset, dd]);
762
- d.dispose();
763
- s.dispose();
764
- return u;
765
- };
766
- return {
767
- packed: w(dst.packed, src.packed),
768
- scales: w(dst.scales, src.scales),
769
- biases: w(dst.biases, src.biases),
770
- };
771
- };
772
- this.keys = writeAll(this.keys!, kq);
773
- this.values = writeAll(this.values!, vq);
774
-
775
- const fetch = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
776
- const f = (a: MlxArray): MlxArray => {
777
- const [b, h, , d] = a.shape as [number, number, number, number];
778
- return a.slice([0, 0, 0, 0], [b, h, this.offset, d]);
779
- };
780
- return { packed: f(t.packed), scales: f(t.scales), biases: f(t.biases) };
781
- };
782
- return [fetch(this.keys), fetch(this.values)];
783
- }
784
-
785
- makeMask(N: number, windowSize: number | null): Mask {
786
- if (N === 1) return { mode: "", arr: null };
787
- // Same rule as KVCache.makeMask (mlx-lm cache.py:114-125): windowless
788
- // multi-token chunks are the string "causal" at any offset. The old
789
- // causalEquivalent escape hatch existed precisely because this case
790
- // used to ship an array; only windowed continuations materialize now.
791
- if (windowSize === null || (this.offset === 0 && N <= windowSize))
792
- return { mode: "causal", arr: null };
793
- return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
794
- }
795
-
796
- /** Compiled decode: growth half of updateAndFetchQuantized (L=1),
797
- * without quantize/write (those live in the compiled graph / after). */
798
- prepareDecodeStep(): DecodeStepPlan {
799
- const prev = this.offset;
800
- if (!this.keys || !this.values) throw new Error("compiled decode on an empty cache");
801
- if (prev + 1 > this.keys.packed.shape[2]!) {
802
- if (prev % QuantizedKVCache.STEP !== 0) {
803
- const trimTo = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
804
- const cut = (a: MlxArray): MlxArray => {
805
- const [b, h, , d] = a.shape as [number, number, number, number];
806
- const s = a.slice([0, 0, 0, 0], [b, h, prev, d]);
807
- a.dispose();
808
- return s;
809
- };
810
- return { packed: cut(t.packed), scales: cut(t.scales), biases: cut(t.biases) };
811
- };
812
- this.keys = trimTo(this.keys);
813
- this.values = trimTo(this.values);
814
- }
815
- const [B, H] = this.keys.packed.shape as [number, number, number, number];
816
- const elPerInt = 32 / this.bits;
817
- const kD = this.keys.packed.shape[3]! * elPerInt;
818
- const vD = this.values.packed.shape[3]! * elPerInt;
819
- const dtype = this.keys.scales.dtype;
820
- this.keys = this.#grow(this.keys, [kD / elPerInt, kD / this.groupSize, kD / this.groupSize], B, H, QuantizedKVCache.STEP, dtype);
821
- this.values = this.#grow(this.values, [vD / elPerInt, vD / this.groupSize, vD / this.groupSize], B, H, QuantizedKVCache.STEP, dtype);
822
- }
823
- return { fetch: "concat", writePos: prev, activeLen: prev };
824
- }
825
-
826
- /** Compiled decode: the write half — six sliceUpdates of the already-
827
- * quantized step row (quantize ran in-graph). Takes ownership of the
828
- * rows; returns the updated buffers to async-eval with the step. */
829
- writeDecodeStep(rows: MlxArray[]): MlxArray[] {
830
- const prev = this.offset;
831
- const w = (d: MlxArray, srcRow: MlxArray): MlxArray => {
832
- const [b, h, , dd] = d.shape as [number, number, number, number];
833
- const u = ops.sliceUpdate(d, srcRow, [0, 0, prev, 0], [b, h, prev + 1, dd]);
834
- d.dispose();
835
- srcRow.dispose();
836
- return u;
837
- };
838
- this.keys = {
839
- packed: w(this.keys!.packed, rows[0]!),
840
- scales: w(this.keys!.scales, rows[1]!),
841
- biases: w(this.keys!.biases, rows[2]!),
842
- };
843
- this.values = {
844
- packed: w(this.values!.packed, rows[3]!),
845
- scales: w(this.values!.scales, rows[4]!),
846
- biases: w(this.values!.biases, rows[5]!),
847
- };
848
- this.offset = prev + 1;
849
- return this.state();
850
- }
851
-
852
- state(): MlxArray[] {
853
- if (!this.keys || !this.values) return [];
854
- return [
855
- this.keys.packed, this.keys.scales, this.keys.biases,
856
- this.values.packed, this.values.scales, this.values.biases,
857
- ];
858
- }
859
-
860
- isTrimmable(): boolean {
861
- return true;
862
- }
863
-
864
- trim(n: number): void {
865
- this.offset = Math.max(0, this.offset - n);
866
- }
867
-
868
- /** Adopt persisted state (takes ownership of the triples' arrays) —
869
- * the quantized twin of KVCache.restoreState (kv-store persistence). */
870
- restoreState(keys: ops.QuantizedTensor, values: ops.QuantizedTensor, offset: number): void {
871
- this.dispose();
872
- this.keys = keys;
873
- this.values = values;
874
- this.offset = offset;
875
- }
876
-
877
- /** Chronological (K, V) triples sliced to offset — the quantized twin of
878
- * KVCache.temporalView (batched merge/extend/filter reads). Caller owns
879
- * the returned views. */
880
- captureDonorAttention(): KvDonorAttention {
881
- const [keys, values] = this.temporalView();
882
- const B = keys.packed.shape[0]!, width = keys.packed.shape[2]!;
883
- return { width, dtype: keys.scales.dtype, offsets: Array(B).fill(this.offset),
884
- starts: Array(B).fill(0), ends: Array(B).fill(width),
885
- attend: (q, scale, mask) => quantizedSdpa(q, keys, values, scale, mask, this.groupSize, this.bits),
886
- dispose() { disposeTriple(keys); disposeTriple(values); },
887
- };
888
- }
889
-
890
- temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
891
- if (!this.keys || !this.values) throw new Error("cache is empty");
892
- const cut = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
893
- const f = (a: MlxArray): MlxArray => {
894
- const [b, h, , d] = a.shape as [number, number, number, number];
895
- return a.slice([0, 0, 0, 0], [b, h, this.offset, d]);
896
- };
897
- return { packed: f(t.packed), scales: f(t.scales), biases: f(t.biases) };
898
- };
899
- return [cut(this.keys), cut(this.values)];
900
- }
901
-
902
- dispose(): void {
903
- for (const a of this.state()) a.dispose();
904
- this.keys = this.values = null;
905
- this.offset = 0;
906
- }
907
- }
908
-
909
- /** Rotating (sliding-window) KV cache — port of mlx-lm RotatingKVCache
910
- * with keep=0 (gemma4's configuration): a ring buffer of max_size
911
- * entries, so decode attends over at most the window. RoPE offsets use
912
- * the true position; masks use the buffer-clamped offset. */
913
- export class RotatingKVCache implements Cache {
914
- declare minimumReusableOffset?: number;
915
- static readonly STEP = 256;
916
- /** Set only by compiled-decode trace adapters (see Cache). */
917
- readonly ropeOffsetArr?: MlxArray;
918
- keys: MlxArray | null = null;
919
- values: MlxArray | null = null;
920
- offset = 0;
921
- #idx = 0;
922
- readonly maxSize: number;
923
-
924
- constructor(maxSize: number) {
925
- this.maxSize = maxSize;
926
- }
927
-
928
- signature(): string { return "kv:rotating-plain"; }
929
-
930
- bytesPerToken(): number {
931
- const steps = this.keys?.shape[2] ?? 0;
932
- return steps > 0 ? (this.keys!.nbytes + this.values!.nbytes) / steps : 0;
933
- }
934
-
935
- /** v with ring contents rearranged into temporal order (keep=0). */
936
- #temporalOrder(v: MlxArray): MlxArray {
937
- const S = v.shape[2]!;
938
- const [B, H, , D] = v.shape as [number, number, number, number];
939
- if (this.#idx === S) return v.slice([0, 0, 0, 0], [B, H, S, D]);
940
- if (this.#idx < this.offset) {
941
- const tail = v.slice([0, 0, this.#idx, 0], [B, H, S, D]);
942
- const head = v.slice([0, 0, 0, 0], [B, H, this.#idx, D]);
943
- const out = ops.concatAxis([tail, head], 2);
944
- tail.dispose();
945
- head.dispose();
946
- return out;
947
- }
948
- return v.slice([0, 0, 0, 0], [B, H, this.#idx, D]);
949
- }
950
-
951
- #trim(trimSize: number, v: MlxArray, append: MlxArray | null): MlxArray {
952
- const [B, H, S, D] = v.shape as [number, number, number, number];
953
- let base: MlxArray;
954
- if (trimSize > 0) {
955
- base = v.slice([0, 0, trimSize, 0], [B, H, S, D]);
956
- } else {
957
- base = v.slice([0, 0, 0, 0], [B, H, S, D]);
958
- }
959
- if (!append) return base;
960
- const out = ops.concatAxis([base, append], 2);
961
- base.dispose();
962
- return out;
963
- }
964
-
965
- #updateConcat(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
966
- const S = k.shape[2]!;
967
- if (!this.keys || !this.values) {
968
- // own copies (full-range slice = cheap view node)
969
- const [B, H, , D] = k.shape as [number, number, number, number];
970
- const vD = v.shape[3]!;
971
- this.keys = k.slice([0, 0, 0, 0], [B, H, S, D]);
972
- this.values = v.slice([0, 0, 0, 0], [B, H, S, vD]);
973
- } else {
974
- const tk = this.#temporalOrder(this.keys);
975
- const tv = this.#temporalOrder(this.values);
976
- this.keys.dispose();
977
- this.values.dispose();
978
- this.#idx = tk.shape[2]!;
979
- const trimSize = this.#idx - this.maxSize + 1;
980
- this.keys = this.#trim(trimSize, tk, k);
981
- this.values = this.#trim(trimSize, tv, v);
982
- tk.dispose();
983
- tv.dispose();
984
- }
985
- this.offset += S;
986
- this.#idx = this.keys.shape[2]!;
987
- return this.#fetchAll();
988
- }
989
-
990
- #updateInPlace(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
991
- const [B, H, S, D] = k.shape as [number, number, number, number];
992
- const vD = v.shape[3]!;
993
- const prev = this.offset;
994
-
995
- if (!this.keys || (prev >= this.keys.shape[2]! && this.keys.shape[2]! < this.maxSize)) {
996
- const newSize = Math.min(RotatingKVCache.STEP, this.maxSize - prev);
997
- const newK = ops.zeros([B, H, newSize, D], k.dtype);
998
- const newV = ops.zeros([B, H, newSize, vD], v.dtype);
999
- if (this.keys && this.values) {
1000
- const ck = ops.concatAxis([this.keys, newK], 2);
1001
- const cv = ops.concatAxis([this.values, newV], 2);
1002
- this.keys.dispose();
1003
- this.values.dispose();
1004
- newK.dispose();
1005
- newV.dispose();
1006
- this.keys = ck;
1007
- this.values = cv;
1008
- } else {
1009
- this.keys = newK;
1010
- this.values = newV;
1011
- }
1012
- this.#idx = prev;
1013
- }
1014
-
1015
- const trimSize = this.keys!.shape[2]! - this.maxSize;
1016
- if (trimSize > 0) {
1017
- const tk = this.#trim(trimSize, this.keys!, null);
1018
- const tv = this.#trim(trimSize, this.values!, null);
1019
- this.keys!.dispose();
1020
- this.values!.dispose();
1021
- this.keys = tk;
1022
- this.values = tv;
1023
- this.#idx = this.maxSize;
1024
- }
1025
-
1026
- if (this.#idx === this.maxSize) this.#idx = 0; // rotate (keep=0)
1027
-
1028
- const [, , SK, DK] = this.keys!.shape as [number, number, number, number];
1029
- const k2 = ops.sliceUpdate(this.keys!, k, [0, 0, this.#idx, 0], [B, H, this.#idx + S, DK]);
1030
- const v2 = ops.sliceUpdate(this.values!, v, [0, 0, this.#idx, 0], [B, H, this.#idx + S, vD]);
1031
- this.keys!.dispose();
1032
- this.values!.dispose();
1033
- this.keys = k2;
1034
- this.values = v2;
1035
- this.offset += S;
1036
- this.#idx += S;
1037
-
1038
- if (this.offset < this.maxSize) {
1039
- const kOut = this.keys.slice([0, 0, 0, 0], [B, H, this.offset, DK]);
1040
- const vOut = this.values.slice([0, 0, 0, 0], [B, H, this.offset, vD]);
1041
- return [kOut, vOut];
1042
- }
1043
- return this.#fetchAll();
1044
- }
1045
-
1046
- #fetchAll(): [MlxArray, MlxArray] {
1047
- const [B, H, S, D] = this.keys!.shape as [number, number, number, number];
1048
- const vD = this.values!.shape[3]!;
1049
- return [
1050
- this.keys!.slice([0, 0, 0, 0], [B, H, S, D]),
1051
- this.values!.slice([0, 0, 0, 0], [B, H, S, vD]),
1052
- ];
1053
- }
1054
-
1055
- updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
1056
- return k.shape[2]! === 1 ? this.#updateInPlace(k, v) : this.#updateConcat(k, v);
1057
- }
1058
-
1059
- makeMask(N: number, windowSize: number | null): Mask {
1060
- const window = windowSize ?? this.maxSize;
1061
- if (N > 1) {
1062
- const offset = Math.min(this.maxSize - 1, this.offset);
1063
- if (offset + N > window)
1064
- return { mode: "array", arr: createCausalMask(N, offset, window) };
1065
- return { mode: "causal", arr: null };
1066
- }
1067
- // N == 1: eviction enforces the window (window === maxSize for gemma4)
1068
- return { mode: "", arr: null };
1069
- }
1070
-
1071
- state(): MlxArray[] {
1072
- return this.keys && this.values ? [this.keys, this.values] : [];
1073
- }
1074
-
1075
- /** Port of mlx-lm RotatingKVCache.is_trimmable/trim: only valid while
1076
- * the ring has never wrapped (still in temporal order). */
1077
- isTrimmable(): boolean {
1078
- return this.offset < this.maxSize;
1079
- }
1080
-
1081
- trim(n: number, bypass = false): void {
1082
- const k = Math.min(this.offset, n);
1083
- if (!bypass) {
1084
- // mlx-lm semantics: only valid pre-wrap (guarded by isTrimmable upstream).
1085
- this.offset -= k;
1086
- this.#idx -= k;
1087
- return;
1088
- }
1089
- // Spec-decode rollback after a >1 #updateConcat write: the buffer is in
1090
- // temporal order with #idx === bufferLen and the newest tokens are the last
1091
- // rows, so physically slice the last k off and keep #idx === new length.
1092
- // (Only sound right after a concat write with k ≤ γ — DSpark's only caller.)
1093
- this.offset -= k;
1094
- if (k > 0 && this.keys && this.values) {
1095
- const [B, H, S, D] = this.keys.shape as [number, number, number, number];
1096
- const vD = this.values.shape[3]!;
1097
- const nk = this.keys.slice([0, 0, 0, 0], [B, H, S - k, D]);
1098
- const nv = this.values.slice([0, 0, 0, 0], [B, H, S - k, vD]);
1099
- this.keys.dispose();
1100
- this.values.dispose();
1101
- this.keys = nk;
1102
- this.values = nv;
1103
- this.#idx = nk.shape[2]!;
1104
- } else {
1105
- this.#idx -= k;
1106
- }
1107
- }
1108
-
1109
- get ringIdx(): number {
1110
- return this.#idx;
1111
- }
1112
-
1113
- /** Compiled decode: host-side bookkeeping of #updateInPlace (L=1) —
1114
- * growth, oversize trim, rotation — without the write. */
1115
- prepareDecodeStep(): DecodeStepPlan {
1116
- const prev = this.offset;
1117
- if (!this.keys || !this.values) throw new Error("compiled decode on an empty cache");
1118
- if (prev >= this.keys.shape[2]! && this.keys.shape[2]! < this.maxSize) {
1119
- const [B, H, , D] = this.keys.shape as [number, number, number, number];
1120
- const vD = this.values.shape[3]!;
1121
- const newSize = Math.min(RotatingKVCache.STEP, this.maxSize - prev);
1122
- const newK = ops.zeros([B, H, newSize, D], this.keys.dtype);
1123
- const newV = ops.zeros([B, H, newSize, vD], this.values.dtype);
1124
- const ck = ops.concatAxis([this.keys, newK], 2);
1125
- const cv = ops.concatAxis([this.values, newV], 2);
1126
- for (const a of [this.keys, this.values, newK, newV]) a.dispose();
1127
- this.keys = ck;
1128
- this.values = cv;
1129
- this.#idx = prev;
1130
- }
1131
- const trimSize = this.keys.shape[2]! - this.maxSize;
1132
- if (trimSize > 0) {
1133
- const tk = this.#trim(trimSize, this.keys, null);
1134
- const tv = this.#trim(trimSize, this.values, null);
1135
- this.keys.dispose();
1136
- this.values.dispose();
1137
- this.keys = tk;
1138
- this.values = tv;
1139
- this.#idx = this.maxSize;
1140
- }
1141
- if (this.#idx === this.maxSize) this.#idx = 0; // rotate (keep=0)
1142
- // Pre-window-fill the attended set is the [0..offset] prefix plus the
1143
- // new row (today's slice of the updated buffer); once writes wrap,
1144
- // it's the whole ring in ring order, which only the in-graph
1145
- // write-then-read-all form reproduces bit-exactly.
1146
- const fetch = prev + 1 < this.maxSize && this.#idx === prev ? "concat" : "ring";
1147
- return { fetch, writePos: this.#idx, activeLen: prev };
1148
- }
1149
-
1150
- /** Compiled decode, concat fetch: the write half. Takes ownership of
1151
- * kNew/vNew; returns the updated buffers to async-eval. */
1152
- writeDecodeStep(kNew: MlxArray, vNew: MlxArray): MlxArray[] {
1153
- const [B, H, , D] = this.keys!.shape as [number, number, number, number];
1154
- const vD = this.values!.shape[3]!;
1155
- const k2 = ops.sliceUpdate(this.keys!, kNew, [0, 0, this.#idx, 0], [B, H, this.#idx + 1, D]);
1156
- const v2 = ops.sliceUpdate(this.values!, vNew, [0, 0, this.#idx, 0], [B, H, this.#idx + 1, vD]);
1157
- this.keys!.dispose();
1158
- this.values!.dispose();
1159
- kNew.dispose();
1160
- vNew.dispose();
1161
- this.keys = k2;
1162
- this.values = v2;
1163
- this.offset += 1;
1164
- this.#idx += 1;
1165
- return [k2, v2];
1166
- }
1167
-
1168
- /** Compiled decode, ring fetch: adopt the in-graph-updated buffers
1169
- * (closure outputs; ownership transfers here). */
1170
- adoptDecodeStep(newKeys: MlxArray, newValues: MlxArray): MlxArray[] {
1171
- this.keys!.dispose();
1172
- this.values!.dispose();
1173
- this.keys = newKeys;
1174
- this.values = newValues;
1175
- this.offset += 1;
1176
- this.#idx += 1;
1177
- return [newKeys, newValues];
1178
- }
1179
-
1180
- /** Chronological (K, V) view, valid length min(offset, maxSize)
1181
- * (port of optiq kv_view._read_cache_temporal). */
1182
- captureDonorRows(): KvDonorRows {
1183
- const [keys, values] = this.temporalView();
1184
- const B = keys.shape[0]!, width = keys.shape[2]!;
1185
- return { keys, values, offsets: Array(B).fill(this.offset),
1186
- starts: Array(B).fill(0), ends: Array(B).fill(width) };
1187
- }
1188
-
1189
- temporalView(): [MlxArray, MlxArray] {
1190
- if (!this.keys || !this.values) throw new Error("cache is empty");
1191
- const tk = this.#temporalOrder(this.keys);
1192
- const tv = this.#temporalOrder(this.values);
1193
- const valid = Math.min(this.offset, this.maxSize);
1194
- const cut = (a: MlxArray): MlxArray => {
1195
- const [B, H, , D] = a.shape as [number, number, number, number];
1196
- const s = a.slice([0, 0, 0, 0], [B, H, valid, D]);
1197
- a.dispose();
1198
- return s;
1199
- };
1200
- return [cut(tk), cut(tv)];
1201
- }
1202
-
1203
- /** Adopt persisted state (takes ownership of the arrays). */
1204
- restoreState(keys: MlxArray, values: MlxArray, offset: number, idx: number): void {
1205
- this.dispose();
1206
- this.keys = keys;
1207
- this.values = values;
1208
- this.offset = offset;
1209
- this.#idx = idx;
1210
- }
1211
-
1212
- /** Port of optiq rotating.py _replay_into_quantized: quantize the
1213
- * whole buffer AS-LAID-OUT (ring order, not temporal order — correct
1214
- * because ringIdx is preserved with it) into a
1215
- * RotatingQuantizedKVCache. */
1216
- toQuantized(groupSize: number, bits: number): RotatingQuantizedKVCache {
1217
- const q = new RotatingQuantizedKVCache(this.maxSize, groupSize, bits);
1218
- if (this.keys && this.values) {
1219
- q.keys = ops.quantize(this.keys, groupSize, bits);
1220
- q.values = ops.quantize(this.values, groupSize, bits);
1221
- }
1222
- q.offset = this.offset;
1223
- q.ringIdx = this.#idx;
1224
- this.dispose();
1225
- return q;
1226
- }
1227
-
1228
- dispose(): void {
1229
- this.keys?.dispose();
1230
- this.values?.dispose();
1231
- this.keys = this.values = null;
1232
- this.offset = 0;
1233
- this.#idx = 0;
1234
- }
1235
- }
1236
-
1237
- export const mapTriple = (
1238
- t: ops.QuantizedTensor, f: (a: MlxArray) => MlxArray,
1239
- ): ops.QuantizedTensor => ({ packed: f(t.packed), scales: f(t.scales), biases: f(t.biases) });
1240
-
1241
- export const disposeTriple = (t: ops.QuantizedTensor): void => {
1242
- t.packed.dispose();
1243
- t.scales.dispose();
1244
- t.biases.dispose();
1245
- };
1246
-
1247
- /** Quantized rotating (sliding-window) KV cache — port of optiq
1248
- * runtime/kv/rotating.py RotatingQuantizedKVCache with keep=0
1249
- * (gemma4's configuration): RotatingKVCache's ring mechanics over
1250
- * (packed, scales, biases) triples, storage convention identical to
1251
- * QuantizedKVCache. Returns ACTIVE QUANTIZED SLICES — the oracle's
1252
- * module docstring claims dequantize-on-read, but its code does not
1253
- * (Phase 9 finding; port follows the code). optiq's producer-registry
1254
- * + SDPA patches are unnecessary here: our SharedKv carries
1255
- * groupSize/bits through the donor→sharer plumbing explicitly. */
1256
- export class RotatingQuantizedKVCache implements Cache {
1257
- get quantizedAttention(): QuantizedAttentionState { return this; }
1258
- declare minimumReusableOffset?: number;
1259
- static readonly STEP = 256;
1260
- /** Set only by compiled-decode trace adapters (see Cache). */
1261
- readonly ropeOffsetArr?: MlxArray;
1262
- keys: ops.QuantizedTensor | null = null;
1263
- values: ops.QuantizedTensor | null = null;
1264
- offset = 0;
1265
- /** Ring write index (oracle `_idx`); public so toQuantized replay
1266
- * and persistence can carry it. */
1267
- ringIdx = 0;
1268
- readonly maxSize: number;
1269
-
1270
- constructor(maxSize: number, readonly groupSize: number, readonly bits: number) {
1271
- this.maxSize = maxSize;
1272
- }
1273
-
1274
- signature(): string { return `kv:rotating-quant:${this.bits}:${this.groupSize}`; }
1275
-
1276
- bytesPerToken(): number {
1277
- const steps = this.keys?.packed.shape[2] ?? 0;
1278
- if (steps === 0 || !this.keys || !this.values) return 0;
1279
- const tensors = [
1280
- this.keys.packed, this.keys.scales, this.keys.biases,
1281
- this.values.packed, this.values.scales, this.values.biases,
1282
- ];
1283
- return tensors.reduce((bytes, array) => bytes + array.nbytes, 0) / steps;
1284
- }
1285
-
1286
- updateAndFetch(): [MlxArray, MlxArray] {
1287
- throw new Error("RotatingQuantizedKVCache: use updateAndFetchQuantized");
1288
- }
1289
-
1290
- #seqLen(): number {
1291
- return this.keys ? this.keys.packed.shape[2]! : 0;
1292
- }
1293
-
1294
- /** Empty (packed, scales, biases) triple of T tokens (oracle _alloc_pair). */
1295
- #allocPair(B: number, H: number, T: number, dim: number, dtype: Dtype): ops.QuantizedTensor {
1296
- const elPerInt = 32 / this.bits;
1297
- return {
1298
- packed: ops.zeros([B, H, T, dim / elPerInt], Dtype.uint32),
1299
- scales: ops.zeros([B, H, T, dim / this.groupSize], dtype),
1300
- biases: ops.zeros([B, H, T, dim / this.groupSize], dtype),
1301
- };
1302
- }
1303
-
1304
- /** Ring contents rearranged into temporal order, per component (keep=0). */
1305
- #temporalOrder(t: ops.QuantizedTensor): ops.QuantizedTensor {
1306
- const S = t.packed.shape[2]!;
1307
- const cut = (a: MlxArray, from: number, to: number): MlxArray => {
1308
- const [B, H, , D] = a.shape as [number, number, number, number];
1309
- return a.slice([0, 0, from, 0], [B, H, to, D]);
1310
- };
1311
- if (this.ringIdx === S) return mapTriple(t, (a) => cut(a, 0, S));
1312
- if (this.ringIdx < this.offset) {
1313
- return mapTriple(t, (a) => {
1314
- const tail = cut(a, this.ringIdx, S);
1315
- const head = cut(a, 0, this.ringIdx);
1316
- const out = ops.concatAxis([tail, head], 2);
1317
- tail.dispose();
1318
- head.dispose();
1319
- return out;
1320
- });
1321
- }
1322
- return mapTriple(t, (a) => cut(a, 0, this.ringIdx));
1323
- }
1324
-
1325
- #trim(trimSize: number, t: ops.QuantizedTensor, append: ops.QuantizedTensor | null): ops.QuantizedTensor {
1326
- const part = (a: MlxArray, ap: MlxArray | null): MlxArray => {
1327
- const [B, H, S, D] = a.shape as [number, number, number, number];
1328
- const base = a.slice([0, 0, trimSize > 0 ? trimSize : 0, 0], [B, H, S, D]);
1329
- if (!ap) return base;
1330
- const out = ops.concatAxis([base, ap], 2);
1331
- base.dispose();
1332
- return out;
1333
- };
1334
- return {
1335
- packed: part(t.packed, append?.packed ?? null),
1336
- scales: part(t.scales, append?.scales ?? null),
1337
- biases: part(t.biases, append?.biases ?? null),
1338
- };
1339
- }
1340
-
1341
- #updateConcat(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
1342
- const S = k.shape[2]!;
1343
- const kq = ops.quantize(k, this.groupSize, this.bits);
1344
- const vq = ops.quantize(v, this.groupSize, this.bits);
1345
- if (!this.keys || !this.values) {
1346
- this.keys = kq;
1347
- this.values = vq;
1348
- } else {
1349
- const tk = this.#temporalOrder(this.keys);
1350
- const tv = this.#temporalOrder(this.values);
1351
- disposeTriple(this.keys);
1352
- disposeTriple(this.values);
1353
- this.ringIdx = tk.packed.shape[2]!;
1354
- const trimSize = this.ringIdx - this.maxSize + 1;
1355
- this.keys = this.#trim(trimSize, tk, kq);
1356
- this.values = this.#trim(trimSize, tv, vq);
1357
- for (const t of [tk, tv, kq, vq]) disposeTriple(t);
1358
- }
1359
- this.offset += S;
1360
- this.ringIdx = this.#seqLen();
1361
- return this.#activeSlices();
1362
- }
1363
-
1364
- #updateInPlace(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
1365
- const [B, H, S, D] = k.shape as [number, number, number, number];
1366
- const vD = v.shape[3]!;
1367
- const prev = this.offset;
1368
-
1369
- if (!this.keys || (prev >= this.#seqLen() && this.#seqLen() < this.maxSize)) {
1370
- const newSize = Math.min(RotatingQuantizedKVCache.STEP, this.maxSize - prev);
1371
- const newK = this.#allocPair(B, H, newSize, D, k.dtype);
1372
- const newV = this.#allocPair(B, H, newSize, vD, v.dtype);
1373
- if (this.keys && this.values) {
1374
- const grow = (old: ops.QuantizedTensor, add: ops.QuantizedTensor): ops.QuantizedTensor => {
1375
- const cat = (a: MlxArray, b: MlxArray): MlxArray => {
1376
- const out = ops.concatAxis([a, b], 2);
1377
- a.dispose();
1378
- b.dispose();
1379
- return out;
1380
- };
1381
- return {
1382
- packed: cat(old.packed, add.packed),
1383
- scales: cat(old.scales, add.scales),
1384
- biases: cat(old.biases, add.biases),
1385
- };
1386
- };
1387
- this.keys = grow(this.keys, newK);
1388
- this.values = grow(this.values, newV);
1389
- } else {
1390
- this.keys = newK;
1391
- this.values = newV;
1392
- }
1393
- this.ringIdx = prev;
1394
- }
1395
-
1396
- const trimSize = this.#seqLen() - this.maxSize;
1397
- if (trimSize > 0) {
1398
- const tk = this.#trim(trimSize, this.keys!, null);
1399
- const tv = this.#trim(trimSize, this.values!, null);
1400
- disposeTriple(this.keys!);
1401
- disposeTriple(this.values!);
1402
- this.keys = tk;
1403
- this.values = tv;
1404
- this.ringIdx = this.maxSize;
1405
- }
1406
-
1407
- if (this.ringIdx === this.maxSize) this.ringIdx = 0; // rotate (keep=0)
1408
-
1409
- const kq = ops.quantize(k, this.groupSize, this.bits);
1410
- const vq = ops.quantize(v, this.groupSize, this.bits);
1411
- const writeAt = (dst: ops.QuantizedTensor, src: ops.QuantizedTensor): ops.QuantizedTensor => ({
1412
- packed: this.#assign(dst.packed, src.packed, S),
1413
- scales: this.#assign(dst.scales, src.scales, S),
1414
- biases: this.#assign(dst.biases, src.biases, S),
1415
- });
1416
- this.keys = writeAt(this.keys!, kq);
1417
- this.values = writeAt(this.values!, vq);
1418
- disposeTriple(kq);
1419
- disposeTriple(vq);
1420
-
1421
- this.offset += S;
1422
- this.ringIdx += S;
1423
- return this.#activeSlices();
1424
- }
1425
-
1426
- #assign(dst: MlxArray, src: MlxArray, S: number): MlxArray {
1427
- const [B, H, , D] = dst.shape as [number, number, number, number];
1428
- const out = ops.sliceUpdate(dst, src, [0, 0, this.ringIdx, 0], [B, H, this.ringIdx + S, D]);
1429
- dst.dispose();
1430
- return out;
1431
- }
1432
-
1433
- /** Active window as quantized triples (oracle _active_slices; fresh
1434
- * view handles so callers own what they dispose). */
1435
- #activeSlices(): [ops.QuantizedTensor, ops.QuantizedTensor] {
1436
- const upTo = this.offset < this.maxSize ? this.offset : this.#seqLen();
1437
- const cut = (a: MlxArray): MlxArray => {
1438
- const [B, H, , D] = a.shape as [number, number, number, number];
1439
- return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
1440
- };
1441
- return [mapTriple(this.keys!, cut), mapTriple(this.values!, cut)];
1442
- }
1443
-
1444
- /** Quantize incoming k/v and write into the ring; returns active
1445
- * quantized triples (S=1 in place, S>1 via temporal-order + concat). */
1446
- updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
1447
- return k.shape[2]! === 1 ? this.#updateInPlace(k, v) : this.#updateConcat(k, v);
1448
- }
1449
-
1450
- /** Same mask formula as RotatingKVCache (inherited in the oracle). */
1451
- makeMask(N: number, windowSize: number | null): Mask {
1452
- const window = windowSize ?? this.maxSize;
1453
- if (N > 1) {
1454
- const offset = Math.min(this.maxSize - 1, this.offset);
1455
- if (offset + N > window)
1456
- return { mode: "array", arr: createCausalMask(N, offset, window) };
1457
- return { mode: "causal", arr: null };
1458
- }
1459
- return { mode: "", arr: null };
1460
- }
1461
-
1462
- state(): MlxArray[] {
1463
- if (!this.keys || !this.values) return [];
1464
- return [
1465
- this.keys.packed, this.keys.scales, this.keys.biases,
1466
- this.values.packed, this.values.scales, this.values.biases,
1467
- ];
1468
- }
1469
-
1470
- /** Ring rule (inherited semantics): trimmable only before wrap. */
1471
- isTrimmable(): boolean {
1472
- return this.offset < this.maxSize;
1473
- }
1474
-
1475
- trim(n: number, bypass = false): void {
1476
- const k = Math.min(this.offset, n);
1477
- if (!bypass) {
1478
- this.offset -= k;
1479
- this.ringIdx -= k;
1480
- return;
1481
- }
1482
- // Spec-decode rollback after a >1 concat write — physically slice the last k
1483
- // off each (packed, scales, biases) component and keep ringIdx === seqLen.
1484
- this.offset -= k;
1485
- if (k > 0 && this.keys && this.values) {
1486
- const S = this.#seqLen();
1487
- const cut = (a: MlxArray): MlxArray => {
1488
- const [B, H, , D] = a.shape as [number, number, number, number];
1489
- return a.slice([0, 0, 0, 0], [B, H, S - k, D]);
1490
- };
1491
- const nk = mapTriple(this.keys, cut);
1492
- const nv = mapTriple(this.values, cut);
1493
- disposeTriple(this.keys);
1494
- disposeTriple(this.values);
1495
- this.keys = nk;
1496
- this.values = nv;
1497
- this.ringIdx = this.#seqLen();
1498
- } else {
1499
- this.ringIdx -= k;
1500
- }
1501
- }
1502
-
1503
- /** Adopt persisted state (takes ownership of the triples' arrays) —
1504
- * ring order as-laid-out, ringIdx carried with it (kv-store persistence). */
1505
- restoreState(keys: ops.QuantizedTensor, values: ops.QuantizedTensor, offset: number, idx: number): void {
1506
- this.dispose();
1507
- this.keys = keys;
1508
- this.values = values;
1509
- this.offset = offset;
1510
- this.ringIdx = idx;
1511
- }
1512
-
1513
- /** Chronological (K, V) triples cut to the valid window — the quantized
1514
- * twin of RotatingKVCache.temporalView (batched merge reads, Phase 3
1515
- * milestone 2). Caller owns the returned views. */
1516
- captureDonorAttention(): KvDonorAttention {
1517
- const [keys, values] = this.temporalView();
1518
- const B = keys.packed.shape[0]!, width = keys.packed.shape[2]!;
1519
- return { width, dtype: keys.scales.dtype, offsets: Array(B).fill(this.offset),
1520
- starts: Array(B).fill(0), ends: Array(B).fill(width),
1521
- attend: (q, scale, mask) => quantizedSdpa(q, keys, values, scale, mask, this.groupSize, this.bits),
1522
- dispose() { disposeTriple(keys); disposeTriple(values); },
1523
- };
1524
- }
1525
-
1526
- temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
1527
- if (!this.keys || !this.values) throw new Error("cache is empty");
1528
- const valid = Math.min(this.offset, this.maxSize);
1529
- const cutValid = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
1530
- mapTriple(t, (a) => {
1531
- const [B, H, , D] = a.shape as [number, number, number, number];
1532
- return a.slice([0, 0, 0, 0], [B, H, valid, D]);
1533
- });
1534
- const tk = this.#temporalOrder(this.keys);
1535
- const tv = this.#temporalOrder(this.values);
1536
- const out: [ops.QuantizedTensor, ops.QuantizedTensor] = [cutValid(tk), cutValid(tv)];
1537
- disposeTriple(tk);
1538
- disposeTriple(tv);
1539
- return out;
1540
- }
1541
-
1542
- /** Oracle: to_quantized on an already-quantized rotating cache is
1543
- * idempotent. */
1544
- toQuantized(): RotatingQuantizedKVCache {
1545
- return this;
1546
- }
1547
-
1548
- /** Compiled decode: host-side bookkeeping of #updateInPlace (L=1) —
1549
- * growth, oversize trim, rotation — without quantize/write. */
1550
- prepareDecodeStep(): DecodeStepPlan {
1551
- const prev = this.offset;
1552
- if (!this.keys || !this.values) throw new Error("compiled decode on an empty cache");
1553
- if (prev >= this.#seqLen() && this.#seqLen() < this.maxSize) {
1554
- const [B, H, , pD] = this.keys.packed.shape as [number, number, number, number];
1555
- const elPerInt = 32 / this.bits;
1556
- const kD = pD * elPerInt;
1557
- const vD = this.values.packed.shape[3]! * elPerInt;
1558
- const dtype = this.keys.scales.dtype;
1559
- const newSize = Math.min(RotatingQuantizedKVCache.STEP, this.maxSize - prev);
1560
- const newK = this.#allocPair(B, H, newSize, kD, dtype);
1561
- const newV = this.#allocPair(B, H, newSize, vD, dtype);
1562
- const grow = (old: ops.QuantizedTensor, add: ops.QuantizedTensor): ops.QuantizedTensor => {
1563
- const cat = (a: MlxArray, b: MlxArray): MlxArray => {
1564
- const out = ops.concatAxis([a, b], 2);
1565
- a.dispose();
1566
- b.dispose();
1567
- return out;
1568
- };
1569
- return {
1570
- packed: cat(old.packed, add.packed),
1571
- scales: cat(old.scales, add.scales),
1572
- biases: cat(old.biases, add.biases),
1573
- };
1574
- };
1575
- this.keys = grow(this.keys, newK);
1576
- this.values = grow(this.values, newV);
1577
- this.ringIdx = prev;
1578
- }
1579
- const trimSize = this.#seqLen() - this.maxSize;
1580
- if (trimSize > 0) {
1581
- const tk = this.#trim(trimSize, this.keys, null);
1582
- const tv = this.#trim(trimSize, this.values, null);
1583
- disposeTriple(this.keys);
1584
- disposeTriple(this.values);
1585
- this.keys = tk;
1586
- this.values = tv;
1587
- this.ringIdx = this.maxSize;
1588
- }
1589
- if (this.ringIdx === this.maxSize) this.ringIdx = 0; // rotate (keep=0)
1590
- const fetch = prev + 1 < this.maxSize && this.ringIdx === prev ? "concat" : "ring";
1591
- return { fetch, writePos: this.ringIdx, activeLen: prev };
1592
- }
1593
-
1594
- /** Compiled decode, concat fetch: six sliceUpdates of the in-graph-
1595
- * quantized step row. Takes ownership; returns updated buffers. */
1596
- writeDecodeStep(rows: MlxArray[]): MlxArray[] {
1597
- const w = (d: MlxArray, srcRow: MlxArray): MlxArray => {
1598
- const [b, h, , dd] = d.shape as [number, number, number, number];
1599
- const u = ops.sliceUpdate(d, srcRow, [0, 0, this.ringIdx, 0], [b, h, this.ringIdx + 1, dd]);
1600
- d.dispose();
1601
- srcRow.dispose();
1602
- return u;
1603
- };
1604
- this.keys = {
1605
- packed: w(this.keys!.packed, rows[0]!),
1606
- scales: w(this.keys!.scales, rows[1]!),
1607
- biases: w(this.keys!.biases, rows[2]!),
1608
- };
1609
- this.values = {
1610
- packed: w(this.values!.packed, rows[3]!),
1611
- scales: w(this.values!.scales, rows[4]!),
1612
- biases: w(this.values!.biases, rows[5]!),
1613
- };
1614
- this.offset += 1;
1615
- this.ringIdx += 1;
1616
- return this.state();
1617
- }
1618
-
1619
- /** Compiled decode, ring fetch: adopt the six in-graph-updated buffers
1620
- * (closure outputs, in state() order; ownership transfers here). */
1621
- adoptDecodeStep(bufs: MlxArray[]): MlxArray[] {
1622
- disposeTriple(this.keys!);
1623
- disposeTriple(this.values!);
1624
- this.keys = { packed: bufs[0]!, scales: bufs[1]!, biases: bufs[2]! };
1625
- this.values = { packed: bufs[3]!, scales: bufs[4]!, biases: bufs[5]! };
1626
- this.offset += 1;
1627
- this.ringIdx += 1;
1628
- return bufs;
1629
- }
1630
-
1631
- dispose(): void {
1632
- if (this.keys) disposeTriple(this.keys);
1633
- if (this.values) disposeTriple(this.values);
1634
- this.keys = this.values = null;
1635
- this.offset = 0;
1636
- this.ringIdx = 0;
1637
- }
1638
- }
1639
-
1640
- /** TurboQuant KV cache — v1 (docs/design/turboquant.md): dequantize-
1641
- * on-fetch. Deliberately does NOT subclass KVCache/RotatingKVCache (or
1642
- * QuantizedKVCache) — a novel class fails every generated-file
1643
- * `#matches()` guard and every batching `instanceof` allow-list, so it
1644
- * gets solo-only routing and monolith fallback for free, the same way
1645
- * QuantizedKVCache does today. Only full-attention (non-rotating)
1646
- * layers use this in v1; sliding-window layers stay bf16.
1647
- *
1648
- * Storage: 5 arrays per the vllm-metal reference's `turbo_quant_encode`
1649
- * tuple — kIdx (int8 unpacked at kBits=8, else bit-packed uint8),
1650
- * kScales/kZeros (fp16, one pair per 32-group), vPacked (bit-packed
1651
- * uint8, no-op at vBits=8), vScales (fp16, one per 32-group).
1652
- * updateAndFetch quantizes ONLY the newly appended tokens (unlike
1653
- * QuantizedKVCache's whole-buffer-toQuantized conversion path — the
1654
- * streaming append is the whole point of a live cache) and returns the
1655
- * DEQUANTIZED bf16 active window so ops.sdpa runs unmodified — v1 pays
1656
- * a full-window dequant every step; the deferred-InvFWHT trick is a
1657
- * documented non-goal until the quality gate passes. */
1658
- export class TurboQuantKVCache implements Cache {
1659
- minimumReusableOffset = 0;
1660
- readonly stateNeedsDispose = true;
1661
- static readonly STEP = 256;
1662
- /** Set only by compiled-decode trace adapters (see Cache) — TurboQuant
1663
- * is not a compiled-decode participant in v1 (novel class, monolith
1664
- * fallback only), so this is always unset; captured once per step by
1665
- * construction, never refreshed mid-step (batched-rotating-quant.ts:55-63). */
1666
- readonly ropeOffsetArr?: MlxArray;
1667
- #kv: TurboQuantTensor | null = null;
1668
- offset = 0;
1669
- /** Validated lazily on first update, once head_dim is known. */
1670
- #headDim: number | null = null;
1671
- /** Experimental operation selection, captured once for this cache. */
1672
- readonly #codec: KvCodec<TurboQuantTensor>;
1673
-
1674
- constructor(readonly kBits: number, readonly vBits: number) {
1675
- this.#codec = new TurboQuantCodec(kBits, vBits, process.env.MLX_BUN_TURBOQUANT_FUSED_DECODE === "1");
1676
- }
1677
-
1678
- captureDonorRows(): KvDonorRows {
1679
- if (!this.#kv || this.#headDim === null) throw new Error("cache is empty");
1680
- const [keys, values] = this.#decode(this.#kv, this.offset, this.#headDim, false);
1681
- const B = keys.shape[0]!;
1682
- return { keys, values, offsets: Array(B).fill(this.offset),
1683
- starts: Array(B).fill(0), ends: Array(B).fill(this.offset) };
1684
- }
1685
- captureDonorAttention(): KvDonorAttention { return decodedKvDonorAttention(this.captureDonorRows()); }
1686
-
1687
- get rotatedValueAttention(): RotatedValueAttentionState { return this; }
1688
- signature(): string { return `kv:turboquant:${this.kBits}:${this.vBits}`; }
1689
-
1690
- bytesPerToken(): number {
1691
- const steps = this.#seqLen();
1692
- if (steps === 0 || !this.#kv) return 0;
1693
- return [
1694
- this.#kv.kIdx, this.#kv.kScales, this.#kv.kZeros,
1695
- this.#kv.vPacked, this.#kv.vScales,
1696
- ].reduce((bytes, array) => bytes + array.nbytes, 0) / steps;
1697
- }
1698
-
1699
- get headDim(): number | null {
1700
- return this.#headDim;
1701
- }
1702
-
1703
- #validateHeadDim(dim: number): void {
1704
- if (this.#headDim !== null) return;
1705
- const supported = [64, 128, 256, 512];
1706
- if (dim % 32 !== 0 || !supported.includes(dim)) {
1707
- throw new Error(
1708
- `TurboQuantKVCache: head_dim ${dim} must be divisible by 32 and in {${supported.join(",")}}`,
1709
- );
1710
- }
1711
- this.#headDim = dim;
1712
- }
1713
-
1714
- #packedKDim(dim: number): number {
1715
- return tq.packedDim(dim, this.kBits);
1716
- }
1717
- #packedVDim(dim: number): number {
1718
- return tq.packedDim(dim, this.vBits);
1719
- }
1720
- #nGroups(dim: number): number {
1721
- return dim / tq.TURBOQUANT_BLOCK_SIZE;
1722
- }
1723
-
1724
- #alloc(B: number, H: number, T: number, dim: number): TurboQuantTensor {
1725
- const kIdxDtype = this.kBits === 8 ? Dtype.int8 : Dtype.uint8;
1726
- return {
1727
- kIdx: ops.zeros([B, H, T, this.#packedKDim(dim)], kIdxDtype),
1728
- kScales: ops.zeros([B, H, T, this.#nGroups(dim)], Dtype.float16),
1729
- kZeros: ops.zeros([B, H, T, this.#nGroups(dim)], Dtype.float16),
1730
- vPacked: ops.zeros([B, H, T, this.#packedVDim(dim)], Dtype.uint8),
1731
- vScales: ops.zeros([B, H, T, this.#nGroups(dim)], Dtype.float16),
1732
- };
1733
- }
1734
-
1735
- #seqLen(): number {
1736
- return this.#kv ? this.#kv.kIdx.shape[2]! : 0;
1737
- }
1738
-
1739
- #encode(k: MlxArray, v: MlxArray): TurboQuantTensor { return this.#codec.encode(k, v); }
1740
- #decode(t: TurboQuantTensor, upTo: number, headDim: number, deferV = false): [MlxArray, MlxArray] {
1741
- return this.#codec.decode(t, upTo, headDim, deferV);
1742
- }
1743
-
1744
- /** Quantize the newly-appended k/v, append into growable packed
1745
- * storage (256-step growth, mirroring KVCache), advance offset, then
1746
- * dequantize the WHOLE active window (v1 semantics) so the standard
1747
- * ops.sdpa path runs unmodified. */
1748
- updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
1749
- const D = this.#append(k, v);
1750
- return this.#decode(this.#kv!, this.offset, D);
1751
- }
1752
-
1753
- /** Deferred-inverse-FWHT read path: same append, but the returned V
1754
- * window stays in the ROTATED domain. The caller MUST un-rotate its
1755
- * attention output with tq.unrotateValues (see SharedKv.vRotated) —
1756
- * same decode by linearity, O(q·d log d) per step instead of
1757
- * O(T·d log d). Opt-in per attention site; sites that don't know about
1758
- * rotation keep calling updateAndFetch and stay correct. */
1759
- updateAndFetchDeferredV(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
1760
- const D = this.#append(k, v);
1761
- return this.#decode(this.#kv!, this.offset, D, true);
1762
- }
1763
-
1764
- /** Shared append: grow storage, encode the new tokens, slice-write them
1765
- * at the previous offset, advance offset. Returns head_dim. */
1766
- #append(k: MlxArray, v: MlxArray): number {
1767
- const [B, H, L, D] = k.shape as [number, number, number, number];
1768
- this.#validateHeadDim(D);
1769
- const prev = this.offset;
1770
-
1771
- if (!this.#kv || prev + L > this.#seqLen()) {
1772
- const nSteps = Math.floor((TurboQuantKVCache.STEP + L - 1) / TurboQuantKVCache.STEP);
1773
- const newSteps = nSteps * TurboQuantKVCache.STEP;
1774
- if (this.#kv && prev % TurboQuantKVCache.STEP !== 0) {
1775
- const trimTo = (t: TurboQuantTensor): TurboQuantTensor => {
1776
- const cut = (a: MlxArray): MlxArray => {
1777
- const [b, h, , d] = a.shape as [number, number, number, number];
1778
- const s = a.slice([0, 0, 0, 0], [b, h, prev, d]);
1779
- a.dispose();
1780
- return s;
1781
- };
1782
- return { kIdx: cut(t.kIdx), kScales: cut(t.kScales), kZeros: cut(t.kZeros),
1783
- vPacked: cut(t.vPacked), vScales: cut(t.vScales) };
1784
- };
1785
- this.#kv = trimTo(this.#kv);
1786
- }
1787
- const grown = this.#alloc(B, H, newSteps, D);
1788
- if (this.#kv) {
1789
- const cat = (a: MlxArray, b: MlxArray): MlxArray => {
1790
- const out = ops.concatAxis([a, b], 2);
1791
- a.dispose();
1792
- b.dispose();
1793
- return out;
1794
- };
1795
- this.#kv = {
1796
- kIdx: cat(this.#kv.kIdx, grown.kIdx),
1797
- kScales: cat(this.#kv.kScales, grown.kScales),
1798
- kZeros: cat(this.#kv.kZeros, grown.kZeros),
1799
- vPacked: cat(this.#kv.vPacked, grown.vPacked),
1800
- vScales: cat(this.#kv.vScales, grown.vScales),
1801
- };
1802
- } else {
1803
- this.#kv = grown;
1804
- }
1805
- }
1806
-
1807
- this.offset += L;
1808
- const enc = this.#encode(k, v);
1809
- const write = (dst: MlxArray, src: MlxArray): MlxArray => {
1810
- const [b, h, , d] = dst.shape as [number, number, number, number];
1811
- const u = ops.sliceUpdate(dst, src, [0, 0, prev, 0], [b, h, this.offset, d]);
1812
- dst.dispose();
1813
- src.dispose();
1814
- return u;
1815
- };
1816
- this.#kv = {
1817
- kIdx: write(this.#kv!.kIdx, enc.kIdx),
1818
- kScales: write(this.#kv!.kScales, enc.kScales),
1819
- kZeros: write(this.#kv!.kZeros, enc.kZeros),
1820
- vPacked: write(this.#kv!.vPacked, enc.vPacked),
1821
- vScales: write(this.#kv!.vScales, enc.vScales),
1822
- };
1823
-
1824
- return D;
1825
- }
1826
-
1827
- /** Same rule as KVCache/QuantizedKVCache.makeMask (mlx-lm cache.py:114-125):
1828
- * windowless multi-token chunks are the string "causal" at any offset —
1829
- * TurboQuant v1 is full-attention-only, never windowed. */
1830
- makeMask(N: number, windowSize: number | null): Mask {
1831
- if (N === 1) return { mode: "", arr: null };
1832
- if (windowSize === null) return { mode: "causal", arr: null };
1833
- if (this.offset === 0 && N <= windowSize) return { mode: "causal", arr: null };
1834
- return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
1835
- }
1836
-
1837
- /** Trimmed-to-offset arrays in documented order: kIdx, kScales, kZeros,
1838
- * vPacked, vScales (kv-store.ts tensor-slot ordering for kind
1839
- * "turboquant"). Empty cache → []. */
1840
- state(): MlxArray[] {
1841
- if (!this.#kv) return [];
1842
- const cut = (a: MlxArray): MlxArray => {
1843
- const [B, H, , D] = a.shape as [number, number, number, number];
1844
- return a.slice([0, 0, 0, 0], [B, H, this.offset, D]);
1845
- };
1846
- return [cut(this.#kv.kIdx), cut(this.#kv.kScales), cut(this.#kv.kZeros),
1847
- cut(this.#kv.vPacked), cut(this.#kv.vScales)];
1848
- }
1849
-
1850
- isTrimmable(): boolean {
1851
- return true;
1852
- }
1853
-
1854
- /** Drop the last n tokens (byte-safe: groups run along head_dim, so a
1855
- * token-axis slice never splits a group). Same front/back convention
1856
- * as KVCache.trim: offset shrinks, storage is overwritten on next write. */
1857
- trim(n: number): void {
1858
- this.offset = Math.max(0, this.offset - n);
1859
- }
1860
-
1861
- /** Adopt persisted/converted state (takes ownership of the arrays). */
1862
- restoreState(t: TurboQuantTensor, offset: number, headDim: number): void {
1863
- this.dispose();
1864
- this.#kv = t;
1865
- this.offset = offset;
1866
- this.#headDim = headDim;
1867
- }
1868
-
1869
- /** Convert an existing bf16 KVCache/RotatingKVCache's live window in one
1870
- * shot: quantize the whole [.., :offset, :] region, preserve offset,
1871
- * dispose the source's arrays. Mirrors KVCache.toQuantized/
1872
- * RotatingKVCache.toQuantized's contract (source cache is consumed). */
1873
- static fromKVCache(cache: KVCache | RotatingKVCache, kBits: number, vBits: number): TurboQuantKVCache {
1874
- const q = new TurboQuantKVCache(kBits, vBits);
1875
- const [k, v] = cache.keys && cache.values ? cache.temporalView() : [null, null];
1876
- if (k && v) {
1877
- const D = k.shape[3]!;
1878
- q.#validateHeadDim(D);
1879
- const enc = q.#encode(k, v);
1880
- k.dispose();
1881
- v.dispose();
1882
- q.#kv = enc;
1883
- q.#headDim = D;
1884
- }
1885
- q.offset = cache.offset;
1886
- q.minimumReusableOffset = (cache as Cache).minimumReusableOffset ?? 0;
1887
- cache.dispose();
1888
- return q;
1889
- }
1890
-
1891
- dispose(): void {
1892
- if (this.#kv) disposeTurboQuant(this.#kv);
1893
- this.#kv = null;
1894
- this.offset = 0;
1895
- this.minimumReusableOffset = 0;
1896
- }
1897
- }
1898
-
1899
- export const FINFO_MIN: Partial<Record<Dtype, number>> = {
1900
- [Dtype.bfloat16]: -3.3895313892515355e38,
1901
- [Dtype.float16]: -65504,
1902
- [Dtype.float32]: -3.4028234663852886e38,
1903
- };
1904
-
1905
- /** Port of base.py quantized_scaled_dot_product_attention (stock,
1906
- * non-tiled): scores and output via quantized_matmul against the
1907
- * quantized KV triples; GQA via a 5-d reshape. Exported for the
1908
- * fused-vs-unfused parity tests. */
1909
- export function quantizedSdpaUnfused(
1910
- q: MlxArray, kq: ops.QuantizedTensor, vq: ops.QuantizedTensor,
1911
- scale: number, mask: Mask, groupSize: number, bits: number,
1912
- ): MlxArray {
1913
- const [B, H, L, D] = q.shape as [number, number, number, number];
1914
- const KV = kq.packed.shape[1]!;
1915
- const nRep = H / KV;
1916
- const N = kq.packed.shape[2]!;
1917
-
1918
- // scale is 1.0 for Gemma4 (q/k are RMS-normed) — skip the identity multiply.
1919
- let queries = q;
1920
- const owned: MlxArray[] = [];
1921
- if (scale !== 1.0) { queries = ops.mulScalar(q, scale); owned.push(queries); }
1922
-
1923
- let kT = kq;
1924
- let vT = vq;
1925
- if (nRep > 1) {
1926
- const qr = ops.reshape(queries, [B, KV, nRep, L, D]);
1927
- owned.push(qr);
1928
- queries = qr;
1929
- const expand = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
1930
- const e = (a: MlxArray): MlxArray => {
1931
- // expand_dims(axis=-3) like the reference — view-preserving;
1932
- // reshape would copy the strided slice and change kernel paths
1933
- const r = ops.expandDims(a, -3);
1934
- owned.push(r);
1935
- return r;
1936
- };
1937
- return { packed: e(t.packed), scales: e(t.scales), biases: e(t.biases) };
1938
- };
1939
- kT = expand(kq);
1940
- vT = expand(vq);
1941
- }
1942
-
1943
- let scores = ops.quantizedMatmulQT(queries, kT, true, groupSize, bits);
1944
- owned.push(scores);
1945
-
1946
- let maskArr: MlxArray | null = null;
1947
- let ownsMask = false;
1948
- if (mask.mode === "causal") {
1949
- const qIdx = ops.arange(N - L, N, 1, Dtype.int32);
1950
- const kIdx = ops.arange(0, N, 1, Dtype.int32);
1951
- const qCol = ops.reshape(qIdx, [L, 1]);
1952
- const kRow = ops.reshape(kIdx, [1, N]);
1953
- maskArr = ops.greaterEqual(qCol, kRow);
1954
- ownsMask = true;
1955
- for (const a of [qIdx, kIdx, qCol, kRow]) a.dispose();
1956
- } else if (mask.mode === "array") {
1957
- maskArr = mask.arr;
1958
- // Batched padding masks are [B,1,L,S] (buildBatchedDecodeMask). When GQA
1959
- // reshaped the scores to 5-D [B,KV,nRep,L,S], trailing-dim broadcasting
1960
- // would misalign the mask's B with KV — insert the unit axis explicitly:
1961
- // [B,1,1,L,S] broadcasts correctly over (KV, nRep).
1962
- if (maskArr && maskArr.ndim === 4 && nRep > 1) {
1963
- maskArr = ops.expandDims(maskArr, 1);
1964
- ownsMask = true;
1965
- }
1966
- }
1967
- if (maskArr) {
1968
- let masked: MlxArray;
1969
- if (maskArr.dtype === Dtype.bool) {
1970
- using ninf = ops.scalarLike(FINFO_MIN[scores.dtype] ?? -3.4e38, scores);
1971
- masked = ops.where(maskArr, scores, ninf);
1972
- } else masked = ops.add(scores, maskArr);
1973
- if (ownsMask) maskArr.dispose();
1974
- owned.push(masked);
1975
- scores = masked;
1976
- }
1977
-
1978
- const probs = ops.softmaxAxis(scores, -1, true);
1979
- owned.push(probs);
1980
- let out = ops.quantizedMatmulQT(probs, vT, false, groupSize, bits);
1981
- if (nRep > 1) {
1982
- const r = ops.reshape(out, [B, H, L, D]);
1983
- out.dispose();
1984
- out = r;
1985
- }
1986
- for (const a of owned) a.dispose();
1987
- return out;
1988
- }
1989
-
1990
- /** Tile size for the fused quantized-SDPA prefill path (oracle default). */
1991
- export const FUSED_N_CHUNK = 512;
1992
-
1993
- /** Port of optiq fused_quant_sdpa._prefill_flashattn_n_tiled: a
1994
- * FlashAttention-2 loop over the KV N axis with mx.quantized_matmul as
1995
- * the inner kernel. Never materializes the full [..., L, N] scores
1996
- * matrix — the per-tile transient is bounded by FUSED_N_CHUNK, which is
1997
- * the whole point (stock u4 KV prefill peaks ABOVE fp16 KV at long
1998
- * context; see the oracle's module docstring). Op composition order
1999
- * mirrors the oracle exactly: parity is tier a (bit-exact) vs the python
2000
- * fused path; vs quantizedSdpaUnfused it is tier b by construction
2001
- * (online softmax ≠ one-shot precise softmax in bf16). */
2002
- export function quantizedSdpaTiled(
2003
- q: MlxArray, kq: ops.QuantizedTensor, vq: ops.QuantizedTensor,
2004
- scale: number, mask: Mask, groupSize: number, bits: number,
2005
- ): MlxArray {
2006
- const [B, H, L, D] = q.shape as [number, number, number, number];
2007
- const KV = kq.packed.shape[1]!;
2008
- const nRep = H / KV;
2009
- const N = kq.packed.shape[2]!;
2010
-
2011
- // scale is 1.0 for Gemma4 (q/k are RMS-normed) — skip the identity multiply.
2012
- let queries = q;
2013
- let ownsQueries = false;
2014
- if (scale !== 1.0) { queries = ops.mulScalar(q, scale); ownsQueries = true; }
2015
-
2016
- let kT = kq;
2017
- let vT = vq;
2018
- const expanded: MlxArray[] = [];
2019
- if (nRep > 1) {
2020
- const qr = ops.reshape(queries, [B, KV, nRep, L, D]);
2021
- if (ownsQueries) queries.dispose();
2022
- queries = qr;
2023
- ownsQueries = true;
2024
- const expand = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
2025
- const e = (a: MlxArray): MlxArray => {
2026
- const r = ops.expandDims(a, -3);
2027
- expanded.push(r);
2028
- return r;
2029
- };
2030
- return { packed: e(t.packed), scales: e(t.scales), biases: e(t.biases) };
2031
- };
2032
- kT = expand(kq);
2033
- vT = expand(vq);
2034
- }
2035
-
2036
- // The oracle builds a bottom-right-aligned [L, N] bool causal matrix
2037
- // and slices columns per tile; our "array" masks (createCausalMask at
2038
- // offset > 0) are that same matrix already materialized — both slice
2039
- // identically below.
2040
- let maskArr: MlxArray | null = null;
2041
- let ownsMask = false;
2042
- if (mask.mode === "causal") {
2043
- const qIdx = ops.arange(N - L, N, 1, Dtype.int32);
2044
- const kIdx = ops.arange(0, N, 1, Dtype.int32);
2045
- const qCol = ops.reshape(qIdx, [L, 1]);
2046
- const kRow = ops.reshape(kIdx, [1, N]);
2047
- maskArr = ops.greaterEqual(qCol, kRow);
2048
- ownsMask = true;
2049
- for (const a of [qIdx, kIdx, qCol, kRow]) a.dispose();
2050
- } else if (mask.mode === "array") {
2051
- maskArr = mask.arr;
2052
- }
2053
-
2054
- // Slice [..., n0:n1, :] (KV triples) / [..., n0:n1] (mask chunks).
2055
- // Under a compiled-decode trace, subrange Slice swaps to DynamicSlice
2056
- // (identical values; shapeless compile rejects Slice — same pattern as
2057
- // the per-layer-input and MoE top-k slices).
2058
- const sliceAxis = (a: MlxArray, axisFromEnd: 1 | 2, n0: number, n1: number): MlxArray => {
2059
- const dims = a.shape;
2060
- const axis = dims.length - axisFromEnd;
2061
- if (isCompiledTrace()) {
2062
- const start = ops.fromInt32([n0], [1]);
2063
- const size = [...dims];
2064
- size[axis] = n1 - n0;
2065
- const out = ops.sliceDynamic(a, start, [axis], size);
2066
- start.dispose();
2067
- return out;
2068
- }
2069
- const start = dims.map(() => 0);
2070
- const stop = [...dims];
2071
- start[axis] = n0;
2072
- stop[axis] = n1;
2073
- return a.slice(start, stop);
2074
- };
2075
-
2076
- let oAcc: MlxArray | null = null;
2077
- let rowMax: MlxArray | null = null;
2078
- let rowSum: MlxArray | null = null;
2079
-
2080
- for (let n0 = 0; n0 < N; n0 += FUSED_N_CHUNK) {
2081
- const n1 = Math.min(n0 + FUSED_N_CHUNK, N);
2082
- const kChunk: ops.QuantizedTensor = {
2083
- packed: sliceAxis(kT.packed, 2, n0, n1),
2084
- scales: sliceAxis(kT.scales, 2, n0, n1),
2085
- biases: sliceAxis(kT.biases, 2, n0, n1),
2086
- };
2087
- const vChunk: ops.QuantizedTensor = {
2088
- packed: sliceAxis(vT.packed, 2, n0, n1),
2089
- scales: sliceAxis(vT.scales, 2, n0, n1),
2090
- biases: sliceAxis(vT.biases, 2, n0, n1),
2091
- };
2092
-
2093
- let scores = ops.quantizedMatmulQT(queries, kChunk, true, groupSize, bits);
2094
- for (const a of [kChunk.packed, kChunk.scales, kChunk.biases]) a.dispose();
2095
-
2096
- if (maskArr) {
2097
- const maskChunk = sliceAxis(maskArr, 1, n0, n1);
2098
- let masked: MlxArray;
2099
- if (maskChunk.dtype === Dtype.bool) {
2100
- const ninf = ops.scalarLike(FINFO_MIN[scores.dtype] ?? -3.4e38, scores);
2101
- masked = ops.where(maskChunk, scores, ninf);
2102
- ninf.dispose();
2103
- } else {
2104
- masked = ops.add(scores, maskChunk);
2105
- }
2106
- maskChunk.dispose();
2107
- scores.dispose();
2108
- scores = masked;
2109
- }
2110
-
2111
- const chunkMax = ops.maxAxis(scores, -1, true);
2112
- if (oAcc === null) {
2113
- rowMax = chunkMax;
2114
- const shifted = ops.sub(scores, rowMax);
2115
- const exps = ops.exp(shifted);
2116
- shifted.dispose();
2117
- rowSum = ops.sumAxis(exps, -1, true);
2118
- oAcc = ops.quantizedMatmulQT(exps, vChunk, false, groupSize, bits);
2119
- exps.dispose();
2120
- } else {
2121
- const newMax = ops.maximum(rowMax!, chunkMax);
2122
- chunkMax.dispose();
2123
- const maxDiff = ops.sub(rowMax!, newMax);
2124
- const factor = ops.exp(maxDiff);
2125
- maxDiff.dispose();
2126
- const shifted = ops.sub(scores, newMax);
2127
- const exps = ops.exp(shifted);
2128
- shifted.dispose();
2129
- // new_sum = factor * row_sum + sum(exps) (association order kept)
2130
- const carried = ops.mul(factor, rowSum!);
2131
- const sumExps = ops.sumAxis(exps, -1, true);
2132
- const newSum = ops.add(carried, sumExps);
2133
- carried.dispose();
2134
- sumExps.dispose();
2135
- const deltaOut = ops.quantizedMatmulQT(exps, vChunk, false, groupSize, bits);
2136
- exps.dispose();
2137
- // o_acc = o_acc * factor + delta_out
2138
- const scaledAcc = ops.mul(oAcc!, factor);
2139
- factor.dispose();
2140
- const nextAcc = ops.add(scaledAcc, deltaOut);
2141
- scaledAcc.dispose();
2142
- deltaOut.dispose();
2143
- oAcc!.dispose();
2144
- oAcc = nextAcc;
2145
- rowMax!.dispose();
2146
- rowMax = newMax;
2147
- rowSum!.dispose();
2148
- rowSum = newSum;
2149
- }
2150
- scores.dispose();
2151
- for (const a of [vChunk.packed, vChunk.scales, vChunk.biases]) a.dispose();
2152
- }
2153
-
2154
- let out = ops.div(oAcc!, rowSum!);
2155
- oAcc!.dispose();
2156
- rowMax!.dispose();
2157
- rowSum!.dispose();
2158
- if (ownsMask) maskArr!.dispose();
2159
- for (const a of expanded) a.dispose();
2160
- if (ownsQueries) queries.dispose();
2161
- if (nRep > 1) {
2162
- const r = ops.reshape(out, [B, H, L, D]);
2163
- out.dispose();
2164
- out = r;
2165
- }
2166
- return out;
2167
- }
2168
-
2169
- /** Gate for the tiled path — port of fused_quant_sdpa._supported plus the
2170
- * wrapper's mask check, with one documented deviation: the oracle wrapper
2171
- * falls back to unfused on ARRAY masks because mlx-lm always hands it the
2172
- * "causal" string for windowless continuations (its make_mask returns
2173
- * "causal" even at offset > 0); our makeMask materializes the equivalent
2174
- * bool matrix — so masks flagged causalEquivalent tile too (the oracle's
2175
- * INNER function handles them with the same column slicing we use).
2176
- * Window/bidir array masks do NOT tile, matching the reference's
2177
- * scenario-level dispatch exactly (Phase 9: sliding-layer quantized
2178
- * prefill is unfused in optiq too). */
2179
- function fusedSdpaSupported(q: MlxArray, mask: Mask, groupSize: number, bits: number): boolean {
2180
- // Escape hatch mirroring optiq serve's --no-fused-kv: forces the
2181
- // stock unfused path everywhere. Also the A/B lever for
2182
- // scripts/bench-levers.ts fused-prefill. Read per call (cheap next to the
2183
- // FFI work) so tests and paired A/B harnesses can flip it in-process.
2184
- if (runtimeValue("MLX_BUN_NO_FUSED_SDPA") === "1") return false;
2185
- if (bits !== 4 && bits !== 8) return false;
2186
- if (groupSize !== 32 && groupSize !== 64 && groupSize !== 128) return false;
2187
- if (q.dtype !== Dtype.bfloat16 && q.dtype !== Dtype.float16) return false;
2188
- if (mask.mode === "causal" || mask.mode === "") return true; // "" = oracle's mask=None
2189
- if (mask.mode === "array")
2190
- return mask.causalEquivalent === true && mask.arr !== null &&
2191
- mask.arr.shape.length === 2 && mask.arr.dtype === Dtype.bool;
2192
- return false;
2193
- }
2194
-
2195
- /** Quantized-cache SDPA dispatch: L > 1 (prefill/continuation) with a
2196
- * supported config goes through the N-tiled fused path; decode (L = 1)
2197
- * and unsupported configs stay on the stock unfused port. Exported for
2198
- * the dispatch-gate tests. */
2199
- export function quantizedSdpa(
2200
- q: MlxArray, kq: ops.QuantizedTensor, vq: ops.QuantizedTensor,
2201
- scale: number, mask: Mask, groupSize: number, bits: number,
2202
- ): MlxArray {
2203
- const tile = q.shape[2]! > 1;
2204
- if (tile && fusedSdpaSupported(q, mask, groupSize, bits)) {
2205
- // The N-tiled loop CANNOT be traced: its tile count and slice extents
2206
- // bake at trace-time N, so under shapeless replay a growing (concat-
2207
- // phase) quantized cache's newest rows are silently never attended.
2208
- // This throw is the backstop for any compiled caller — it lands in the
2209
- // trace's transactional fallback instead of producing silently wrong
2210
- // attention.
2211
- if (isCompiledTrace())
2212
- throw new Error(
2213
- "quantizedSdpaTiled inside a compiled trace: the tile loop bakes trace-time N " +
2214
- "(growing KV rows would be silently dropped) — run this generation uncompiled",
2215
- );
2216
- return quantizedSdpaTiled(q, kq, vq, scale, mask, groupSize, bits);
2217
- }
2218
- return quantizedSdpaUnfused(q, kq, vq, scale, mask, groupSize, bits);
2219
- }
2220
-
2221
- /** The runtime-only half of fusedSdpaSupported (env flag, dtype, mask
2222
- * kind) — generated models (Phase D) bake the (bits, group_size) half
2223
- * as a compile-time constant and call this for the rest. The combined
2224
- * predicate is exactly fusedSdpaSupported. */
2225
- export function fusedSdpaRuntimeOk(q: MlxArray, mask: Mask): boolean {
2226
- if (runtimeValue("MLX_BUN_NO_FUSED_SDPA") === "1") return false;
2227
- if (q.dtype !== Dtype.bfloat16 && q.dtype !== Dtype.float16) return false;
2228
- if (mask.mode === "causal" || mask.mode === "") return true;
2229
- if (mask.mode === "array")
2230
- return mask.causalEquivalent === true && mask.arr !== null &&
2231
- mask.arr.shape.length === 2 && mask.arr.dtype === Dtype.bool;
2232
- return false;
2233
- }
2234
-
2235
- /** Port of base.py create_causal_mask (bool, [N, offset+N]). */
2236
- export function createCausalMask(N: number, offset: number, windowSize: number | null): MlxArray {
2237
- const rinds = ops.arange(0, offset + N, 1, Dtype.int32);
2238
- const lindsFlat = offset ? ops.arange(offset, offset + N, 1, Dtype.int32) : rinds;
2239
- const linds = ops.reshape(lindsFlat, [N, 1]);
2240
- const rindsB = ops.reshape(rinds, [1, offset + N]);
2241
- let mask = ops.greaterEqual(linds, rindsB);
2242
- if (windowSize !== null) {
2243
- const w = ops.fromInt32([windowSize], []);
2244
- const rPlusW = ops.add(rindsB, w);
2245
- const inWindow = ops.less(linds, rPlusW);
2246
- const combined = ops.logicalAnd(mask, inWindow);
2247
- for (const a of [w, rPlusW, inWindow, mask]) a.dispose();
2248
- mask = combined;
2249
- }
2250
- if (lindsFlat !== rinds) lindsFlat.dispose();
2251
- rinds.dispose();
2252
- linds.dispose();
2253
- rindsB.dispose();
2254
- return mask;
2255
- }
2256
- /** Port of switch_layers.QuantizedSwitchLinear (gather_qmm over stacked
2257
- * expert weights; rhs_indices selects the expert per row). */
2258
- export class QuantizedSwitchLinear {
2259
- constructor(
2260
- readonly w: MlxArray,
2261
- readonly scales: MlxArray,
2262
- readonly biases: MlxArray | null,
2263
- readonly spec: ops.QuantSpec,
2264
- ) {}
2265
-
2266
- static load(weights: Weights, path: string, config: ModelConfig): QuantizedSwitchLinear {
2267
- if (!weights.has(`${path}.scales`))
2268
- throw new Error(`${path}: expected quantized switch linear (no .scales tensor)`);
2269
- const spec = quantFor(config.quantization, path);
2270
- if (!spec) throw new Error(`${path}: no quant spec`);
2271
- // Expert WEIGHT (the ~94% of expert bytes) comes from the page-aligned
2272
- // offload mmap when --expert-offload is active (else resident); scales/
2273
- // biases stay resident (small). Same bytes either way → bit-exact.
2274
- const wName = `${path}.weight`;
2275
- return new QuantizedSwitchLinear(
2276
- expertOffloadArray(wName) ?? weights.tensor(wName),
2277
- weights.tensor(`${path}.scales`),
2278
- weights.has(`${path}.biases`) ? weights.tensor(`${path}.biases`) : null,
2279
- spec,
2280
- );
2281
- }
2282
-
2283
- forward(x: MlxArray, indices: MlxArray, sortedIndices: boolean): MlxArray {
2284
- return ops.gatherQmm(x, this.w, this.scales, this.biases, indices, this.spec, sortedIndices);
2285
- }
2286
- }
2287
- /** Dispose `old` and return `next` — for h = disposing(h, op(h)) chains. */
2288
- export function disposing(old: MlxArray, next: MlxArray): MlxArray {
2289
- old.dispose();
2290
- return next;
2291
- }
2292
-
2293
- // Compiled-decode trace mode (set by compiled-decode.ts around its trace,
2294
- // which runs synchronously on the serialized generation queue — same
2295
- // safety argument as LoraState). Inside a shapeless-compiled graph, mlx's
2296
- // Slice primitive cannot re-infer output shapes, so the two subrange
2297
- // slices on the decode path (per-layer-input split, MoE top-k) swap to
2298
- // DynamicSlice — identical values, shapeless-safe — ONLY while tracing.
2299
- // The uncompiled path keeps the exact oracle op sequence.
2300
- let compiledTrace = false;
2301
- export function setCompiledTrace(v: boolean): void {
2302
- compiledTrace = v;
2303
- }
2304
- export function isCompiledTrace(): boolean {
2305
- return compiledTrace;
2306
- }
2307
- /** Causal(+window) mask OR'd with image×image bidirectional attention. */
2308
- export function bidirMask(L: number, windowSize: number | null, bidir: MlxArray): Mask {
2309
- const causal = createCausalMask(L, 0, windowSize);
2310
- const col = ops.reshape(bidir, [L, 1]);
2311
- const row = ops.reshape(bidir, [1, L]);
2312
- const outer = ops.logicalAnd(col, row);
2313
- col.dispose();
2314
- row.dispose();
2315
- const allow = ops.logicalOr(causal, outer);
2316
- causal.dispose();
2317
- outer.dispose();
2318
- return { mode: "array", arr: allow };
2319
- }
2320
-
2321
- /** tanh(x / cap) * cap with weak-scalar semantics. */
2322
- export function logitSoftcap(logits: MlxArray, cap: number): MlxArray {
2323
- const capArr = ops.scalarLike(cap, logits);
2324
- const scaled = ops.div(logits, capArr);
2325
- const t = ops.tanh(scaled);
2326
- scaled.dispose();
2327
- const out = ops.mul(t, capArr);
2328
- t.dispose();
2329
- capArr.dispose();
2330
- return out;
2331
- }
2332
-
2333
- /** argmax over vocab at the last sequence position of [1, L, V]. */
2334
- export function argmaxLastPosition(logits: MlxArray): number {
2335
- const [, L, V] = logits.shape as [number, number, number];
2336
- const last = logits.slice([0, L - 1, 0], [1, L, V]);
2337
- const am = ops.argmaxAxis(last, -1);
2338
- const id = ops.itemUint32(am);
2339
- last.dispose();
2340
- am.dispose();
2341
- return id;
2342
- }
2343
-
2344
- /** Last-position logits as f32 (for the parity harness). */
2345
- export function lastPositionLogits(logits: MlxArray): Float32Array {
2346
- const [, L, V] = logits.shape as [number, number, number];
2347
- const last = logits.slice([0, L - 1, 0], [1, L, V]);
2348
- const f32 = last.toFloat32();
2349
- last.dispose();
2350
- return f32;
2351
- }