mlx-bun 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (640) hide show
  1. package/README.md +1449 -109
  2. package/THIRD_PARTY_NOTICES.md +882 -0
  3. package/bin/mlx-bun.convert.mjs +4 -0
  4. package/bin/mlx-bun.fuse.mjs +4 -0
  5. package/bin/mlx-bun.generate.mjs +4 -0
  6. package/bin/mlx-bun.lora.mjs +4 -0
  7. package/bin/mlx-bun.mjs +13 -21
  8. package/bin/mlx-bun.server.mjs +4 -0
  9. package/bin/mlx-bun.upload.mjs +4 -0
  10. package/dist/web/app.js +9584 -0
  11. package/package.json +50 -53
  12. package/src/cli/ane-install.ts +76 -0
  13. package/src/cli/args.ts +179 -0
  14. package/src/cli/engine-entry.ts +68 -0
  15. package/src/cli/inference.ts +214 -0
  16. package/src/cli/job-entry.ts +97 -0
  17. package/src/cli/library-host.ts +117 -0
  18. package/src/cli/main.ts +57 -0
  19. package/src/cli/memory-engine.ts +163 -0
  20. package/src/cli/memory.ts +36 -0
  21. package/src/cli/mlx-lm-aliases.ts +309 -0
  22. package/src/cli/model-choice.ts +33 -0
  23. package/src/cli/model-selection.ts +84 -0
  24. package/src/cli/module-host.ts +11 -0
  25. package/src/cli/module-verbs.ts +50 -0
  26. package/src/cli/numerical-policy.ts +12 -0
  27. package/src/cli/perplexity.ts +89 -0
  28. package/src/cli/serve-host.ts +423 -0
  29. package/src/cli/serve-isolated.ts +217 -0
  30. package/src/cli/serve-options.ts +106 -0
  31. package/src/cli/serve-state.ts +205 -0
  32. package/src/cli/serve.ts +500 -0
  33. package/src/cli/served-model-host.ts +102 -0
  34. package/src/cli/server-entry.ts +148 -0
  35. package/src/cli/serving-unit.ts +177 -0
  36. package/src/{tui.ts → cli/terminal.ts} +13 -8
  37. package/src/cli/worker-adapters.ts +34 -0
  38. package/src/cli/worker-entry.ts +272 -0
  39. package/src/cli/worker-unit.ts +179 -0
  40. package/src/engine/adapter-operation.ts +26 -0
  41. package/src/engine/cache-services.ts +114 -0
  42. package/src/engine/completion.ts +39 -0
  43. package/src/engine/generation-gateway.ts +358 -0
  44. package/src/engine/index.ts +59 -0
  45. package/src/engine/kv-budget.ts +120 -0
  46. package/src/engine/media-preparation.ts +232 -0
  47. package/src/engine/model-binding.ts +24 -0
  48. package/src/engine/model-host.ts +513 -0
  49. package/src/engine/model-serving.ts +89 -0
  50. package/src/{serve → engine}/preparation.ts +7 -5
  51. package/src/{serve → engine}/session-completion-engine.ts +6 -6
  52. package/src/engine/telemetry.ts +109 -0
  53. package/src/hub/downloads.ts +116 -0
  54. package/src/jobs/db.ts +16 -7
  55. package/src/jobs/events.ts +29 -0
  56. package/src/jobs/executable.ts +5 -0
  57. package/src/jobs/host.ts +51 -0
  58. package/src/jobs/in-process.ts +55 -0
  59. package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
  60. package/src/jobs/runner.ts +150 -231
  61. package/src/jobs/service.ts +75 -0
  62. package/src/jobs/sse.ts +59 -23
  63. package/src/jobs/worker-memory.ts +46 -0
  64. package/src/jobs/worker-process.ts +163 -0
  65. package/src/jobs/worker-supervisor.ts +258 -0
  66. package/src/modules.ts +58 -0
  67. package/src/publishing/catalog-hub.ts +18 -0
  68. package/src/publishing/credentials.ts +31 -0
  69. package/src/publishing/upload.ts +37 -0
  70. package/src/residency/leased-adapters.ts +21 -0
  71. package/src/residency/local-records.ts +16 -0
  72. package/src/residency/model-residency.ts +405 -0
  73. package/src/residency/record-index.ts +28 -0
  74. package/src/residency/resident-estimate.ts +50 -0
  75. package/src/{anthropic.ts → server/anthropic.ts} +3 -81
  76. package/src/server/cache-routes.ts +36 -0
  77. package/src/{serve → server}/chat-request.ts +10 -10
  78. package/src/{serve → server}/chat-stage.ts +14 -14
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +33 -41
  81. package/src/server/decision-body.ts +55 -0
  82. package/src/server/decision-routes.ts +134 -0
  83. package/src/server/decision-wire.ts +228 -0
  84. package/src/{serve → server}/discovery-routes.ts +25 -82
  85. package/src/{serve → server}/http.ts +25 -13
  86. package/src/{serve → server}/inference-request.ts +5 -5
  87. package/src/server/job-routes.ts +20 -0
  88. package/src/server/media-prompt.ts +63 -0
  89. package/src/server/memory-completion-client.ts +46 -0
  90. package/src/server/model-routes.ts +192 -0
  91. package/src/{serve → server}/openai-wire.ts +3 -2
  92. package/src/{serve → server}/pipeline.ts +1 -1
  93. package/src/server/prompt-builder.ts +13 -0
  94. package/src/server/prompt-contracts.ts +17 -0
  95. package/src/server/proxy-routes.ts +187 -0
  96. package/src/server/publishing-routes.ts +41 -0
  97. package/src/{serve → server}/request-plan.ts +6 -8
  98. package/src/{serve → server}/request-prep.ts +47 -50
  99. package/src/{serve → server}/responses-client.ts +30 -13
  100. package/src/{responses.ts → server/responses.ts} +12 -93
  101. package/src/server/routes.ts +205 -0
  102. package/src/server/start.ts +72 -0
  103. package/src/server/status-routes.ts +108 -0
  104. package/src/{serve → server}/text-completion-stage.ts +25 -4
  105. package/src/server/text-prompt.ts +12 -0
  106. package/src/{serve → server}/token-streams.ts +36 -38
  107. package/src/server/worker-routes.ts +311 -0
  108. package/src/storage/paths.ts +61 -0
  109. package/src/web/assets.ts +37 -0
  110. package/src/web/{src → browser}/api.ts +5 -23
  111. package/src/web/browser/chat-host.ts +17 -0
  112. package/src/web/browser/installed-panels.ts +6 -0
  113. package/src/web/browser/main.ts +55 -0
  114. package/src/web/browser/markdown.ts +279 -0
  115. package/src/web/browser/memory-panel.ts +9 -0
  116. package/src/web/browser/palette.ts +64 -0
  117. package/src/web/browser/protocol.ts +13 -0
  118. package/src/web/browser/shell.ts +467 -0
  119. package/src/web/{src → browser}/status.ts +32 -36
  120. package/src/web/build.ts +51 -0
  121. package/src/web/public/app.html +1054 -0
  122. package/src/web/{sw.js → public/sw.js} +4 -3
  123. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  124. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  125. package/THIRD_PARTY_LICENSES.md +0 -51
  126. package/docs/README.md +0 -107
  127. package/docs/archive/hlg-sampling.md +0 -350
  128. package/docs/archive/investigations/audio-input-plan.md +0 -356
  129. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +0 -3617
  130. package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
  131. package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
  132. package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
  133. package/docs/archive/investigations/curve-bisector-routing.md +0 -123
  134. package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
  135. package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
  136. package/docs/archive/investigations/dspark-handoff.md +0 -224
  137. package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
  138. package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
  139. package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
  140. package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
  141. package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
  142. package/docs/archive/investigations/lab-build-journal.md +0 -157
  143. package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
  144. package/docs/archive/investigations/openjarvis-fit.md +0 -198
  145. package/docs/archive/investigations/openwiki-evaluation.md +0 -249
  146. package/docs/archive/investigations/optimization_plan.md +0 -346
  147. package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
  148. package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
  149. package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
  150. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
  151. package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
  152. package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
  153. package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
  154. package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
  155. package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
  156. package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
  157. package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
  158. package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
  159. package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
  160. package/docs/archive/planning/chat-ui-vision.md +0 -50
  161. package/docs/archive/planning/chunk-task-roadmap.md +0 -77
  162. package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
  163. package/docs/archive/planning/journal.md +0 -316
  164. package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
  165. package/docs/archive/planning/pi-integration-review.md +0 -178
  166. package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
  167. package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
  168. package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
  169. package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
  170. package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
  171. package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
  172. package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
  173. package/docs/archive/planning/release-notes-v0.4.0.md +0 -45
  174. package/docs/archive/planning/release-notes-v0.5.0.md +0 -37
  175. package/docs/archive/planning/web-ui-pass-plan.md +0 -213
  176. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  177. package/docs/design/batching.md +0 -2054
  178. package/docs/design/decode-speed-program.md +0 -2404
  179. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  180. package/docs/design/generic-model-support.md +0 -821
  181. package/docs/design/kv-cache.md +0 -993
  182. package/docs/design/orpo-training.md +0 -680
  183. package/docs/design/speculative-decoding.md +0 -2129
  184. package/docs/design/turboquant.md +0 -2225
  185. package/docs/design/unified-engine-frontier-plan.md +0 -1987
  186. package/docs/design/web-chat-redesign.md +0 -695
  187. package/docs/planning/PRODUCT_ROADMAP.md +0 -593
  188. package/docs/planning/ResearchTopics.md +0 -119
  189. package/docs/planning/public-readiness-handoff.md +0 -244
  190. package/docs/planning/public-readiness.md +0 -149
  191. package/docs/planning/video-series.md +0 -421
  192. package/docs/reference/benchmarks.md +0 -3587
  193. package/docs/reference/cli.md +0 -646
  194. package/docs/reference/distribution.md +0 -380
  195. package/docs/reference/environment.md +0 -494
  196. package/docs/reference/glossary.md +0 -39
  197. package/docs/reference/library-api.md +0 -688
  198. package/docs/reference/memory.md +0 -260
  199. package/docs/reference/models.md +0 -346
  200. package/docs/reference/server-api.md +0 -1945
  201. package/docs/reference/server-config.md +0 -973
  202. package/docs/reference/training.md +0 -462
  203. package/docs/reference/troubleshooting.md +0 -158
  204. package/src/admission-plan.ts +0 -18
  205. package/src/audio/audiotoolbox.ts +0 -112
  206. package/src/audio/conformer.ts +0 -655
  207. package/src/audio/decode.ts +0 -133
  208. package/src/audio/features.ts +0 -202
  209. package/src/audio/mic-capture.ts +0 -98
  210. package/src/audio/silero-vad.ts +0 -379
  211. package/src/audio/transcode.ts +0 -67
  212. package/src/audio/whisper-decode.ts +0 -774
  213. package/src/audio/whisper-fast.ts +0 -732
  214. package/src/audio/whisper-format.ts +0 -73
  215. package/src/audio/whisper-mel.ts +0 -185
  216. package/src/audio/whisper-timing.ts +0 -274
  217. package/src/audio/whisper-tokenizer.ts +0 -239
  218. package/src/audio/whisper-transcribe.ts +0 -410
  219. package/src/backends/mlx/assistant-target.ts +0 -46
  220. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  221. package/src/backends/mlx/autoregressive.ts +0 -129
  222. package/src/backends/mlx/batch-group.ts +0 -1874
  223. package/src/backends/mlx/cache-attachment.ts +0 -67
  224. package/src/backends/mlx/cache-layout.ts +0 -31
  225. package/src/backends/mlx/checkpoint-state.ts +0 -53
  226. package/src/backends/mlx/continuation-persistence.ts +0 -107
  227. package/src/backends/mlx/continuation-request.ts +0 -43
  228. package/src/backends/mlx/continuation.ts +0 -77
  229. package/src/backends/mlx/diffusion.ts +0 -39
  230. package/src/backends/mlx/draft-target.ts +0 -32
  231. package/src/backends/mlx/encoder-cache.ts +0 -25
  232. package/src/backends/mlx/evaluation.ts +0 -105
  233. package/src/backends/mlx/fill-append.ts +0 -58
  234. package/src/backends/mlx/fill-group.ts +0 -497
  235. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  236. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  237. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  238. package/src/backends/mlx/gateway-binding.ts +0 -174
  239. package/src/backends/mlx/graph.ts +0 -38
  240. package/src/backends/mlx/kv-codec.ts +0 -9
  241. package/src/backends/mlx/kv-maintenance.ts +0 -161
  242. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  243. package/src/backends/mlx/memory-client.ts +0 -189
  244. package/src/backends/mlx/methods.ts +0 -90
  245. package/src/backends/mlx/mixed-iteration.ts +0 -62
  246. package/src/backends/mlx/model-prompt.ts +0 -178
  247. package/src/backends/mlx/model-serving.ts +0 -80
  248. package/src/backends/mlx/paged-cache-codec.ts +0 -27
  249. package/src/backends/mlx/prefill-cohort.ts +0 -100
  250. package/src/backends/mlx/prefill-policy.ts +0 -26
  251. package/src/backends/mlx/prefill-rows.ts +0 -244
  252. package/src/backends/mlx/prefill.ts +0 -33
  253. package/src/backends/mlx/projected-context-rows.ts +0 -72
  254. package/src/backends/mlx/prompt-input.ts +0 -29
  255. package/src/backends/mlx/qwen-prompt-input.ts +0 -40
  256. package/src/backends/mlx/request-policy.ts +0 -20
  257. package/src/backends/mlx/request-state-policy.ts +0 -54
  258. package/src/backends/mlx/rollback.ts +0 -92
  259. package/src/backends/mlx/row-sampling.ts +0 -49
  260. package/src/backends/mlx/serial-executor.ts +0 -251
  261. package/src/backends/mlx/speculative-group.ts +0 -438
  262. package/src/backends/mlx/speculative-round.ts +0 -161
  263. package/src/backends/mlx/speculative.ts +0 -131
  264. package/src/backends/mlx/state-rows.ts +0 -104
  265. package/src/backends/mlx/state-views.ts +0 -46
  266. package/src/backends/mlx/target-layout-capability.ts +0 -35
  267. package/src/backends/mlx/text-engine.ts +0 -73
  268. package/src/backends/mlx/transitioning-kv-rows.ts +0 -173
  269. package/src/chat-template.ts +0 -382
  270. package/src/cli.ts +0 -3490
  271. package/src/config.ts +0 -399
  272. package/src/contracts/completion.ts +0 -16
  273. package/src/contracts/execution.ts +0 -70
  274. package/src/contracts/generation.ts +0 -103
  275. package/src/contracts/host.ts +0 -7
  276. package/src/contracts/object-cache.ts +0 -8
  277. package/src/contracts/pi-provider.ts +0 -30
  278. package/src/contracts/pi.ts +0 -261
  279. package/src/contracts/prefix-cache.ts +0 -33
  280. package/src/contracts/resources.ts +0 -25
  281. package/src/contracts/sampling.ts +0 -23
  282. package/src/contracts/scheduling.ts +0 -54
  283. package/src/contracts/tsconfig.json +0 -14
  284. package/src/dataset/generators.ts +0 -1087
  285. package/src/dataset/index.ts +0 -12
  286. package/src/dataset/job.ts +0 -81
  287. package/src/dataset/llm.ts +0 -71
  288. package/src/dataset/registry.ts +0 -528
  289. package/src/diffusion/diffusion-generate.ts +0 -507
  290. package/src/download.ts +0 -505
  291. package/src/embed.ts +0 -77
  292. package/src/engine/admission.ts +0 -59
  293. package/src/engine/cancellation.ts +0 -38
  294. package/src/engine/engine.ts +0 -56
  295. package/src/engine/execution-coordinator.ts +0 -70
  296. package/src/engine/execution-plan.ts +0 -64
  297. package/src/engine/execution-tasks.ts +0 -44
  298. package/src/engine/namespaced-cache.ts +0 -13
  299. package/src/engine/resources.ts +0 -53
  300. package/src/engine/scheduler.ts +0 -92
  301. package/src/engine/session.ts +0 -247
  302. package/src/eval/README.md +0 -130
  303. package/src/eval/ifeval.ts +0 -81
  304. package/src/eval/kl-prompts.ts +0 -29
  305. package/src/eval/kl.ts +0 -590
  306. package/src/eval/perplexity.ts +0 -164
  307. package/src/eval/quality-db.ts +0 -92
  308. package/src/eval/reference-logits.ts +0 -207
  309. package/src/eval/runner.ts +0 -124
  310. package/src/eval/score.ts +0 -49
  311. package/src/eval/tasks/bfcl.ts +0 -535
  312. package/src/eval/tasks/gsm8k.ts +0 -98
  313. package/src/eval/tasks/hashhop.ts +0 -139
  314. package/src/eval/tasks/humaneval.ts +0 -275
  315. package/src/eval/tasks/ifeval.ts +0 -525
  316. package/src/eval/tasks/mmlu.ts +0 -157
  317. package/src/eval/ultrafeedback.ts +0 -60
  318. package/src/evaldb.ts +0 -199
  319. package/src/expert-io.ts +0 -393
  320. package/src/expert-offload-build.ts +0 -77
  321. package/src/expert-offload.ts +0 -76
  322. package/src/expert-residency.ts +0 -1134
  323. package/src/expert-usage.ts +0 -415
  324. package/src/fill/echo-index.ts +0 -188
  325. package/src/fill/fill-session.ts +0 -463
  326. package/src/fill/proposal.ts +0 -68
  327. package/src/fill/schema-rows.ts +0 -506
  328. package/src/fill/tool-boundary.ts +0 -103
  329. package/src/fit.ts +0 -249
  330. package/src/generate.ts +0 -1352
  331. package/src/grammar.ts +0 -604
  332. package/src/harness-pi.ts +0 -184
  333. package/src/hf-push.ts +0 -628
  334. package/src/hub-rest.ts +0 -242
  335. package/src/index.ts +0 -63
  336. package/src/inference/denoising.ts +0 -17
  337. package/src/inference/draft-acceptance.ts +0 -41
  338. package/src/inference/draft-output.ts +0 -49
  339. package/src/inference/graph.ts +0 -33
  340. package/src/inference/prefill.ts +0 -45
  341. package/src/inference/rollback.ts +0 -7
  342. package/src/jobs/index.ts +0 -31
  343. package/src/jobs/job-entry.ts +0 -107
  344. package/src/jobs/task-client.ts +0 -19
  345. package/src/jobs/types.ts +0 -2
  346. package/src/kv-scheme.ts +0 -279
  347. package/src/kv-store.ts +0 -1135
  348. package/src/lab/README.md +0 -22
  349. package/src/lab/curve/curve-designer.html +0 -303
  350. package/src/lab/curve/curve-sampler.ts +0 -122
  351. package/src/lab/expert-trace/expert-trace.ts +0 -69
  352. package/src/lab/paged-kv/paged-attention.ts +0 -117
  353. package/src/lab/paged-kv/paged-kv-rows.ts +0 -111
  354. package/src/lab/paged-kv/paged-kv.ts +0 -336
  355. package/src/library.ts +0 -40
  356. package/src/lora.ts +0 -408
  357. package/src/md-modules.d.ts +0 -54
  358. package/src/media-fetch.ts +0 -442
  359. package/src/memory/article.ts +0 -558
  360. package/src/memory/chunk-validation.ts +0 -150
  361. package/src/memory/chunk.ts +0 -318
  362. package/src/memory/cluster.ts +0 -351
  363. package/src/memory/coverage.ts +0 -134
  364. package/src/memory/crosslink.ts +0 -0
  365. package/src/memory/db.ts +0 -253
  366. package/src/memory/entity.ts +0 -203
  367. package/src/memory/gate.ts +0 -187
  368. package/src/memory/history.ts +0 -152
  369. package/src/memory/ingest.ts +0 -335
  370. package/src/memory/model.ts +0 -183
  371. package/src/memory/normalize.ts +0 -671
  372. package/src/memory/parse.ts +0 -22
  373. package/src/memory/pipeline.ts +0 -275
  374. package/src/memory/prompts.ts +0 -65
  375. package/src/memory/query.ts +0 -334
  376. package/src/memory/reconcile.ts +0 -734
  377. package/src/memory/redirect.ts +0 -108
  378. package/src/memory/reindex.ts +0 -325
  379. package/src/memory/resolve.ts +0 -307
  380. package/src/memory/rest.ts +0 -245
  381. package/src/memory/route.ts +0 -272
  382. package/src/memory/schedule.ts +0 -165
  383. package/src/memory/stages.ts +0 -777
  384. package/src/memory/synthesize.ts +0 -1638
  385. package/src/memory/tools.ts +0 -749
  386. package/src/memory/vault.ts +0 -1194
  387. package/src/memory/wikify.ts +0 -1064
  388. package/src/mlx/array.ts +0 -425
  389. package/src/mlx/autograd.ts +0 -304
  390. package/src/mlx/checkpoint.ts +0 -149
  391. package/src/mlx/compile.ts +0 -168
  392. package/src/mlx/custom-vjp.ts +0 -188
  393. package/src/mlx/ffi.ts +0 -531
  394. package/src/mlx/materialize.ts +0 -23
  395. package/src/mlx/metal-kernel.ts +0 -159
  396. package/src/mlx/normalized-argmax.ts +0 -116
  397. package/src/mlx/ops.ts +0 -1108
  398. package/src/mlx/quantized-rows.ts +0 -36
  399. package/src/mlx/token-bitmask.ts +0 -52
  400. package/src/mlx/turboquant-kv-decode.ts +0 -82
  401. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  402. package/src/mlx/turboquant-ops.ts +0 -861
  403. package/src/mlx/turboquant-tables.ts +0 -237
  404. package/src/mmap.ts +0 -90
  405. package/src/model/artifact-identity.ts +0 -73
  406. package/src/model/batched-kv.ts +0 -235
  407. package/src/model/batched-mask.ts +0 -279
  408. package/src/model/batched-quant.ts +0 -212
  409. package/src/model/batched-quantized-kv.ts +0 -144
  410. package/src/model/batched-rotating-quant.ts +0 -353
  411. package/src/model/batched-rotating-state.ts +0 -149
  412. package/src/model/batched-rotating.ts +0 -338
  413. package/src/model/batched-row-storage.ts +0 -179
  414. package/src/model/batched-ssm.ts +0 -52
  415. package/src/model/batched-turboquant-kv.ts +0 -94
  416. package/src/model/compiled-decode.ts +0 -712
  417. package/src/model/decoded-kv-donor.ts +0 -12
  418. package/src/model/delayed-quantized-kv.ts +0 -72
  419. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  420. package/src/model/delayed-turboquant-kv.ts +0 -57
  421. package/src/model/diffusion-gemma.ts +0 -1026
  422. package/src/model/factory.ts +0 -215
  423. package/src/model/fingerprint.ts +0 -45
  424. package/src/model/flash-attention.ts +0 -694
  425. package/src/model/full-prefill-padding.ts +0 -56
  426. package/src/model/full-prefill-row.ts +0 -105
  427. package/src/model/gemma4-base.ts +0 -2370
  428. package/src/model/gemma4.ts +0 -1395
  429. package/src/model/generated/gemma4-12b.ts +0 -429
  430. package/src/model/generated/gemma4-26b.ts +0 -448
  431. package/src/model/generated/gemma4-e4b.ts +0 -717
  432. package/src/model/generated/index.ts +0 -18
  433. package/src/model/glm52-atlas.ts +0 -511
  434. package/src/model/glm52-cache.ts +0 -792
  435. package/src/model/glm52-config.ts +0 -262
  436. package/src/model/glm52-container.ts +0 -305
  437. package/src/model/glm52-coupling.ts +0 -388
  438. package/src/model/glm52-dsa.ts +0 -606
  439. package/src/model/glm52-expert-layout.ts +0 -253
  440. package/src/model/glm52-layout.ts +0 -152
  441. package/src/model/glm52-memory.ts +0 -500
  442. package/src/model/glm52-mla.ts +0 -792
  443. package/src/model/glm52-moe.ts +0 -223
  444. package/src/model/glm52-pilot.ts +0 -537
  445. package/src/model/glm52-reference.ts +0 -445
  446. package/src/model/glm52-residency.ts +0 -557
  447. package/src/model/glm52-resident-weights.ts +0 -135
  448. package/src/model/glm52-streamed-experts.ts +0 -453
  449. package/src/model/glm52-streamed-metal.ts +0 -610
  450. package/src/model/glm52-weights.ts +0 -503
  451. package/src/model/glm52.ts +0 -1132
  452. package/src/model/implementation.ts +0 -69
  453. package/src/model/kv-attention-view.ts +0 -70
  454. package/src/model/minicpm5.ts +0 -376
  455. package/src/model/profile.ts +0 -468
  456. package/src/model/quantized-append-attention.ts +0 -31
  457. package/src/model/qwen-append.ts +0 -12
  458. package/src/model/qwen-conv.ts +0 -52
  459. package/src/model/qwen3-delta-state.ts +0 -74
  460. package/src/model/qwen3-delta.ts +0 -451
  461. package/src/model/qwen3-moe.ts +0 -404
  462. package/src/model/qwen3-mrope.ts +0 -250
  463. package/src/model/qwen3.ts +0 -277
  464. package/src/model/qwen3_5-checkpoint.ts +0 -164
  465. package/src/model/qwen3_5.ts +0 -881
  466. package/src/model/rotating-kv-donor.ts +0 -35
  467. package/src/model/rotating-kv-layout.ts +0 -262
  468. package/src/model/rotating-row-transaction.ts +0 -35
  469. package/src/model/speculative-rotating-kv.ts +0 -101
  470. package/src/model/ssm-prefill-padding.ts +0 -39
  471. package/src/model/support.ts +0 -110
  472. package/src/model/token-groups.ts +0 -66
  473. package/src/model/trellis-balanced-scatter.ts +0 -51
  474. package/src/model/trellis-linear.ts +0 -672
  475. package/src/model/trellis-shared-m.ts +0 -151
  476. package/src/model/trellis-shared-scatter.ts +0 -144
  477. package/src/model/trellis-splitk-prefill.ts +0 -158
  478. package/src/model/trellis-tiled-prefill.ts +0 -136
  479. package/src/model/trellis-vector-expand.ts +0 -62
  480. package/src/model/trellis-wide-prefill.ts +0 -94
  481. package/src/model/turboquant-codec.ts +0 -89
  482. package/src/model/universal/archs.ts +0 -332
  483. package/src/model/universal/dense.ts +0 -612
  484. package/src/model/universal/modules.ts +0 -170
  485. package/src/model/universal/rope.ts +0 -381
  486. package/src/model/whisper.ts +0 -435
  487. package/src/native/expert_io.c +0 -523
  488. package/src/native/frame_extract.swift +0 -140
  489. package/src/native/mic_capture.swift +0 -92
  490. package/src/native-pack.ts +0 -163
  491. package/src/pi-provider.ts +0 -53
  492. package/src/pi-session.ts +0 -81
  493. package/src/pi-terminal.ts +0 -274
  494. package/src/pi-web.ts +0 -1940
  495. package/src/preflight.ts +0 -158
  496. package/src/prompt-cache.ts +0 -682
  497. package/src/quantize/allocator.ts +0 -627
  498. package/src/quantize/atomic-output.ts +0 -38
  499. package/src/quantize/calibration.ts +0 -181
  500. package/src/quantize/config-writer.ts +0 -145
  501. package/src/quantize/index.ts +0 -84
  502. package/src/quantize/job.ts +0 -162
  503. package/src/quantize/quantizer.ts +0 -666
  504. package/src/quantize/rotate.ts +0 -603
  505. package/src/quantize/safetensors-writer.ts +0 -249
  506. package/src/quantize/sensitivity.ts +0 -216
  507. package/src/quantize/trellis.ts +0 -586
  508. package/src/quantize/weight-transform.ts +0 -292
  509. package/src/registry.ts +0 -602
  510. package/src/runtime-config.ts +0 -77
  511. package/src/safetensors.ts +0 -165
  512. package/src/sampler.ts +0 -1012
  513. package/src/serve/admin-routes.ts +0 -184
  514. package/src/serve/assets/curve-terrain.html +0 -105
  515. package/src/serve/assets/training-inference-map.html +0 -575
  516. package/src/serve/audio-routes.ts +0 -323
  517. package/src/serve/aux-routes.ts +0 -132
  518. package/src/serve/batch-scheduler.ts +0 -8
  519. package/src/serve/checkpoint-identity.ts +0 -64
  520. package/src/serve/generation-gateway.ts +0 -537
  521. package/src/serve/isolate.ts +0 -609
  522. package/src/serve/kv-budget.ts +0 -14
  523. package/src/serve/lab-routes.ts +0 -341
  524. package/src/serve/lane-registry.ts +0 -45
  525. package/src/serve/model-admin-routes.ts +0 -155
  526. package/src/serve/model-binding.ts +0 -49
  527. package/src/serve/model-host.ts +0 -511
  528. package/src/serve/parent-application.ts +0 -56
  529. package/src/serve/prompt-response-trace.ts +0 -161
  530. package/src/serve/response-lease.ts +0 -24
  531. package/src/serve/session-search.ts +0 -326
  532. package/src/serve/static-routes.ts +0 -101
  533. package/src/serve/transcription-server.ts +0 -64
  534. package/src/serve/transcription-service.ts +0 -502
  535. package/src/server.ts +0 -1282
  536. package/src/spec/assistant-rows.ts +0 -93
  537. package/src/spec/assistant-source.ts +0 -126
  538. package/src/spec/cache-identity.ts +0 -22
  539. package/src/spec/deepspec-rows.ts +0 -29
  540. package/src/spec/deepspec-source.ts +0 -145
  541. package/src/spec/dflash-source.ts +0 -149
  542. package/src/spec/drafter.ts +0 -304
  543. package/src/spec/dspark/ab-stats.ts +0 -136
  544. package/src/spec/dspark/calibration.ts +0 -112
  545. package/src/spec/dspark/data-dflash.ts +0 -156
  546. package/src/spec/dspark/data.ts +0 -201
  547. package/src/spec/dspark/deepspec-module.ts +0 -776
  548. package/src/spec/dspark/generate-dflash.ts +0 -245
  549. package/src/spec/dspark/generate.ts +0 -290
  550. package/src/spec/dspark/loader.ts +0 -23
  551. package/src/spec/dspark/loss.ts +0 -213
  552. package/src/spec/dspark/module-dflash.ts +0 -560
  553. package/src/spec/dspark/module.ts +0 -540
  554. package/src/spec/dspark/quantize-drafter.ts +0 -91
  555. package/src/spec/dspark/sample.ts +0 -112
  556. package/src/spec/generate.ts +0 -276
  557. package/src/spec/glm52-mtp-graph.ts +0 -29
  558. package/src/spec/glm52-mtp-rows.ts +0 -132
  559. package/src/spec/glm52-mtp-source.ts +0 -318
  560. package/src/spec/glm52-mtp-state.ts +0 -33
  561. package/src/spec/ngram-source.ts +0 -226
  562. package/src/spec/projected-draft-rows.ts +0 -73
  563. package/src/spec/qwen-mtp-module.ts +0 -171
  564. package/src/spec/qwen-mtp-rows.ts +0 -230
  565. package/src/spec/qwen-mtp-source.ts +0 -386
  566. package/src/spec/qwen-mtp-state.ts +0 -30
  567. package/src/spec/serve-loop.ts +0 -587
  568. package/src/spec/source.ts +0 -228
  569. package/src/spec/two-model-rows.ts +0 -62
  570. package/src/spec/two-model.ts +0 -113
  571. package/src/ssd-cache.ts +0 -431
  572. package/src/ssd-durability.ts +0 -209
  573. package/src/storage/host-buffer.ts +0 -24
  574. package/src/storage/kv-writer.ts +0 -83
  575. package/src/storage/kv-writer.worker.js +0 -264
  576. package/src/storage/retention-policy.ts +0 -37
  577. package/src/tiered-prompt-cache.ts +0 -45
  578. package/src/tokenizer.ts +0 -335
  579. package/src/tool-approvals.ts +0 -131
  580. package/src/tool-call.ts +0 -695
  581. package/src/train/dataset.ts +0 -484
  582. package/src/train/diffusion-lora.ts +0 -216
  583. package/src/train/export.ts +0 -32
  584. package/src/train/flash-cce.ts +0 -1234
  585. package/src/train/forward.ts +0 -266
  586. package/src/train/fuse.ts +0 -243
  587. package/src/train/index.ts +0 -46
  588. package/src/train/job.ts +0 -205
  589. package/src/train/lora-params.ts +0 -254
  590. package/src/train/loss.ts +0 -1591
  591. package/src/train/merge.ts +0 -195
  592. package/src/train/optimizer.ts +0 -183
  593. package/src/train/prefix-shared.ts +0 -490
  594. package/src/train/rank.ts +0 -137
  595. package/src/train/segmented.ts +0 -1828
  596. package/src/train/steel-qmm-header.ts +0 -1461
  597. package/src/train/trainer.ts +0 -1221
  598. package/src/train/watch.ts +0 -542
  599. package/src/vision/diffusion-vision.ts +0 -620
  600. package/src/vision/embedder.ts +0 -176
  601. package/src/vision/preprocess.ts +0 -247
  602. package/src/vision/prompt.ts +0 -409
  603. package/src/vision/qwen3vl-preprocess.ts +0 -409
  604. package/src/vision/qwen3vl-prompt.ts +0 -233
  605. package/src/vision/qwen3vl-tower.ts +0 -566
  606. package/src/vision/siglip.ts +0 -607
  607. package/src/vision/video-frames.ts +0 -148
  608. package/src/web/app.html +0 -2155
  609. package/src/web/app.js +0 -5863
  610. package/src/web/skills/memory/SKILL.md +0 -120
  611. package/src/web/skills/web-research/SKILL.md +0 -46
  612. package/src/web/skills.ts +0 -97
  613. package/src/web/src/adapters-panel.ts +0 -288
  614. package/src/web/src/assistant.ts +0 -451
  615. package/src/web/src/chat.ts +0 -1060
  616. package/src/web/src/composer.ts +0 -962
  617. package/src/web/src/dataset.ts +0 -132
  618. package/src/web/src/finetune.ts +0 -267
  619. package/src/web/src/hub.ts +0 -428
  620. package/src/web/src/main.ts +0 -98
  621. package/src/web/src/markdown.ts +0 -0
  622. package/src/web/src/memory-panel.ts +0 -572
  623. package/src/web/src/model-picker.ts +0 -159
  624. package/src/web/src/palette.ts +0 -346
  625. package/src/web/src/protocol.ts +0 -26
  626. package/src/web/src/quantize.ts +0 -167
  627. package/src/web/src/rag.ts +0 -274
  628. package/src/web/src/sessions.ts +0 -469
  629. package/src/web/src/shell.ts +0 -804
  630. package/src/web/src/ui-catalog.ts +0 -78
  631. package/src/web/src/voice.ts +0 -141
  632. package/src/web/vendor/README.md +0 -61
  633. package/src/web-assets.ts +0 -47
  634. package/src/web-tools.ts +0 -478
  635. package/src/weights.ts +0 -255
  636. /package/src/{serve → server}/completion-sink.ts +0 -0
  637. /package/src/{serve → server}/generated-token-history.ts +0 -0
  638. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  639. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  640. /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
package/src/train/loss.ts DELETED
@@ -1,1591 +0,0 @@
1
- // Training losses: masked cross-entropy (SFT) and DPO.
2
- //
3
- // SFT — port of mlx_lm/tuner/trainer.py default_loss:
4
- // logits = forward(ids[:, :-1]); targets = ids[:, 1:]
5
- // ce[b,t] = logsumexp(logits[b,t]) - logits[b,t, targets[b,t]]
6
- // mask[b,t] = 1 iff position predicts a response token that is NOT padding,
7
- // i.e. (t+1) >= promptLen[b] AND (t+1) < length[b]
8
- // loss = sum(ce * mask) / sum(mask) (token-mean over the whole batch)
9
- // For B=1 with no padding this reduces to exactly the original single-example
10
- // loss (mask[t] = (t+1) >= promptLen, length == ids.length).
11
- //
12
- // DPO — port of optiq/lora/dpo.py: per-sequence response log-prob via
13
- // _seq_logp, reference log-probs computed with the adapter scale forced to 0
14
- // and stop_gradient'd, then L = -log σ(β·((πc-refc) - (πr-refr))) averaged
15
- // over the batch. Padding never contributes: the response mask is 0 at pad
16
- // positions and the batched attention mask hides padded keys.
17
-
18
- import { MlxArray } from "../mlx/array";
19
- import { Dtype } from "../mlx/ffi";
20
- import * as ops from "../mlx/ops";
21
- import { Checkpoint } from "../mlx/checkpoint";
22
- import { CustomVjp } from "../mlx/custom-vjp";
23
- import type { RuntimeModel } from "../model/factory";
24
- import { Gemma4Model } from "../model/gemma4";
25
- import { MiniCPM5Model } from "../model/minicpm5";
26
- import { logitSoftcap } from "../model/gemma4-base";
27
- import { trainForward, trainForwardHidden } from "./forward";
28
- import { setLoraScale, type TrainableLora } from "./lora-params";
29
- import { flashCceForward, flashCceBackward, type FlashCceHead } from "./flash-cce";
30
- import { runtimeValue } from "../runtime-config";
31
-
32
- /** The quantized LM-head weights + final-logit softcap, accessed uniformly across
33
- * models for the fused linear-CE head: Gemma is tied (`embed.asLinear`, softcap
34
- * from config), MiniCPM5 is the separate `lmHead` (no softcap). The head is NOT
35
- * a default LoRA target, so the base quantized weights are authoritative. */
36
- interface HeadQuant {
37
- w: MlxArray; scales: MlxArray; biases: MlxArray | null;
38
- spec: ops.QuantSpec; softcap: number | null;
39
- }
40
- function headQuant(model: RuntimeModel): HeadQuant {
41
- if (model instanceof Gemma4Model) {
42
- const e = model.embed;
43
- return { w: e.w, scales: e.scales, biases: e.biases, spec: e.spec, softcap: model.config.text.finalLogitSoftcapping };
44
- }
45
- if (model instanceof MiniCPM5Model) {
46
- const h = model.lmHead;
47
- return { w: h.w, scales: h.scales, biases: h.biases, spec: h.spec, softcap: null };
48
- }
49
- throw new Error("fused linear-CE head is not wired for this model type");
50
- }
51
-
52
- /** Token-chunking context for the ORPO loss head. When `chunkSize > 0`, the B=1
53
- * response-only head is computed in token-chunks, each wrapped in a Checkpoint
54
- * so its `[chunk, vocab]` logits are RECOMPUTED in the backward instead of all
55
- * `[M, vocab]` being retained — bounding the dominant large-vocab term to
56
- * `[chunkSize, vocab]`. The checkpoints recompute during the enclosing
57
- * value_and_grad backward, so they must outlive `orpoLoss`: they are pushed
58
- * into `sink`, which the caller (orpoLoop) disposes AFTER `vag.apply` returns
59
- * (i.e. after the backward). Exact — rematerialization is numerically
60
- * identical, and MLX computes the (quantized) head backward itself. */
61
- export interface ChunkCtx {
62
- chunkSize: number;
63
- /** When set, use the FUSED linear-CE head (`fusedLogpMeanB1`: one CustomVjp with
64
- * an analytic softmax−onehot backward, no autograd through the head and no
65
- * retained `[M,V]` logits) instead of the per-chunk `Checkpoint` head. Same
66
- * token `chunkSize`; the fused path bounds large-vocab memory in both
67
- * directions and is the Liger/CCE structure (the moat). Off → the Checkpoint
68
- * token-chunked head (unchanged). */
69
- fused?: boolean;
70
- /** When set (with `fused`), route the head through the flash-CCE Metal kernel
71
- * (`makeFlashCceHeadVjp`): in-kernel quantized logits + online softmax (fwd) and
72
- * `dh` accumulation (bwd), so neither `[M,V]` logits nor a dequantized head touch
73
- * HBM — the lowest-memory head, and the fastest on large vocab (e4b/CPM). The
74
- * Apple-CCE coeff filter is on by default in the backward. */
75
- flash?: boolean;
76
- /** Disposables whose lifetime must extend past the enclosing value_and_grad
77
- * backward: the per-chunk `Checkpoint`s / the fused `CustomVjp` (they recompute
78
- * in the backward) AND the hidden `h` / chunk slices they read (these
79
- * deliberately drop their input activations, so freeing them early corrupts the
80
- * recompute — and even the forward eval). The caller (orpoLoop) disposes these
81
- * only AFTER the step's grads are eval'd (the `afterMicroEval` hook). */
82
- sink: Array<{ dispose(): void }>;
83
- }
84
- import {
85
- rowLength, dpoChosenLength, dpoRejectedLength,
86
- type SftBatch, type DpoBatch,
87
- } from "./dataset";
88
-
89
- // ---------------------------------------------------------------------------
90
- // SFT
91
- // ---------------------------------------------------------------------------
92
-
93
- /** Masked cross-entropy over an SFT batch (B>=1).
94
- * Returns a scalar MlxArray (caller owns).
95
- *
96
- * Every row is padded to the batch length L. The forward runs over
97
- * ids[:, :-1] (a single [B, L-1] pass) with a padding-aware attention mask;
98
- * the loss mask then keeps only (response AND non-padding) target positions.
99
- * Loss = sum(ce·mask) / sum(mask) — a token-mean over the whole batch, the
100
- * same reduction as mlx-lm default_loss. */
101
- export function sftLoss(model: RuntimeModel, batch: SftBatch): MlxArray {
102
- const B = batch.ids.length;
103
- const L = batch.ids[0]!.length;
104
- if (L < 2) throw new Error("sftLoss: sequence too short (need >= 2 tokens)");
105
- for (let b = 0; b < B; b++)
106
- if (batch.ids[b]!.length !== L)
107
- throw new Error("sftLoss: all rows in a batch must share length L (pad first)");
108
-
109
- // inputs = ids[:, :-1] (flatten row-major into [B, L-1]).
110
- const T = L - 1;
111
- const inputHost = new Int32Array(B * T);
112
- for (let b = 0; b < B; b++)
113
- for (let t = 0; t < T; t++) inputHost[b * T + t] = batch.ids[b]![t]!;
114
- const inputIds = MlxArray.fromInt32(inputHost, [B, T]);
115
-
116
- // Per-input-row valid length = min(rowLength, L) - 1 (inputs dropped the
117
- // last token). Keys beyond this are padding and must not be attended.
118
- const inputValid = Array.from({ length: B }, (_, b) =>
119
- Math.max(0, Math.min(rowLength(batch, b), L) - 1),
120
- );
121
-
122
- let scalar: MlxArray;
123
- try {
124
- if (B === 1) {
125
- // Response-only path: run the forward to hidden states, then apply the
126
- // LM head ONLY at supervised positions. Avoids the [1, T, 262k] logits
127
- // (+ its grad) — the dominant long-context training-memory term.
128
- const h = trainForwardHidden(model, inputIds, inputValid); // [1, T, hidden]
129
- scalar = responseOnlyCe(model, h, batch);
130
- h.dispose();
131
- } else {
132
- const logits = trainForward(model, inputIds, inputValid); // [B, T, V]
133
- scalar = maskedCe(logits, batch);
134
- logits.dispose();
135
- }
136
- } finally {
137
- inputIds.dispose();
138
- }
139
- return scalar;
140
- }
141
-
142
- /** model.logitsFromHidden for TRAINING heads, with the upstream mlx
143
- * quantized_matmul bug worked around: transpose=false returns WRONG values at
144
- * row count 2 or 3 (reproduced on stock mlx 0.31.2; M=1 and M>=4 correct,
145
- * transpose=true correct at every M). mlx's autograd computes dh for a
146
- * transpose=true head qmm via an INTERNAL transpose=false call at the
147
- * cotangent's row count — so any differentiated [M,V] logits with M in {2,3}
148
- * (2-3-token responses, 2-3-token tail chunks, tiny prompt spans) got a
149
- * silently wrong dh (measured relnorm ~0.35-0.99 vs the f32 ground truth).
150
- * Fix: pad the span to 4 rows with zeros and slice the logits back — the pad
151
- * rows' cotangents are exactly zero (slice adjoint), and the internal vjp
152
- * then runs at M=4 where the kernel is correct. */
153
- export function logitsFromHiddenPadM(
154
- model: { logitsFromHidden(h: MlxArray): MlxArray },
155
- hSpan: MlxArray, // [1, M, hidden]
156
- ): MlxArray {
157
- const M = hSpan.shape[1]!;
158
- if (M !== 2 && M !== 3) return model.logitsFromHidden(hSpan);
159
- const [B, , H] = hSpan.shape as [number, number, number];
160
- const pad = ops.zeros([B, 4 - M, H], hSpan.dtype);
161
- const hPad = ops.concatAxis([hSpan, pad], 1);
162
- pad.dispose();
163
- const lgPad = model.logitsFromHidden(hPad); // [1, 4, V]
164
- hPad.dispose();
165
- const start = MlxArray.fromInt32(new Int32Array([0]), [1]);
166
- const lg = ops.sliceDynamic(lgPad, start, [1], [B, M, lgPad.shape[2]!]);
167
- start.dispose();
168
- lgPad.dispose();
169
- return lg;
170
- }
171
-
172
- /** B=1 masked CE that applies the LM head only at the supervised (response)
173
- * span. The supervised input positions are the contiguous range
174
- * [promptLen-1, len-1) (each predicts the next, response, token). The LM head
175
- * is position-independent and prompt positions get zero loss weight, so this
176
- * is numerically identical to maskedCe for B=1 — but it never materializes
177
- * logits at prompt positions. Gradients to prompt-position LoRA params still
178
- * flow: the response hidden states depend on the prompt through causal
179
- * attention, which is fully captured upstream in `h`. */
180
- export function responseOnlyCe(model: RuntimeModel, h: MlxArray, batch: SftBatch): MlxArray {
181
- const L = batch.ids[0]!.length;
182
- const promptLen = batch.promptLens[0]!;
183
- const len = Math.min(rowLength(batch, 0), L);
184
- const startT = Math.max(0, promptLen - 1); // first supervised input position
185
- const M = (len - 1) - startT; // # supervised positions (predict ids[startT+1 .. len-1])
186
- if (M <= 0) throw new Error("sftLoss: no response tokens to supervise");
187
-
188
- // h[:, startT : startT+M, :] → [1, M, hidden] (sliceSize is the full output shape)
189
- const hidden = h.shape[2]!;
190
- const start = MlxArray.fromInt32(new Int32Array([startT]), [1]);
191
- const hResp = ops.sliceDynamic(h, start, [1], [1, M, hidden]);
192
- const logits = logitsFromHiddenPadM(model, hResp); // [1, M, V] (M in {2,3} padded — mlx qmm bug)
193
- const V = logits.shape[2]!;
194
- const logits2d = ops.reshape(logits, [M, V]);
195
-
196
- const targetsHost = new Int32Array(M);
197
- for (let i = 0; i < M; i++) targetsHost[i] = batch.ids[0]![startT + 1 + i]!;
198
- const targets = MlxArray.fromInt32(targetsHost, [M, 1]);
199
-
200
- const lse = ops.logsumexpAxis(logits2d, -1, false); // [M]
201
- const gathered = ops.takeAlongAxis(logits2d, targets, -1); // [M, 1]
202
- const picked = ops.reshape(gathered, [M]); // [M]
203
- const ce = ops.sub(lse, picked); // [M]
204
- const ceF = ce.dtype === Dtype.float32 ? ce : ce.astype(Dtype.float32);
205
- const sumCe = ops.sumAxis(ceF, 0, false); // scalar
206
- const loss = ops.mulScalar(sumCe, 1 / M); // token-mean over the response
207
-
208
- for (const a of [start, hResp, logits, logits2d, targets, lse, gathered, picked, ce, sumCe]) a.dispose();
209
- if (ceF !== ce) ceF.dispose();
210
- return loss;
211
- }
212
-
213
- /** B=1 response-only length-normalized mean log-prob from pre-computed final-hidden h [1, T, hidden].
214
- * Mirror of responseOnlyCe but returns mean response logp (scalar wrapped in [1]) instead of
215
- * CE loss. Used by SegmentedBackwardOrpo's head VJP. Disposes nothing; caller owns h and result. */
216
- export function responseOnlyLogpMean(
217
- model: RuntimeModel,
218
- h: MlxArray, // [1, T, hidden] post-finalNorm
219
- ids: number[],
220
- mask: number[],
221
- ): MlxArray {
222
- const L = ids.length;
223
- const T = L - 1; // h has T positions
224
- // Find response span: supervised positions where mask[t+1] == 1
225
- let startT = -1; let M = 0;
226
- for (let t = 0; t < T; t++) if (mask[t + 1]) { if (startT < 0) startT = t; M++; }
227
- if (M <= 0) return MlxArray.fromFloat32(new Float32Array([0]), [1]);
228
-
229
- const hidden = h.shape[2]!;
230
- const start = MlxArray.fromInt32(new Int32Array([startT]), [1]);
231
- const hResp = ops.sliceDynamic(h, start, [1], [1, M, hidden]); // [1, M, hidden]
232
- const logits = logitsFromHiddenPadM(model, hResp); // [1, M, V] (M in {2,3} padded — mlx qmm bug)
233
- const V = logits.shape[2]!;
234
- const logits2d = ops.reshape(logits, [M, V]);
235
-
236
- const targetsHost = new Int32Array(M);
237
- for (let i = 0; i < M; i++) targetsHost[i] = ids[startT + 1 + i]!;
238
- const targets = MlxArray.fromInt32(targetsHost, [M, 1]);
239
-
240
- const lse = ops.logsumexpAxis(logits2d, -1, false); // [M]
241
- const gathered = ops.takeAlongAxis(logits2d, targets, -1); // [M, 1]
242
- const picked = ops.reshape(gathered, [M]); // [M]
243
- const logp = ops.sub(picked, lse); // [M] log_softmax at label
244
- const logpF = logp.dtype === Dtype.float32 ? logp : logp.astype(Dtype.float32);
245
- const sumLogp = ops.sumAxis(logpF, 0, false); // scalar
246
- const meanScalar = ops.mulScalar(sumLogp, 1 / M);
247
- const mean = ops.reshape(meanScalar, [1]); // [1]
248
-
249
- for (const a of [start, hResp, logits, logits2d, targets, lse, gathered, picked, logp, sumLogp, meanScalar]) a.dispose();
250
- if (logpF !== logp) logpF.dispose();
251
- return mean;
252
- }
253
-
254
- /** Build the token-mean masked-CE scalar from batched logits [B, T, V]
255
- * (T = L-1) and the batch's host ids/boundaries. */
256
- export function maskedCe(logits: MlxArray, batch: SftBatch): MlxArray {
257
- const B = batch.ids.length;
258
- const L = batch.ids[0]!.length;
259
- const T = L - 1;
260
- const V = logits.shape[2]!;
261
-
262
- const targetsHost = new Int32Array(B * T);
263
- const maskHost = new Float32Array(B * T);
264
- let ntoks = 0;
265
- for (let b = 0; b < B; b++) {
266
- const promptLen = batch.promptLens[b]!;
267
- const len = Math.min(rowLength(batch, b), L);
268
- for (let t = 0; t < T; t++) {
269
- const flat = b * T + t;
270
- targetsHost[flat] = batch.ids[b]![t + 1]!;
271
- // predicted token is ids[t+1]; supervised iff it is a response token
272
- // (t+1 >= promptLen) AND a real (non-pad) token (t+1 < len).
273
- if (t + 1 >= promptLen && t + 1 < len) {
274
- maskHost[flat] = 1;
275
- ntoks++;
276
- }
277
- }
278
- }
279
- if (ntoks === 0) throw new Error("sftLoss: no response tokens to supervise");
280
-
281
- const N = B * T;
282
- const logits2d = ops.reshape(logits, [N, V]); // [B*T, V]
283
- const targets = MlxArray.fromInt32(targetsHost, [N, 1]);
284
- const mask = MlxArray.fromFloat32(maskHost, [N]);
285
-
286
- // ce[n] = logsumexp(logits[n]) - logits[n, target[n]]
287
- const lse = ops.logsumexpAxis(logits2d, -1, false); // [N]
288
- const gathered = ops.takeAlongAxis(logits2d, targets, -1); // [N, 1]
289
- const picked = ops.reshape(gathered, [N]); // [N]
290
- const ce = ops.sub(lse, picked); // [N]
291
- const ceF = ce.dtype === Dtype.float32 ? ce : ce.astype(Dtype.float32);
292
-
293
- const masked = ops.mul(ceF, mask); // [N]
294
- const sumCe = ops.sumAxis(masked, 0, false); // scalar
295
- const loss = ops.mulScalar(sumCe, 1 / ntoks); // scalar (token-mean)
296
-
297
- for (const a of [logits2d, targets, mask, lse, gathered, picked, ce, masked, sumCe]) a.dispose();
298
- if (ceF !== ce) ceF.dispose();
299
- return loss;
300
- }
301
-
302
- // ---------------------------------------------------------------------------
303
- // DPO
304
- // ---------------------------------------------------------------------------
305
-
306
- /** Per-sequence sum of response-position log-probs (B>=1), port of
307
- * dpo.py _seq_logp. `ids`/`mask` are `[B][L]` host arrays; `logits` are
308
- * [B, L-1, V] (the forward over ids[:, :-1]). Returns a [B] MlxArray — one
309
- * summed response log-prob per row. Pad positions contribute 0 because the
310
- * response mask is 0 there. */
311
- function seqLogp(logits: MlxArray, ids: number[][], mask: number[][]): MlxArray {
312
- const B = ids.length;
313
- const L = ids[0]!.length;
314
- const T = L - 1;
315
- const V = logits.shape[2]!;
316
- const N = B * T;
317
-
318
- const targetsHost = new Int32Array(N);
319
- const maskHost = new Float32Array(N);
320
- for (let b = 0; b < B; b++)
321
- for (let t = 0; t < T; t++) {
322
- const flat = b * T + t;
323
- targetsHost[flat] = ids[b]![t + 1]!;
324
- maskHost[flat] = mask[b]![t + 1]!; // resp_mask[:, 1:]
325
- }
326
- const logits2d = ops.reshape(logits, [N, V]);
327
- const targets = MlxArray.fromInt32(targetsHost, [N, 1]);
328
- const m = MlxArray.fromFloat32(maskHost, [N]);
329
-
330
- const lse = ops.logsumexpAxis(logits2d, -1, false); // [N]
331
- const gathered = ops.takeAlongAxis(logits2d, targets, -1); // [N,1]
332
- const picked = ops.reshape(gathered, [N]); // [N]
333
- const logp = ops.sub(picked, lse); // log_softmax at the label
334
- const logpF = logp.dtype === Dtype.float32 ? logp : logp.astype(Dtype.float32);
335
- const masked = ops.mul(logpF, m); // [N]
336
- const masked2d = ops.reshape(masked, [B, T]); // [B, T]
337
- const perRow = ops.sumAxis(masked2d, 1, false); // [B]
338
-
339
- for (const a of [logits2d, targets, m, lse, gathered, picked, logp, masked, masked2d]) a.dispose();
340
- if (logpF !== logp) logpF.dispose();
341
- return perRow;
342
- }
343
-
344
- /** Forward + per-sequence response log-prob for one branch (B>=1). `validLen`
345
- * is each row's true (unpadded) length, used to mask padded keys in the
346
- * attention; passing it makes the batched forward ignore padding bit-for-bit
347
- * the way separate per-row forwards would. */
348
- function branchLogp(
349
- model: RuntimeModel,
350
- ids: number[][],
351
- mask: number[][],
352
- validLen: number[],
353
- ): MlxArray {
354
- const B = ids.length;
355
- const L = ids[0]!.length;
356
- const T = L - 1;
357
- const inputHost = new Int32Array(B * T);
358
- for (let b = 0; b < B; b++)
359
- for (let t = 0; t < T; t++) inputHost[b * T + t] = ids[b]![t]!;
360
- const inputIds = MlxArray.fromInt32(inputHost, [B, T]);
361
- // input row valid length = min(len, L) - 1 (last token dropped from inputs).
362
- const inputValid = validLen.map((v) => Math.max(0, Math.min(v, L) - 1));
363
- try {
364
- const logits = trainForward(model, inputIds, inputValid);
365
- const lp = seqLogp(logits, ids, mask);
366
- logits.dispose();
367
- return lp;
368
- } finally {
369
- inputIds.dispose();
370
- }
371
- }
372
-
373
- /** Branch valid lengths from a DPO batch (defaults to full length for B=1). */
374
- function chosenLengths(batch: DpoBatch): number[] {
375
- return batch.chosenIds.map((_, i) => dpoChosenLength(batch, i));
376
- }
377
- function rejectedLengths(batch: DpoBatch): number[] {
378
- return batch.rejectedIds.map((_, i) => dpoRejectedLength(batch, i));
379
- }
380
-
381
- /** Reference log-probs for a DPO batch: forward with the adapter disabled
382
- * (scale 0), materialized + stop_gradient'd so the policy forwards don't
383
- * hold the reference activations. Returns detached scalars (caller owns). */
384
- export function dpoRefLogps(
385
- model: RuntimeModel,
386
- lora: TrainableLora,
387
- batch: DpoBatch,
388
- ): { refChosen: MlxArray; refRejected: MlxArray } {
389
- const original = lora.targets[0]?.lw.scale ?? lora.scale;
390
- const cLen = chosenLengths(batch);
391
- const rLen = rejectedLengths(batch);
392
- setLoraScale(lora, 0);
393
- try {
394
- const c = branchLogp(model, batch.chosenIds, batch.chosenMask, cLen); // [B]
395
- const r = branchLogp(model, batch.rejectedIds, batch.rejectedMask, rLen); // [B]
396
- ops.evalAll([c, r]); // free reference activations before policy forwards
397
- const refChosen = ops.stopGradient(c);
398
- const refRejected = ops.stopGradient(r);
399
- c.dispose();
400
- r.dispose();
401
- return { refChosen, refRejected };
402
- } finally {
403
- setLoraScale(lora, original);
404
- }
405
- }
406
-
407
- /** DPO loss for one batch given precomputed reference log-probs ([B] each).
408
- * Runs the two policy forwards (adapter active) and reduces to the scalar
409
- * mean over the batch of -logσ(adv) (matching dpo.py _policy_dpo_loss).
410
- * Returns a scalar MlxArray (caller owns). */
411
- export function dpoLoss(
412
- model: RuntimeModel,
413
- batch: DpoBatch,
414
- beta: number,
415
- refChosen: MlxArray,
416
- refRejected: MlxArray,
417
- ): MlxArray {
418
- const cLen = chosenLengths(batch);
419
- const rLen = rejectedLengths(batch);
420
- const piC = branchLogp(model, batch.chosenIds, batch.chosenMask, cLen); // [B]
421
- const piR = branchLogp(model, batch.rejectedIds, batch.rejectedMask, rLen); // [B]
422
-
423
- const logrC = ops.sub(piC, refChosen); // [B] πc - refc
424
- const logrR = ops.sub(piR, refRejected); // [B] πr - refr
425
- const diff = ops.sub(logrC, logrR);
426
- const adv = ops.mulScalar(diff, beta); // [B] β·(logrC - logrR)
427
- // -log σ(adv) = softplus(-adv) = logaddexp(0, -adv); mean over the batch.
428
- const negAdv = ops.neg(adv);
429
- const zero = ops.scalarLike(0, adv);
430
- const perRow = ops.logaddexp(zero, negAdv); // [B]
431
- const loss = ops.meanAll(perRow, false); // scalar
432
-
433
- for (const a of [piC, piR, logrC, logrR, diff, adv, negAdv, zero, perRow]) a.dispose();
434
- return loss;
435
- }
436
-
437
- /** DPO diagnostics (no grad): loss, accuracy (mean of adv>0), reward margin
438
- * (mean of β·(logrC-logrR)) over the batch. Computes its own references. */
439
- export function dpoMetrics(
440
- model: RuntimeModel,
441
- lora: TrainableLora,
442
- batch: DpoBatch,
443
- beta: number,
444
- ): { loss: number; accuracy: number; margin: number } {
445
- const { refChosen, refRejected } = dpoRefLogps(model, lora, batch);
446
- const cLen = chosenLengths(batch);
447
- const rLen = rejectedLengths(batch);
448
- const piC = branchLogp(model, batch.chosenIds, batch.chosenMask, cLen); // [B]
449
- const piR = branchLogp(model, batch.rejectedIds, batch.rejectedMask, rLen); // [B]
450
-
451
- const logrC = ops.sub(piC, refChosen);
452
- const logrR = ops.sub(piR, refRejected);
453
- const diff = ops.sub(logrC, logrR);
454
- const adv = ops.mulScalar(diff, beta); // [B]
455
- const negAdv = ops.neg(adv);
456
- const zero = ops.scalarLike(0, adv);
457
- const perRow = ops.logaddexp(zero, negAdv); // [B]
458
- const loss = ops.meanAll(perRow, false);
459
-
460
- const advVals = adv.toFloat32(); // [B]
461
- const lossVal = loss.toFloat32()[0]!;
462
- let acc = 0;
463
- let margin = 0;
464
- for (let b = 0; b < advVals.length; b++) {
465
- if (advVals[b]! > 0) acc++;
466
- margin += advVals[b]!;
467
- }
468
- acc /= advVals.length;
469
- margin /= advVals.length;
470
-
471
- for (const a of [refChosen, refRejected, piC, piR, logrC, logrR, diff, adv, negAdv, zero, perRow, loss]) a.dispose();
472
- return { loss: lossVal, accuracy: acc, margin };
473
- }
474
-
475
- // ---------------------------------------------------------------------------
476
- // ORPO (Odds Ratio Preference Optimization, Hong et al. 2024)
477
- // ---------------------------------------------------------------------------
478
- //
479
- // Reference-FREE: one objective, no reference model (vs DPO's scale-0 ref pass).
480
- // L = L_NLL(chosen) + λ · L_OR
481
- // L_NLL = the chosen-sequence SFT term (unweighted); scope selected by sft_scope:
482
- // "full" (default in TrainConfig; paper/TRL-faithful): mean cross-entropy
483
- // over ALL non-pad supervised positions of the chosen sequence —
484
- // prompt AND response (TRL masks only padding to -100 for the
485
- // chosen_nll_loss; nn.CrossEntropyLoss reduction="mean").
486
- // "response" (the pre-2026-07 mlx-bun behavior, bit-exact): -ℓw, the negative
487
- // mean response log-prob.
488
- // L_OR = -log σ(log_odds)
489
- // log_odds = (ℓw - ℓr) - (log1mexp(ℓw) - log1mexp(ℓr))
490
- // where ℓ is the LENGTH-NORMALIZED (mean over response tokens) log-prob — the
491
- // paper's choice (and TRL's), distinct from DPO's summed seqLogp. CRITICAL:
492
- // ℓw/ℓr stay RESPONSE-ONLY in BOTH sft_scope modes (TRL's get_batch_logps uses
493
- // prompt-masked labels for the odds ratio; only the NLL term is full-sequence).
494
- // Two facts shape the code: (1) ℓw feeds BOTH terms (under "full" the chosen
495
- // forward additionally yields the prompt-position log-probs — same forward, the
496
- // head just runs over the extended span); (2) only 2 forwards/step (chosen +
497
- // rejected), both with grad, no reference. The REJECTED branch never needs
498
- // prompt logps. This is the naïve full-logits oracle path — the chunked /
499
- // response-only / segmented tiers validate against it (see the design's oracle
500
- // ladder, docs/design/orpo-training.md).
501
-
502
- /** SFT-term scope for ORPO's L_NLL(chosen): "full" = prompt+response token-mean
503
- * CE (paper/TRL-faithful); "response" = -ℓw (the pre-change behavior). The
504
- * odds-ratio ℓ terms are response-only in both modes. */
505
- export type SftScope = "full" | "response";
506
-
507
- /** A contiguous supervised input-position span [startT, startT+M): position t
508
- * predicts token ids[t+1]. The response span comes from the mask; "full" mode
509
- * additionally runs the head over the prompt span {startT: 0, M: respStartT}. */
510
- export interface HeadSpan { startT: number; M: number }
511
-
512
- /** The response span from a DPO-row mask (mask[t+1]==1 ⇔ input position t is
513
- * supervised): first supervised position + count. M<=0 ⇔ no response. */
514
- export function respSpanFromMask(mask: number[], T: number): HeadSpan {
515
- let startT = -1;
516
- let M = 0;
517
- for (let t = 0; t < T; t++) if (mask[t + 1]) { if (startT < 0) startT = t; M++; }
518
- return { startT, M };
519
- }
520
-
521
- /** Length-normalized mean log-prob over the contiguous input-position span
522
- * [s0, s0+n) of `h [1, T, hidden]` (post-finalNorm), scoring targets
523
- * ids[s0+1 .. s0+n]. The span-parameterized generalization of
524
- * responseOnlyLogpMean (same ops); used for the prompt span under
525
- * sft_scope:"full". Whole-vocab autograd head. Returns [1] (caller owns);
526
- * disposes nothing it didn't create. */
527
- export function spanLogpMeanFromHidden(
528
- model: { logitsFromHidden(h: MlxArray): MlxArray },
529
- h: MlxArray, // [1, T, hidden] post-finalNorm
530
- ids: number[],
531
- s0: number,
532
- n: number,
533
- ): MlxArray {
534
- if (n <= 0) return MlxArray.fromFloat32(new Float32Array([0]), [1]);
535
- const hidden = h.shape[2]!;
536
- const start = MlxArray.fromInt32(new Int32Array([s0]), [1]);
537
- const hSpan = ops.sliceDynamic(h, start, [1], [1, n, hidden]); // [1, n, hidden]
538
- const logits = logitsFromHiddenPadM(model, hSpan); // [1, n, V] (n in {2,3} padded — mlx qmm bug)
539
- const V = logits.shape[2]!;
540
- const logits2d = ops.reshape(logits, [n, V]);
541
-
542
- const targetsHost = new Int32Array(n);
543
- for (let i = 0; i < n; i++) targetsHost[i] = ids[s0 + 1 + i]!;
544
- const targets = MlxArray.fromInt32(targetsHost, [n, 1]);
545
-
546
- const lse = ops.logsumexpAxis(logits2d, -1, false); // [n]
547
- const gathered = ops.takeAlongAxis(logits2d, targets, -1); // [n, 1]
548
- const picked = ops.reshape(gathered, [n]); // [n]
549
- const logp = ops.sub(picked, lse); // [n] log_softmax at the label
550
- const logpF = logp.dtype === Dtype.float32 ? logp : logp.astype(Dtype.float32);
551
- const sumLogp = ops.sumAxis(logpF, 0, false); // scalar
552
- const meanScalar = ops.mulScalar(sumLogp, 1 / n);
553
- const mean = ops.reshape(meanScalar, [1]); // [1]
554
-
555
- for (const a of [start, hSpan, logits, logits2d, targets, lse, gathered, picked, logp, sumLogp, meanScalar]) a.dispose();
556
- if (logpF !== logp) logpF.dispose();
557
- return mean;
558
- }
559
-
560
- /** Combine the prompt-span mean logp (`promptMean`, over nPrompt positions) and
561
- * the response mean logp (`lw`, over nResp positions) into the FULL-scope chosen
562
- * NLL: `-(nPrompt·promptMean + nResp·lw) / (nPrompt + nResp)` — the token-mean
563
- * CE over all prompt+response supervised positions (nn.CrossEntropyLoss
564
- * reduction="mean" over the label-shifted non-pad positions). Graph-preserving
565
- * (pure ops on the two means), so the head gradient flows to BOTH spans. Does
566
- * NOT dispose promptMean/lw (caller owns); returns [1] (caller owns). */
567
- export function combineFullNll(
568
- promptMean: MlxArray | null,
569
- lw: MlxArray,
570
- nPrompt: number,
571
- nResp: number,
572
- ): MlxArray {
573
- if (!promptMean || nPrompt <= 0) return ops.neg(lw); // no prompt predictions → full == response
574
- const ps = ops.mulScalar(promptMean, nPrompt); // Σ prompt logp (reconstructed)
575
- const rs = ops.mulScalar(lw, nResp); // Σ response logp (reconstructed)
576
- const sum = ops.add(ps, rs);
577
- const mean = ops.mulScalar(sum, 1 / (nPrompt + nResp));
578
- const out = ops.neg(mean);
579
- for (const a of [ps, rs, sum, mean]) a.dispose();
580
- return out;
581
- }
582
-
583
- /** Per-row LENGTH-NORMALIZED response log-prob for one branch (B>=1): the
584
- * summed response logp divided by the per-row response-token count. Pad/prompt
585
- * positions contribute 0 (mask is 0 there) and never inflate the count.
586
- * Returns a [B] MlxArray (caller owns).
587
- *
588
- * B=1 takes the response-only path (mirrors `responseOnlyCe`): the LM head is
589
- * applied ONLY at the supervised response positions, so the dominant
590
- * `[1, T, vocab]` logits term (and its grad) is never materialized — the same
591
- * long-context memory win the SFT B=1 path already gets, doubly important for
592
- * ORPO since it forwards two responses. Numerically identical to the B>1
593
- * full-logits path (same positions, same count). B>1 uses the full-logits
594
- * path (the batched forward already materializes logits). */
595
- function branchLogpMean(
596
- model: RuntimeModel,
597
- ids: number[][],
598
- mask: number[][],
599
- validLen: number[],
600
- chunk?: ChunkCtx,
601
- ): MlxArray {
602
- if (ids.length === 1) {
603
- if (chunk && chunk.chunkSize > 0)
604
- return chunk.fused
605
- ? fusedLogpMeanB1(model, ids[0]!, mask[0]!, validLen[0]!, chunk.chunkSize, chunk.sink, 0, chunk.flash ?? false)
606
- : chunkedLogpMeanB1(model, ids[0]!, mask[0]!, validLen[0]!, chunk.chunkSize, chunk.sink);
607
- return branchLogpMeanB1(model, ids[0]!, mask[0]!, validLen[0]!);
608
- }
609
-
610
- const sum = branchLogp(model, ids, mask, validLen); // [B] summed response logp
611
- // Per-row response-token count = Σ_t mask[b][t+1] (resp_mask[:, 1:]); clamp to
612
- // >=1 so an all-prompt row (no response) can't divide by zero. Multiply by the
613
- // host-side reciprocal (avoids needing an elementwise divide op).
614
- const B = ids.length;
615
- const L = ids[0]!.length;
616
- const recip = new Float32Array(B);
617
- for (let b = 0; b < B; b++) {
618
- let c = 0;
619
- for (let t = 0; t < L - 1; t++) c += mask[b]![t + 1]!;
620
- recip[b] = 1 / Math.max(c, 1);
621
- }
622
- const recipArr = MlxArray.fromFloat32(recip, [B]);
623
- const mean = ops.mul(sum, recipArr); // [B] length-normalized
624
- sum.dispose();
625
- recipArr.dispose();
626
- return mean;
627
- }
628
-
629
- /** B=1 response-only length-normalized log-prob → [1]. The supervised input
630
- * positions are exactly `{ t in [0, T) : mask[t+1] == 1 }` (the same set the
631
- * full-logits seqLogp sums), which for prompt+response data is the contiguous
632
- * span `[startT, startT+M)`; the LM head runs only on that slice. Grads to
633
- * prompt-position LoRA still flow — the response hidden states depend on the
634
- * prompt through causal attention, captured upstream in `h`. */
635
- export function branchLogpMeanB1(
636
- model: RuntimeModel,
637
- ids: number[],
638
- mask: number[],
639
- validLen: number,
640
- ): MlxArray {
641
- const L = ids.length;
642
- const T = L - 1;
643
- // Supervised input positions (contiguous for prompt+response): first/count.
644
- let startT = -1;
645
- let M = 0;
646
- for (let t = 0; t < T; t++) if (mask[t + 1]) { if (startT < 0) startT = t; M++; }
647
- if (M <= 0) return MlxArray.fromFloat32(new Float32Array([0]), [1]); // no response → 0 (matches clamp)
648
-
649
- const inputHost = new Int32Array(T);
650
- for (let t = 0; t < T; t++) inputHost[t] = ids[t]!;
651
- const inputIds = MlxArray.fromInt32(inputHost, [1, T]);
652
- const inputValid = [Math.max(0, Math.min(validLen, L) - 1)];
653
-
654
- const h = trainForwardHidden(model, inputIds, inputValid); // [1, T, hidden]
655
- const hidden = h.shape[2]!;
656
- const start = MlxArray.fromInt32(new Int32Array([startT]), [1]);
657
- const hResp = ops.sliceDynamic(h, start, [1], [1, M, hidden]); // [1, M, hidden]
658
- const logits = logitsFromHiddenPadM(model, hResp); // [1, M, V] — head only on the response span (M in {2,3} padded — mlx qmm bug)
659
- const V = logits.shape[2]!;
660
- const logits2d = ops.reshape(logits, [M, V]);
661
-
662
- const targetsHost = new Int32Array(M);
663
- for (let i = 0; i < M; i++) targetsHost[i] = ids[startT + 1 + i]!;
664
- const targets = MlxArray.fromInt32(targetsHost, [M, 1]);
665
-
666
- const lse = ops.logsumexpAxis(logits2d, -1, false); // [M]
667
- const gathered = ops.takeAlongAxis(logits2d, targets, -1); // [M, 1]
668
- const picked = ops.reshape(gathered, [M]); // [M]
669
- const logp = ops.sub(picked, lse); // [M] log_softmax at the label
670
- const logpF = logp.dtype === Dtype.float32 ? logp : logp.astype(Dtype.float32);
671
- const sumLogp = ops.sumAxis(logpF, 0, false); // scalar
672
- const meanScalar = ops.mulScalar(sumLogp, 1 / M); // scalar (length-normalized)
673
- const mean = ops.reshape(meanScalar, [1]); // [1] to match the [B] contract
674
-
675
- for (const a of [inputIds, h, start, hResp, logits, logits2d, targets, lse, gathered, picked, logp, sumLogp, meanScalar]) a.dispose();
676
- if (logpF !== logp) logpF.dispose();
677
- return mean;
678
- }
679
-
680
- /** B=1 chunked response-only mean log-prob → [1]. Same result as
681
- * `branchLogpMeanB1` but the LM head over the M response positions is computed
682
- * in token-chunks of `chunkSize`, each wrapped in a `Checkpoint`: the
683
- * `[chunk, vocab]` logits are recomputed in the backward rather than all
684
- * `[M, vocab]` retained, so peak large-vocab memory is bounded to
685
- * `[chunkSize, vocab]`. Each checkpoint must survive until the enclosing
686
- * value_and_grad backward (it recomputes there), so it is pushed into `sink`
687
- * for the caller to dispose after `vag.apply`. Grads flow chunk → slice → `h`
688
- * → layer-stack LoRA exactly as the single-shot path. */
689
- function chunkedLogpMeanB1(
690
- model: RuntimeModel,
691
- ids: number[],
692
- mask: number[],
693
- validLen: number,
694
- chunkSize: number,
695
- sink: Array<{ dispose(): void }>,
696
- ): MlxArray {
697
- const L = ids.length;
698
- const T = L - 1;
699
- let startT = -1;
700
- let M = 0;
701
- for (let t = 0; t < T; t++) if (mask[t + 1]) { if (startT < 0) startT = t; M++; }
702
- if (M <= 0) return MlxArray.fromFloat32(new Float32Array([0]), [1]);
703
-
704
- const inputHost = new Int32Array(T);
705
- for (let t = 0; t < T; t++) inputHost[t] = ids[t]!;
706
- const inputIds = MlxArray.fromInt32(inputHost, [1, T]);
707
- const inputValid = [Math.max(0, Math.min(validLen, L) - 1)];
708
- const h = trainForwardHidden(model, inputIds, inputValid); // [1, T, hidden]
709
- inputIds.dispose();
710
- // `h` and the chunk slices feed Checkpoints, which drop their input
711
- // activations — so they must NOT be freed until after the backward. Defer to
712
- // the sink (disposed post-eval by the caller).
713
- sink.push(h);
714
- const hidden = h.shape[2]!;
715
-
716
- let total: MlxArray | null = null;
717
- for (let c0 = startT; c0 < startT + M; c0 += chunkSize) {
718
- const c1 = Math.min(c0 + chunkSize, startT + M);
719
- const Cc = c1 - c0;
720
- const startArr = MlxArray.fromInt32(new Int32Array([c0]), [1]);
721
- const hChunk = ops.sliceDynamic(h, startArr, [1], [1, Cc, hidden]); // [1, Cc, hidden]
722
- startArr.dispose();
723
- sink.push(hChunk); // checkpoint input — alive until post-eval
724
-
725
- // Targets for this chunk, captured immutably by the checkpoint closure so
726
- // the backward recompute reproduces exactly these positions' logp.
727
- const tHost = new Int32Array(Cc);
728
- for (let i = 0; i < Cc; i++) tHost[i] = ids[c0 + 1 + i]!;
729
-
730
- const ckpt = new Checkpoint((inp) => {
731
- const hc = inp[0]!;
732
- const logits = logitsFromHiddenPadM(model, hc); // [1, Cc, V] — recomputed in backward (Cc in {2,3} padded — mlx qmm bug)
733
- const V = logits.shape[2]!;
734
- const l2 = ops.reshape(logits, [Cc, V]);
735
- const targets = MlxArray.fromInt32(tHost, [Cc, 1]);
736
- const lse = ops.logsumexpAxis(l2, -1, false); // [Cc]
737
- const gathered = ops.takeAlongAxis(l2, targets, -1); // [Cc, 1]
738
- const picked = ops.reshape(gathered, [Cc]); // [Cc]
739
- const logp = ops.sub(picked, lse); // [Cc]
740
- const logpF = logp.dtype === Dtype.float32 ? logp : logp.astype(Dtype.float32);
741
- const s = ops.sumAxis(logpF, 0, false); // scalar — Σ logp over the chunk
742
- for (const a of [logits, l2, targets, lse, gathered, picked, logp]) a.dispose();
743
- if (logpF !== logp) logpF.dispose();
744
- return [s];
745
- });
746
- sink.push(ckpt); // disposed by the caller after the backward (vag.apply)
747
- const [s] = ckpt.apply([hChunk]);
748
- if (total === null) total = s!;
749
- else {
750
- const sum = ops.add(total, s!);
751
- total.dispose();
752
- s!.dispose();
753
- total = sum;
754
- }
755
- }
756
-
757
- const meanScalar = ops.mulScalar(total!, 1 / M); // length-normalized
758
- total!.dispose();
759
- const mean = ops.reshape(meanScalar, [1]); // [1] to match the [B] contract
760
- meanScalar.dispose();
761
- return mean;
762
- }
763
-
764
- /** CHECKPOINT token-chunked response-only mean logp from a PRE-COMPUTED
765
- * post-finalNorm hidden `h [1,T,hidden]` (the segmented-backward head variant of
766
- * `chunkedLogpMeanB1`). Each token-chunk's `logitsFromHidden` is wrapped in a
767
- * `Checkpoint`, so the per-chunk `[chunk,V]` logits are RECOMPUTED in the
768
- * backward and freed sequentially — the recompute boundary is what actually
769
- * bounds the head term under MLX's lazy eval when nested inside the segmented
770
- * `mlx_vjp` (unlike the analytic CustomVjp, whose per-chunk graph isn't freed
771
- * incrementally inside an outer vjp). Checkpoints + slices go in `sink`. */
772
- export function chunkedLogpMeanFromHidden(
773
- model: RuntimeModel,
774
- h: MlxArray, // [1, T, hidden] post-finalNorm
775
- ids: number[],
776
- mask: number[],
777
- chunkSize: number,
778
- sink: Array<{ dispose(): void }>,
779
- span?: HeadSpan, // override the mask-derived response span (sft_scope:"full" prompt span)
780
- ): MlxArray {
781
- const L = ids.length;
782
- const T = L - 1;
783
- const { startT, M } = span ?? respSpanFromMask(mask, T);
784
- if (M <= 0) return MlxArray.fromFloat32(new Float32Array([0]), [1]);
785
- const hidden = h.shape[2]!;
786
-
787
- let total: MlxArray | null = null;
788
- for (let c0 = startT; c0 < startT + M; c0 += chunkSize) {
789
- const c1 = Math.min(c0 + chunkSize, startT + M);
790
- const Cc = c1 - c0;
791
- const startArr = MlxArray.fromInt32(new Int32Array([c0]), [1]);
792
- const hChunk = ops.sliceDynamic(h, startArr, [1], [1, Cc, hidden]); // [1, Cc, hidden]
793
- startArr.dispose();
794
- sink.push(hChunk);
795
- const tHost = new Int32Array(Cc);
796
- for (let i = 0; i < Cc; i++) tHost[i] = ids[c0 + 1 + i]!;
797
- const ckpt = new Checkpoint((inp) => {
798
- const hc = inp[0]!;
799
- const logits = logitsFromHiddenPadM(model, hc); // [1, Cc, V] — recomputed in backward (Cc in {2,3} padded — mlx qmm bug)
800
- const V = logits.shape[2]!;
801
- const l2 = ops.reshape(logits, [Cc, V]);
802
- const targets = MlxArray.fromInt32(tHost, [Cc, 1]);
803
- const lse = ops.logsumexpAxis(l2, -1, false);
804
- const gathered = ops.takeAlongAxis(l2, targets, -1);
805
- const picked = ops.reshape(gathered, [Cc]);
806
- const logp = ops.sub(picked, lse);
807
- const logpF = logp.dtype === Dtype.float32 ? logp : logp.astype(Dtype.float32);
808
- const s = ops.sumAxis(logpF, 0, false);
809
- for (const a of [logits, l2, targets, lse, gathered, picked, logp]) a.dispose();
810
- if (logpF !== logp) logpF.dispose();
811
- return [s];
812
- });
813
- sink.push(ckpt);
814
- const [s] = ckpt.apply([hChunk]);
815
- if (total === null) total = s!;
816
- else { const sum = ops.add(total, s!); total.dispose(); s!.dispose(); total = sum; }
817
- }
818
- const meanScalar = ops.mulScalar(total!, 1 / M);
819
- total!.dispose();
820
- const mean = ops.reshape(meanScalar, [1]);
821
- meanScalar.dispose();
822
- return mean;
823
- }
824
-
825
- /** B=1 FUSED linear cross-entropy response-only mean log-prob → [1]. Same result
826
- * as `branchLogpMeanB1` but the LM head is a single `CustomVjp` with an ANALYTIC
827
- * backward — the Liger `FusedLinearCrossEntropy` structure ported to MLX:
828
- * - FORWARD token-chunks the M response rows; per chunk materializes only
829
- * `[Cc, V]` logits (= `h_c @ Wᵀ` via `quantizedMatmul`, + Gemma softcap),
830
- * reduces to `logp = target_logit − logsumexp`, and DROPS the logits. Returns
831
- * Σ logp (the `1/M` mean is applied outside, a cheap linear scale).
832
- * - BACKWARD recomputes `[Cc, V]` per chunk and forms the gradient in CLOSED
833
- * FORM — `∂(Σlogp)/∂logit = onehot_target − softmax` (× `sech²` through the
834
- * softcap), scaled by the incoming cotangent — instead of autograd-ing through
835
- * logsumexp+gather (what the `Checkpoint` token-chunked path does). `dh_c =
836
- * grad_logits_c @ dequant(W)` uses `quantizedMatmul(…, transpose=false)`, which
837
- * IS mlx's own x-vjp of the `transpose=true` head matmul (the path LoRA
838
- * training already differentiates through), so it contracts the vocab axis
839
- * correctly without dequantizing the full `[V, hidden]` head (1.3 GB at 262k).
840
- * Peak large-vocab memory is bounded to `[chunkSize, V]` in BOTH directions, the
841
- * full `[M, V]` logits never materialize, and the head backward needs no autograd
842
- * graph. The `CustomVjp` recomputes in the backward (it must outlive the
843
- * enclosing value_and_grad), so it — and `h` — are pushed into `sink`, disposed
844
- * by the caller after the step's grads are eval'd. The head is NOT a LoRA target,
845
- * so only `dh` flows back (seeding the layer-stack LoRA backward); a head-LoRA
846
- * `∇A/∇B` fold-in is a follow-on. Exact within the established bf16 class. */
847
- export function fusedLogpMeanB1(
848
- model: RuntimeModel,
849
- ids: number[],
850
- mask: number[],
851
- validLen: number,
852
- chunkSize: number,
853
- sink: Array<{ dispose(): void }>,
854
- vocabBlock = 0,
855
- flash = false,
856
- ): MlxArray {
857
- const L = ids.length;
858
- const T = L - 1;
859
- let startT = -1;
860
- let M = 0;
861
- for (let t = 0; t < T; t++) if (mask[t + 1]) { if (startT < 0) startT = t; M++; }
862
- if (M <= 0) return MlxArray.fromFloat32(new Float32Array([0]), [1]);
863
-
864
- const inputHost = new Int32Array(T);
865
- for (let t = 0; t < T; t++) inputHost[t] = ids[t]!;
866
- const inputIds = MlxArray.fromInt32(inputHost, [1, T]);
867
- const inputValid = [Math.max(0, Math.min(validLen, L) - 1)];
868
- const h = trainForwardHidden(model, inputIds, inputValid); // [1, T, hidden]
869
- inputIds.dispose();
870
- sink.push(h); // primal of the CustomVjp (via the slice below) — alive until post-eval
871
- const hidden = h.shape[2]!;
872
-
873
- // Response hiddens [M, hidden] — the CustomVjp primal.
874
- const startArr = MlxArray.fromInt32(new Int32Array([startT]), [1]);
875
- const hRespSlice = ops.sliceDynamic(h, startArr, [1], [1, M, hidden]); // [1, M, hidden]
876
- startArr.dispose();
877
- const hResp = ops.reshape(hRespSlice, [M, hidden]); // [M, hidden]
878
- hRespSlice.dispose();
879
- sink.push(hResp);
880
-
881
- const targetsHost = new Int32Array(M);
882
- for (let i = 0; i < M; i++) targetsHost[i] = ids[startT + 1 + i]!;
883
- return fusedRespLogpMean(model, hResp, targetsHost, chunkSize, sink, vocabBlock, flash);
884
- }
885
-
886
- /** Fused linear-CE log-prob from a PRE-COMPUTED post-finalNorm hidden
887
- * `h [1, T, hidden]` (the segmented-backward head: finalNorm is applied by the
888
- * caller). Mirrors `responseOnlyLogpMean`'s response-span selection but routes
889
- * the head through the fused analytic CustomVjp, so the head term inside the
890
- * segmented backward is bounded to `[chunk, V]` instead of the full `[M, V]`.
891
- * Composes with the segmented `mlx_vjp` (the CustomVjp nests inside it, like the
892
- * fused-GeGLU kernel). The CustomVjp + the response slice are pushed into `sink`,
893
- * disposed by the caller AFTER the head vjp is eval'd. */
894
- export function fusedLogpMeanFromHidden(
895
- model: RuntimeModel,
896
- h: MlxArray, // [1, T, hidden] post-finalNorm
897
- ids: number[],
898
- mask: number[],
899
- chunkSize: number,
900
- sink: Array<{ dispose(): void }>,
901
- vocabBlock = 0,
902
- flash = false,
903
- span?: HeadSpan, // override the mask-derived response span (sft_scope:"full" prompt span)
904
- ): MlxArray {
905
- const L = ids.length;
906
- const T = L - 1;
907
- const { startT, M } = span ?? respSpanFromMask(mask, T);
908
- if (M <= 0) return MlxArray.fromFloat32(new Float32Array([0]), [1]);
909
- const hidden = h.shape[2]!;
910
- const startArr = MlxArray.fromInt32(new Int32Array([startT]), [1]);
911
- const hRespSlice = ops.sliceDynamic(h, startArr, [1], [1, M, hidden]); // [1, M, hidden]
912
- startArr.dispose();
913
- const hResp = ops.reshape(hRespSlice, [M, hidden]); // [M, hidden]
914
- hRespSlice.dispose();
915
- sink.push(hResp);
916
- const targetsHost = new Int32Array(M);
917
- for (let i = 0; i < M; i++) targetsHost[i] = ids[startT + 1 + i]!;
918
- return fusedRespLogpMean(model, hResp, targetsHost, chunkSize, sink, vocabBlock, flash);
919
- }
920
-
921
- /** The fused linear-CE head core: response hidden `hResp [M, hidden]`
922
- * (post-finalNorm) + target ids → length-normalized mean logp [1], via the
923
- * analytic CustomVjp (token-chunked forward, softmax−onehot backward, dh through
924
- * mlx's quantized x-vjp). Shared by `fusedLogpMeanB1` (re-runs the forward) and
925
- * `fusedLogpMeanFromHidden` (segmented head). Pushes the CustomVjp into `sink`;
926
- * the caller owns `hResp` (and is responsible for its sink lifetime). */
927
- /** VOCAB-BLOCKED online-softmax fused head (full Cut Cross Entropy structure):
928
- * even `[chunk, V]` never exists — the vocab axis is tiled into `vocabBlock`-wide
929
- * blocks and the logsumexp is built with the [online-softmax](https://arxiv.org/abs/1805.02867)
930
- * running (max `m`, sumexp `d`) recurrence, so only `[chunk, vocabBlock]` is ever
931
- * live (MLX frees each block's tensor after the running scalars update — the
932
- * blocks are sequentially dependent through `m`/`d`). The target logit is captured
933
- * per block (host-side membership mask, so no full-row gather). The backward
934
- * recomputes per `(token-chunk, vocab-block)`: `softmax_block = exp(logit_block −
935
- * lse)`, `grad = cotangent·(onehot_block − softmax_block)·sech²`, accumulating
936
- * `dh += grad @ dequant(W_block)` (mlx x-vjp, `transpose=false`) into a
937
- * `[chunk, hidden]` running buffer — `lse` is recomputed by one online pass (the
938
- * scalar `[chunk]` lse is cheap; stashing it forward→backward is the FLOP
939
- * optimization left as a follow-on). NOT bit-exact to the whole-vocab head (the
940
- * block-wise reduction re-associates), but within the bf16 class.
941
- * `W_block` = an axis-0 (vocab) row-slice of the quantized `w`/`scales`/`biases`. */
942
- function makeVocabBlockedHeadVjp(
943
- head: HeadQuant, targetsHost: Int32Array, M: number, hidden: number,
944
- tokenChunk: number, vocabBlock: number,
945
- ): CustomVjp {
946
- const cap = head.softcap;
947
- const V = head.scales.shape[0]!;
948
- const wCols = head.w.shape[1]!, scCols = head.scales.shape[1]!, biCols = head.biases?.shape[1] ?? 0;
949
- const sliceHead = (v0: number, v1: number) => ({
950
- w: head.w.slice([v0, 0], [v1, wCols]),
951
- scales: head.scales.slice([v0, 0], [v1, scCols]),
952
- biases: head.biases ? head.biases.slice([v0, 0], [v1, biCols]) : null,
953
- });
954
- // Post-softcap logits [Cc,Vb] for a vocab block, plus sech² (null if no softcap)
955
- // for the backward chain rule. The block weights `wb` are RETURNED (the backward
956
- // reuses them for dh = grad @ dequant(W_block)); caller disposes them.
957
- const blockLogits = (hc: MlxArray, wb: { w: MlxArray; scales: MlxArray; biases: MlxArray | null }):
958
- { lb: MlxArray; sech2: MlxArray | null } => {
959
- const raw = ops.quantizedMatmul(hc, wb.w, wb.scales, wb.biases, head.spec, true); // [Cc, Vb]
960
- if (cap === null) return { lb: raw, sech2: null };
961
- const capArr = ops.scalarLike(cap, raw);
962
- const div = ops.div(raw, capArr);
963
- const th = ops.tanh(div); div.dispose(); // bind the nested div so its wrapper is freed
964
- raw.dispose();
965
- const lb = ops.mul(th, capArr); // cap·tanh(raw/cap)
966
- const th2 = ops.mul(th, th); th.dispose();
967
- const one = ops.scalarLike(1, th2);
968
- const sech2 = ops.sub(one, th2); // 1 − tanh²
969
- one.dispose(); th2.dispose(); capArr.dispose();
970
- return { lb, sech2 };
971
- };
972
- // One online-softmax pass over vocab blocks for a token-chunk hc [Cc,hidden]:
973
- // running (max m, sumexp d) so only [Cc,Vb] is ever live → lse [Cc] + the
974
- // captured target logit [Cc]. Used by fwd and (to recompute lse) by the vjp.
975
- const onlinePass = (hc: MlxArray, c0: number, Cc: number): { lse: MlxArray; tgt: MlxArray } => {
976
- let m: MlxArray | null = null; // [Cc] running max
977
- let d: MlxArray | null = null; // [Cc] running sumexp (relative to m)
978
- let tgt: MlxArray | null = null; // [Cc] accumulated (masked) target logit
979
- for (let v0 = 0; v0 < V; v0 += vocabBlock) {
980
- const v1 = Math.min(v0 + vocabBlock, V);
981
- const wb = sliceHead(v0, v1);
982
- const { lb: lbRaw, sech2 } = blockLogits(hc, wb); // [Cc, Vb]
983
- sech2?.dispose();
984
- wb.w.dispose(); wb.scales.dispose(); wb.biases?.dispose();
985
- // Online (m, d) accumulation in f32 — bf16 across many blocks loses the
986
- // precision that softmax = exp(logit − lse) is exponentially sensitive to
987
- // (matches logsumexpAxis's f32-internal reduction).
988
- const lb = lbRaw.dtype === Dtype.float32 ? lbRaw : lbRaw.astype(Dtype.float32);
989
- if (lb !== lbRaw) lbRaw.dispose();
990
- const bmax = ops.maxAxis(lb, -1, false); // [Cc]
991
- const mNew: MlxArray = m === null ? bmax : ops.maximum(m, bmax);
992
- if (m !== null) bmax.dispose();
993
- const mCol = ops.reshape(mNew, [Cc, 1]);
994
- const shifted = ops.sub(lb, mCol); mCol.dispose();
995
- const ex = ops.exp(shifted); shifted.dispose(); // [Cc, Vb]
996
- const blockSum = ops.sumAxis(ex, -1, false); ex.dispose(); // [Cc]
997
- let dNew: MlxArray;
998
- if (d === null) dNew = blockSum;
999
- else {
1000
- const md = ops.sub(m!, mNew); // prevM − mNew [Cc]
1001
- const sc = ops.exp(md); md.dispose();
1002
- const rescaled = ops.mul(d, sc); sc.dispose();
1003
- dNew = ops.add(rescaled, blockSum);
1004
- rescaled.dispose(); blockSum.dispose();
1005
- }
1006
- // Target logit capture: host membership mask (each target lands in one
1007
- // block), gather the clamped local index, accumulate masked across blocks.
1008
- const locHost = new Int32Array(Cc), maskHost = new Float32Array(Cc);
1009
- for (let t = 0; t < Cc; t++) {
1010
- const tg = targetsHost[c0 + t]!;
1011
- const inb = tg >= v0 && tg < v1;
1012
- locHost[t] = inb ? tg - v0 : 0;
1013
- maskHost[t] = inb ? 1 : 0;
1014
- }
1015
- const locArr = MlxArray.fromInt32(locHost, [Cc, 1]);
1016
- const gathered = ops.reshape(ops.takeAlongAxis(lb, locArr, -1), [Cc]); // [Cc]
1017
- locArr.dispose();
1018
- const maskA = MlxArray.fromFloat32(maskHost, [Cc]);
1019
- const maskM = maskA.dtype === gathered.dtype ? maskA : maskA.astype(gathered.dtype);
1020
- const contrib = ops.mul(gathered, maskM); gathered.dispose();
1021
- if (maskM !== maskA) maskA.dispose();
1022
- maskM.dispose();
1023
- const tgtNew: MlxArray = tgt === null ? contrib : ops.add(tgt, contrib);
1024
- if (tgt !== null) { tgt.dispose(); contrib.dispose(); }
1025
- lb.dispose();
1026
- if (m !== null) m.dispose();
1027
- if (d !== null) d.dispose();
1028
- m = mNew; d = dNew; tgt = tgtNew;
1029
- }
1030
- const logd = ops.log(d!);
1031
- const lse = ops.add(m!, logd);
1032
- logd.dispose(); m!.dispose(); d!.dispose();
1033
- return { lse, tgt: tgt! };
1034
- };
1035
-
1036
- return new CustomVjp(
1037
- // FORWARD: Σ over the response of (target_logit − lse), token-chunked, with
1038
- // the lse built vocab-block-by-block (online). Only [Cc,Vb] ever materializes.
1039
- (inputs) => {
1040
- const hR = inputs[0]!; // [M, hidden]
1041
- let sum: MlxArray | null = null;
1042
- for (let c0 = 0; c0 < M; c0 += tokenChunk) {
1043
- const c1 = Math.min(c0 + tokenChunk, M);
1044
- const Cc = c1 - c0;
1045
- const hc = hR.slice([c0, 0], [c1, hidden]);
1046
- const { lse, tgt } = onlinePass(hc, c0, Cc);
1047
- hc.dispose();
1048
- const logp = ops.sub(tgt, lse); // [Cc]
1049
- tgt.dispose(); lse.dispose();
1050
- const logpF = logp.dtype === Dtype.float32 ? logp : logp.astype(Dtype.float32);
1051
- const s = ops.sumAxis(logpF, 0, false);
1052
- logp.dispose(); if (logpF !== logp) logpF.dispose();
1053
- if (sum === null) sum = s;
1054
- else { const n = ops.add(sum, s); sum.dispose(); s.dispose(); sum = n; }
1055
- }
1056
- const out = ops.reshape(sum!, [1]);
1057
- sum!.dispose();
1058
- return [out];
1059
- },
1060
- // BACKWARD: per token-chunk, recompute lse (online), then per vocab block form
1061
- // softmax_block = exp(logit_block − lse), grad = cot·(onehot − softmax)·sech²,
1062
- // accumulate dh += grad @ dequant(W_block). Peak stays [Cc,Vb].
1063
- (primals, cots) => {
1064
- const hR = primals[0]!; // [M, hidden]
1065
- const c = cots[0]!; // [1] scalar = ∂loss/∂(Σlogp)
1066
- const dtype = hR.dtype;
1067
- const cMatch = c.dtype === dtype ? c : c.astype(dtype);
1068
- const chunks: MlxArray[] = [];
1069
- for (let c0 = 0; c0 < M; c0 += tokenChunk) {
1070
- const c1 = Math.min(c0 + tokenChunk, M);
1071
- const Cc = c1 - c0;
1072
- const hc = hR.slice([c0, 0], [c1, hidden]);
1073
- const { lse, tgt } = onlinePass(hc, c0, Cc);
1074
- tgt.dispose();
1075
- const lseCol = ops.reshape(lse, [Cc, 1]); lse.dispose();
1076
- let dh: MlxArray | null = null; // [Cc, hidden]
1077
- for (let v0 = 0; v0 < V; v0 += vocabBlock) {
1078
- const v1 = Math.min(v0 + vocabBlock, V);
1079
- const Vb = v1 - v0;
1080
- const wb = sliceHead(v0, v1);
1081
- const { lb, sech2 } = blockLogits(hc, wb); // [Cc, Vb]
1082
- const softmax = ops.exp(ops.sub(lb, lseCol)); // [Cc, Vb]
1083
- lb.dispose();
1084
- // onehot_block: global col == target.
1085
- const colFlat = ops.arange(v0, v1, 1, Dtype.int32);
1086
- const col = ops.reshape(colFlat, [1, Vb]); colFlat.dispose();
1087
- const tcol = MlxArray.fromInt32(targetsHost.slice(c0, c1), [Cc, 1]);
1088
- const ge1 = ops.greaterEqual(col, tcol);
1089
- const ge2 = ops.greaterEqual(tcol, col);
1090
- const eqMask = ops.logicalAnd(ge1, ge2); // bind the nested compares so their wrappers free
1091
- ge1.dispose(); ge2.dispose();
1092
- col.dispose(); tcol.dispose();
1093
- const onehot = eqMask.astype(softmax.dtype); eqMask.dispose();
1094
- let g = ops.sub(onehot, softmax); // onehot − softmax [Cc, Vb]
1095
- onehot.dispose(); softmax.dispose();
1096
- if (sech2) { const gg = ops.mul(g, sech2); g.dispose(); sech2.dispose(); g = gg; }
1097
- const gScaled = ops.mul(g, cMatch); g.dispose();
1098
- const gT = gScaled.dtype === dtype ? gScaled : gScaled.astype(dtype);
1099
- if (gT !== gScaled) gScaled.dispose();
1100
- const dhcBf = ops.quantizedMatmul(gT, wb.w, wb.scales, wb.biases, head.spec, false); // [Cc, hidden]
1101
- gT.dispose();
1102
- wb.w.dispose(); wb.scales.dispose(); wb.biases?.dispose();
1103
- // Accumulate dh in f32: the whole-vocab qmm reduces V in f32 internally,
1104
- // but summing hundreds of bf16 block-dh's re-introduces an error that
1105
- // scales with block count (the dominant grad bug at small vocabBlock).
1106
- const dhc = dhcBf.dtype === Dtype.float32 ? dhcBf : dhcBf.astype(Dtype.float32);
1107
- if (dhc !== dhcBf) dhcBf.dispose();
1108
- if (dh === null) dh = dhc;
1109
- else { const n = ops.add(dh, dhc); dh.dispose(); dhc.dispose(); dh = n; }
1110
- }
1111
- lseCol.dispose(); hc.dispose();
1112
- const dhFinal = dh!.dtype === dtype ? dh! : dh!.astype(dtype);
1113
- if (dhFinal !== dh!) dh!.dispose();
1114
- chunks.push(dhFinal);
1115
- }
1116
- if (cMatch !== c) cMatch.dispose();
1117
- const dh = chunks.length === 1 ? chunks[0]! : ops.concatAxis(chunks, 0); // [M, hidden]
1118
- if (chunks.length > 1) for (const a of chunks) a.dispose();
1119
- return [dh];
1120
- },
1121
- );
1122
- }
1123
-
1124
- /** flash-CCE head as a CustomVjp: forward runs the Metal kernel → Σ logp (stashing
1125
- * `lse` for the backward, avoiding a forward recompute); backward runs the kernel's
1126
- * `dh` for unit cotangent then scales by the scalar cotangent `c` (loss = Σ logp →
1127
- * ∂loss/∂logp_t = c for all t). The coeff filter + vocab-block skip default ON at
1128
- * 1e-5 (2026-07-02 flip; real-data gates in flash-cce.ts) — set
1129
- * MLX_BUN_CCE_BWD_{FILTER,BLOCK}_EPS=0 for the EXACT dh. */
1130
- function makeFlashCceHeadVjp(
1131
- head: HeadQuant, targetsHost: Int32Array, M: number,
1132
- sink: Array<{ dispose(): void }>,
1133
- ): CustomVjp {
1134
- if (!head.biases) throw new Error("flash-CCE head requires affine (biased) quantization");
1135
- const fch: FlashCceHead = {
1136
- w: head.w, scales: head.scales, biases: head.biases,
1137
- bits: head.spec.bits, groupSize: head.spec.groupSize, softcap: head.softcap,
1138
- };
1139
- const targets = Array.from(targetsHost);
1140
- let savedLse: MlxArray | null = null;
1141
- let savedBlockMax: MlxArray | null = null;
1142
- return new CustomVjp(
1143
- (inputs) => {
1144
- const hR = inputs[0]!; // [M, hidden]
1145
- const { logp, lse, blockMax } = flashCceForward(hR, fch, targets); // logp[M], lse[M], blockMax[M,NBLK]
1146
- savedLse = lse; sink.push(lse); // stash lse + blockMax → backward (no recompute;
1147
- savedBlockMax = blockMax; sink.push(blockMax); // blockMax drives the vocab-block skip)
1148
- const logpF = logp.dtype === Dtype.float32 ? logp : logp.astype(Dtype.float32);
1149
- const s = ops.sumAxis(logpF, 0, false); // Σ logp (scalar)
1150
- if (logpF !== logp) logpF.dispose();
1151
- logp.dispose();
1152
- const out = ops.reshape(s, [1]); s.dispose();
1153
- return [out];
1154
- },
1155
- (primals, cots) => {
1156
- const hR = primals[0]!; // [M, hidden]
1157
- const c = cots[0]!; // [1] = ∂loss/∂(Σ logp); uniform over tokens
1158
- // dh for unit cotangent, then × c (avoids a host read of the cotangent).
1159
- const ones = new Array(M).fill(1) as number[];
1160
- // filter + block-skip default ON at 1e-5 (env-overridable; =0 → exact dh).
1161
- // savedBlockMax feeds the ~lossless vocab-block skip.
1162
- const dhUnit = flashCceBackward(hR, fch, targets, savedLse!, ones, undefined, savedBlockMax!); // [M, hidden] f32
1163
- const cM = c.dtype === dhUnit.dtype ? c : c.astype(dhUnit.dtype);
1164
- const dh = ops.mul(dhUnit, cM); // broadcast [M,hidden]·[1]
1165
- if (cM !== c) cM.dispose();
1166
- dhUnit.dispose();
1167
- const dhO = dh.dtype === hR.dtype ? dh : dh.astype(hR.dtype);
1168
- if (dhO !== dh) dh.dispose();
1169
- return [dhO];
1170
- },
1171
- );
1172
- }
1173
-
1174
- export function fusedRespLogpMean(
1175
- model: RuntimeModel,
1176
- hResp: MlxArray, // [M, hidden]
1177
- targetsHost: Int32Array,
1178
- chunkSize: number,
1179
- sink: Array<{ dispose(): void }>,
1180
- vocabBlock = 0,
1181
- flash = false,
1182
- ): MlxArray {
1183
- const M = hResp.shape[0]!;
1184
- const hidden = hResp.shape[1]!;
1185
- const head = headQuant(model);
1186
- const cap = head.softcap;
1187
- const chunk = Math.max(1, chunkSize);
1188
- const V = head.scales.shape[0]!;
1189
- // Vocab-blocked (full CCE) path: even [chunk,V] never materializes — only
1190
- // [chunk,vocabBlock]. Otherwise the whole-vocab analytic head below.
1191
- const vb = vocabBlock > 0 && vocabBlock < V ? vocabBlock : 0;
1192
- // Auto-dispatch by M (kernel backlog #3; measured 2026-07-02 on the M1 Max,
1193
- // head-dispatch-sweep.ts (deleted 2026-08-23; git history)): the fused QM head is FASTER
1194
- // at short M (e4b M=64 1.6×, M=512 1.13×; CPM5 faster at EVERY M) — the
1195
- // flash head's win is residency, plus time at long M on e4b (M≥2048 after
1196
- // the 2026-07-02 filter/blockMax flip; 8K: 10.7 vs 13.2 s). So a
1197
- // requested-flash batch with M below MLX_BUN_FLASH_MIN_M (default 1024, the
1198
- // e4b time-tie point where flash's memory win takes over) takes the EXACT
1199
- // fused head instead — short rows get the faster exact head for free.
1200
- // MLX_BUN_FLASH_MIN_M=0 always honors flash (the pre-dispatch behavior).
1201
- const flashMinM = Number(runtimeValue("MLX_BUN_FLASH_MIN_M") ?? "1024");
1202
- const useFlash = flash && (flashMinM <= 0 || M >= flashMinM);
1203
- let op: CustomVjp;
1204
- if (useFlash) {
1205
- op = makeFlashCceHeadVjp(head, targetsHost, M, sink);
1206
- } else if (vb > 0) {
1207
- op = makeVocabBlockedHeadVjp(head, targetsHost, M, hidden, chunk, vb);
1208
- } else {
1209
-
1210
- // logits for one token-chunk [Cc, V] (+ softcap). Shared by fwd and vjp.
1211
- const chunkLogits = (hc: MlxArray): MlxArray => {
1212
- const raw = ops.quantizedMatmul(hc, head.w, head.scales, head.biases, head.spec, true); // [Cc, V]
1213
- if (cap === null) return raw;
1214
- const capped = logitSoftcap(raw, cap); // EXACT match to logitsFromHidden (div by a bf16 cap array)
1215
- raw.dispose();
1216
- return capped;
1217
- };
1218
-
1219
- op = new CustomVjp(
1220
- // FORWARD: Σ over the response of (target_logit − logsumexp), token-chunked.
1221
- (inputs) => {
1222
- const hR = inputs[0]!; // [M, hidden]
1223
- let sum: MlxArray | null = null;
1224
- for (let c0 = 0; c0 < M; c0 += chunk) {
1225
- const c1 = Math.min(c0 + chunk, M);
1226
- const Cc = c1 - c0;
1227
- const hc = hR.slice([c0, 0], [c1, hidden]); // [Cc, hidden]
1228
- const logits = chunkLogits(hc); // [Cc, V]
1229
- hc.dispose();
1230
- const lse = ops.logsumexpAxis(logits, -1, false); // [Cc]
1231
- const tgt = MlxArray.fromInt32(targetsHost.slice(c0, c1), [Cc, 1]);
1232
- const gathered = ops.takeAlongAxis(logits, tgt, -1); // [Cc, 1]
1233
- const picked = ops.reshape(gathered, [Cc]);
1234
- const logp = ops.sub(picked, lse); // [Cc]
1235
- const logpF = logp.dtype === Dtype.float32 ? logp : logp.astype(Dtype.float32);
1236
- const s = ops.sumAxis(logpF, 0, false); // scalar
1237
- for (const a of [logits, lse, tgt, gathered, picked, logp]) a.dispose();
1238
- if (logpF !== logp) logpF.dispose();
1239
- if (sum === null) sum = s;
1240
- else { const n = ops.add(sum, s); sum.dispose(); s.dispose(); sum = n; }
1241
- }
1242
- const out = ops.reshape(sum!, [1]);
1243
- sum!.dispose();
1244
- return [out];
1245
- },
1246
- // BACKWARD: dh = (cotangent · (onehot − softmax) · sech²) @ dequant(W).
1247
- (primals, cots) => {
1248
- const hR = primals[0]!; // [M, hidden]
1249
- const c = cots[0]!; // [1] scalar = ∂loss/∂(Σlogp)
1250
- const dtype = hR.dtype;
1251
- const cMatch = c.dtype === dtype ? c : c.astype(dtype);
1252
- const chunks: MlxArray[] = [];
1253
- for (let c0 = 0; c0 < M; c0 += chunk) {
1254
- const c1 = Math.min(c0 + chunk, M);
1255
- const Cc = c1 - c0;
1256
- const hc = hR.slice([c0, 0], [c1, hidden]); // [Cc, hidden]
1257
- // recompute logits, keeping sech² through the softcap if present
1258
- const raw = ops.quantizedMatmul(hc, head.w, head.scales, head.biases, head.spec, true); // [Cc, V]
1259
- hc.dispose();
1260
- let logits = raw;
1261
- let sech2: MlxArray | null = null;
1262
- if (cap !== null) {
1263
- // Replicate logitSoftcap internals (div by a bf16 cap array) so the
1264
- // recomputed logits match the forward exactly, and keep tanh for sech².
1265
- const capArr = ops.scalarLike(cap, raw);
1266
- const scaled = ops.div(raw, capArr);
1267
- raw.dispose();
1268
- const th = ops.tanh(scaled);
1269
- scaled.dispose();
1270
- logits = ops.mul(th, capArr); // cap·tanh(raw/cap), bit-exact to logitSoftcap
1271
- const th2 = ops.mul(th, th);
1272
- th.dispose();
1273
- const one = ops.scalarLike(1, th2);
1274
- sech2 = ops.sub(one, th2); // 1 − tanh² = sech²
1275
- one.dispose(); th2.dispose(); capArr.dispose();
1276
- }
1277
- const lse = ops.logsumexpAxis(logits, -1, false); // [Cc]
1278
- const lseCol = ops.reshape(lse, [Cc, 1]);
1279
- const softmax = ops.exp(ops.sub(logits, lseCol)); // [Cc, V]
1280
- logits.dispose(); lse.dispose(); lseCol.dispose();
1281
- // onehot via equality (no scatter op): col == target.
1282
- const colFlat = ops.arange(0, softmax.shape[1]!, 1, Dtype.int32);
1283
- const col = ops.reshape(colFlat, [1, softmax.shape[1]!]);
1284
- colFlat.dispose();
1285
- const tcol = MlxArray.fromInt32(targetsHost.slice(c0, c1), [Cc, 1]);
1286
- const ge = ops.greaterEqual(col, tcol);
1287
- const le = ops.greaterEqual(tcol, col);
1288
- const eqMask = ops.logicalAnd(ge, le); // [Cc, V] bool, true at target
1289
- for (const a of [col, tcol, ge, le]) a.dispose();
1290
- const onehot = eqMask.astype(softmax.dtype);
1291
- eqMask.dispose();
1292
- let g = ops.sub(onehot, softmax); // onehot − softmax [Cc, V]
1293
- onehot.dispose(); softmax.dispose();
1294
- if (sech2) { const gg = ops.mul(g, sech2); g.dispose(); sech2.dispose(); g = gg; }
1295
- const gScaled = ops.mul(g, cMatch); // × cotangent (broadcast [Cc,V]·[1])
1296
- g.dispose();
1297
- const gT = gScaled.dtype === dtype ? gScaled : gScaled.astype(dtype);
1298
- if (gT !== gScaled) gScaled.dispose();
1299
- // dh_c = g @ dequant(W) — mlx's x-vjp of the transpose=true head matmul.
1300
- const dhc = ops.quantizedMatmul(gT, head.w, head.scales, head.biases, head.spec, false); // [Cc, hidden]
1301
- gT.dispose();
1302
- chunks.push(dhc);
1303
- }
1304
- if (cMatch !== c) cMatch.dispose();
1305
- const dh = chunks.length === 1 ? chunks[0]! : ops.concatAxis(chunks, 0); // [M, hidden]
1306
- if (chunks.length > 1) for (const a of chunks) a.dispose();
1307
- return [dh];
1308
- },
1309
- );
1310
- } // end whole-vocab branch
1311
- sink.push(op);
1312
-
1313
- const [sumLogp] = op.apply([hResp]);
1314
- const mean = ops.mulScalar(sumLogp!, 1 / M); // length-normalized
1315
- sumLogp!.dispose();
1316
- const out = ops.reshape(mean, [1]);
1317
- mean.dispose();
1318
- return out;
1319
- }
1320
-
1321
- /** Numerically-guarded log(1 - exp(x)) for x < 0 (x = a mean log-prob, so
1322
- * exp(x) ∈ (0,1) and comfortably below 1 — average per-token logp is well
1323
- * negative). Clamp `1 - exp(x)` to >= eps so the log never hits -inf in the
1324
- * pathological near-zero case. Caller owns the result. */
1325
- export function log1mexp(x: MlxArray): MlxArray {
1326
- const ex = ops.exp(x);
1327
- const one = ops.scalarLike(1, x);
1328
- const oneMinus = ops.sub(one, ex);
1329
- const eps = ops.scalarLike(1e-7, x);
1330
- const clamped = ops.maximum(oneMinus, eps);
1331
- const out = ops.log(clamped);
1332
- for (const a of [ex, one, oneMinus, eps, clamped]) a.dispose();
1333
- return out;
1334
- }
1335
-
1336
- /** Per-row ORPO log-odds from the two length-normalized log-probs ([B] each):
1337
- * `log_odds = (ℓw - ℓr) - (log1mexp(ℓw) - log1mexp(ℓr))`. Caller owns lw/lr
1338
- * (not disposed here) and the [B] result. */
1339
- function orpoLogOdds(lw: MlxArray, lr: MlxArray): MlxArray {
1340
- const diff = ops.sub(lw, lr);
1341
- const lmw = log1mexp(lw);
1342
- const lmr = log1mexp(lr);
1343
- const lmDiff = ops.sub(lmw, lmr);
1344
- const logOdds = ops.sub(diff, lmDiff); // [B]
1345
- for (const a of [diff, lmw, lmr, lmDiff]) a.dispose();
1346
- return logOdds;
1347
- }
1348
-
1349
- /** The chosen branch under sft_scope:"full" (B=1): ONE forward yields both the
1350
- * response-only mean logp ℓw (fed to the odds ratio — computed exactly as the
1351
- * "response" path computes it, same span/head) AND the full-scope chosen NLL
1352
- * (the head additionally runs over the PROMPT span [0, startT), combined via
1353
- * combineFullNll). No third forward: the hidden states already cover the
1354
- * prompt. Routes both spans through the same head tier the response path uses
1355
- * (flash/fused CustomVjp, Checkpoint token-chunks, or the whole-vocab autograd
1356
- * head), so the memory bounds hold for the extended span too. Caller owns both
1357
- * returned arrays ([1] each). */
1358
- function chosenLogpMeanAndFullNllB1(
1359
- model: RuntimeModel,
1360
- ids: number[],
1361
- mask: number[],
1362
- validLen: number,
1363
- chunk?: ChunkCtx,
1364
- ): { lw: MlxArray; nllFull: MlxArray } {
1365
- const L = ids.length;
1366
- const T = L - 1;
1367
- const { startT, M } = respSpanFromMask(mask, T);
1368
- if (M <= 0)
1369
- return {
1370
- lw: MlxArray.fromFloat32(new Float32Array([0]), [1]),
1371
- nllFull: MlxArray.fromFloat32(new Float32Array([0]), [1]),
1372
- };
1373
-
1374
- const inputHost = new Int32Array(T);
1375
- for (let t = 0; t < T; t++) inputHost[t] = ids[t]!;
1376
- const inputIds = MlxArray.fromInt32(inputHost, [1, T]);
1377
- const inputValid = [Math.max(0, Math.min(validLen, L) - 1)];
1378
- const h = trainForwardHidden(model, inputIds, inputValid); // [1, T, hidden]
1379
- inputIds.dispose();
1380
-
1381
- const useFused = !!chunk && chunk.chunkSize > 0 && !!(chunk.fused || chunk.flash);
1382
- const useChunked = !!chunk && chunk.chunkSize > 0 && !useFused;
1383
- let lw: MlxArray;
1384
- let pm: MlxArray | null = null; // prompt-span mean logp (positions [0, startT))
1385
- if (useFused) {
1386
- chunk!.sink.push(h); // CustomVjp/flash primal source — alive until post-eval
1387
- lw = fusedLogpMeanFromHidden(model, h, ids, mask, chunk!.chunkSize, chunk!.sink, 0, chunk!.flash ?? false, { startT, M });
1388
- if (startT > 0)
1389
- pm = fusedLogpMeanFromHidden(model, h, ids, mask, chunk!.chunkSize, chunk!.sink, 0, chunk!.flash ?? false, { startT: 0, M: startT });
1390
- } else if (useChunked) {
1391
- chunk!.sink.push(h); // Checkpoint input — alive until post-eval
1392
- lw = chunkedLogpMeanFromHidden(model, h, ids, mask, chunk!.chunkSize, chunk!.sink, { startT, M });
1393
- if (startT > 0)
1394
- pm = chunkedLogpMeanFromHidden(model, h, ids, mask, chunk!.chunkSize, chunk!.sink, { startT: 0, M: startT });
1395
- } else {
1396
- lw = spanLogpMeanFromHidden(model, h, ids, startT, M);
1397
- if (startT > 0) pm = spanLogpMeanFromHidden(model, h, ids, 0, startT);
1398
- h.dispose(); // whole-vocab path builds a normal autograd graph — the loss retains it
1399
- }
1400
- const nllFull = combineFullNll(pm, lw, startT, M);
1401
- pm?.dispose();
1402
- return { lw, nllFull };
1403
- }
1404
-
1405
- /** The chosen branch under sft_scope:"full" for B>1 (the batched full-logits
1406
- * path): one forward, one label-logp pass, then TWO masked reductions —
1407
- * per-row response means for ℓw [B] (identical positions/count to the
1408
- * "response" path) and the flattened token-mean full NLL over all non-pad
1409
- * supervised positions across the batch (nn.CrossEntropyLoss
1410
- * reduction="mean"). Caller owns both returned arrays. */
1411
- function chosenLogpMeanAndFullNllBatch(
1412
- model: RuntimeModel,
1413
- ids: number[][],
1414
- mask: number[][],
1415
- validLen: number[],
1416
- ): { lw: MlxArray; nllFull: MlxArray } {
1417
- const B = ids.length;
1418
- const L = ids[0]!.length;
1419
- const T = L - 1;
1420
- const inputHost = new Int32Array(B * T);
1421
- for (let b = 0; b < B; b++)
1422
- for (let t = 0; t < T; t++) inputHost[b * T + t] = ids[b]![t]!;
1423
- const inputIds = MlxArray.fromInt32(inputHost, [B, T]);
1424
- const inputValid = validLen.map((v) => Math.max(0, Math.min(v, L) - 1));
1425
- const logits = trainForward(model, inputIds, inputValid); // [B, T, V]
1426
- inputIds.dispose();
1427
- const V = logits.shape[2]!;
1428
- const N = B * T;
1429
-
1430
- const targetsHost = new Int32Array(N);
1431
- const respMaskHost = new Float32Array(N);
1432
- const fullMaskHost = new Float32Array(N);
1433
- const respRecip = new Float32Array(B);
1434
- let fullCount = 0;
1435
- for (let b = 0; b < B; b++) {
1436
- const len = Math.min(validLen[b]!, L);
1437
- let c = 0;
1438
- for (let t = 0; t < T; t++) {
1439
- const flat = b * T + t;
1440
- targetsHost[flat] = ids[b]![t + 1]!;
1441
- respMaskHost[flat] = mask[b]![t + 1]!; // resp_mask[:, 1:]
1442
- c += mask[b]![t + 1]!;
1443
- // full scope: every non-pad supervised position (prompt + response),
1444
- // i.e. the predicted token ids[t+1] is a real token (t+1 < len).
1445
- if (t + 1 < len) { fullMaskHost[flat] = 1; fullCount++; }
1446
- }
1447
- respRecip[b] = 1 / Math.max(c, 1);
1448
- }
1449
-
1450
- const logits2d = ops.reshape(logits, [N, V]);
1451
- logits.dispose();
1452
- const targets = MlxArray.fromInt32(targetsHost, [N, 1]);
1453
- const lse = ops.logsumexpAxis(logits2d, -1, false); // [N]
1454
- const gathered = ops.takeAlongAxis(logits2d, targets, -1); // [N, 1]
1455
- const picked = ops.reshape(gathered, [N]); // [N]
1456
- const logp = ops.sub(picked, lse); // [N] log_softmax at the label
1457
- const logpF = logp.dtype === Dtype.float32 ? logp : logp.astype(Dtype.float32);
1458
-
1459
- // ℓw: per-row response length-normalized mean (same as branchLogpMean B>1).
1460
- const rm = MlxArray.fromFloat32(respMaskHost, [N]);
1461
- const respMasked = ops.mul(logpF, rm); // [N]
1462
- const resp2d = ops.reshape(respMasked, [B, T]);
1463
- const respSum = ops.sumAxis(resp2d, 1, false); // [B]
1464
- const recipArr = MlxArray.fromFloat32(respRecip, [B]);
1465
- const lw = ops.mul(respSum, recipArr); // [B]
1466
-
1467
- // Full NLL: token-mean over ALL non-pad supervised positions, batch-flattened.
1468
- const fm = MlxArray.fromFloat32(fullMaskHost, [N]);
1469
- const fullMasked = ops.mul(logpF, fm); // [N]
1470
- const fullSum = ops.sumAxis(fullMasked, 0, false); // scalar Σ logp
1471
- const fullMean = ops.mulScalar(fullSum, 1 / Math.max(fullCount, 1));
1472
- const nllFull = ops.neg(fullMean); // scalar
1473
-
1474
- for (const a of [logits2d, targets, lse, gathered, picked, logp, rm, respMasked, resp2d, respSum, recipArr, fm, fullMasked, fullSum, fullMean]) a.dispose();
1475
- if (logpF !== logp) logpF.dispose();
1476
- return { lw, nllFull };
1477
- }
1478
-
1479
- /** The pure ORPO reduction from the two length-normalized log-probs ([B] each):
1480
- * `L_NLL + λ·L_OR` with `L_OR = mean(softplus(-log_odds))`. The SFT term:
1481
- * when `nllFull` is ABSENT (sft_scope:"response"), `L_NLL = mean(-ℓw)` —
1482
- * bit-exact to the pre-sft_scope behavior; when PRESENT (sft_scope:"full"),
1483
- * `L_NLL = meanAll(nllFull)`, the separately-computed full-scope chosen NLL
1484
- * ([1]/[B]/scalar all reduce correctly). The odds-ratio term uses the
1485
- * response-only ℓw/ℓr in BOTH modes (matching TRL). Model-free and
1486
- * graph-preserving — the forwards live in `orpoLoss`, the math (and its grad
1487
- * path) lives here, so the loss can be unit-tested against a hand reference
1488
- * with synthetic lw/lr. Does NOT dispose lw/lr/nllFull (the caller owns them);
1489
- * returns the scalar loss. */
1490
- export function orpoLossFromLogps(lw: MlxArray, lr: MlxArray, lambda: number, nllFull?: MlxArray): MlxArray {
1491
- const negLw = nllFull ? null : ops.neg(lw);
1492
- const nll = ops.meanAll(nllFull ?? negLw!, false); // scalar L_NLL
1493
- const logOdds = orpoLogOdds(lw, lr); // [B]
1494
- const negLO = ops.neg(logOdds);
1495
- const orPer = ops.softplus(negLO); // [B] -log σ(log_odds)
1496
- const orLoss = ops.meanAll(orPer, false); // scalar L_OR
1497
- const lamOr = ops.mulScalar(orLoss, lambda);
1498
- const loss = ops.add(nll, lamOr);
1499
- for (const a of [nll, logOdds, negLO, orPer, orLoss, lamOr]) a.dispose();
1500
- negLw?.dispose();
1501
- return loss;
1502
- }
1503
-
1504
- /** ORPO loss for one preference batch (B>=1). Runs the chosen + rejected policy
1505
- * forwards (adapter active), reduces via {@link orpoLossFromLogps}. No
1506
- * reference forward. `sftScope` selects the L_NLL scope: "response" (default —
1507
- * bit-exact to the pre-sft_scope code path) or "full" (paper/TRL-faithful:
1508
- * the chosen NLL covers prompt+response, computed from the SAME chosen forward
1509
- * — no third forward; the rejected branch is untouched). Returns a scalar
1510
- * MlxArray (caller owns). */
1511
- export function orpoLoss(
1512
- model: RuntimeModel,
1513
- batch: DpoBatch,
1514
- lambda: number,
1515
- chunk?: ChunkCtx,
1516
- sftScope: SftScope = "response",
1517
- ): MlxArray {
1518
- const cLen = chosenLengths(batch);
1519
- const rLen = rejectedLengths(batch);
1520
- let lw: MlxArray;
1521
- let nllFull: MlxArray | null = null;
1522
- if (sftScope === "full") {
1523
- const r = batch.chosenIds.length === 1
1524
- ? chosenLogpMeanAndFullNllB1(model, batch.chosenIds[0]!, batch.chosenMask[0]!, cLen[0]!, chunk)
1525
- : chosenLogpMeanAndFullNllBatch(model, batch.chosenIds, batch.chosenMask, cLen);
1526
- lw = r.lw;
1527
- nllFull = r.nllFull;
1528
- } else {
1529
- lw = branchLogpMean(model, batch.chosenIds, batch.chosenMask, cLen, chunk); // [B]
1530
- }
1531
- const lr = branchLogpMean(model, batch.rejectedIds, batch.rejectedMask, rLen, chunk); // [B]
1532
- const loss = orpoLossFromLogps(lw, lr, lambda, nllFull ?? undefined);
1533
- // Safe to drop the lw/lr/nllFull wrappers now — the loss graph retains the
1534
- // underlying nodes (same pattern as dpoLoss disposing piC/piR before returning).
1535
- lw.dispose();
1536
- lr.dispose();
1537
- nllFull?.dispose();
1538
- return loss;
1539
- }
1540
-
1541
- /** ORPO diagnostics (no grad): the total loss and its `nll`/`or` split,
1542
- * preference accuracy (mean of ℓw > ℓr), and reward margin (mean log_odds).
1543
- * Runs its own two forwards. `sftScope` selects the reported nll's scope so
1544
- * the diagnostic loss matches the training objective ("response" default). */
1545
- export function orpoMetrics(
1546
- model: RuntimeModel,
1547
- batch: DpoBatch,
1548
- lambda: number,
1549
- sftScope: SftScope = "response",
1550
- ): { loss: number; nll: number; or: number; accuracy: number; margin: number } {
1551
- const cLen = chosenLengths(batch);
1552
- const rLen = rejectedLengths(batch);
1553
- let lw: MlxArray;
1554
- let nllFull: MlxArray | null = null;
1555
- if (sftScope === "full") {
1556
- const r = batch.chosenIds.length === 1
1557
- ? chosenLogpMeanAndFullNllB1(model, batch.chosenIds[0]!, batch.chosenMask[0]!, cLen[0]!)
1558
- : chosenLogpMeanAndFullNllBatch(model, batch.chosenIds, batch.chosenMask, cLen);
1559
- lw = r.lw;
1560
- nllFull = r.nllFull;
1561
- } else {
1562
- lw = branchLogpMean(model, batch.chosenIds, batch.chosenMask, cLen); // [B]
1563
- }
1564
- const lr = branchLogpMean(model, batch.rejectedIds, batch.rejectedMask, rLen); // [B]
1565
-
1566
- const negLw = ops.neg(lw); // (diagnostic -ℓw; the nll split uses nllFull under "full")
1567
- const nllArr = nllFull ? ops.meanAll(nllFull, false) : ops.meanAll(negLw, false);
1568
- const logOdds = orpoLogOdds(lw, lr); // [B]
1569
- const negLO = ops.neg(logOdds);
1570
- const orPer = ops.softplus(negLO);
1571
- const orArr = ops.meanAll(orPer, false);
1572
-
1573
- const lwVals = lw.toFloat32();
1574
- const lrVals = lr.toFloat32();
1575
- const loVals = logOdds.toFloat32();
1576
- const nll = nllArr.toFloat32()[0]!;
1577
- const or = orArr.toFloat32()[0]!;
1578
- const loss = nll + lambda * or;
1579
- let acc = 0;
1580
- let margin = 0;
1581
- for (let b = 0; b < lwVals.length; b++) {
1582
- if (lwVals[b]! > lrVals[b]!) acc++;
1583
- margin += loVals[b]!;
1584
- }
1585
- acc /= lwVals.length;
1586
- margin /= loVals.length;
1587
-
1588
- for (const a of [lw, lr, negLw, nllArr, logOdds, negLO, orPer, orArr]) a.dispose();
1589
- nllFull?.dispose();
1590
- return { loss, nll, or, accuracy: acc, margin };
1591
- }