mlx-bun 0.4.0 → 1.0.0

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