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,1221 +0,0 @@
1
- // LoRA trainer: SFT (masked cross-entropy) and DPO loops over a single
2
- // quantized base, differentiating only the LoRA A/B leaves.
3
- //
4
- // The value_and_grad closure differentiates [...flatA, ...flatB]. Inside the
5
- // closure we swap the incoming primal handles into the LoraWeights so the
6
- // model's forward builds its graph against the differentiated leaves; after
7
- // AdamW.step replaces a leaf, the writeback callback pushes the new handle
8
- // back into the LoraWeights so the next forward (and the next closure swap)
9
- // sees it. Base quantized weights are never in argnums — they stay frozen.
10
-
11
- import { MlxArray, pinnedBufferCount } from "../mlx/array";
12
- import { clearCache, peakMemory, activeMemory, cacheMemory, resetPeakMemory } from "../mlx/ffi";
13
- import { runtimeValue } from "../runtime-config";
14
-
15
- const MEM_LOG = runtimeValue("MLX_BUN_MEM_LOG") === "1";
16
- const gbStr = (b: number) => `${(b / 1e9).toFixed(2)}GB`;
17
- import { ValueAndGrad } from "../mlx/autograd";
18
- import * as ops from "../mlx/ops";
19
- import { evalAll } from "../mlx/ops";
20
- import type { RuntimeModel } from "../model/factory";
21
- import { Gemma4Model, type GradCheckpointCtx } from "../model/gemma4";
22
- import { MiniCPM5Model } from "../model/minicpm5";
23
- import { setTrainingAttn } from "../model/flash-attention";
24
- import { SegmentedBackward, SegmentedBackwardGemma4, SegmentedBackwardOrpo, SegmentedBackwardOrpoGemma4, SegmentedBackwardOrpoPrefix, SegmentedBackwardOrpoPrefixGemma4, planSegmentsBySize } from "./segmented";
25
- import type { LoadedTokenizer } from "../tokenizer";
26
- import type { ChatTemplate } from "../chat-template";
27
- import type { Emit } from "../jobs/types";
28
- import {
29
- loadSftDataset, iterateSftBatches, loadDpoDataset, iterateDpoBatches,
30
- rowLength, type SftBatch, type DpoBatch,
31
- } from "./dataset";
32
- import {
33
- buildTrainableLora, attachForTraining, detachTraining, flatParams,
34
- saveAdapter, disposeLora, warmStartFromAdapter, type TrainableLora, type SaveAdapterConfig,
35
- } from "./lora-params";
36
- import { resolveRanks, bitsMapFromModel, readPerLayerKl, DEFAULT_TARGET_MODULES, type RankScaling } from "./rank";
37
- import { sftLoss, dpoLoss, dpoRefLogps, dpoMetrics, orpoLoss, orpoMetrics, type ChunkCtx, type SftScope } from "./loss";
38
- import { orpoLossPrefixShared, orpoLossPrefixSharedGemma, splitPrefixBatch } from "./prefix-shared";
39
- import { AdamW, warmupCosineSchedule } from "./optimizer";
40
- import { writeFileSync, appendFileSync, mkdirSync } from "node:fs";
41
-
42
- export interface TrainConfig {
43
- method: "sft" | "dpo" | "orpo";
44
- rank: number;
45
- scale: number;
46
- rankScaling: RankScaling;
47
- targetModules: string[];
48
- numLayers: number;
49
- iters: number;
50
- learningRate: number;
51
- maxSeqLen: number;
52
- batchSize: number;
53
- gradAccumSteps: number;
54
- seed: number;
55
- stepsPerReport: number;
56
- stepsPerEval: number;
57
- betas: [number, number];
58
- weightDecay: number;
59
- /** LoRA-input dropout (PEFT-style), 0 = off. Training-only regularizer for
60
- * small preference sets. Recompute-safe: the mask is keyed by (step, layer)
61
- * so segmented/checkpointed recompute reproduces it. */
62
- loraDropout: number;
63
- /** rsLoRA: scale the LoRA update by α/√rank instead of α, so per-layer rank
64
- * scaling (by_bits/by_kl) changes capacity, not step size. Saved in the
65
- * adapter config so inference applies the same per-layer scale. */
66
- rsLora: boolean;
67
- /** LoRA+: LR multiplier for the B leaves (A stays at the base LR). >1 speeds
68
- * the B-driven early learning (B is zero-init). 1 = off. */
69
- loraPlusRatio: number;
70
- /** Gradient checkpointing: recompute layer activations in the backward pass
71
- * to bound memory at long context (mlx-lm grad_checkpoint; default in optiq).
72
- * Numerically identical to off — pure memory↔compute trade. */
73
- gradCheckpoint: boolean;
74
- /** Intra-layer MLP split (lever 4): with gradCheckpoint on (Gemma4 only),
75
- * checkpoint the attention and MLP sub-blocks separately with the post-attn
76
- * residual as the boundary, so the backward recompute holds max(attn,MLP)+hMid
77
- * instead of the whole layer. Numerically identical to gradCheckpoint alone —
78
- * a finer memory↔compute split. No effect unless gradCheckpoint is on. */
79
- mlpSplit: boolean;
80
- /** Segmented backward: layers per segment (0 = off). Streams the SFT backward
81
- * segment-by-segment so only one segment's activations are live at a time —
82
- * beats per-layer checkpointing at long context, where naive checkpointing
83
- * holds every layer's recompute activations at once and crashes. B=1 SFT
84
- * only (the responseOnlyCe path); mutually exclusive with gradCheckpoint.
85
- * Numerically identical to off. See docs/design/orpo-training.md. */
86
- segmentSize: number;
87
- /** Keep ALL eval-step checkpoints (full mountable adapters under
88
- * checkpoints/step-<NNNNN>-val<loss>/) + write metrics.json. Off by default
89
- * so server finetune jobs and tests stay clean; the chunk-finetune script
90
- * turns it on so the best-ON-TASK checkpoint can be chosen by the real eval
91
- * afterward (val is only a proxy — we keep them all and let the eval pick). */
92
- saveCheckpoints: boolean;
93
- /** Gradient clipping: scale grads so their global L2 norm ≤ this before the
94
- * optimizer step (0 = off). Standard ORPO/DPO stability guard — without it a
95
- * single high-norm batch makes AdamW take a huge step and the loss diverges. */
96
- gradClipNorm: number;
97
- /** Cap validation examples evaluated per eval step (0 = all). Val accuracy is
98
- * correct/total over a FIXED subset — bounds the denominator AND the per-eval
99
- * cost (full val over thousands of pairs is minutes each). */
100
- valMaxExamples: number;
101
- // DPO
102
- dpoBeta: number;
103
- dpoWarmupIters: number;
104
- dpoLrSchedule: "constant" | "cosine";
105
- // ORPO (reference-free; method-prefixed knobs, mirroring the dpo trio).
106
- /** λ — weights ONLY the odds-ratio term (the SFT-NLL term stays unweighted). */
107
- orpoLambda: number;
108
- orpoWarmupIters: number;
109
- orpoLrSchedule: "constant" | "cosine";
110
- /** Token-chunk size for the ORPO head (0 = off). >0 computes the B=1
111
- * response-only LM head in token-chunks, each rematerialized in the backward,
112
- * bounding the dominant `[M, vocab]` logits term to `[chunkSize, vocab]`.
113
- * Exact (numerically identical). Non-segmented path only (segmentSize takes
114
- * precedence). See docs/design/orpo-training.md → chunked fused log-prob. */
115
- orpoChunkSize: number;
116
- /** Use the FUSED linear cross-entropy head (Liger/CCE-style): one CustomVjp with
117
- * an analytic softmax−onehot backward — no autograd through the head, no
118
- * retained `[M,vocab]` logits in either direction. Composes with
119
- * `orpoChunkSize` as the token-chunk size (defaults to 512 when unset). B=1,
120
- * non-segmented path only; Gemma + MiniCPM5 heads. Value bit-exact vs the
121
- * full-logits head, grads in the bf16 class. See docs/design/orpo-training.md
122
- * → fused linear-CE head. */
123
- orpoFusedCe: boolean;
124
- /** Route the fused linear-CE head through the flash-CCE Metal kernel: in-kernel
125
- * quantized logits + online softmax (fwd) and dh accumulation (bwd) — neither
126
- * `[M,vocab]` nor a dequantized head touches HBM. Lowest memory + fastest on
127
- * large vocab (e4b 262k / CPM 130k). Implies the fused head; the Apple-CCE coeff
128
- * filter is on by default in the backward. B=1, non-segmented path. */
129
- orpoFlashCe: boolean;
130
- /** Shared prompt-prefix ORPO (lever 7): one forward over [prompt; chosen; rejected]
131
- * with a block-sparse mask + block-wise RoPE, so the shared prompt is encoded ONCE
132
- * (token cost 2(P+R) → P+2R). B=1, non-segmented path; MiniCPM5 + Gemma4 (e4b).
133
- * Falls back to the two-forward orpoLoss for rows whose chosen/rejected prompts
134
- * differ. Composes with orpoFlashCe/orpoFusedCe (each branch routes through the
135
- * [M,V]-free head). See src/train/prefix-shared.ts. */
136
- orpoPrefixShared: boolean;
137
- /** ORPO L_SFT scope (`sft_scope`). "full" (default; paper/TRL-faithful):
138
- * the chosen-NLL term is the token-mean cross-entropy over the FULL
139
- * prompt+response (only padding excluded), computed from the same chosen
140
- * forward — matching TRL's ORPOTrainer chosen_nll_loss. "response":
141
- * L_NLL = -ℓw (response-only), bit-exact to pre-sft_scope runs. The
142
- * odds-ratio ℓw/ℓr terms are response-only length-normalized means in BOTH
143
- * modes (that also matches TRL). Applies to every ORPO path (naive /
144
- * chunked / fused / flash / prefix-shared / segmented). orpo only. */
145
- sftScope: SftScope;
146
- /** Warm-start: path to an existing adapter dir (with adapters.safetensors) whose
147
- * LoRA weights initialize this run instead of the random/zero init. Continues
148
- * training from a checkpoint's weights; the optimizer state and LR schedule restart.
149
- * Rank/targets must match the checkpoint. "" = off (fresh init). */
150
- warmStartAdapter: string;
151
- // output
152
- adapterPath: string;
153
- baseModel: string;
154
- }
155
-
156
- export interface TrainResult {
157
- adapterPath: string;
158
- appliedRanks: Record<string, number>;
159
- numIters: number;
160
- }
161
-
162
- export const DEFAULT_TRAIN_CONFIG: TrainConfig = {
163
- method: "sft",
164
- rank: 8,
165
- scale: 1.0,
166
- rankScaling: "by_bits",
167
- targetModules: [...DEFAULT_TARGET_MODULES],
168
- numLayers: -1,
169
- iters: 100,
170
- learningRate: 2e-4,
171
- maxSeqLen: 512,
172
- batchSize: 1,
173
- gradAccumSteps: 1,
174
- seed: 0,
175
- stepsPerReport: 10,
176
- stepsPerEval: 50,
177
- betas: [0.9, 0.999],
178
- weightDecay: 0.01,
179
- loraDropout: 0.0, // off by default
180
- rsLora: false, // off by default (opt-in; recommended when rank_scaling != constant)
181
- loraPlusRatio: 1.0, // 1 = off
182
- mlpSplit: false, // default off; only effective with gradCheckpoint (Gemma4)
183
- gradCheckpoint: false, // default off until validated; flip to true (optiq default) after
184
- segmentSize: 0, // default off; >0 enables segmented backward (MiniCPM5 SFT B=1)
185
- saveCheckpoints: false, // off by default; scripts opt in (keep-all-checkpoints + metrics.json)
186
- gradClipNorm: 1.0, // on by default (standard ORPO/DPO guard); 0 = off
187
- valMaxExamples: 256, // fixed val subset → fast, meaningful correct/total
188
-
189
- dpoBeta: 0.1,
190
- dpoWarmupIters: 0,
191
- dpoLrSchedule: "cosine",
192
-
193
- orpoLambda: 0.1, // paper's primary value + TRL default; weights only L_OR
194
- orpoWarmupIters: 0,
195
- orpoLrSchedule: "cosine",
196
- orpoChunkSize: 0, // default off; >0 = token-chunked head (bounds the [M,vocab] term)
197
- orpoFusedCe: false, // default off; true = fused linear-CE head (analytic backward, no [M,vocab])
198
- orpoFlashCe: false, // default off; true = flash-CCE Metal-kernel head (implies fused)
199
- orpoPrefixShared: false, // default off; true = shared prompt-prefix single forward (B=1)
200
- sftScope: "full", // paper/TRL-faithful chosen-NLL over prompt+response; "response" = pre-2026-07 behavior
201
- warmStartAdapter: "", // default off; path to an adapter dir to warm-start LoRA weights from
202
- adapterPath: "adapters",
203
- baseModel: "",
204
- };
205
-
206
- /** Build the per-leaf LoRA+ LR multipliers for flatParams' stable
207
- * `[...A, ...B]` order. `undefined` keeps AdamW's uniform-LR fast path. */
208
- export function loraPlusLrScale(
209
- paramCount: number,
210
- targetCount: number,
211
- ratio: number,
212
- ): number[] | undefined {
213
- if (!Number.isFinite(ratio) || ratio < 1)
214
- throw new RangeError(`loraPlusRatio must be a finite number >= 1 (got ${ratio})`);
215
- if (paramCount !== targetCount * 2)
216
- throw new Error(
217
- `LoRA+ expected ${targetCount * 2} A/B leaves for ${targetCount} targets, got ${paramCount}`,
218
- );
219
- if (ratio === 1) return undefined;
220
- return Array.from({ length: paramCount }, (_, i) => (i < targetCount ? 1 : ratio));
221
- }
222
-
223
- /** One optimizer policy for SFT, DPO, and ORPO. Keeping LoRA+ here prevents a
224
- * method-specific loop from silently accepting the knob without applying it. */
225
- function createLoraOptimizer(
226
- params: MlxArray[],
227
- lora: TrainableLora,
228
- cfg: TrainConfig,
229
- emit: Emit,
230
- ): AdamW {
231
- const lrScale = loraPlusLrScale(params.length, lora.targets.length, cfg.loraPlusRatio);
232
- if (lrScale) emit({
233
- type: "stage",
234
- stage: "setup",
235
- progress: 0.04,
236
- message: `LoRA+ : B leaves at ${cfg.loraPlusRatio}× LR`,
237
- });
238
- return new AdamW(
239
- params,
240
- {
241
- lr: cfg.learningRate,
242
- betas: cfg.betas,
243
- eps: 1e-8,
244
- weightDecay: cfg.weightDecay,
245
- lrScale,
246
- },
247
- (i, p) => writeParam(lora, i, p),
248
- );
249
- }
250
-
251
- /** Run a LoRA fine-tune. Emits per-step train metrics, periodic val metrics,
252
- * and a final stage:done with the adapter path + applied ranks. Saves the
253
- * adapter (last + best-on-val) and returns where it landed. */
254
- export async function trainLora(
255
- model: RuntimeModel,
256
- tok: LoadedTokenizer,
257
- tmpl: ChatTemplate,
258
- dataDir: string,
259
- cfg: TrainConfig,
260
- emit: Emit,
261
- ): Promise<TrainResult> {
262
- emit({ type: "stage", stage: "setup", progress: 0.02, message: "resolving ranks" });
263
-
264
- // Mixed-precision rank scaling: feed the per-layer bits (from the loaded
265
- // model's quant specs) and, for by_kl, the recorded KL sensitivities
266
- // (optiq_metadata.json). Without these maps, by_bits/by_kl silently fell back
267
- // to uniform rank — so the "give the sensitive (8-bit) layers more adapter
268
- // capacity" policy (optiq: one sensitivity signal, two optimizations — the
269
- // bit assignment AND the LoRA rank) never actually fired.
270
- const bitsMap = bitsMapFromModel(model);
271
- const klMap = cfg.rankScaling === "by_kl" ? await readPerLayerKl(cfg.baseModel) : undefined;
272
- const ranks = resolveRanks(model, {
273
- rank: cfg.rank,
274
- rankScaling: cfg.rankScaling,
275
- targetModules: cfg.targetModules,
276
- numLayers: cfg.numLayers,
277
- bitsMap,
278
- klMap,
279
- });
280
- const appliedRanks: Record<string, number> = Object.fromEntries(ranks);
281
-
282
- // Surface the rank spread so mixed-precision scaling is observable (e.g.
283
- // "r8×140 r16×28" means 28 sensitive layers got the wider adapter).
284
- const rankCounts = new Map<number, number>();
285
- for (const r of ranks.values()) rankCounts.set(r, (rankCounts.get(r) ?? 0) + 1);
286
- const rankSpread = [...rankCounts.entries()]
287
- .sort((a, b) => a[0] - b[0])
288
- .map(([r, n]) => `r${r}×${n}`)
289
- .join(" ");
290
- emit({ type: "stage", stage: "setup", progress: 0.03,
291
- message: `ranks (${cfg.rankScaling}): ${rankSpread}` });
292
-
293
- const lora = buildTrainableLora(model, ranks, cfg.scale, cfg.seed, cfg.rsLora);
294
- // Warm-start: continue from a saved adapter/checkpoint's weights (optimizer + LR
295
- // schedule restart fresh). Must run before the optimizer is built (below) so it
296
- // tracks the loaded leaves. Rank/targets must match the checkpoint.
297
- if (cfg.warmStartAdapter) {
298
- const n = warmStartFromAdapter(lora, cfg.warmStartAdapter);
299
- emit({ type: "stage", stage: "setup", progress: 0.035,
300
- message: `warm-start: loaded ${n} LoRA targets from ${cfg.warmStartAdapter} (optimizer + LR schedule restart)` });
301
- }
302
- attachForTraining(model, lora, "train");
303
- model.loraState.dropoutRate = cfg.loraDropout; // per-step seed set inside the loop
304
- // Training-mode fused GeGLU (Gemma e4b): kernel forward + hand-derived vjp, so
305
- // autograd flows and the backward recomputes the gelu from the primal instead
306
- // of retaining the spelled-out intermediates. No-op for non-Gemma models.
307
- // Cleared in the finally below.
308
-
309
- const saveCfg: SaveAdapterConfig = {
310
- rank: cfg.rank,
311
- scale: cfg.scale,
312
- rankScaling: cfg.rankScaling,
313
- targetModules: cfg.targetModules,
314
- numLayers: cfg.numLayers,
315
- method: cfg.method,
316
- baseModel: cfg.baseModel,
317
- rsLora: cfg.rsLora,
318
- };
319
-
320
- // Keep-ALL-checkpoints + a durable metrics record. At each eval step we
321
- // snapshot the adapter to checkpoints/step-<NNNNN>-val<loss>/ — a full,
322
- // mountable adapter — so the best-ON-TASK checkpoint can be chosen by the
323
- // real eval afterward (val is only a proxy for task quality, so we do NOT
324
- // pick for the user). The loss/val trajectory is written to metrics.json so
325
- // runs are comparable and survive (the tee'd stdout log is ephemeral). We
326
- // wrap `emit` rather than touching the loops: it already streams train/val
327
- // metrics, and the live LoRA leaves are the current step's weights at emit
328
- // time, so saving here captures exactly that checkpoint.
329
- const trainHistory: { step: number; loss: number }[] = [];
330
- // For preference methods (dpo/orpo) the val MARGIN (mean chosen−rejected
331
- // log-odds) and accuracy are the meaningful early-stop signals — val loss is a
332
- // weak proxy. Capture them when the loop emits them so metrics.json lets you
333
- // pick the best-MARGIN checkpoint (overfit shows as train↑ / val-margin flat).
334
- const valHistory: { step: number; loss: number; margin?: number; accuracy?: number; checkpoint: string | null }[] = [];
335
- const checkpointSaves: Promise<void>[] = [];
336
- const startedAt = Date.now();
337
-
338
- // Live, append-only metrics stream (the source `mlx-bun train-watch` tails).
339
- // Always on — it's the durable per-step record, a few MB at most, single
340
- // writer, append-only. The first line is a `meta` record (totals + config) so
341
- // a viewer attaching mid-run has the denominators (iters) and run identity
342
- // without replaying. Each subsequent line is a metric event + a wall-clock `t`
343
- // (ms) so the viewer derives s/step and ETA itself. Best-effort: a logging
344
- // failure must never kill a run.
345
- mkdirSync(cfg.adapterPath, { recursive: true });
346
- const metricsJsonl = `${cfg.adapterPath}/metrics.jsonl`;
347
- const writeMetricLine = (o: Record<string, unknown>) => {
348
- try { appendFileSync(metricsJsonl, JSON.stringify(o) + "\n"); } catch { /* never throw */ }
349
- };
350
- try { writeFileSync(metricsJsonl, ""); } catch { /* truncate prior run's stream */ }
351
- writeMetricLine({
352
- type: "meta", t: startedAt, method: cfg.method, model: cfg.baseModel,
353
- iters: cfg.iters, learning_rate: cfg.learningRate, max_seq_length: cfg.maxSeqLen,
354
- batch_size: cfg.batchSize, rank: cfg.rank, scale: cfg.scale,
355
- orpo_lambda: cfg.method === "orpo" ? cfg.orpoLambda : undefined,
356
- sft_scope: cfg.method === "orpo" ? cfg.sftScope : undefined,
357
- adapter_path: cfg.adapterPath,
358
- });
359
-
360
- const collect: Emit = (e) => {
361
- if (e.type === "metric") writeMetricLine({ ...e, t: Date.now() });
362
- if (e.type === "metric" && e.kind === "train") {
363
- trainHistory.push({ step: e.step, loss: e.loss });
364
- } else if (e.type === "metric" && e.kind === "val") {
365
- let checkpoint: string | null = null;
366
- if (cfg.saveCheckpoints) {
367
- const tag = `step-${String(e.step).padStart(5, "0")}-val${e.loss.toFixed(4)}`;
368
- checkpoint = `${cfg.adapterPath}/checkpoints/${tag}`;
369
- const save = saveAdapter(lora, checkpoint, saveCfg, appliedRanks);
370
- checkpointSaves.push(save);
371
- // Keep a handler attached while training continues. Promise.all below
372
- // still surfaces the original rejection before the run can succeed.
373
- void save.catch(() => {});
374
- emit({ type: "stage", stage: "checkpoint", progress: e.step / cfg.iters,
375
- message: `checkpoint ${tag}` });
376
- }
377
- const ev = e as { step: number; loss: number; margin?: number; accuracy?: number };
378
- valHistory.push({ step: ev.step, loss: ev.loss, margin: ev.margin, accuracy: ev.accuracy, checkpoint });
379
- }
380
- emit(e);
381
- };
382
-
383
- try {
384
- const result =
385
- cfg.method === "dpo"
386
- ? await dpoLoop(model, tok, tmpl, dataDir, cfg, lora, collect)
387
- : cfg.method === "orpo"
388
- ? await orpoLoop(model, tok, tmpl, dataDir, cfg, lora, collect)
389
- : await sftLoop(model, tok, tmpl, dataDir, cfg, lora, collect);
390
-
391
- await Promise.all(checkpointSaves);
392
-
393
- // Final save (last adapter).
394
- detachTraining(model, lora);
395
- await saveAdapter(lora, cfg.adapterPath, saveCfg, appliedRanks);
396
-
397
- // Durable, structured run record alongside the adapter (only when we kept
398
- // checkpoints). Pick the best-val one for convenience, but ship ALL of them
399
- // — the real eval decides; val is only a proxy.
400
- if (cfg.saveCheckpoints) {
401
- let bestVal: { step: number; loss: number; checkpoint: string | null } | null = null;
402
- for (const v of valHistory) if (bestVal === null || v.loss < bestVal.loss) bestVal = v;
403
- // Best by MARGIN too (preference methods) — the better checkpoint signal.
404
- let bestMargin: typeof valHistory[number] | null = null;
405
- for (const v of valHistory)
406
- if (v.margin !== undefined && (bestMargin === null || v.margin > (bestMargin.margin ?? -Infinity))) bestMargin = v;
407
- writeFileSync(`${cfg.adapterPath}/metrics.json`, JSON.stringify({
408
- config: {
409
- method: cfg.method, rank: cfg.rank, scale: cfg.scale,
410
- learningRate: cfg.learningRate, maxSeqLen: cfg.maxSeqLen,
411
- segmentSize: cfg.segmentSize, iters: cfg.iters,
412
- numLayers: cfg.numLayers, baseModel: cfg.baseModel,
413
- orpoLambda: cfg.method === "orpo" ? cfg.orpoLambda : undefined,
414
- sftScope: cfg.method === "orpo" ? cfg.sftScope : undefined,
415
- },
416
- wallSeconds: (Date.now() - startedAt) / 1000,
417
- peakGb: peakMemory() / 1e9,
418
- finalTrainLoss: trainHistory.at(-1)?.loss ?? null,
419
- finalValLoss: valHistory.at(-1)?.loss ?? null,
420
- bestVal,
421
- bestMargin, // null for SFT (no margin); the recommended pick for dpo/orpo
422
- valTrajectory: valHistory,
423
- trainLosses: trainHistory,
424
- }, null, 2));
425
- }
426
-
427
- emit({
428
- type: "stage",
429
- stage: "done",
430
- progress: 1,
431
- message: `adapter saved to ${cfg.adapterPath}`,
432
- adapter_path: cfg.adapterPath,
433
- applied_ranks: appliedRanks,
434
- });
435
- return { adapterPath: cfg.adapterPath, appliedRanks, numIters: result.numIters };
436
- } finally {
437
- disposeLora(lora);
438
- }
439
- }
440
-
441
- // ---------------------------------------------------------------------------
442
- // Segment planning: uniform planSegmentsBySize only. A full-attention
443
- // ISOLATION planner (kernel-review backlog #2 / docs/design/orpo-training.md
444
- // §5) was built and A/B-measured 2026-07-02: ZERO peak win — mlx's sdpa
445
- // backward is O(L²) for EVERY layer (~3.5 GB/layer @8K e4b), sliding included,
446
- // so the worst segment is set by layer count alone and segment_size is the
447
- // whole knob (seg1 = 14.59 GB @8K, +3% step time — fits the 24 GB M4 Pro).
448
- // Evidence: seg-isolation-smoke.ts (deleted 2026-08-23; git history); note in segmented.ts.
449
- // ---------------------------------------------------------------------------
450
-
451
- // ---------------------------------------------------------------------------
452
- // SFT loop
453
- // ---------------------------------------------------------------------------
454
-
455
- async function sftLoop(
456
- model: RuntimeModel,
457
- tok: LoadedTokenizer,
458
- tmpl: ChatTemplate,
459
- dataDir: string,
460
- cfg: TrainConfig,
461
- lora: TrainableLora,
462
- emit: Emit,
463
- ): Promise<{ numIters: number }> {
464
- emit({ type: "stage", stage: "data", progress: 0.05, message: "loading SFT dataset" });
465
- const train = await loadSftDataset(`${dataDir}/train.jsonl`, tok, tmpl);
466
- const valid = (await fileExists(`${dataDir}/valid.jsonl`))
467
- ? await loadSftDataset(`${dataDir}/valid.jsonl`, tok, tmpl)
468
- : [];
469
-
470
- const params = flatParams(lora);
471
- const opt = createLoraOptimizer(params, lora, cfg, emit);
472
-
473
- // Segmented backward: stream the SFT backward segment-by-segment (only one
474
- // segment's activations live at a time). Phase A is MiniCPM5 SFT B=1 only;
475
- // it replaces the single value_and_grad below and is mutually exclusive with
476
- // gradient checkpointing (see docs/design/orpo-training.md).
477
- const useSegmented = cfg.segmentSize > 0;
478
- let segmented: SegmentedBackward | SegmentedBackwardGemma4 | null = null;
479
- if (useSegmented) {
480
- if (!(model instanceof MiniCPM5Model) && !(model instanceof Gemma4Model))
481
- throw new Error("segmented backward (segmentSize > 0) is only wired for MiniCPM5 and Gemma4");
482
- const ranges = planSegmentsBySize(model.layers.length, cfg.segmentSize);
483
- segmented = model instanceof MiniCPM5Model
484
- ? new SegmentedBackward(model, lora, ranges)
485
- : new SegmentedBackwardGemma4(model, lora, ranges);
486
- emit({ type: "stage", stage: "setup", progress: 0.04,
487
- message: `segmented backward: ${ranges.length} segments of <=${cfg.segmentSize} layers` });
488
- }
489
-
490
- // The value_and_grad loss closure: temporarily swap the differentiated
491
- // primals into the LoraWeights so the forward graph differentiates them,
492
- // build the SFT loss against the active batch, then restore the original
493
- // leaf wrappers (the primal wrappers get disposed by the autograd closure;
494
- // the graph already captured their handles). Not built on the segmented path
495
- // (segmentedSftGrads runs its own per-segment value_and_grads).
496
- let currentBatch: SftBatch | null = null;
497
- const vag = useSegmented
498
- ? null
499
- : new ValueAndGrad((primals) => {
500
- const saved = swapPrimals(lora, primals);
501
- try {
502
- return sftLoss(model, currentBatch!);
503
- } finally {
504
- restorePrimals(lora, saved);
505
- }
506
- }, params.map((_, i) => i));
507
-
508
- // Gradient checkpointing: group each layer's LoRA weights so forwardLayers
509
- // can thread them as explicit checkpoint inputs (see Gemma4Model.gradCkpt).
510
- // Checkpoints are created per forward and disposed after each step's backward.
511
- let ckptCtx: GradCheckpointCtx | null = null;
512
- if (!useSegmented && cfg.gradCheckpoint && model instanceof Gemma4Model) {
513
- // Partition each layer's LoRA into the attention sub-block (self_attn.*) and
514
- // the MLP sub-block (everything else in the layer: gate/up/down_proj +
515
- // per_layer_*) so the split-MLP checkpoint can wrap them independently.
516
- const byLayer: GradCheckpointCtx["byLayer"] = new Map();
517
- for (const t of lora.targets) {
518
- const m = t.modulePath.match(/\.layers\.(\d+)\./);
519
- if (!m) continue;
520
- const li = Number(m[1]);
521
- const ll = byLayer.get(li) ?? byLayer.set(li, { attn: [], mlp: [] }).get(li)!;
522
- (t.modulePath.includes(".self_attn.") ? ll.attn : ll.mlp).push(t.lw);
523
- }
524
- ckptCtx = { byLayer, splitMlp: cfg.mlpSplit, keepAlive: [] };
525
- model.gradCkpt = ckptCtx;
526
- if (cfg.mlpSplit) emit({ type: "stage", stage: "setup", progress: 0.04,
527
- message: "grad checkpoint: intra-layer attn/MLP split" });
528
- }
529
-
530
- // Training attention. Default = mlx fused SDPA (ops.sdpa): exact dQ/dK/dV
531
- // (finite-difference verified) and fast, but O(L²) backward memory. The
532
- // hand-rolled flash kernel is an opt-in (MLX_BUN_TRAIN_ATTN=flash) O(L)-memory
533
- // path for memory-bound long context — now correct (its dK-transpose and
534
- // dQ-causal-barrier bugs were fixed; FD-validated in
535
- // flash-fd-check.ts (deleted 2026-08-23; git history)), though ~30× slower than ops.sdpa.
536
- // Cleared in finally.
537
- //
538
- // GEMMA GUARD (same convention as the launcher's env sanitization in
539
- // cli.ts / job.ts, but enforced at the trainer): e4b on this path SIGTRAPed
540
- // (uncatchable native crash) at multi-K sequence lengths (>=2K, reproduced in
541
- // segmented-grad-test-e4b.ts (deleted 2026-08-23; git history); docs/reference/training.md)
542
- // and has NOT been re-validated at that scale since the two kernel fixes —
543
- // the regression tests stop at T<=256. cli.ts even defaults e4b seq to 8192,
544
- // so a stale `export MLX_BUN_TRAIN_ATTN=flash` from a MiniCPM5 experiment
545
- // would otherwise ride silently into a crash mid-run. Refuse it for Gemma
546
- // until the >=2K re-validation lands; MiniCPM5 stays allowed.
547
- if (runtimeValue("MLX_BUN_TRAIN_ATTN") === "flash") {
548
- if (model instanceof Gemma4Model)
549
- throw new Error(
550
- "MLX_BUN_TRAIN_ATTN=flash is disabled for Gemma models: e4b SIGTRAPs on this " +
551
- "path at seq >= 2048 (docs/reference/training.md) and it has not been " +
552
- "re-validated at that scale since the kernel fixes. Unset MLX_BUN_TRAIN_ATTN " +
553
- "(ops.sdpa, the default, is exact and ~30x faster) or train MiniCPM5.",
554
- );
555
- emit({ type: "stage", stage: "setup", progress: 0.045,
556
- message: "training attention: hand-rolled flash kernel (MLX_BUN_TRAIN_ATTN=flash; O(L) memory, ~30x slower than ops.sdpa)" });
557
- setTrainingAttn("flash");
558
- }
559
-
560
- // Pad with the eos id when available (a real sentinel token); padded
561
- // positions are excluded from both the loss and attention masks, so the
562
- // exact pad value never affects the result — it just must be a valid id.
563
- const padId = tok.eosTokenId ?? 0;
564
- const batches = iterateSftBatches(train, cfg.batchSize, cfg.maxSeqLen, cfg.seed, true, padId);
565
- // tokens/sec follows mlx-lm's tuner convention: accumulate the response-token
566
- // count across the REPORT WINDOW and divide by the window's own wall time
567
- // (validation time excluded) — not one step's count extrapolated over the
568
- // whole run, which jumps ~10x step-to-step on variable-length data.
569
- let windowTokens = 0;
570
- let windowStart = Date.now();
571
-
572
- try {
573
- for (let step = 1; step <= cfg.iters; step++) {
574
- if (MEM_LOG) resetPeakMemory();
575
-
576
- // One optimizer step over cfg.gradAccumSteps micro-batches (pass-through
577
- // when 1). afterMicroEval frees each micro-batch's gradient-checkpoint
578
- // closures right after its backward so they don't pile up across
579
- // accumulation.
580
- const { loss: lossVal, grads } = accumulateStep(
581
- cfg.gradAccumSteps,
582
- () => {
583
- currentBatch = batches.next().value as SftBatch;
584
- windowTokens += countResponseTokens(currentBatch);
585
- return segmented ? segmented.step(currentBatch!) : vag!.apply(flatParams(lora));
586
- },
587
- ckptCtx
588
- ? () => {
589
- for (const ck of ckptCtx!.keepAlive) ck.dispose();
590
- ckptCtx!.keepAlive.length = 0;
591
- }
592
- : undefined,
593
- );
594
-
595
- const gradNorm = globalNorm(grads); // grads already materialized above
596
- clipGradsByNorm(grads, gradNorm, cfg.gradClipNorm);
597
- opt.step(grads);
598
- opt.evalState();
599
- if (MEM_LOG) {
600
- const pk = peakMemory(), act = activeMemory(), ch = cacheMemory();
601
- clearCache();
602
- console.log(` [mem] step ${step}: PEAK(live)=${gbStr(pk)} active=${gbStr(act)} cache=${gbStr(ch)} -> after clearCache active=${gbStr(activeMemory())}`);
603
- } else {
604
- clearCache();
605
- }
606
-
607
- if (step % cfg.stepsPerReport === 0 || step === 1) {
608
- const elapsed = (Date.now() - windowStart) / 1000;
609
- emit({
610
- type: "metric",
611
- kind: "train",
612
- step,
613
- loss: lossVal,
614
- grad_norm: gradNorm,
615
- learning_rate: opt.lr,
616
- tokens_per_sec: elapsed > 0 ? windowTokens / elapsed : 0,
617
- progress: step / cfg.iters,
618
- message: `step ${step}/${cfg.iters} loss=${lossVal.toFixed(4)}`,
619
- });
620
- windowTokens = 0;
621
- windowStart = Date.now();
622
- }
623
-
624
- if (valid.length > 0 && step % cfg.stepsPerEval === 0) {
625
- // Eval is forward-only (no backward), so disable checkpointing for it —
626
- // it adds no benefit and a forward-only pass fits without it.
627
- if (ckptCtx) (model as Gemma4Model).gradCkpt = null;
628
- const tVal = Date.now();
629
- const { loss: vLoss, used, skipped } = evalSftLoss(model, valid, cfg);
630
- if (ckptCtx) (model as Gemma4Model).gradCkpt = ckptCtx;
631
- // A silently-shrinking val set must be visible: rows whose supervised
632
- // span is empty after truncation are skipped BY DESIGN (mlx-lm drops
633
- // them at dataset build time) — report how many, and over what n the
634
- // mean was taken. Any other per-row failure now rethrows (see
635
- // evalSftLoss).
636
- emit({ type: "metric", kind: "val", step, loss: vLoss, progress: step / cfg.iters,
637
- val_rows_used: used, val_rows_skipped: skipped,
638
- ...(skipped > 0 ? { message: `val loss over ${used} rows (${skipped} skipped: no response tokens to supervise)` } : {}) });
639
- // validation wall time is not training throughput — keep it out of the window
640
- windowStart += Date.now() - tVal;
641
- }
642
- }
643
- } finally {
644
- setTrainingAttn(null);
645
- vag?.dispose();
646
- segmented?.dispose();
647
- opt.dispose();
648
- if (ckptCtx) {
649
- (model as Gemma4Model).gradCkpt = null;
650
- for (const ck of ckptCtx.keepAlive) ck.dispose();
651
- }
652
- }
653
- return { numIters: cfg.iters };
654
- }
655
-
656
- function evalSftLoss(
657
- model: RuntimeModel, valid: ReturnType<typeof Array.prototype.slice>, cfg: TrainConfig,
658
- ): { loss: number; used: number; skipped: number } {
659
- let total = 0;
660
- let n = 0;
661
- let skipped = 0;
662
- for (const ex of valid as { ids: number[]; promptLen: number }[]) {
663
- let ids = ex.ids;
664
- let promptLen = ex.promptLen;
665
- if (ids.length < 2) { skipped++; continue; }
666
- if (ids.length > cfg.maxSeqLen) {
667
- ids = ids.slice(0, cfg.maxSeqLen);
668
- promptLen = Math.min(promptLen, Math.max(0, ids.length - 1));
669
- }
670
- const batch: SftBatch = { ids: [ids], promptLens: [promptLen] };
671
- try {
672
- const loss = sftLoss(model, batch);
673
- total += loss.toFloat32()[0]!;
674
- loss.dispose();
675
- n++;
676
- } catch (e) {
677
- // ONLY the empty-supervision row is skippable (a truncated row whose
678
- // response fell entirely past maxSeqLen — mlx-lm drops these at dataset
679
- // build). Everything else (MLX OOM/shape/vjp errors surface as catchable
680
- // JS Errors via the ffi handler) must rethrow: swallowing them silently
681
- // reduces the val set to its easy rows and reports a plausible loss.
682
- if (e instanceof Error && e.message === "sftLoss: no response tokens to supervise") skipped++;
683
- else throw e;
684
- }
685
- clearCache();
686
- }
687
- return { loss: n > 0 ? total / n : 0, used: n, skipped };
688
- }
689
-
690
- // ---------------------------------------------------------------------------
691
- // DPO loop
692
- // ---------------------------------------------------------------------------
693
-
694
- async function dpoLoop(
695
- model: RuntimeModel,
696
- tok: LoadedTokenizer,
697
- tmpl: ChatTemplate,
698
- dataDir: string,
699
- cfg: TrainConfig,
700
- lora: TrainableLora,
701
- emit: Emit,
702
- ): Promise<{ numIters: number }> {
703
- emit({ type: "stage", stage: "data", progress: 0.05, message: "loading DPO dataset" });
704
- const train = await loadDpoDataset(`${dataDir}/train.jsonl`, tok, tmpl, cfg.maxSeqLen);
705
- const valid = (await fileExists(`${dataDir}/valid.jsonl`))
706
- ? await loadDpoDataset(`${dataDir}/valid.jsonl`, tok, tmpl, cfg.maxSeqLen)
707
- : [];
708
-
709
- const params = flatParams(lora);
710
- const opt = createLoraOptimizer(params, lora, cfg, emit);
711
-
712
- const schedule =
713
- cfg.dpoLrSchedule === "cosine" || cfg.dpoWarmupIters > 0
714
- ? warmupCosineSchedule(cfg.learningRate, cfg.dpoWarmupIters, cfg.iters)
715
- : null;
716
-
717
- // Reference log-probs are computed each step OUTSIDE value_and_grad (so the
718
- // reference activations free before the policy forwards), then read as
719
- // detached constants inside the closure.
720
- let currentBatch: DpoBatch | null = null;
721
- let refChosen: MlxArray | null = null;
722
- let refRejected: MlxArray | null = null;
723
-
724
- const vag = new ValueAndGrad((primals) => {
725
- const saved = swapPrimals(lora, primals);
726
- try {
727
- return dpoLoss(model, currentBatch!, cfg.dpoBeta, refChosen!, refRejected!);
728
- } finally {
729
- restorePrimals(lora, saved);
730
- }
731
- }, params.map((_, i) => i));
732
-
733
- const padId = tok.eosTokenId ?? 0;
734
- const batches = iterateDpoBatches(train, cfg.batchSize, cfg.seed, true, padId);
735
- const t0 = Date.now();
736
-
737
- try {
738
- for (let step = 1; step <= cfg.iters; step++) {
739
- if (schedule) opt.lr = schedule(step);
740
-
741
- // One optimizer step over cfg.gradAccumSteps micro-batches (pass-through
742
- // when 1). Reference log-probs are recomputed per micro-batch OUTSIDE the
743
- // policy value_and_grad and read as detached constants inside it;
744
- // afterMicroEval frees them once that micro-batch's grads are materialized.
745
- const { loss: lossVal, grads } = accumulateStep(
746
- cfg.gradAccumSteps,
747
- () => {
748
- currentBatch = batches.next().value as DpoBatch;
749
- const refs = dpoRefLogps(model, lora, currentBatch);
750
- refChosen = refs.refChosen;
751
- refRejected = refs.refRejected;
752
- return vag.apply(flatParams(lora));
753
- },
754
- () => {
755
- refChosen?.dispose();
756
- refRejected?.dispose();
757
- refChosen = refRejected = null;
758
- },
759
- );
760
-
761
- const gradNorm = globalNorm(grads);
762
- clipGradsByNorm(grads, gradNorm, cfg.gradClipNorm);
763
- opt.step(grads);
764
- opt.evalState();
765
- clearCache();
766
-
767
- if (step % cfg.stepsPerReport === 0 || step === 1) {
768
- const m = dpoMetrics(model, lora, currentBatch!, cfg.dpoBeta);
769
- const elapsed = (Date.now() - t0) / 1000;
770
- emit({
771
- type: "metric",
772
- kind: "train",
773
- step,
774
- loss: lossVal,
775
- grad_norm: gradNorm,
776
- learning_rate: opt.lr,
777
- accuracy: m.accuracy,
778
- margin: m.margin,
779
- tokens_per_sec: 0,
780
- progress: step / cfg.iters,
781
- message: `step ${step}/${cfg.iters} loss=${lossVal.toFixed(4)} acc=${m.accuracy.toFixed(2)} margin=${m.margin.toFixed(3)}`,
782
- });
783
- clearCache();
784
- }
785
-
786
- if (valid.length > 0 && step % cfg.stepsPerEval === 0) {
787
- let vl = 0, va = 0, vm = 0, vn = 0;
788
- for (const ex of valid) {
789
- const vb: DpoBatch = {
790
- chosenIds: [ex.chosenIds], rejectedIds: [ex.rejectedIds],
791
- chosenMask: [ex.chosenMask], rejectedMask: [ex.rejectedMask],
792
- };
793
- const m = dpoMetrics(model, lora, vb, cfg.dpoBeta);
794
- vl += m.loss; va += m.accuracy; vm += m.margin; vn++;
795
- clearCache();
796
- }
797
- emit({
798
- type: "metric", kind: "val", step,
799
- loss: vn ? vl / vn : 0,
800
- accuracy: vn ? va / vn : 0,
801
- margin: vn ? vm / vn : 0,
802
- progress: step / cfg.iters,
803
- });
804
- }
805
- }
806
- } finally {
807
- vag.dispose();
808
- opt.dispose();
809
- // Cast: refs are only ever assigned inside the accumulateStep closures, so
810
- // TS's flow analysis narrows these to their `null` initializer here.
811
- (refChosen as MlxArray | null)?.dispose();
812
- (refRejected as MlxArray | null)?.dispose();
813
- }
814
- return { numIters: cfg.iters };
815
- }
816
-
817
- // ---------------------------------------------------------------------------
818
- // ORPO loop (reference-free: 2 forwards/step, no reference pass)
819
- // ---------------------------------------------------------------------------
820
-
821
- async function orpoLoop(
822
- model: RuntimeModel,
823
- tok: LoadedTokenizer,
824
- tmpl: ChatTemplate,
825
- dataDir: string,
826
- cfg: TrainConfig,
827
- lora: TrainableLora,
828
- emit: Emit,
829
- ): Promise<{ numIters: number }> {
830
- emit({ type: "stage", stage: "data", progress: 0.05, message: "loading preference dataset" });
831
- const train = await loadDpoDataset(`${dataDir}/train.jsonl`, tok, tmpl, cfg.maxSeqLen);
832
- const valid = (await fileExists(`${dataDir}/valid.jsonl`))
833
- ? await loadDpoDataset(`${dataDir}/valid.jsonl`, tok, tmpl, cfg.maxSeqLen)
834
- : [];
835
-
836
- const params = flatParams(lora);
837
- const opt = createLoraOptimizer(params, lora, cfg, emit);
838
-
839
- const schedule =
840
- cfg.orpoLrSchedule === "cosine" || cfg.orpoWarmupIters > 0
841
- ? warmupCosineSchedule(cfg.learningRate, cfg.orpoWarmupIters, cfg.iters)
842
- : null;
843
-
844
- // Segmented backward for ORPO: stream chosen+rejected backwards segment-by-segment.
845
- // MiniCPM5 + Gemma4 (e4b) — same model coverage as SFT segmented. B=1 only.
846
- const useSegmented = cfg.segmentSize > 0;
847
- // Prefix-sharing + segmented backward COMPOSE (M3-composition): the segmented
848
- // backward streams the ONE prefix-shared concat forward [prompt; chosen; rejected]
849
- // (block-sparse mask via PrefixSharedCache + block-wise RoPE via the prefix plan)
850
- // segment-by-segment — so the prompt-encode-once saving holds AT long seq. Composed
851
- // for BOTH MiniCPM5 and Gemma4/e4b (the e4b variant threads donor-KV + the
852
- // logical-position sliding-window prefix mask through the segment boundaries).
853
- const useSegmentedPrefix = cfg.orpoPrefixShared && useSegmented;
854
- if (cfg.orpoPrefixShared && !(model instanceof MiniCPM5Model || model instanceof Gemma4Model))
855
- throw new Error("orpoPrefixShared is only wired for MiniCPM5 and Gemma4");
856
- // Fused linear-CE head INSIDE the segmented backward (orpo_fused_ce): bounds the
857
- // head term to [chunk,V] alongside the per-segment layer savings. 0 = the
858
- // full-[M,V] responseOnlyLogpMean head (unchanged). Reuses orpoChunkSize as the
859
- // token-chunk (default 512), same as the non-segmented path.
860
- // "flash implies fused": orpoFlashCe alone must still bound the segmented head to
861
- // [chunk,V]. The non-prefix segmented class has no flash path, so flash routes
862
- // through its fused/checkpoint chunked head (boundedHeadFromHidden) — never the
863
- // full-[M,V] responseOnlyLogpMean, which would reintroduce [M,vocab] on e4b.
864
- const segUsesFusedHead = cfg.orpoFusedCe || cfg.orpoFlashCe;
865
- const segFusedChunk = segUsesFusedHead ? (cfg.orpoChunkSize > 0 ? cfg.orpoChunkSize : 512) : 0;
866
- // The prefix segmented head gathers response positions then routes per-branch
867
- // through the [M,V]-free flash/fused head (branchLogpMeanGathered, which takes a
868
- // ChunkCtx). Its own sink is managed per-step inside the class.
869
- const segPrefixChunk: ChunkCtx | undefined =
870
- cfg.orpoFusedCe || cfg.orpoFlashCe
871
- ? { chunkSize: cfg.orpoChunkSize > 0 ? cfg.orpoChunkSize : 512, fused: true, flash: cfg.orpoFlashCe, sink: [] }
872
- : undefined;
873
- let segmentedOrpo: SegmentedBackwardOrpo | SegmentedBackwardOrpoGemma4 | null = null;
874
- let segmentedOrpoPrefix: SegmentedBackwardOrpoPrefix | SegmentedBackwardOrpoPrefixGemma4 | null = null;
875
- if (useSegmented) {
876
- if (!(model instanceof MiniCPM5Model) && !(model instanceof Gemma4Model))
877
- throw new Error("segmented backward (segmentSize > 0) for ORPO is only wired for MiniCPM5 and Gemma4");
878
- const ranges = planSegmentsBySize(model.layers.length, cfg.segmentSize);
879
- if (useSegmentedPrefix) {
880
- // The composed prefix-shared segmented backward (MiniCPM5 or Gemma4/e4b), plus a
881
- // plain segmented-ORPO fallback for rows whose chosen/rejected prompts differ
882
- // (splitPrefixBatch returns null -> two-forward segmented step).
883
- if (model instanceof MiniCPM5Model) {
884
- segmentedOrpoPrefix = new SegmentedBackwardOrpoPrefix(model, lora, ranges, cfg.orpoLambda, segPrefixChunk, cfg.sftScope);
885
- segmentedOrpo = new SegmentedBackwardOrpo(model, lora, ranges, cfg.orpoLambda, segFusedChunk, cfg.sftScope);
886
- } else {
887
- segmentedOrpoPrefix = new SegmentedBackwardOrpoPrefixGemma4(model as Gemma4Model, lora, ranges, cfg.orpoLambda, segPrefixChunk, cfg.sftScope);
888
- segmentedOrpo = new SegmentedBackwardOrpoGemma4(model as Gemma4Model, lora, ranges, cfg.orpoLambda, segFusedChunk, cfg.sftScope);
889
- }
890
- emit({ type: "stage", stage: "setup", progress: 0.04,
891
- message: `orpo segmented + prefix-share (${model instanceof Gemma4Model ? "e4b" : "MiniCPM5"}): single concat forward streamed over ${ranges.length} segments of <=${cfg.segmentSize} layers (two-forward fallback on prompt mismatch)` });
892
- } else if (model instanceof MiniCPM5Model)
893
- segmentedOrpo = new SegmentedBackwardOrpo(model, lora, ranges, cfg.orpoLambda, segFusedChunk, cfg.sftScope);
894
- else
895
- segmentedOrpo = new SegmentedBackwardOrpoGemma4(model as Gemma4Model, lora, ranges, cfg.orpoLambda, segFusedChunk, cfg.sftScope);
896
- if (!useSegmentedPrefix) emit({ type: "stage", stage: "setup", progress: 0.04,
897
- message: `orpo segmented backward: ${ranges.length} segments of <=${cfg.segmentSize} layers${segFusedChunk > 0 ? ` + fused linear-CE head (${segFusedChunk}/chunk)` : ""}` });
898
- }
899
-
900
- // Token-chunked head (non-segmented path only): the per-chunk checkpoints are
901
- // collected here and disposed after each micro-step's grads are eval'd (the
902
- // afterMicroEval hook below) — that is when their backward recompute has run.
903
- const chunkSink: Array<{ dispose(): void }> = [];
904
- // Fused linear-CE head reuses orpoChunkSize as the token-chunk (default 512 when
905
- // unset); otherwise the Checkpoint token-chunked head fires only when
906
- // orpoChunkSize > 0. Both are non-segmented-path only.
907
- const fusedChunkSize = cfg.orpoChunkSize > 0 ? cfg.orpoChunkSize : 512;
908
- const chunk: ChunkCtx | undefined = useSegmented
909
- ? undefined
910
- : cfg.orpoFusedCe || cfg.orpoFlashCe
911
- ? { chunkSize: fusedChunkSize, fused: true, flash: cfg.orpoFlashCe, sink: chunkSink }
912
- : cfg.orpoChunkSize > 0
913
- ? { chunkSize: cfg.orpoChunkSize, sink: chunkSink }
914
- : undefined;
915
- if (chunk) emit({ type: "stage", stage: "setup", progress: 0.04,
916
- message: chunk.flash
917
- ? `orpo flash-CCE (Metal kernel) head: ${chunk.chunkSize} tokens/chunk`
918
- : chunk.fused
919
- ? `orpo fused linear-CE head: ${chunk.chunkSize} tokens/chunk`
920
- : `orpo chunked head: ${cfg.orpoChunkSize} tokens/chunk` });
921
- if (cfg.orpoPrefixShared && !useSegmentedPrefix) emit({ type: "stage", stage: "setup", progress: 0.04,
922
- message: `orpo prefix-sharing: single forward over [prompt; chosen; rejected] (two-forward fallback on prompt mismatch)` });
923
- const disposeChunkSink = () => { for (const c of chunkSink) c.dispose(); chunkSink.length = 0; };
924
-
925
- // No reference forward (the whole point of ORPO) — the closure just builds the
926
- // monolithic L_NLL + λ·L_OR over the active batch against the swapped primals.
927
- let currentBatch: DpoBatch | null = null;
928
- const vag = useSegmented
929
- ? null
930
- : new ValueAndGrad((primals) => {
931
- const saved = swapPrimals(lora, primals);
932
- try {
933
- // Prefix-sharing (B=1): one forward over [prompt; chosen; rejected] when the
934
- // row's chosen/rejected share an identical prompt; else fall back two-forward.
935
- if (cfg.orpoPrefixShared) {
936
- const split = splitPrefixBatch(currentBatch!);
937
- if (split) {
938
- if (model instanceof MiniCPM5Model)
939
- return orpoLossPrefixShared(model, split.promptIds, split.chosenResp, split.rejectedResp, cfg.orpoLambda, chunk, cfg.sftScope);
940
- return orpoLossPrefixSharedGemma(model as Gemma4Model, split.promptIds, split.chosenResp, split.rejectedResp, cfg.orpoLambda, chunk, cfg.sftScope);
941
- }
942
- notePrefixFallback();
943
- }
944
- return orpoLoss(model, currentBatch!, cfg.orpoLambda, chunk, cfg.sftScope);
945
- } finally {
946
- restorePrimals(lora, saved);
947
- }
948
- }, params.map((_, i) => i));
949
-
950
- const padId = tok.eosTokenId ?? 0;
951
- const batches = iterateDpoBatches(train, cfg.batchSize, cfg.seed, true, padId);
952
- const t0 = Date.now();
953
-
954
- let dropoutCounter = 0;
955
- // Prefix-share falls back to the two-forward path for any row whose chosen/rejected
956
- // prompts aren't byte-identical (splitPrefixBatch → null). Log the FIRST fallback +
957
- // count them, so a silent loss of the prompt-encode-once saving is visible.
958
- let prefixFallbacks = 0;
959
- const notePrefixFallback = () => {
960
- prefixFallbacks++;
961
- if (prefixFallbacks === 1) emit({ type: "stage", stage: "train", progress: 0,
962
- message: `prefix-share: row prompt mismatch → two-forward fallback for this row (saving lost only on mismatched rows; counting…)` });
963
- };
964
- try {
965
- for (let step = 1; step <= cfg.iters; step++) {
966
- if (schedule) opt.lr = schedule(step);
967
-
968
- // One optimizer step over cfg.gradAccumSteps micro-batches (pass-through
969
- // when 1) — the long-context lever for a larger effective batch without
970
- // the B>1 activation memory (see docs/design/orpo-training.md, Batching).
971
- const { loss: lossVal, grads } = accumulateStep(cfg.gradAccumSteps, () => {
972
- currentBatch = batches.next().value as DpoBatch;
973
- // Per-micro-step dropout seed: constant across this micro-step's forward
974
- // AND its backward recompute (segmented/chunked), so the mask reproduces.
975
- if (cfg.loraDropout > 0) model.loraState.dropoutSeed = ++dropoutCounter;
976
- // Segmented + prefix-share: stream the single concat forward when the row's
977
- // chosen/rejected share an identical prompt; else fall back to the plain
978
- // two-forward segmented step (segmentedOrpo, built alongside).
979
- if (segmentedOrpoPrefix) {
980
- const split = splitPrefixBatch(currentBatch!);
981
- if (split) return segmentedOrpoPrefix.stepPrefix(split.promptIds, split.chosenResp, split.rejectedResp);
982
- notePrefixFallback();
983
- return segmentedOrpo!.step(currentBatch!);
984
- }
985
- return segmentedOrpo ? segmentedOrpo.step(currentBatch!) : vag!.apply(flatParams(lora));
986
- }, disposeChunkSink); // free this micro-step's head checkpoints once its grads are eval'd
987
-
988
- model.loraState.dropoutSeed = null; // metrics/val below run dropout-free
989
- const gradNorm = globalNorm(grads);
990
- clipGradsByNorm(grads, gradNorm, cfg.gradClipNorm);
991
- opt.step(grads);
992
- opt.evalState();
993
- clearCache();
994
-
995
- if (step % cfg.stepsPerReport === 0 || step === 1) {
996
- // Diagnostics recompute the two forwards (like dpoMetrics) — cheap at
997
- // B=1 (response-only head, no full logits) and gated by stepsPerReport.
998
- // Reusing the value_and_grad forward is a shared-with-DPO follow-on.
999
- const m = orpoMetrics(model, currentBatch!, cfg.orpoLambda, cfg.sftScope);
1000
- const elapsed = (Date.now() - t0) / 1000;
1001
- emit({
1002
- type: "metric",
1003
- kind: "train",
1004
- step,
1005
- loss: lossVal,
1006
- grad_norm: gradNorm,
1007
- learning_rate: opt.lr,
1008
- accuracy: m.accuracy,
1009
- margin: m.margin,
1010
- tokens_per_sec: 0,
1011
- peak_gb: peakMemory() / 1e9,
1012
- active_gb: activeMemory() / 1e9,
1013
- pinned: pinnedBufferCount(),
1014
- progress: step / cfg.iters,
1015
- message: `step ${step}/${cfg.iters} loss=${lossVal.toFixed(4)} nll=${m.nll.toFixed(4)} or=${m.or.toFixed(4)} acc=${m.accuracy.toFixed(2)} margin=${m.margin.toFixed(3)}`,
1016
- });
1017
- clearCache();
1018
- }
1019
-
1020
- if (valid.length > 0 && step % cfg.stepsPerEval === 0) {
1021
- // Val accuracy is correct/total over a FIXED subset (cap bounds both the
1022
- // denominator and the per-eval cost). Each val example is a single B=1
1023
- // pair, so m.accuracy ∈ {0,1} and va sums to the integer correct count.
1024
- const valSet = cfg.valMaxExamples > 0 ? valid.slice(0, cfg.valMaxExamples) : valid;
1025
- let vl = 0, va = 0, vm = 0, vn = 0;
1026
- for (const ex of valSet) {
1027
- const vb: DpoBatch = {
1028
- chosenIds: [ex.chosenIds], rejectedIds: [ex.rejectedIds],
1029
- chosenMask: [ex.chosenMask], rejectedMask: [ex.rejectedMask],
1030
- };
1031
- const m = orpoMetrics(model, vb, cfg.orpoLambda, cfg.sftScope);
1032
- vl += m.loss; va += m.accuracy; vm += m.margin; vn++;
1033
- clearCache();
1034
- }
1035
- emit({
1036
- type: "metric", kind: "val", step,
1037
- loss: vn ? vl / vn : 0,
1038
- accuracy: vn ? va / vn : 0,
1039
- n_correct: Math.round(va),
1040
- n_total: vn,
1041
- margin: vn ? vm / vn : 0,
1042
- progress: step / cfg.iters,
1043
- });
1044
- }
1045
- }
1046
- } finally {
1047
- disposeChunkSink(); // defensive: free any head checkpoints a throw left behind
1048
- segmentedOrpoPrefix?.dispose();
1049
- segmentedOrpo?.dispose();
1050
- vag?.dispose();
1051
- opt.dispose();
1052
- }
1053
- return { numIters: cfg.iters };
1054
- }
1055
-
1056
- // ---------------------------------------------------------------------------
1057
- // helpers
1058
- // ---------------------------------------------------------------------------
1059
-
1060
- /** Swap the incoming value_and_grad primals into the LoraWeights so the
1061
- * forward graph differentiates them. Order: [...A, ...B] over sorted targets.
1062
- * Returns the displaced original leaves so the caller can restore them (the
1063
- * primal wrappers are disposed by the autograd closure after this returns). */
1064
- function swapPrimals(lora: TrainableLora, primals: MlxArray[]): MlxArray[] {
1065
- const n = lora.targets.length;
1066
- const saved: MlxArray[] = [];
1067
- for (let i = 0; i < n; i++) {
1068
- saved.push(lora.targets[i]!.lw.a, lora.targets[i]!.lw.b);
1069
- lora.targets[i]!.lw.a = primals[i]!;
1070
- lora.targets[i]!.lw.b = primals[n + i]!;
1071
- }
1072
- return saved;
1073
- }
1074
-
1075
- /** Restore the original leaves displaced by swapPrimals (saved order is
1076
- * [a0, b0, a1, b1, ...]). */
1077
- function restorePrimals(lora: TrainableLora, saved: MlxArray[]): void {
1078
- for (let i = 0; i < lora.targets.length; i++) {
1079
- lora.targets[i]!.lw.a = saved[2 * i]!;
1080
- lora.targets[i]!.lw.b = saved[2 * i + 1]!;
1081
- }
1082
- }
1083
-
1084
- /** Write a replaced param (AdamW output) back into its LoraWeights leaf. */
1085
- function writeParam(lora: TrainableLora, i: number, p: MlxArray): void {
1086
- const n = lora.targets.length;
1087
- if (i < n) lora.targets[i]!.lw.a = p;
1088
- else lora.targets[i - n]!.lw.b = p;
1089
- }
1090
-
1091
- /** Run one optimizer step's worth of gradient over `accumSteps` micro-batches:
1092
- * backward each micro-batch, mean-accumulate the per-micro grads (each scaled
1093
- * by 1/accumSteps), and return the mean loss + summed grads — a drop-in for the
1094
- * single value_and_grad the loops used to call inline. The caller passes the
1095
- * returned grads to opt.step (which takes ownership and disposes them).
1096
- *
1097
- * accumSteps <= 1 is a pass-through identical to the pre-accumulation path (one
1098
- * micro-batch, no scaling) so the proven B=1 loops stay byte-for-byte unchanged.
1099
- * For accumSteps > 1 the running accumulator (and that micro-batch's loss) is
1100
- * eval'd before the next micro-batch's forward builds, so only ONE micro-batch's
1101
- * activations are ever live — peak memory does NOT grow with accumSteps (the
1102
- * whole point: a larger effective batch without the B>1 activation memory).
1103
- *
1104
- * The combined `evalAll([value, ...grads])` per micro-batch is deliberate: a
1105
- * separate eval of `value` first would materialize then free the forward,
1106
- * forcing the backward to recompute and hold every layer's activations — which
1107
- * silently defeats gradient checkpointing (verified). Keep loss + grads in one
1108
- * eval.
1109
- *
1110
- * `afterMicroEval` runs once per micro-batch right after its grads are
1111
- * materialized (its backward is complete), in BOTH the pass-through and
1112
- * accumulating paths — it frees per-micro-batch state the loss closure captured
1113
- * (gradient-checkpoint closures for SFT, reference log-probs for DPO) so that
1114
- * state does not pile up across accumulation.
1115
- *
1116
- * `micro` returns the micro-batch loss scalar + grads in flatParams order
1117
- * ([...A, ...B] over targets); it transfers ownership of both to this helper,
1118
- * which disposes the per-micro value and grads. */
1119
- export function accumulateStep(
1120
- accumSteps: number,
1121
- micro: () => { value: MlxArray; grads: MlxArray[] },
1122
- afterMicroEval?: () => void,
1123
- ): { loss: number; grads: MlxArray[] } {
1124
- if (accumSteps <= 1) {
1125
- const { value, grads } = micro();
1126
- evalAll([value, ...grads]);
1127
- const loss = value.toFloat32()[0]!;
1128
- value.dispose();
1129
- afterMicroEval?.();
1130
- return { loss, grads };
1131
- }
1132
-
1133
- const scale = 1 / accumSteps;
1134
- let acc: MlxArray[] | null = null;
1135
- let lossSum = 0;
1136
- try {
1137
- for (let i = 0; i < accumSteps; i++) {
1138
- const { value, grads } = micro();
1139
- if (acc === null) {
1140
- acc = grads.map((g) => {
1141
- const s = ops.mulScalar(g, scale);
1142
- g.dispose();
1143
- return s;
1144
- });
1145
- } else {
1146
- const prev = acc;
1147
- acc = grads.map((g, j) => {
1148
- const s = ops.mulScalar(g, scale);
1149
- const sum = ops.add(prev[j]!, s);
1150
- s.dispose();
1151
- prev[j]!.dispose();
1152
- g.dispose();
1153
- return sum;
1154
- });
1155
- }
1156
- // Eval the running accumulator (+ this micro-batch's loss) now so the
1157
- // micro-batch's forward/backward graph frees before the next one builds —
1158
- // this is what keeps peak flat across accumSteps.
1159
- evalAll([value, ...acc]);
1160
- lossSum += value.toFloat32()[0]!;
1161
- value.dispose();
1162
- afterMicroEval?.();
1163
- clearCache();
1164
- }
1165
- const grads = acc!;
1166
- acc = null; // ownership handed to the caller
1167
- return { loss: lossSum / accumSteps, grads };
1168
- } finally {
1169
- // Non-null only if a micro-batch threw mid-accumulation (nulled on success).
1170
- if (acc) for (const a of acc) a.dispose();
1171
- }
1172
- }
1173
-
1174
- /** L2 norm over all grads (also forces eval). */
1175
- function globalNorm(grads: MlxArray[]): number {
1176
- const partial: MlxArray[] = grads.map((g) => {
1177
- const sq = ops.square(g);
1178
- const flat = ops.reshape(sq, [g.size]);
1179
- const sum = ops.sumAxis(flat, 0, false);
1180
- sq.dispose();
1181
- flat.dispose();
1182
- return sum;
1183
- });
1184
- evalAll(partial);
1185
- let sumSq = 0;
1186
- for (const p of partial) {
1187
- sumSq += p.toFloat32()[0]!;
1188
- p.dispose();
1189
- }
1190
- return Math.sqrt(sumSq);
1191
- }
1192
-
1193
- /** Clip grads in place to a global L2 norm of `maxNorm` (no-op if off or under
1194
- * the cap). Scales every grad by maxNorm/normVal and disposes the originals so
1195
- * the optimizer takes ownership of the clipped copies. `normVal` is the already-
1196
- * computed global norm (avoids a second pass). */
1197
- function clipGradsByNorm(grads: MlxArray[], normVal: number, maxNorm: number): void {
1198
- if (maxNorm <= 0 || !(normVal > maxNorm)) return;
1199
- const s = maxNorm / normVal;
1200
- for (let i = 0; i < grads.length; i++) {
1201
- const g = grads[i]!;
1202
- grads[i] = ops.mulScalar(g, s);
1203
- g.dispose();
1204
- }
1205
- }
1206
-
1207
- /** Supervised (response) tokens across ALL rows of the batch — the count
1208
- * tokens/sec is reported over (mlx-lm's n_tokens counts every row too). */
1209
- export function countResponseTokens(batch: SftBatch): number {
1210
- let total = 0;
1211
- for (let r = 0; r < batch.ids.length; r++) {
1212
- const promptLen = batch.promptLens[r]!;
1213
- const len = rowLength(batch, r);
1214
- total += Math.max(0, len - Math.max(promptLen, 1));
1215
- }
1216
- return total;
1217
- }
1218
-
1219
- async function fileExists(path: string): Promise<boolean> {
1220
- return Bun.file(path).exists();
1221
- }