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,1395 +0,0 @@
1
- // Gemma 4 text model — line-for-line port of mlx-lm's gemma4_text.py
2
- // (oracle venv site-packages), covering the paths our target models
3
- // exercise: 12B (dense), e4b (per-layer-input embeddings + KV-shared
4
- // layers), 26B-A4B (MoE block: router + gather_qmm experts).
5
- //
6
- // Parity notes (see PLAN.md Phase 2 findings):
7
- // - SDPA scale is 1.0 (Gemma4 normalizes q/k instead).
8
- // - Full-attention layers: global_head_dim 512, 1 global KV head,
9
- // attention_k_eq_v (V = same projection as K, with un-scaled RMS norm);
10
- // ProportionalRoPE rotates only partial_rotary_factor·dims dims.
11
- // - Python-float scalars promote weakly to the array dtype.
12
- // - Replicate mlx python helper implementations exactly (x**3 is
13
- // mx.power, not x·x·x — they round differently in bf16).
14
- //
15
- // Masks: ports base.py create_attention_mask/create_causal_mask. Sliding
16
- // layers use a plain (non-rotating) cache + window masks — numerically
17
- // identical to mlx-lm's RotatingKVCache, at the cost of unbounded cache
18
- // growth past the window (memory optimization deferred).
19
-
20
- import type { ModelConfig } from "../config";
21
- import { quantFor } from "../config";
22
- import type { Weights } from "../weights";
23
- import { MlxArray } from "../mlx/array";
24
- import { Dtype } from "../mlx/ffi";
25
- import * as ops from "../mlx/ops";
26
- import { isExpertTracing, recordRouting } from "../lab/expert-trace/expert-trace";
27
-
28
- // The shared, config-independent machinery lives in gemma4-base.ts
29
- // (Phase B extraction); this file keeps the architecture-specific
30
- // assembly that Phase C generates per model. Re-export the base so
31
- // existing importers keep one entry point.
32
- export * from "./gemma4-base";
33
- import {
34
- argmaxLastPosition,
35
- bidirMask,
36
- disposing,
37
- isCompiledTrace,
38
- KVCache,
39
- logitSoftcap,
40
- LoraState,
41
- QuantizedEmbedding,
42
- QuantizedKVCache,
43
- QuantizedLinear,
44
- QuantizedSwitchLinear,
45
- RMSNorm,
46
- RotatingKVCache,
47
- RotatingQuantizedKVCache,
48
- quantizedSdpa,
49
- type Cache,
50
- type Mask,
51
- type SharedKv,
52
- captureRopeOffsets,
53
- type LoraWeights,
54
- } from "./gemma4-base";
55
- import { mapPackedTokens, type TokenGroup } from "./token-groups";
56
- import { Checkpoint } from "../mlx/checkpoint";
57
- import { flashAttention, getTrainingAttn, flashSupported } from "./flash-attention";
58
- import { unrotateValues as tqUnrotateValues } from "../mlx/turboquant-ops";
59
- import { CompiledFunction } from "../mlx/compile";
60
- import { runtimeFlag } from "../runtime-config";
61
-
62
- /** Verbatim port of mlx_lm/models/gemma4_text.py:
63
- * `@partial(mx.compile, shapeless=True) def geglu(gate, x): return nn.gelu_approx(gate) * x`
64
- * The oracle @mx.compile's this and uses it for EVERY MLP shape (dense MLP
65
- * __call__ line 114, and the SwitchGLU GeGLU activation line 149-150). Our
66
- * default MLP runs the SAME math UNFUSED (standalone ops.geluApprox + ops.mul),
67
- * a different dispatched kernel set + the per-op host tax the MiniCPM5 swiglu
68
- * fix measured at ~5%. This closure fuses geluApprox's ~9 element-wise ops + the
69
- * final mul into one traced-once/replayed graph, matching the oracle's kernel
70
- * set. `geluApprox` is composed op-for-op from the same primitives as the
71
- * oracle's nn.gelu_approx (0.5·x·(1+tanh(√(2/π)(x+0.044715·x³)))), so the
72
- * compiled graph is the oracle's geglu graph. Autograd-safe (verified: the vjp
73
- * through this closure is bit-identical to the plain composition). */
74
- /** True when gemma's geglu activation runs through the compiled closure (matching
75
- * the oracle's @mx.compile geglu) instead of the spelled-out ops.geluApprox+ops.mul.
76
- * DEFAULT ON: compiled geglu is bit-exact to mlx-lm's @mx.compile geglu (same
77
- * libmlx graph → same kernel) AND dispatches one fused kernel set instead of the
78
- * ~9-op path. `MLX_BUN_COMPILED_GEGLU=0` (--compiled-activations off) selects the
79
- * uncompiled composition — same L1 parity, slower (the A/B opt-out). */
80
- export function compiledGegluActive(): boolean {
81
- return runtimeFlag("MLX_BUN_COMPILED_GEGLU", true);
82
- }
83
-
84
- let _gegluClosure: CompiledFunction | null = null;
85
- export function compiledGeglu(gate: MlxArray, up: MlxArray): MlxArray {
86
- if (!_gegluClosure) {
87
- _gegluClosure = new CompiledFunction((inputs) => {
88
- const g = inputs[0]!, u = inputs[1]!; // nn.gelu_approx(gate) * x
89
- const act = ops.geluApprox(g);
90
- const out = ops.mul(act, u); act.dispose();
91
- return [out];
92
- });
93
- }
94
- return _gegluClosure.apply([gate, up])[0]!;
95
- }
96
-
97
- /** gemma4_text.py `@partial(mx.compile) def logit_softcap(softcap, x):
98
- * return mx.tanh(x / softcap) * softcap`
99
- * The oracle fuses divide+tanh+multiply into ONE kernel; our default
100
- * `logitSoftcap` runs them UNFUSED (standalone div + tanh + mul → a separate
101
- * `vn_Tanh` + `vsn_Divide`). Keyed by cap so the softcap is baked as a graph
102
- * constant (matching the oracle's weak-scalar → the `…Divide…Tanh…Multiply…_VC_`
103
- * kernel). The default (matches mlx-lm's dispatched kernel set). */
104
- const _softcapClosures = new Map<number, CompiledFunction>();
105
- export function compiledLogitSoftcap(x: MlxArray, cap: number): MlxArray {
106
- let fn = _softcapClosures.get(cap);
107
- if (!fn) {
108
- fn = new CompiledFunction((inputs) => {
109
- const xx = inputs[0]!; // tanh(x / softcap) * softcap
110
- const capArr = ops.scalarLike(cap, xx);
111
- const scaled = ops.div(xx, capArr);
112
- const t = ops.tanh(scaled); scaled.dispose();
113
- const out = ops.mul(t, capArr); t.dispose(); capArr.dispose();
114
- return [out];
115
- });
116
- _softcapClosures.set(cap, fn);
117
- }
118
- return fn.apply([x])[0]!;
119
- }
120
-
121
- /** Optional gradient-checkpointing context for the training forward. When set,
122
- * forwardLayers wraps each layer in a Checkpoint so its interior activations
123
- * are recomputed in the backward pass. `byLayer` maps a layer index to that
124
- * layer's trainable LoRA weights, partitioned into the attention sub-block
125
- * (q/k/v/o_proj) and the MLP sub-block (gate/up/down_proj + per-layer gate/
126
- * projection) — re-swapped inside the closure so they are explicit checkpoint
127
- * inputs (required because the autograd primals are disposed before the
128
- * recompute). When `splitMlp` is set, the layer is wrapped in TWO checkpoints
129
- * (attn sub-block + MLP sub-block) with the post-attn residual `hMid` as the
130
- * boundary, so the backward recompute holds `max(attn,MLP)+hMid` instead of the
131
- * whole layer (the intra-layer MLP split, lever 4). `keepAlive` collects the
132
- * Checkpoint objects; the trainer disposes them after value_and_grad. */
133
- export interface LayerLoras {
134
- attn: LoraWeights[];
135
- mlp: LoraWeights[];
136
- }
137
- export interface GradCheckpointCtx {
138
- byLayer: Map<number, LayerLoras>;
139
- splitMlp: boolean;
140
- keepAlive: Checkpoint[];
141
- }
142
-
143
- // Shared prompt-prefix plan (lever 7, e4b — docs/design/orpo-training.md). When
144
- // set, Attention ropes the concatenated [prompt(P); chosenResp(Rc); rejectedResp(Rr)]
145
- // sequence BLOCK-WISE — prompt at offset 0, EACH response reset to logical offset
146
- // P — instead of the uniform cache.offset, so ONE forward over the concat is
147
- // bit-exact with the two separate [prompt;resp] forwards (each response sees the
148
- // prompt's RoPE positions, not shifted-by-the-other-response). The matching
149
- // block-sparse + LOGICAL-window attention mask rides in via the prefix cache's
150
- // makeMask (src/train/prefix-shared.ts). Set around the single forward
151
- // (single-threaded), cleared after; null → the normal uniform-offset rope (every
152
- // other forward is untouched).
153
- export interface GemmaPrefixPlan { P: number; Rc: number; Rr: number }
154
- let _gemmaPrefixPlan: GemmaPrefixPlan | null = null;
155
- export function setGemmaPrefixPlan(p: GemmaPrefixPlan | null): void {
156
- _gemmaPrefixPlan = p;
157
- }
158
-
159
- class Attention {
160
- readonly isSliding: boolean;
161
- readonly windowSize: number;
162
- readonly useKEqV: boolean;
163
- readonly hasKv: boolean;
164
- readonly headDim: number;
165
- readonly nHeads: number;
166
- readonly nKvHeads: number;
167
- readonly ropeBase: number | null;
168
- readonly ropeFreqs: MlxArray | null;
169
- readonly qProj: QuantizedLinear;
170
- readonly kProj: QuantizedLinear | null;
171
- readonly vProj: QuantizedLinear | null;
172
- readonly oProj: QuantizedLinear;
173
- readonly qNorm: RMSNorm;
174
- readonly kNorm: RMSNorm | null;
175
- readonly vNorm: RMSNorm | null;
176
-
177
- constructor(
178
- weights: Weights, config: ModelConfig, prefix: string, layerType: string,
179
- hasKv = true,
180
- ) {
181
- const t = config.text;
182
- this.hasKv = hasKv;
183
- this.isSliding = layerType === "sliding_attention";
184
- this.windowSize = t.slidingWindow;
185
- this.headDim = this.isSliding ? t.headDim : t.globalHeadDim;
186
- this.nHeads = t.numAttentionHeads;
187
- this.useKEqV = t.attentionKEqV && !this.isSliding;
188
- this.nKvHeads = this.useKEqV ? t.numGlobalKeyValueHeads : t.numKeyValueHeads;
189
-
190
- const rp = t.ropeParameters[this.isSliding ? "sliding_attention" : "full_attention"]!;
191
- if (rp.ropeType === "default") {
192
- this.ropeBase = rp.ropeTheta;
193
- this.ropeFreqs = null;
194
- } else if (rp.ropeType === "proportional") {
195
- // Port of rope_utils.ProportionalRoPE.__init__. The rotated freqs
196
- // MUST be computed on-device in f32 (arange/dims, then base**x)
197
- // exactly like the reference — computing them host-side in f64 and
198
- // rounding to f32 lands 17/64 of them 1 ulp off (f64 pow ≠ f32
199
- // powf), a latent knife-edge that Phase 10's tiled values exposed
200
- // as a 1-ulp q-rope divergence at layer 11 (Phase 2 porting rule:
201
- // replicate the helper's IMPLEMENTATION, not its formula).
202
- const rotated = Math.floor(this.headDim * rp.partialRotaryFactor);
203
- const n = this.headDim / 2;
204
- const exponentsRaw = ops.arange(0, rotated, 2, Dtype.float32);
205
- const dims = ops.scalarLike(this.headDim, exponentsRaw);
206
- const exponents = ops.div(exponentsRaw, dims);
207
- const base = ops.scalarLike(rp.ropeTheta, exponents);
208
- let rotFreqs = ops.pow(base, exponents);
209
- if (rp.factor !== 1.0) {
210
- const f = ops.scalarLike(rp.factor, rotFreqs);
211
- rotFreqs = disposing(rotFreqs, ops.mul(f, rotFreqs));
212
- f.dispose();
213
- }
214
- this.ropeBase = null;
215
- const tailLen = n - rotated / 2;
216
- if (tailLen > 0) {
217
- const tail = MlxArray.fromFloat32(
218
- new Float32Array(tailLen).fill(Infinity), [tailLen],
219
- );
220
- this.ropeFreqs = ops.concatAxis([rotFreqs, tail], 0);
221
- tail.dispose();
222
- } else {
223
- this.ropeFreqs = rotFreqs;
224
- }
225
- for (const a of [exponentsRaw, dims, exponents, base])
226
- a.dispose();
227
- if (this.ropeFreqs !== rotFreqs) rotFreqs.dispose();
228
- } else {
229
- throw new Error(`unsupported rope_type ${rp.ropeType}`);
230
- }
231
-
232
- this.qProj = QuantizedLinear.load(weights, `${prefix}.q_proj`, config);
233
- this.kProj = hasKv ? QuantizedLinear.load(weights, `${prefix}.k_proj`, config) : null;
234
- this.vProj = hasKv && !this.useKEqV
235
- ? QuantizedLinear.load(weights, `${prefix}.v_proj`, config) : null;
236
- this.oProj = QuantizedLinear.load(weights, `${prefix}.o_proj`, config);
237
- this.qNorm = new RMSNorm(weights.tensor(`${prefix}.q_norm.weight`), t.rmsNormEps);
238
- this.kNorm = hasKv
239
- ? new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps) : null;
240
- this.vNorm = hasKv ? new RMSNorm(null, t.rmsNormEps) : null;
241
- }
242
-
243
- /** A number offset takes the static fast::rope; an array offset (set by
244
- * compiled-decode trace adapters) takes the dynamic variant — same
245
- * kernel, offset read from the array (bit-exactness asserted in
246
- * tests/unit/compile.test.ts). */
247
- rope(x: MlxArray, offset: number | MlxArray): MlxArray {
248
- return typeof offset === "number"
249
- ? ops.rope(x, this.headDim, this.ropeBase, offset, this.ropeFreqs)
250
- : ops.ropeDynamic(x, this.headDim, this.ropeBase, offset, this.ropeFreqs);
251
- }
252
-
253
- /** Block-wise RoPE for the prefix-shared concat [prompt; chosen; rejected] along
254
- * the sequence axis (axis 2 of [B,H,T,D]): prompt at offset 0, EACH response
255
- * reset to logical offset P. RoPE is per-token, so roping each contiguous block
256
- * at its scalar offset and concatenating == roping with per-token position-ids
257
- * [0..P-1, P..P+Rc-1, P..P+Rr-1]. Uses this.rope per block so the layer's
258
- * proportional/ropeFreqs + sliding-vs-full geometry applies identically to the
259
- * two-forward path. Caller disposes the input. */
260
- ropeBlocks(x: MlxArray, plan: GemmaPrefixPlan): MlxArray {
261
- const { P, Rc, Rr } = plan;
262
- const [B, H, , D] = x.shape as [number, number, number, number];
263
- const blocks = [
264
- { start: 0, len: P, off: 0 },
265
- { start: P, len: Rc, off: P },
266
- { start: P + Rc, len: Rr, off: P },
267
- ].filter((b) => b.len > 0);
268
- const parts = blocks.map((b) => {
269
- const sl = x.slice([0, 0, b.start, 0], [B, H, b.start + b.len, D]);
270
- const r = this.rope(sl, b.off);
271
- sl.dispose();
272
- return r;
273
- });
274
- const out = ops.concatAxis(parts, 2);
275
- for (const p of parts) p.dispose();
276
- return out;
277
- }
278
-
279
- /** Returns the attention output plus the fetched KV (for KV-shared
280
- * sharer layers and the speculative drafter). The SharedKv arrays are
281
- * owned by the caller (forwardLayers) and disposed after the pass. */
282
- forward(
283
- x: MlxArray, mask: Mask, cache: Cache | null, sharedIn: SharedKv | null,
284
- ): { out: MlxArray; shared: SharedKv } {
285
- const [B, L] = x.shape as [number, number, number];
286
-
287
- let q = this.qProj.forward(x);
288
- q = disposing(q, ops.reshape(q, [B, L, this.nHeads, this.headDim]));
289
- q = disposing(q, this.qNorm.forward(q));
290
-
291
- let shared: SharedKv;
292
- if (!this.hasKv) {
293
- if (!sharedIn) throw new Error("KV-shared layer received no shared KV");
294
- shared = sharedIn;
295
- } else {
296
- if (!cache) throw new Error("donor layer requires a cache");
297
- let k = this.kProj!.forward(x);
298
- k = disposing(k, ops.reshape(k, [B, L, this.nKvHeads, this.headDim]));
299
- let v: MlxArray;
300
- if (this.vProj) {
301
- v = this.vProj.forward(x);
302
- v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
303
- } else {
304
- v = k; // attention_k_eq_v: shared projection; norms differ below
305
- }
306
-
307
- const offset = cache.offset;
308
- // Own the pre-write positions through every query and shared-KV
309
- // consumer. Advancing a row cache may replace and release its borrowed
310
- // offset array before Q's RoPE is built. The fetched shared state owns
311
- // this retained handle and releases it with K/V at the end of the pass.
312
- const offsetArr = captureRopeOffsets(cache);
313
-
314
- const kNormed = this.kNorm!.forward(k);
315
- const kT = ops.transposeAxes(kNormed, [0, 2, 1, 3]);
316
- kNormed.dispose();
317
- const kRoped = _gemmaPrefixPlan
318
- ? this.ropeBlocks(kT, _gemmaPrefixPlan)
319
- : this.rope(kT, offsetArr ?? offset);
320
- kT.dispose();
321
-
322
- const vNormed = this.vNorm!.forward(v);
323
- const vT = ops.transposeAxes(vNormed, [0, 2, 1, 3]);
324
- vNormed.dispose();
325
- if (v !== k) v.dispose();
326
- k.dispose();
327
-
328
- if (cache.attentionState) {
329
- const attention = cache.attentionState.appendAndFetch(kRoped, vT);
330
- kRoped.dispose(); vT.dispose();
331
- shared = { kind: "view", attention, offset, offsetArr };
332
- } else if (cache.quantizedAttention) {
333
- const quantized = cache.quantizedAttention;
334
- const [kq, vq] = quantized.updateAndFetchQuantized(kRoped, vT);
335
- kRoped.dispose();
336
- vT.dispose();
337
- shared = {
338
- kind: "quant", keys: kq, values: vq, offset,
339
- groupSize: quantized.groupSize, bits: quantized.bits,
340
- offsetArr,
341
- };
342
- } else if (cache.rotatedValueAttention) {
343
- // Deferred-V read: values stay rotated; the attention output is
344
- // un-rotated below (shared.vRotated) — including by KV-shared
345
- // consumer layers, which see the flag through sharedIn.
346
- const [keys, values] = cache.rotatedValueAttention.updateAndFetchDeferredV(kRoped, vT);
347
- kRoped.dispose();
348
- vT.dispose();
349
- shared = { kind: "plain", keys, values, offset, offsetArr, vRotated: true,
350
- restoreValues: cache.rotatedValueAttention.captureValueTransform?.() };
351
- } else {
352
- const [keys, values] = cache.updateAndFetch(kRoped, vT);
353
- kRoped.dispose();
354
- vT.dispose();
355
- shared = { kind: "plain", keys, values, offset, offsetArr };
356
- }
357
- }
358
-
359
- q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
360
- q = disposing(q, _gemmaPrefixPlan
361
- ? this.ropeBlocks(q, _gemmaPrefixPlan)
362
- : this.rope(q, shared.offsetArr ?? shared.offset));
363
-
364
- let attn: MlxArray;
365
- const ta = getTrainingAttn();
366
- if (shared.kind === "view") {
367
- attn = shared.attention.attend(q, 1.0, mask);
368
- } else if (shared.kind === "quant") {
369
- attn = quantizedSdpa(q, shared.keys, shared.values, 1.0, mask, shared.groupSize, shared.bits);
370
- } else if (
371
- ta === "flash" && flashSupported(q) && (mask.mode === "causal" || mask.mode === "array")
372
- ) {
373
- // Training, OPT-IN flash path (MLX_BUN_TRAIN_ATTN=flash) for O(L)-memory
374
- // long context. NOTE the default (ops.sdpa, below) is the CORRECT and
375
- // verified route: its dQ/dK/dV vjps are exact (finite-difference checked;
376
- // it's what mlx-lm's tuner differentiates). The two historical bugs were
377
- // in THIS flash kernel (spurious dK transpose, divergent dQ barrier),
378
- // both fixed and gated by tests/unit/flash-attention.test.ts vs the ops.sdpa
379
- // oracle — see flash-attention.ts's routing comment. Flash also remains
380
- // Gemma-guarded in the trainer (e4b >=2K SIGTRAP not yet re-validated).
381
- // Full layers → window 0 (pure causal); sliding layers → their window.
382
- const window = this.isSliding ? this.windowSize : 0;
383
- attn = flashAttention(q, shared.keys, shared.values, 1.0, true, window);
384
- } else {
385
- attn = ops.sdpa(q, shared.keys, shared.values, 1.0, mask.mode, mask.arr);
386
- }
387
- q.dispose();
388
- if (shared.kind === "plain" && shared.vRotated) {
389
- // Attention is linear in V: one InvFWHT on the [B,H,L,D] output
390
- // replaces one per cached token (TurboQuant deferred-V).
391
- attn = disposing(attn, shared.restoreValues ? shared.restoreValues(attn) : tqUnrotateValues(attn));
392
- }
393
- const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
394
- attn.dispose();
395
- const merged = ops.reshape(attnT, [B, L, -1]);
396
- attnT.dispose();
397
- const out = this.oProj.forward(merged);
398
- merged.dispose();
399
- return { out, shared };
400
- }
401
- }
402
-
403
- class MLP {
404
- readonly gate: QuantizedLinear;
405
- readonly up: QuantizedLinear;
406
- readonly down: QuantizedLinear;
407
-
408
- constructor(weights: Weights, config: ModelConfig, prefix: string) {
409
- this.gate = QuantizedLinear.load(weights, `${prefix}.gate_proj`, config);
410
- this.up = QuantizedLinear.load(weights, `${prefix}.up_proj`, config);
411
- this.down = QuantizedLinear.load(weights, `${prefix}.down_proj`, config);
412
- }
413
-
414
- forward(x: MlxArray): MlxArray {
415
- const g = this.gate.forward(x);
416
- const u = this.up.forward(x);
417
- let h: MlxArray;
418
- if (compiledGegluActive() && !isCompiledTrace()) {
419
- // Oracle geglu(gate_proj(x), up_proj(x)): @mx.compile'd gelu_approx·mul in
420
- // one kernel set (the default; = mlx-lm). Autograd-safe → covers training.
421
- h = compiledGeglu(g, u);
422
- g.dispose();
423
- u.dispose();
424
- } else {
425
- const act = ops.geluApprox(g);
426
- g.dispose();
427
- h = ops.mul(act, u);
428
- act.dispose();
429
- u.dispose();
430
- }
431
- const out = this.down.forward(h);
432
- h.dispose();
433
- return out;
434
- }
435
- }
436
-
437
- // --- MoE block (26B-A4B) — port of reference Router/Experts/SwitchGLU ----
438
- // (gemma4_text.py + switch_layers.py in the oracle venv). The checkpoint
439
- // ships pre-stacked switch_glu tensors [experts, out, in/packed]; only the
440
- // quantized path exists here (all our targets are OptiQ quants).
441
-
442
- /** Port of reference Router: norm -> scale -> project -> top-k -> renormalize. */
443
- class Router {
444
- readonly proj: QuantizedLinear;
445
- /** scale · hidden_size^-0.5, precomputed (identical ops to the per-call
446
- * product in the reference: bf16 array × weak scalar). */
447
- readonly normWeight: MlxArray;
448
- readonly perExpertScale: MlxArray;
449
- readonly eps: number;
450
- readonly numExperts: number;
451
- readonly topK: number;
452
- /** Decoder layer index (parsed from the weight prefix) — for E0 tracing. */
453
- readonly layerIdx: number;
454
-
455
- constructor(weights: Weights, config: ModelConfig, prefix: string) {
456
- const t = config.text;
457
- this.layerIdx = Number(prefix.match(/layers\.(\d+)/)?.[1] ?? -1);
458
- this.proj = QuantizedLinear.load(weights, `${prefix}.proj`, config);
459
- // weights.tensor() returns the Weights-owned cached array — do NOT
460
- // dispose it (a second model over the same Weights would get a dead
461
- // handle; Weights.dispose() frees it at end of life)
462
- const scale = weights.tensor(`${prefix}.scale`);
463
- this.normWeight = ops.mulScalar(scale, Math.pow(t.hiddenSize, -0.5));
464
- this.perExpertScale = weights.tensor(`${prefix}.per_expert_scale`);
465
- this.eps = t.rmsNormEps;
466
- this.numExperts = t.numExperts;
467
- this.topK = t.topKExperts;
468
- }
469
-
470
- /** x [B, L, H] → { indices [B, L, k] (uint32), weights [B, L, k] }. */
471
- forward(x: MlxArray): { indices: MlxArray; weights: MlxArray } {
472
- const normed = ops.rmsNorm(x, this.normWeight, this.eps);
473
- const scores = this.proj.forward(normed);
474
- normed.dispose();
475
-
476
- const part = ops.argpartitionAxis(scores, this.numExperts - this.topK, -1);
477
- const [B, L] = scores.shape as [number, number, number];
478
- let indices: MlxArray;
479
- if (isCompiledTrace()) {
480
- const start = ops.fromInt32([this.numExperts - this.topK], [1]);
481
- indices = ops.sliceDynamic(part, start, [2], [B, L, this.topK]);
482
- start.dispose();
483
- } else {
484
- indices = part.slice([0, 0, this.numExperts - this.topK], [B, L, this.numExperts]);
485
- }
486
- part.dispose();
487
-
488
- let w = ops.takeAlongAxis(scores, indices, -1);
489
- scores.dispose();
490
- w = disposing(w, ops.softmaxAxis(w, -1, false));
491
- const gathered = ops.takeAxis(this.perExpertScale, indices, 0);
492
- w = disposing(w, ops.mul(w, gathered));
493
- gathered.dispose();
494
- if (isExpertTracing()) recordRouting(this.layerIdx, indices);
495
- return { indices, weights: w };
496
- }
497
- }
498
-
499
- /** Port of switch_layers.SwitchGLU (geglu activation), quantized path. */
500
- class SwitchGLU {
501
- readonly gate: QuantizedSwitchLinear;
502
- readonly up: QuantizedSwitchLinear;
503
- readonly down: QuantizedSwitchLinear;
504
-
505
- constructor(weights: Weights, config: ModelConfig, prefix: string) {
506
- this.gate = QuantizedSwitchLinear.load(weights, `${prefix}.gate_proj`, config);
507
- this.up = QuantizedSwitchLinear.load(weights, `${prefix}.up_proj`, config);
508
- this.down = QuantizedSwitchLinear.load(weights, `${prefix}.down_proj`, config);
509
- }
510
-
511
- /** x [B, L, H], indices [B, L, k] → [B, L, k, H]. */
512
- forward(x: MlxArray, indices: MlxArray): MlxArray {
513
- // x → [B, L, 1, 1, H] (reference expand_dims(x, (-2, -3)))
514
- let h = ops.expandDims(x, -2);
515
- h = disposing(h, ops.expandDims(h, -3));
516
-
517
- // Sort tokens by expert when there are many rows (reference threshold).
518
- const doSort = indices.size >= 64;
519
- let idx = indices;
520
- let invOrder: MlxArray | null = null;
521
- let order: MlxArray | null = null;
522
- if (doSort) {
523
- // _gather_sort: flatten indices, argsort, gather rows by order // k
524
- const k = indices.shape[indices.ndim - 1]!;
525
- const idxFlat = ops.reshape(indices, [indices.size]);
526
- order = ops.argsortAxis(idxFlat, 0);
527
- invOrder = ops.argsortAxis(order, 0);
528
- const kScalar = ops.scalarLike(k, order);
529
- const rowIdx = ops.floorDivide(order, kScalar);
530
- kScalar.dispose();
531
- const [, , , , H] = h.shape as number[];
532
- const flat = ops.reshape(h, [-1, 1, H!]);
533
- h.dispose();
534
- h = ops.takeAxis(flat, rowIdx, 0);
535
- flat.dispose();
536
- rowIdx.dispose();
537
- idx = ops.takeAxis(idxFlat, order, 0);
538
- idxFlat.dispose();
539
- }
540
-
541
- const xUp = this.up.forward(h, idx, doSort);
542
- const xGate = this.gate.forward(h, idx, doSort);
543
- h.dispose();
544
- // GeGLU activation: gelu_approx(gate) · up (same composition as MLP). The
545
- // oracle SwitchGLU uses the SAME @mx.compile'd geglu (activation=GeGLU(),
546
- // switch_layers.py) → the compiled path routes through the shared closure.
547
- let mid: MlxArray;
548
- if (compiledGegluActive() && !isCompiledTrace()) {
549
- mid = compiledGeglu(xGate, xUp);
550
- xGate.dispose();
551
- xUp.dispose();
552
- } else {
553
- const act = ops.geluApprox(xGate);
554
- xGate.dispose();
555
- mid = ops.mul(act, xUp);
556
- act.dispose();
557
- xUp.dispose();
558
- }
559
- let y = this.down.forward(mid, idx, doSort);
560
- mid.dispose();
561
- if (idx !== indices) idx.dispose();
562
-
563
- if (doSort) {
564
- // _scatter_unsort: restore row order, unflatten to indices.shape
565
- y = disposing(y, ops.takeAxis(y, invOrder!, 0));
566
- invOrder!.dispose();
567
- order!.dispose();
568
- const [B, L, k] = indices.shape as number[];
569
- const H = y.shape[y.ndim - 1]!;
570
- y = disposing(y, ops.reshape(y, [B!, L!, k!, 1, H]));
571
- }
572
-
573
- // squeeze(-2)
574
- const shape = y.shape;
575
- shape.splice(shape.length - 2, 1);
576
- return disposing(y, ops.reshape(y, shape));
577
- }
578
- }
579
-
580
- /** Port of reference Experts: switch_glu then top-k weighted sum. */
581
- class Experts {
582
- readonly switchGlu: SwitchGLU;
583
-
584
- constructor(weights: Weights, config: ModelConfig, prefix: string) {
585
- this.switchGlu = new SwitchGLU(weights, config, `${prefix}.switch_glu`);
586
- }
587
-
588
- forward(x: MlxArray, indices: MlxArray, weights: MlxArray): MlxArray {
589
- const w = ops.expandDims(weights, -1);
590
- const y = this.switchGlu.forward(x, indices);
591
- const wy = ops.mul(w, y);
592
- w.dispose();
593
- y.dispose();
594
- return disposing(wy, ops.sumAxis(wy, -2, false));
595
- }
596
- }
597
-
598
- export class DecoderLayer {
599
- readonly attn: Attention;
600
- readonly mlp: MLP;
601
- readonly inputNorm: RMSNorm;
602
- readonly postAttnNorm: RMSNorm;
603
- readonly preFfNorm: RMSNorm;
604
- readonly postFfNorm: RMSNorm;
605
- readonly layerScalar: MlxArray | null;
606
- readonly layerType: string;
607
- // 26B-A4B MoE block (dense MLP + routed experts in parallel branches)
608
- readonly router: Router | null;
609
- readonly experts: Experts | null;
610
- readonly postFfNorm1: RMSNorm | null;
611
- readonly postFfNorm2: RMSNorm | null;
612
- readonly preFfNorm2: RMSNorm | null;
613
- // e2b/e4b per-layer input gating
614
- readonly perLayerGate: QuantizedLinear | null;
615
- readonly perLayerProjection: QuantizedLinear | null;
616
- readonly postPerLayerNorm: RMSNorm | null;
617
-
618
- constructor(weights: Weights, config: ModelConfig, prefixBase: string, idx: number, hasKv: boolean) {
619
- const prefix = `${prefixBase}.layers.${idx}`;
620
- const t = config.text;
621
- this.layerType = t.layerTypes[idx]!;
622
- this.attn = new Attention(weights, config, `${prefix}.self_attn`, this.layerType, hasKv);
623
- this.mlp = new MLP(weights, config, `${prefix}.mlp`);
624
- const norm = (n: string) => new RMSNorm(weights.tensor(`${prefix}.${n}.weight`), t.rmsNormEps);
625
- this.inputNorm = norm("input_layernorm");
626
- this.postAttnNorm = norm("post_attention_layernorm");
627
- this.preFfNorm = norm("pre_feedforward_layernorm");
628
- this.postFfNorm = norm("post_feedforward_layernorm");
629
- this.layerScalar = weights.has(`${prefix}.layer_scalar`)
630
- ? weights.tensor(`${prefix}.layer_scalar`)
631
- : null;
632
- if (t.enableMoeBlock) {
633
- this.router = new Router(weights, config, `${prefix}.router`);
634
- this.experts = new Experts(weights, config, `${prefix}.experts`);
635
- this.postFfNorm1 = norm("post_feedforward_layernorm_1");
636
- this.postFfNorm2 = norm("post_feedforward_layernorm_2");
637
- this.preFfNorm2 = norm("pre_feedforward_layernorm_2");
638
- } else {
639
- this.router = this.experts = null;
640
- this.postFfNorm1 = this.postFfNorm2 = this.preFfNorm2 = null;
641
- }
642
- if (t.hiddenSizePerLayerInput > 0) {
643
- this.perLayerGate = QuantizedLinear.load(weights, `${prefix}.per_layer_input_gate`, config);
644
- this.perLayerProjection = QuantizedLinear.load(weights, `${prefix}.per_layer_projection`, config);
645
- this.postPerLayerNorm = norm("post_per_layer_input_norm");
646
- } else {
647
- this.perLayerGate = this.perLayerProjection = null;
648
- this.postPerLayerNorm = null;
649
- }
650
- }
651
-
652
- forward(
653
- x: MlxArray, mask: Mask, cache: Cache | null,
654
- sharedIn: SharedKv | null, perLayerInput: MlxArray | null,
655
- ): { h: MlxArray; shared: SharedKv } {
656
- // Composed from the two sub-blocks (the intra-layer min-cut is the post-attn
657
- // residual `hMid` [1,T,hidden]). Behaviour is identical to the spelled-out
658
- // body; the split lets the checkpoint path rematerialize attn and MLP
659
- // separately (peak attn+MLP → max(attn,MLP)+hMid). `forwardMlp` borrows hMid.
660
- const { h: hMid, shared } = this.forwardAttn(x, mask, cache, sharedIn);
661
- const h = this.forwardMlp(hMid, perLayerInput);
662
- hMid.dispose();
663
- return { h, shared };
664
- }
665
-
666
- /** Attention sub-block: inputNorm → attn → postAttnNorm → residual add.
667
- * Returns the post-attn residual stream `h` [1,T,hidden] (the intra-layer
668
- * boundary) and the attention's fetched/produced K/V (`shared`). `x` is
669
- * borrowed (never disposed). The e4b donor-KV threading lives entirely here. */
670
- forwardAttn(
671
- x: MlxArray, mask: Mask, cache: Cache | null, sharedIn: SharedKv | null,
672
- ): { h: MlxArray; shared: SharedKv } {
673
- let h = this.inputNorm.forward(x);
674
- const { out, shared } = this.attn.forward(h, mask, cache, sharedIn);
675
- h.dispose();
676
- h = out;
677
- h = disposing(h, this.postAttnNorm.forward(h));
678
- h = disposing(h, ops.add(x, h));
679
- return { h, shared };
680
- }
681
-
682
- /** MLP sub-block: FFN (dense or MoE) + postFfNorm + residual add, then the
683
- * e4b per-layer-input gate and layer scalar. `hMid` (the attn sub-block's
684
- * output) is BORROWED — the caller owns/disposes it (it is the checkpoint
685
- * boundary leaf), so this never disposes it. */
686
- forwardMlp(hMid: MlxArray, perLayerInput: MlxArray | null): MlxArray {
687
- let f: MlxArray;
688
- if (this.router && this.experts) {
689
- // MoE: dense MLP and routed experts as parallel branches off hMid
690
- // (reference DecoderLayer, enable_moe path)
691
- let h1 = this.preFfNorm.forward(hMid);
692
- h1 = disposing(h1, this.mlp.forward(h1));
693
- h1 = disposing(h1, this.postFfNorm1!.forward(h1));
694
-
695
- const { indices, weights: topKWeights } = this.router.forward(hMid);
696
- let h2 = this.preFfNorm2!.forward(hMid);
697
- h2 = disposing(h2, this.experts.forward(h2, indices, topKWeights));
698
- h2 = disposing(h2, this.postFfNorm2!.forward(h2));
699
- indices.dispose();
700
- topKWeights.dispose();
701
-
702
- f = ops.add(h1, h2);
703
- h1.dispose();
704
- h2.dispose();
705
- } else {
706
- f = this.preFfNorm.forward(hMid);
707
- f = disposing(f, this.mlp.forward(f));
708
- }
709
- f = disposing(f, this.postFfNorm.forward(f));
710
- let h = ops.add(hMid, f); // hMid borrowed — NOT disposed here
711
- f.dispose();
712
-
713
- // per-layer input gating (reference gemma4_text DecoderLayer). gelu(gate)·pli
714
- // is the second GeGLU site — the bit-exact spelled-out gelu+mul.
715
- if (this.perLayerGate && this.perLayerProjection && this.postPerLayerNorm && perLayerInput) {
716
- const res2 = h;
717
- let gate = this.perLayerGate.forward(h);
718
- gate = disposing(gate, ops.geluApprox(gate));
719
- gate = disposing(gate, ops.mul(gate, perLayerInput));
720
- gate = disposing(gate, this.perLayerProjection.forward(gate));
721
- gate = disposing(gate, this.postPerLayerNorm.forward(gate));
722
- h = ops.add(res2, gate);
723
- res2.dispose();
724
- gate.dispose();
725
- }
726
-
727
- if (this.layerScalar) h = disposing(h, ops.mul(h, this.layerScalar));
728
- return h;
729
- }
730
- }
731
-
732
- export class Gemma4Model {
733
- readonly config: ModelConfig;
734
- /** Total weight-shard bytes (for the conditional wired-limit scope). */
735
- readonly weightsBytes: number;
736
- /** Weight-name prefix ("language_model.model" or "model"). */
737
- readonly prefixBase: string;
738
- /** Active LoRA adapters for the current generation (see LoraState). */
739
- readonly loraState = new LoraState();
740
- readonly embed: QuantizedEmbedding;
741
- readonly layers: DecoderLayer[];
742
- readonly finalNorm: RMSNorm;
743
- readonly embedScale: number;
744
- readonly windowSize: number;
745
- /** Donor count: layers [0, numDonors) own caches; the rest share. */
746
- readonly numDonors: number;
747
- /** layer idx → donor layer idx whose fetched KV it consumes (self for donors). */
748
- readonly previousKvs: number[];
749
- /** layer idx → cache index (donors only; -1 for sharers). */
750
- readonly cacheIndex: number[];
751
- /** Set by the trainer to enable gradient checkpointing in forwardLayers
752
- * (null = off; the non-checkpointed path is untouched). */
753
- gradCkpt: GradCheckpointCtx | null = null;
754
- /** Optional per-forward hidden-state tap for DSpark's DFlash H_ctx (paper
755
- * Eq 2). null = off → byte-identical forward. `layers`: residual-output
756
- * indices to capture (0..nLayers-1 = post-layer residual; nLayers =
757
- * post-finalNorm). `pos`: if set, capture only [1,1,H] at that seq position
758
- * (live decode); else the full [1,L,H] (offline regen). The caller owns and
759
- * disposes the captured arrays. Additive/read-only → parity untouched. */
760
- hiddenTap: { layers: Set<number>; pos?: number; captured: Map<number, MlxArray> } | null = null;
761
- // e2b/e4b per-layer input machinery
762
- readonly perLayerEmbed: QuantizedEmbedding | null;
763
- readonly perLayerModelProjection: QuantizedLinear | null;
764
- readonly perLayerProjectionNorm: RMSNorm | null;
765
- readonly perLayerWidth: number;
766
-
767
- constructor(weights: Weights, config: ModelConfig) {
768
- const t = config.text;
769
- if (t.hiddenSize === 0 || !config.text.tieWordEmbeddings)
770
- throw new Error("only tied-embedding text configs are supported");
771
-
772
- this.config = config;
773
- this.weightsBytes = [...weights.shards.files.values()]
774
- .reduce((a, f) => a + f.mmap.size, 0);
775
- // gemma4_unified prefixes weights with language_model.; gemma4_text doesn't
776
- const prefixBase = weights.has("language_model.model.embed_tokens.weight")
777
- ? "language_model.model" : "model";
778
- this.prefixBase = prefixBase;
779
- this.embed = QuantizedEmbedding.load(weights, `${prefixBase}.embed_tokens`, config);
780
-
781
- this.numDonors = t.numHiddenLayers - t.numKvSharedLayers;
782
- this.layers = Array.from(
783
- { length: t.numHiddenLayers },
784
- (_, i) => new DecoderLayer(weights, config, prefixBase, i, i < this.numDonors),
785
- );
786
- // previous_kvs (reference Gemma4TextModel): sharer j uses the LAST
787
- // donor of its own layer type
788
- this.previousKvs = this.layers.map((_, i) => i);
789
- if (t.numKvSharedLayers > 0) {
790
- const lastByType: Record<string, number> = {};
791
- for (let i = 0; i < this.numDonors; i++)
792
- lastByType[this.layers[i]!.layerType] = i;
793
- for (let j = this.numDonors; j < this.layers.length; j++) {
794
- const donor = lastByType[this.layers[j]!.layerType];
795
- if (donor === undefined)
796
- throw new Error(`no donor layer of type ${this.layers[j]!.layerType}`);
797
- this.previousKvs[j] = donor;
798
- }
799
- }
800
- this.cacheIndex = this.layers.map((_, i) => (i < this.numDonors ? i : -1));
801
-
802
- this.finalNorm = new RMSNorm(
803
- weights.tensor(`${prefixBase}.norm.weight`),
804
- t.rmsNormEps,
805
- );
806
- this.embedScale = Math.sqrt(t.hiddenSize);
807
- this.windowSize = t.slidingWindow;
808
-
809
- if (t.hiddenSizePerLayerInput > 0) {
810
- this.perLayerEmbed = QuantizedEmbedding.load(weights, `${prefixBase}.embed_tokens_per_layer`, config);
811
- this.perLayerModelProjection = QuantizedLinear.load(weights, `${prefixBase}.per_layer_model_projection`, config);
812
- this.perLayerProjectionNorm = new RMSNorm(
813
- weights.tensor(`${prefixBase}.per_layer_projection_norm.weight`), t.rmsNormEps,
814
- );
815
- this.perLayerWidth = t.hiddenSizePerLayerInput;
816
- } else {
817
- this.perLayerEmbed = this.perLayerModelProjection = null;
818
- this.perLayerProjectionNorm = null;
819
- this.perLayerWidth = 0;
820
- }
821
- }
822
-
823
- /** LoRA-mountable linears, keyed by weight-file module path: optiq
824
- * mount.py's 7 target suffixes PLUS the e2b/e4b per-layer-input
825
- * projections — mlx-lm's trainer targets those on e4b and optiq's
826
- * mount silently drops their trained weights (deviation documented in
827
- * PLAN Phase 8 findings; we apply every adapter weight we can map).
828
- * Expert pools stay non-targets (LoRASwitchLinear is future work). */
829
- loraTargets(): Map<string, QuantizedLinear> {
830
- const out = new Map<string, QuantizedLinear>();
831
- for (let i = 0; i < this.layers.length; i++) {
832
- const l = this.layers[i]!;
833
- const p = `${this.prefixBase}.layers.${i}`;
834
- const add = (suffix: string, lin: QuantizedLinear | null) => {
835
- if (lin) out.set(`${p}.${suffix}`, lin);
836
- };
837
- add("self_attn.q_proj", l.attn.qProj);
838
- add("self_attn.k_proj", l.attn.kProj);
839
- add("self_attn.v_proj", l.attn.vProj);
840
- add("self_attn.o_proj", l.attn.oProj);
841
- add("mlp.gate_proj", l.mlp.gate);
842
- add("mlp.up_proj", l.mlp.up);
843
- add("mlp.down_proj", l.mlp.down);
844
- add("per_layer_input_gate", l.perLayerGate);
845
- add("per_layer_projection", l.perLayerProjection);
846
- }
847
- return out;
848
- }
849
-
850
- /** Caches for donor layers only (sharers consume donors' fetched KV). */
851
- makeCache(): Cache[] {
852
- return this.layers.slice(0, this.numDonors).map((l) =>
853
- l.layerType === "sliding_attention"
854
- ? new RotatingKVCache(this.windowSize)
855
- : new KVCache(),
856
- );
857
- }
858
-
859
- /** Computed model constants must retain their constructor arithmetic when
860
- * a restored request enters compiled decode without a prefill first. */
861
- materializeGraphConstants(): void {
862
- const constants = this.layers.flatMap(layer => layer.attn.ropeFreqs ? [layer.attn.ropeFreqs] : []);
863
- if (constants.length) ops.evalAll(constants);
864
- }
865
-
866
- /** ids [1, L] → final-norm hidden states [1, L, hidden]. */
867
- forwardHidden(ids: MlxArray, cache: Cache[]): MlxArray {
868
- let h = this.embed.encode(ids);
869
- h = disposing(h, ops.mulScalar(h, this.embedScale));
870
- return this.forwardLayers(h, cache, null, ids);
871
- }
872
-
873
- /** Per-layer inputs (reference _get_per_layer_inputs +
874
- * _project_per_layer_inputs): [1, L, nLayers, perLayerWidth]. */
875
- protected computePerLayerInputs(ids: MlxArray, hScaled: MlxArray): MlxArray {
876
- const t = this.config.text;
877
- const B = ids.shape[0]!;
878
- const L = ids.shape[1]!;
879
- let pli = this.perLayerEmbed!.encode(ids); // [B, L, nLayers*width]
880
- pli = disposing(pli, ops.mulScalar(pli, Math.sqrt(this.perLayerWidth)));
881
- pli = disposing(pli, ops.reshape(pli, [B, L, t.numHiddenLayers, this.perLayerWidth]));
882
-
883
- let proj = this.perLayerModelProjection!.forward(hScaled);
884
- proj = disposing(proj, ops.mulScalar(proj, 1 / Math.sqrt(t.hiddenSize)));
885
- proj = disposing(proj, ops.reshape(proj, [B, L, t.numHiddenLayers, this.perLayerWidth]));
886
- proj = disposing(proj, this.perLayerProjectionNorm!.forward(proj));
887
-
888
- let combined = ops.add(proj, pli);
889
- proj.dispose();
890
- pli.dispose();
891
- combined = disposing(combined, ops.mulScalar(combined, Math.SQRT1_2));
892
- return combined;
893
- }
894
-
895
- /** Pre-merged (unscaled) input embeddings → hidden states. Used by the
896
- * multimodal (vision/audio) path; `bidir` (bool [L]) marks image tokens,
897
- * which attend bidirectionally among themselves (use_bidirectional_attention:
898
- * "vision" — text stays causal; audio prompts pass NO bidir mask, and a
899
- * mixed image+audio prompt drops the image overlay too, §3.3 Q1 of
900
- * docs/design/generic-model-support.md). `ids` ([1, L], the spliced token ids)
901
- * is required for per-layer-input models (e2b/e4b): multimodal soft-token
902
- * positions get token 0's per-layer embedding, and their tower content
903
- * enters only through the projection term (the merged hidden). Matches
904
- * optiq's frontend: `zeroed = where(text_mask, input_ids, 0)`.
905
- *
906
- * `multimodal` (bool [L]) is the per-layer-input ZEROING mask — the UNION
907
- * of multimodal soft tokens (image | audio, §3.3 Q2), decoupled from the
908
- * attention overlay: an audio-only prompt has no bidir mask but its audio
909
- * positions must still be zeroed. When null (the legacy vision call
910
- * shape), the zeroing mask falls back to `bidir` — vision passes the same
911
- * image mask for both roles, so that path is unchanged. */
912
- forwardEmbeddings(
913
- embeds: MlxArray, cache: Cache[], bidir: MlxArray | null,
914
- ids: MlxArray | null = null, multimodal: MlxArray | null = null,
915
- ): MlxArray {
916
- const h = ops.mulScalar(embeds, this.embedScale);
917
- if (this.perLayerWidth === 0) return this.forwardLayers(h, cache, bidir, null);
918
-
919
- if (!ids)
920
- throw new Error("per-layer-input multimodal models require token ids");
921
- const zeroing = multimodal ?? bidir;
922
- let plIds = ids;
923
- if (zeroing) {
924
- const cond = ops.reshape(zeroing, ids.shape);
925
- const zero = ops.zeros(ids.shape, ids.dtype);
926
- plIds = ops.where(cond, zero, ids);
927
- cond.dispose();
928
- zero.dispose();
929
- }
930
- const out = this.forwardLayers(h, cache, bidir, plIds);
931
- if (plIds !== ids) plIds.dispose();
932
- return out;
933
- }
934
-
935
- /** Store layer `i`'s residual stream `h` ([1,L,H]) if the hidden tap requests
936
- * it. Copies (astype bf16, optional slice) so the loop's `h.dispose()` can't
937
- * free the capture. No-op and no graph ops when the tap is null → the forward
938
- * is byte-identical to an untapped run. (DSpark DFlash H_ctx.) */
939
- protected captureLayer(i: number, h: MlxArray): void {
940
- const tap = this.hiddenTap;
941
- if (!tap || !tap.layers.has(i)) return;
942
- const H = h.shape[2]!;
943
- const src = tap.pos !== undefined ? h.slice([0, tap.pos, 0], [1, tap.pos + 1, H]) : h;
944
- const copy = src.astype(this.embed.scales.dtype); // bf16, matches regen dump dtype
945
- if (src !== h) src.dispose();
946
- tap.captured.set(i, copy);
947
- }
948
-
949
- /** Consumes h. */
950
- protected forwardLayers(
951
- h0: MlxArray, cache: Cache[], bidir: MlxArray | null, ids: MlxArray | null,
952
- ): MlxArray {
953
- const B = h0.shape[0]!;
954
- const L = h0.shape[1]!;
955
- let h = h0;
956
-
957
- // one mask per layer type, computed from the first cache of that type
958
- const masks = new Map<string, Mask>();
959
- for (let i = 0; i < this.numDonors; i++) {
960
- const type = this.layers[i]!.layerType;
961
- if (!masks.has(type)) {
962
- const window = type === "sliding_attention" ? this.windowSize : null;
963
- if (bidir && L > 1) {
964
- const c = cache[i]!;
965
- if (c.offset !== 0)
966
- throw new Error("bidirectional image masks require offset-0 prefill");
967
- masks.set(type, bidirMask(L, window, bidir));
968
- } else {
969
- masks.set(type, cache[i]!.makeMask(L, window));
970
- }
971
- }
972
- }
973
-
974
- // per-layer inputs (e2b/e4b)
975
- let perLayer: MlxArray | null = null;
976
- if (this.perLayerWidth > 0) {
977
- if (!ids) throw new Error("per-layer-input models require token ids");
978
- perLayer = this.computePerLayerInputs(ids, h);
979
- }
980
-
981
- const intermediates: (SharedKv | null)[] = Array(this.layers.length).fill(null);
982
- for (let i = 0; i < this.layers.length; i++) {
983
- const layer = this.layers[i]!;
984
- const ci = this.cacheIndex[i]!;
985
- const sharedIn = ci === -1 ? (intermediates[this.previousKvs[i]!] ?? null) : null;
986
- let pls: MlxArray | null = null;
987
- if (perLayer) {
988
- if (isCompiledTrace()) {
989
- const start = ops.fromInt32([i], [1]);
990
- pls = ops.sliceDynamic(perLayer, start, [2], [B, L, 1, this.perLayerWidth]);
991
- start.dispose();
992
- } else {
993
- pls = perLayer.slice([0, 0, i, 0], [B, L, i + 1, this.perLayerWidth]);
994
- }
995
- const r = ops.reshape(pls, [B, L, this.perLayerWidth]);
996
- pls.dispose();
997
- pls = r;
998
- }
999
- const mask = masks.get(layer.layerType)!;
1000
- const layerCache = ci === -1 ? null : cache[ci]!;
1001
- const { h: next, shared } = this.gradCkpt
1002
- ? this.runCheckpointedLayer(i, layer, h, mask, layerCache, sharedIn, pls)
1003
- : layer.forward(h, mask, layerCache, sharedIn, pls);
1004
- pls?.dispose();
1005
- h.dispose();
1006
- h = next;
1007
- this.captureLayer(i, h); // DFlash H_ctx tap (no-op unless hiddenTap set)
1008
- intermediates[i] = shared;
1009
- }
1010
- perLayer?.dispose();
1011
- // dispose donor-fetched KV (each donor's shared entry exactly once)
1012
- for (let i = 0; i < this.numDonors; i++) {
1013
- const s = intermediates[i];
1014
- if (!s) continue;
1015
- Gemma4Model.disposeSharedKv(s);
1016
- }
1017
- for (const m of masks.values()) m.arr?.dispose();
1018
-
1019
- h = disposing(h, this.finalNorm.forward(h));
1020
- this.captureLayer(this.layers.length, h); // post-finalNorm sentinel (index = nLayers)
1021
- return h;
1022
- }
1023
-
1024
- /** Mixed token execution: attention retains each group's cache, positions and
1025
- * SDPA shape; feed-forward work packs only real tokens. Kernel selection for
1026
- * packed matmuls can differ from a solo GEMV, so this is an explicit Lab port
1027
- * until its matching packed oracle and serving comparison are accepted. */
1028
- forwardHiddenMixed(work: readonly TokenGroup[]): MlxArray[] {
1029
- if (work.length === 1 && !work[0]!.captureLayer) return [this.forwardHidden(work[0]!.ids, work[0]!.cache)];
1030
- const groups: Array<{ ids: MlxArray; cache: Cache[]; h: MlxArray;
1031
- masks: Map<string, Mask>; perLayer: MlxArray | null; intermediates: (SharedKv | null)[] }> = [];
1032
- const results: MlxArray[] = [];
1033
- const packedMlp = runtimeFlag("MLX_BUN_MIXED_PACKED_MLP", true) && !work.some(group => group.preserveTokenGeometry);
1034
- try {
1035
- for (const { ids, cache } of work) {
1036
- const group = { ids, cache, h: this.embed.encode(ids), masks: new Map<string, Mask>(),
1037
- perLayer: null as MlxArray | null, intermediates: Array<SharedKv | null>(this.layers.length).fill(null) };
1038
- groups.push(group);
1039
- group.h = disposing(group.h, ops.mulScalar(group.h, this.embedScale));
1040
- for (let i = 0; i < this.numDonors; i++) {
1041
- const type = this.layers[i]!.layerType;
1042
- if (!group.masks.has(type)) group.masks.set(type,
1043
- cache[i]!.makeMask(ids.shape[1]!, type === "sliding_attention" ? this.windowSize : null));
1044
- }
1045
- if (this.perLayerWidth > 0) group.perLayer = this.computePerLayerInputs(ids, group.h);
1046
- }
1047
- for (let i = 0; i < this.layers.length; i++) {
1048
- const layer = this.layers[i]!, ci = this.cacheIndex[i]!;
1049
- const mids: MlxArray[] = [], inputs: MlxArray[] = [];
1050
- try {
1051
- for (const group of groups) {
1052
- const sharedIn = ci === -1 ? group.intermediates[this.previousKvs[i]!]! : null;
1053
- const { h, shared } = layer.forwardAttn(group.h, group.masks.get(layer.layerType)!,
1054
- ci === -1 ? null : group.cache[ci]!, sharedIn);
1055
- mids.push(h); group.intermediates[i] = shared;
1056
- if (group.perLayer) {
1057
- const [B, L] = group.ids.shape;
1058
- using part = group.perLayer.slice([0, 0, i, 0], [B!, L!, i + 1, this.perLayerWidth]);
1059
- inputs.push(ops.reshape(part, [B!, L!, this.perLayerWidth]));
1060
- }
1061
- }
1062
- // Per-layer inputs use the same token order as the hidden states.
1063
- let packedInput: MlxArray | null = null;
1064
- if (packedMlp && inputs.length) {
1065
- const flat = inputs.map(input => ops.reshape(input, [1, -1, this.perLayerWidth]));
1066
- try { packedInput = ops.concatAxis(flat, 1); }
1067
- finally { for (const input of flat) input.dispose(); }
1068
- }
1069
- using pli = packedInput;
1070
- const outputs: MlxArray[] = [];
1071
- try {
1072
- if (packedMlp) outputs.push(...mapPackedTokens(mids, packed => layer.forwardMlp(packed, pli)));
1073
- else for (const [row, mid] of mids.entries()) outputs.push(layer.forwardMlp(mid, inputs[row] ?? null));
1074
- } catch (error) { for (const output of outputs) output.dispose(); throw error; }
1075
- for (const [index, group] of groups.entries()) {
1076
- group.h.dispose(); group.h = outputs[index]!;
1077
- }
1078
- for (const [index, group] of groups.entries()) work[index]!.captureLayer?.(i, group.h);
1079
- } finally {
1080
- for (const mid of mids) mid.dispose();
1081
- for (const input of inputs) input.dispose();
1082
- }
1083
- }
1084
- for (const [index, group] of groups.entries()) {
1085
- const hidden = this.finalNorm.forward(group.h); results.push(hidden);
1086
- work[index]!.captureLayer?.(this.layers.length, hidden);
1087
- }
1088
- return results;
1089
- } catch (error) { for (const result of results) result.dispose(); throw error; }
1090
- finally {
1091
- for (const group of groups) {
1092
- group.h.dispose(); group.perLayer?.dispose();
1093
- for (let i = 0; i < this.numDonors; i++) {
1094
- const shared = group.intermediates[i];
1095
- if (shared) Gemma4Model.disposeSharedKv(shared);
1096
- }
1097
- for (const mask of group.masks.values()) mask.arr?.dispose();
1098
- }
1099
- }
1100
- }
1101
-
1102
-
1103
- // --- Segmented-backward support (docs/design/orpo-training.md
1104
- // §4 Phase B). These are ADDITIVE — forwardLayers is untouched. The segmented
1105
- // driver builds masks + per-layer inputs ONCE, then drives runLayerRange per
1106
- // segment, threading the KV-shared donor K/V across segment boundaries.
1107
-
1108
- /** Donor layer indices that KV-sharing sharers reuse (e4b: {22,23} — the last
1109
- * donor of each attention type; empty for dense models). Their fetched K/V
1110
- * must survive across segment boundaries (saved as detached boundaries). */
1111
- get reusedDonors(): Set<number> {
1112
- return new Set(this.previousKvs.filter((d, i) => d !== i));
1113
- }
1114
-
1115
- /** Per-layer-type training masks (offset-0, full-sequence, no bidir) — built
1116
- * once, shared across all segments. Mirrors forwardLayers' mask construction.
1117
- * Caller owns the returned arrays (dispose each mask.arr). */
1118
- makeTrainingMasks(cache: Cache[], L: number): Map<string, Mask> {
1119
- const masks = new Map<string, Mask>();
1120
- for (let i = 0; i < this.numDonors; i++) {
1121
- const type = this.layers[i]!.layerType;
1122
- if (!masks.has(type)) {
1123
- const window = type === "sliding_attention" ? this.windowSize : null;
1124
- masks.set(type, cache[i]!.makeMask(L, window));
1125
- }
1126
- }
1127
- return masks;
1128
- }
1129
-
1130
- /** e2b/e4b training entry: the SCALED input embedding (the first segment
1131
- * boundary) plus the per-layer-input tensor [B,L,nLayers,width] (a detached
1132
- * boundary, sliced per layer inside runLayerRange). perLayer is null for dense
1133
- * models. Caller owns both. */
1134
- embedForSegmented(ids: MlxArray): { hScaled: MlxArray; perLayer: MlxArray | null } {
1135
- let h = this.embed.encode(ids);
1136
- h = disposing(h, ops.mulScalar(h, this.embedScale));
1137
- const perLayer = this.perLayerWidth > 0 ? this.computePerLayerInputs(ids, h) : null;
1138
- return { hScaled: h, perLayer };
1139
- }
1140
-
1141
- /** Dispose a fetched donor K/V (plain or quantized) — mirrors forwardLayers'
1142
- * donor cleanup. */
1143
- private static disposeSharedKv(s: SharedKv): void {
1144
- s.offsetArr?.dispose();
1145
- if (s.kind === "view") {
1146
- s.attention.dispose();
1147
- } else if (s.kind === "plain") {
1148
- s.keys.dispose();
1149
- s.values.dispose();
1150
- } else {
1151
- for (const t of [s.keys, s.values])
1152
- for (const a of [t.packed, t.scales, t.biases]) a.dispose();
1153
- }
1154
- }
1155
-
1156
- /** Run decoder layers `[aIdx, bIdx)` on hidden `h`, returning the residual
1157
- * stream after the last layer in the range (NO finalNorm) plus the fetched
1158
- * K/V of any reused-donor layers in the range (`donorKvOut`, for later
1159
- * segments). The input `h` is NEVER disposed (the caller / vjp owns the
1160
- * boundary leaf); intra-range hidden intermediates and per-layer slices ARE
1161
- * disposed. Non-reused donors' K/V are disposed; reused donors' K/V are
1162
- * returned (caller threads + disposes). NOT gradient-checkpointed — the
1163
- * segmentation IS the memory strategy.
1164
- *
1165
- * @param masks per-layer-type masks (makeTrainingMasks), shared, caller-owned.
1166
- * @param perLayer the [B,L,nLayers,width] tensor (caller-owned) or null.
1167
- * @param donorKvIn fetched K/V of reused donors from EARLIER segments, keyed
1168
- * by donor layer index (caller-owned; not disposed here). */
1169
- runLayerRange(
1170
- h: MlxArray, aIdx: number, bIdx: number, cache: Cache[],
1171
- masks: Map<string, Mask>, perLayer: MlxArray | null,
1172
- donorKvIn: Map<number, SharedKv>,
1173
- ): { h: MlxArray; donorKvOut: Map<number, SharedKv> } {
1174
- const donorKvOut = new Map<number, SharedKv>();
1175
- if (aIdx >= bIdx) return { h, donorKvOut };
1176
- const B = h.shape[0]!;
1177
- const L = h.shape[1]!;
1178
- const reused = this.reusedDonors;
1179
- let cur = h;
1180
- for (let i = aIdx; i < bIdx; i++) {
1181
- const layer = this.layers[i]!;
1182
- const ci = this.cacheIndex[i]!;
1183
- const isDonor = ci !== -1;
1184
- let sharedIn: SharedKv | null = null;
1185
- if (!isDonor) {
1186
- const donor = this.previousKvs[i]!;
1187
- sharedIn = donorKvOut.get(donor) ?? donorKvIn.get(donor) ?? null;
1188
- if (!sharedIn) throw new Error(`runLayerRange: donor ${donor} K/V not available for sharer ${i}`);
1189
- }
1190
- let pls: MlxArray | null = null;
1191
- if (perLayer) {
1192
- const sl = perLayer.slice([0, 0, i, 0], [B, L, i + 1, this.perLayerWidth]);
1193
- pls = ops.reshape(sl, [B, L, this.perLayerWidth]);
1194
- sl.dispose();
1195
- }
1196
- const mask = masks.get(layer.layerType)!;
1197
- const { h: next, shared } = layer.forward(cur, mask, isDonor ? cache[ci]! : null, sharedIn, pls);
1198
- pls?.dispose();
1199
- if (i > aIdx) cur.dispose(); // keep the input boundary leaf; drop interiors
1200
- cur = next;
1201
- if (isDonor) {
1202
- if (reused.has(i)) donorKvOut.set(i, shared);
1203
- else Gemma4Model.disposeSharedKv(shared);
1204
- }
1205
- // sharer: `shared` === sharedIn (owned by donorKvIn/Out) — do not dispose.
1206
- }
1207
- return { h: cur, donorKvOut };
1208
- }
1209
-
1210
- /** Run one layer under gradient checkpointing: its interior activations are
1211
- * dropped and recomputed in the backward pass. The layer's LoRA params, h,
1212
- * the mask array (if any), pls, and a sharer's donor KV are threaded as
1213
- * explicit checkpoint inputs (the closure must be pure and self-contained —
1214
- * it re-runs during the backward, and captured autograd primals would be
1215
- * stale). A donor layer also emits its fetched K/V as checkpoint outputs so
1216
- * the (retained, small) tensors reach the sharer layers downstream. */
1217
- private runCheckpointedLayer(
1218
- i: number, layer: DecoderLayer, h: MlxArray, mask: Mask,
1219
- layerCache: Cache | null, sharedIn: SharedKv | null, pls: MlxArray | null,
1220
- ): { h: MlxArray; shared: SharedKv | null } {
1221
- const ctx = this.gradCkpt!;
1222
- const ll = ctx.byLayer.get(i) ?? { attn: [], mlp: [] };
1223
- return ctx.splitMlp
1224
- ? this.runSplitCheckpointedLayer(layer, h, mask, layerCache, sharedIn, pls, ll, ctx)
1225
- : this.runSingleCheckpointedLayer(layer, h, mask, layerCache, sharedIn, pls, ll, ctx);
1226
- }
1227
-
1228
- /** One Checkpoint over the whole layer (the original path). */
1229
- private runSingleCheckpointedLayer(
1230
- layer: DecoderLayer, h: MlxArray, mask: Mask,
1231
- layerCache: Cache | null, sharedIn: SharedKv | null, pls: MlxArray | null,
1232
- ll: LayerLoras, ctx: GradCheckpointCtx,
1233
- ): { h: MlxArray; shared: SharedKv | null } {
1234
- const lws = [...ll.attn, ...ll.mlp];
1235
- const isSharer = layerCache === null;
1236
- const sIn = isSharer && sharedIn && sharedIn.kind === "plain" ? sharedIn : null;
1237
- const hasMaskArr = mask.arr !== null;
1238
- const hasPls = pls !== null;
1239
- const maskMode = mask.mode;
1240
-
1241
- // Input order: [h, maskArr?, pls?, sharedInK?, sharedInV?, (lora_a, lora_b)*]
1242
- const inputs: MlxArray[] = [h];
1243
- if (hasMaskArr) inputs.push(mask.arr!);
1244
- if (hasPls) inputs.push(pls!);
1245
- if (sIn) inputs.push(sIn.keys, sIn.values);
1246
- for (const lw of lws) inputs.push(lw.a, lw.b);
1247
-
1248
- const ck = new Checkpoint((ins) => {
1249
- let k = 0;
1250
- const hIn = ins[k++]!;
1251
- const mArr = hasMaskArr ? ins[k++]! : null;
1252
- const plsIn = hasPls ? ins[k++]! : null;
1253
- const sharedInRebuilt: SharedKv | null = sIn
1254
- ? { kind: "plain", keys: ins[k++]!, values: ins[k++]!, offset: 0 }
1255
- : null;
1256
- const saved = lws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
1257
- for (const lw of lws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
1258
- try {
1259
- const out = layer.forward(hIn, { mode: maskMode, arr: mArr }, layerCache, sharedInRebuilt, plsIn);
1260
- if (!isSharer && out.shared && out.shared.kind === "plain")
1261
- return [out.h, out.shared.keys, out.shared.values];
1262
- return [out.h];
1263
- } finally {
1264
- lws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
1265
- }
1266
- });
1267
- ctx.keepAlive.push(ck);
1268
-
1269
- const outs = ck.apply(inputs);
1270
- const shared: SharedKv | null =
1271
- !isSharer && outs.length === 3
1272
- ? { kind: "plain", keys: outs[1]!, values: outs[2]!, offset: 0 }
1273
- : null;
1274
- return { h: outs[0]!, shared };
1275
- }
1276
-
1277
- /** Intra-layer MLP split (lever 4): TWO checkpoints — the attention sub-block
1278
- * (forwardAttn: inputNorm → attn → residual add, threading the donor K/V) and
1279
- * the MLP sub-block (forwardMlp: FFN + per-layer gate) — with the post-attn
1280
- * residual `hMid` as the boundary. The backward recompute holds only one
1281
- * sub-block's activations at a time, dropping per-layer peak from `attn+MLP`
1282
- * to `max(attn,MLP)+hMid`. Attn LoRA / pls / donor-KV go to the attn ck; MLP
1283
- * LoRA to the MLP ck. */
1284
- private runSplitCheckpointedLayer(
1285
- layer: DecoderLayer, h: MlxArray, mask: Mask,
1286
- layerCache: Cache | null, sharedIn: SharedKv | null, pls: MlxArray | null,
1287
- ll: LayerLoras, ctx: GradCheckpointCtx,
1288
- ): { h: MlxArray; shared: SharedKv | null } {
1289
- const isSharer = layerCache === null;
1290
- const sIn = isSharer && sharedIn && sharedIn.kind === "plain" ? sharedIn : null;
1291
- const hasMaskArr = mask.arr !== null;
1292
- const hasPls = pls !== null;
1293
- const maskMode = mask.mode;
1294
- const attnLws = ll.attn;
1295
- const mlpLws = ll.mlp;
1296
-
1297
- // --- attn sub-block checkpoint: [h, maskArr?, sharedInK?, sharedInV?, attnLora*]
1298
- const attnInputs: MlxArray[] = [h];
1299
- if (hasMaskArr) attnInputs.push(mask.arr!);
1300
- if (sIn) attnInputs.push(sIn.keys, sIn.values);
1301
- for (const lw of attnLws) attnInputs.push(lw.a, lw.b);
1302
-
1303
- const attnCk = new Checkpoint((ins) => {
1304
- let k = 0;
1305
- const hIn = ins[k++]!;
1306
- const mArr = hasMaskArr ? ins[k++]! : null;
1307
- const sharedInRebuilt: SharedKv | null = sIn
1308
- ? { kind: "plain", keys: ins[k++]!, values: ins[k++]!, offset: 0 }
1309
- : null;
1310
- const saved = attnLws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
1311
- for (const lw of attnLws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
1312
- try {
1313
- const out = layer.forwardAttn(hIn, { mode: maskMode, arr: mArr }, layerCache, sharedInRebuilt);
1314
- if (!isSharer && out.shared && out.shared.kind === "plain")
1315
- return [out.h, out.shared.keys, out.shared.values];
1316
- return [out.h];
1317
- } finally {
1318
- attnLws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
1319
- }
1320
- });
1321
- ctx.keepAlive.push(attnCk);
1322
- const attnOuts = attnCk.apply(attnInputs);
1323
- const hMid = attnOuts[0]!;
1324
- const shared: SharedKv | null =
1325
- !isSharer && attnOuts.length === 3
1326
- ? { kind: "plain", keys: attnOuts[1]!, values: attnOuts[2]!, offset: 0 }
1327
- : null;
1328
-
1329
- // --- MLP sub-block checkpoint: [hMid, pls?, mlpLora*] -> [hOut]
1330
- const mlpInputs: MlxArray[] = [hMid];
1331
- if (hasPls) mlpInputs.push(pls!);
1332
- for (const lw of mlpLws) mlpInputs.push(lw.a, lw.b);
1333
-
1334
- const mlpCk = new Checkpoint((ins) => {
1335
- let k = 0;
1336
- const hMidIn = ins[k++]!;
1337
- const plsIn = hasPls ? ins[k++]! : null;
1338
- const saved = mlpLws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
1339
- for (const lw of mlpLws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
1340
- try {
1341
- return [layer.forwardMlp(hMidIn, plsIn)];
1342
- } finally {
1343
- mlpLws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
1344
- }
1345
- });
1346
- ctx.keepAlive.push(mlpCk);
1347
- const mlpOuts = mlpCk.apply(mlpInputs);
1348
- hMid.dispose(); // boundary leaf consumed by the MLP ck (mlx retains it in-graph)
1349
-
1350
- return { h: mlpOuts[0]!, shared };
1351
- }
1352
-
1353
- /** hidden [1, L, hidden] → softcapped logits [1, L, vocab]. */
1354
- logitsFromHidden(h: MlxArray): MlxArray {
1355
- let logits = this.embed.asLinear(h);
1356
- const softcap = this.config.text.finalLogitSoftcapping;
1357
- if (softcap !== null)
1358
- logits = disposing(logits, compiledGegluActive() && !isCompiledTrace()
1359
- ? compiledLogitSoftcap(logits, softcap) // fused, matches @mx.compile logit_softcap
1360
- : logitSoftcap(logits, softcap));
1361
- return logits;
1362
- }
1363
-
1364
- /** tokens → logits [1, L, vocab] (compat path used by parity tests). */
1365
- forward(tokens: number[] | MlxArray, cache: Cache[]): MlxArray {
1366
- const ids = Array.isArray(tokens)
1367
- ? ops.fromInt32(tokens, [1, tokens.length])
1368
- : tokens;
1369
- const h = this.forwardHidden(ids, cache);
1370
- if (Array.isArray(tokens)) ids.dispose();
1371
- const logits = this.logitsFromHidden(h);
1372
- h.dispose();
1373
- return logits;
1374
- }
1375
-
1376
- /** Greedy generation (parity harness); returns generated token ids. */
1377
- generate(promptTokens: number[], maxTokens: number, eosIds: number[] = []): number[] {
1378
- const cache = this.makeCache();
1379
- const out: number[] = [];
1380
- try {
1381
- let tokens = promptTokens;
1382
- for (let step = 0; step < maxTokens; step++) {
1383
- const logits = this.forward(tokens, cache);
1384
- const next = argmaxLastPosition(logits);
1385
- logits.dispose();
1386
- if (eosIds.includes(next)) break;
1387
- out.push(next);
1388
- tokens = [next];
1389
- }
1390
- } finally {
1391
- for (const c of cache) c.dispose();
1392
- }
1393
- return out;
1394
- }
1395
- }