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