mlx-bun 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (640) hide show
  1. package/README.md +1449 -109
  2. package/THIRD_PARTY_NOTICES.md +882 -0
  3. package/bin/mlx-bun.convert.mjs +4 -0
  4. package/bin/mlx-bun.fuse.mjs +4 -0
  5. package/bin/mlx-bun.generate.mjs +4 -0
  6. package/bin/mlx-bun.lora.mjs +4 -0
  7. package/bin/mlx-bun.mjs +13 -21
  8. package/bin/mlx-bun.server.mjs +4 -0
  9. package/bin/mlx-bun.upload.mjs +4 -0
  10. package/dist/web/app.js +9584 -0
  11. package/package.json +50 -53
  12. package/src/cli/ane-install.ts +76 -0
  13. package/src/cli/args.ts +179 -0
  14. package/src/cli/engine-entry.ts +68 -0
  15. package/src/cli/inference.ts +214 -0
  16. package/src/cli/job-entry.ts +97 -0
  17. package/src/cli/library-host.ts +117 -0
  18. package/src/cli/main.ts +57 -0
  19. package/src/cli/memory-engine.ts +163 -0
  20. package/src/cli/memory.ts +36 -0
  21. package/src/cli/mlx-lm-aliases.ts +309 -0
  22. package/src/cli/model-choice.ts +33 -0
  23. package/src/cli/model-selection.ts +84 -0
  24. package/src/cli/module-host.ts +11 -0
  25. package/src/cli/module-verbs.ts +50 -0
  26. package/src/cli/numerical-policy.ts +12 -0
  27. package/src/cli/perplexity.ts +89 -0
  28. package/src/cli/serve-host.ts +423 -0
  29. package/src/cli/serve-isolated.ts +217 -0
  30. package/src/cli/serve-options.ts +106 -0
  31. package/src/cli/serve-state.ts +205 -0
  32. package/src/cli/serve.ts +500 -0
  33. package/src/cli/served-model-host.ts +102 -0
  34. package/src/cli/server-entry.ts +148 -0
  35. package/src/cli/serving-unit.ts +177 -0
  36. package/src/{tui.ts → cli/terminal.ts} +13 -8
  37. package/src/cli/worker-adapters.ts +34 -0
  38. package/src/cli/worker-entry.ts +272 -0
  39. package/src/cli/worker-unit.ts +179 -0
  40. package/src/engine/adapter-operation.ts +26 -0
  41. package/src/engine/cache-services.ts +114 -0
  42. package/src/engine/completion.ts +39 -0
  43. package/src/engine/generation-gateway.ts +358 -0
  44. package/src/engine/index.ts +59 -0
  45. package/src/engine/kv-budget.ts +120 -0
  46. package/src/engine/media-preparation.ts +232 -0
  47. package/src/engine/model-binding.ts +24 -0
  48. package/src/engine/model-host.ts +513 -0
  49. package/src/engine/model-serving.ts +89 -0
  50. package/src/{serve → engine}/preparation.ts +7 -5
  51. package/src/{serve → engine}/session-completion-engine.ts +6 -6
  52. package/src/engine/telemetry.ts +109 -0
  53. package/src/hub/downloads.ts +116 -0
  54. package/src/jobs/db.ts +16 -7
  55. package/src/jobs/events.ts +29 -0
  56. package/src/jobs/executable.ts +5 -0
  57. package/src/jobs/host.ts +51 -0
  58. package/src/jobs/in-process.ts +55 -0
  59. package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
  60. package/src/jobs/runner.ts +150 -231
  61. package/src/jobs/service.ts +75 -0
  62. package/src/jobs/sse.ts +59 -23
  63. package/src/jobs/worker-memory.ts +46 -0
  64. package/src/jobs/worker-process.ts +163 -0
  65. package/src/jobs/worker-supervisor.ts +258 -0
  66. package/src/modules.ts +58 -0
  67. package/src/publishing/catalog-hub.ts +18 -0
  68. package/src/publishing/credentials.ts +31 -0
  69. package/src/publishing/upload.ts +37 -0
  70. package/src/residency/leased-adapters.ts +21 -0
  71. package/src/residency/local-records.ts +16 -0
  72. package/src/residency/model-residency.ts +405 -0
  73. package/src/residency/record-index.ts +28 -0
  74. package/src/residency/resident-estimate.ts +50 -0
  75. package/src/{anthropic.ts → server/anthropic.ts} +3 -81
  76. package/src/server/cache-routes.ts +36 -0
  77. package/src/{serve → server}/chat-request.ts +10 -10
  78. package/src/{serve → server}/chat-stage.ts +14 -14
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +33 -41
  81. package/src/server/decision-body.ts +55 -0
  82. package/src/server/decision-routes.ts +134 -0
  83. package/src/server/decision-wire.ts +228 -0
  84. package/src/{serve → server}/discovery-routes.ts +25 -82
  85. package/src/{serve → server}/http.ts +25 -13
  86. package/src/{serve → server}/inference-request.ts +5 -5
  87. package/src/server/job-routes.ts +20 -0
  88. package/src/server/media-prompt.ts +63 -0
  89. package/src/server/memory-completion-client.ts +46 -0
  90. package/src/server/model-routes.ts +192 -0
  91. package/src/{serve → server}/openai-wire.ts +3 -2
  92. package/src/{serve → server}/pipeline.ts +1 -1
  93. package/src/server/prompt-builder.ts +13 -0
  94. package/src/server/prompt-contracts.ts +17 -0
  95. package/src/server/proxy-routes.ts +187 -0
  96. package/src/server/publishing-routes.ts +41 -0
  97. package/src/{serve → server}/request-plan.ts +6 -8
  98. package/src/{serve → server}/request-prep.ts +47 -50
  99. package/src/{serve → server}/responses-client.ts +30 -13
  100. package/src/{responses.ts → server/responses.ts} +12 -93
  101. package/src/server/routes.ts +205 -0
  102. package/src/server/start.ts +72 -0
  103. package/src/server/status-routes.ts +108 -0
  104. package/src/{serve → server}/text-completion-stage.ts +25 -4
  105. package/src/server/text-prompt.ts +12 -0
  106. package/src/{serve → server}/token-streams.ts +36 -38
  107. package/src/server/worker-routes.ts +311 -0
  108. package/src/storage/paths.ts +61 -0
  109. package/src/web/assets.ts +37 -0
  110. package/src/web/{src → browser}/api.ts +5 -23
  111. package/src/web/browser/chat-host.ts +17 -0
  112. package/src/web/browser/installed-panels.ts +6 -0
  113. package/src/web/browser/main.ts +55 -0
  114. package/src/web/browser/markdown.ts +279 -0
  115. package/src/web/browser/memory-panel.ts +9 -0
  116. package/src/web/browser/palette.ts +64 -0
  117. package/src/web/browser/protocol.ts +13 -0
  118. package/src/web/browser/shell.ts +467 -0
  119. package/src/web/{src → browser}/status.ts +32 -36
  120. package/src/web/build.ts +51 -0
  121. package/src/web/public/app.html +1054 -0
  122. package/src/web/{sw.js → public/sw.js} +4 -3
  123. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  124. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  125. package/THIRD_PARTY_LICENSES.md +0 -51
  126. package/docs/README.md +0 -107
  127. package/docs/archive/hlg-sampling.md +0 -350
  128. package/docs/archive/investigations/audio-input-plan.md +0 -356
  129. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +0 -3617
  130. package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
  131. package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
  132. package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
  133. package/docs/archive/investigations/curve-bisector-routing.md +0 -123
  134. package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
  135. package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
  136. package/docs/archive/investigations/dspark-handoff.md +0 -224
  137. package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
  138. package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
  139. package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
  140. package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
  141. package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
  142. package/docs/archive/investigations/lab-build-journal.md +0 -157
  143. package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
  144. package/docs/archive/investigations/openjarvis-fit.md +0 -198
  145. package/docs/archive/investigations/openwiki-evaluation.md +0 -249
  146. package/docs/archive/investigations/optimization_plan.md +0 -346
  147. package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
  148. package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
  149. package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
  150. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
  151. package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
  152. package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
  153. package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
  154. package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
  155. package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
  156. package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
  157. package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
  158. package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
  159. package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
  160. package/docs/archive/planning/chat-ui-vision.md +0 -50
  161. package/docs/archive/planning/chunk-task-roadmap.md +0 -77
  162. package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
  163. package/docs/archive/planning/journal.md +0 -316
  164. package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
  165. package/docs/archive/planning/pi-integration-review.md +0 -178
  166. package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
  167. package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
  168. package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
  169. package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
  170. package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
  171. package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
  172. package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
  173. package/docs/archive/planning/release-notes-v0.4.0.md +0 -45
  174. package/docs/archive/planning/release-notes-v0.5.0.md +0 -37
  175. package/docs/archive/planning/web-ui-pass-plan.md +0 -213
  176. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  177. package/docs/design/batching.md +0 -2054
  178. package/docs/design/decode-speed-program.md +0 -2404
  179. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  180. package/docs/design/generic-model-support.md +0 -821
  181. package/docs/design/kv-cache.md +0 -993
  182. package/docs/design/orpo-training.md +0 -680
  183. package/docs/design/speculative-decoding.md +0 -2129
  184. package/docs/design/turboquant.md +0 -2225
  185. package/docs/design/unified-engine-frontier-plan.md +0 -1987
  186. package/docs/design/web-chat-redesign.md +0 -695
  187. package/docs/planning/PRODUCT_ROADMAP.md +0 -593
  188. package/docs/planning/ResearchTopics.md +0 -119
  189. package/docs/planning/public-readiness-handoff.md +0 -244
  190. package/docs/planning/public-readiness.md +0 -149
  191. package/docs/planning/video-series.md +0 -421
  192. package/docs/reference/benchmarks.md +0 -3587
  193. package/docs/reference/cli.md +0 -646
  194. package/docs/reference/distribution.md +0 -380
  195. package/docs/reference/environment.md +0 -494
  196. package/docs/reference/glossary.md +0 -39
  197. package/docs/reference/library-api.md +0 -688
  198. package/docs/reference/memory.md +0 -260
  199. package/docs/reference/models.md +0 -346
  200. package/docs/reference/server-api.md +0 -1945
  201. package/docs/reference/server-config.md +0 -973
  202. package/docs/reference/training.md +0 -462
  203. package/docs/reference/troubleshooting.md +0 -158
  204. package/src/admission-plan.ts +0 -18
  205. package/src/audio/audiotoolbox.ts +0 -112
  206. package/src/audio/conformer.ts +0 -655
  207. package/src/audio/decode.ts +0 -133
  208. package/src/audio/features.ts +0 -202
  209. package/src/audio/mic-capture.ts +0 -98
  210. package/src/audio/silero-vad.ts +0 -379
  211. package/src/audio/transcode.ts +0 -67
  212. package/src/audio/whisper-decode.ts +0 -774
  213. package/src/audio/whisper-fast.ts +0 -732
  214. package/src/audio/whisper-format.ts +0 -73
  215. package/src/audio/whisper-mel.ts +0 -185
  216. package/src/audio/whisper-timing.ts +0 -274
  217. package/src/audio/whisper-tokenizer.ts +0 -239
  218. package/src/audio/whisper-transcribe.ts +0 -410
  219. package/src/backends/mlx/assistant-target.ts +0 -46
  220. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  221. package/src/backends/mlx/autoregressive.ts +0 -129
  222. package/src/backends/mlx/batch-group.ts +0 -1874
  223. package/src/backends/mlx/cache-attachment.ts +0 -67
  224. package/src/backends/mlx/cache-layout.ts +0 -31
  225. package/src/backends/mlx/checkpoint-state.ts +0 -53
  226. package/src/backends/mlx/continuation-persistence.ts +0 -107
  227. package/src/backends/mlx/continuation-request.ts +0 -43
  228. package/src/backends/mlx/continuation.ts +0 -77
  229. package/src/backends/mlx/diffusion.ts +0 -39
  230. package/src/backends/mlx/draft-target.ts +0 -32
  231. package/src/backends/mlx/encoder-cache.ts +0 -25
  232. package/src/backends/mlx/evaluation.ts +0 -105
  233. package/src/backends/mlx/fill-append.ts +0 -58
  234. package/src/backends/mlx/fill-group.ts +0 -497
  235. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  236. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  237. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  238. package/src/backends/mlx/gateway-binding.ts +0 -174
  239. package/src/backends/mlx/graph.ts +0 -38
  240. package/src/backends/mlx/kv-codec.ts +0 -9
  241. package/src/backends/mlx/kv-maintenance.ts +0 -161
  242. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  243. package/src/backends/mlx/memory-client.ts +0 -189
  244. package/src/backends/mlx/methods.ts +0 -90
  245. package/src/backends/mlx/mixed-iteration.ts +0 -62
  246. package/src/backends/mlx/model-prompt.ts +0 -178
  247. package/src/backends/mlx/model-serving.ts +0 -80
  248. package/src/backends/mlx/paged-cache-codec.ts +0 -27
  249. package/src/backends/mlx/prefill-cohort.ts +0 -100
  250. package/src/backends/mlx/prefill-policy.ts +0 -26
  251. package/src/backends/mlx/prefill-rows.ts +0 -244
  252. package/src/backends/mlx/prefill.ts +0 -33
  253. package/src/backends/mlx/projected-context-rows.ts +0 -72
  254. package/src/backends/mlx/prompt-input.ts +0 -29
  255. package/src/backends/mlx/qwen-prompt-input.ts +0 -40
  256. package/src/backends/mlx/request-policy.ts +0 -20
  257. package/src/backends/mlx/request-state-policy.ts +0 -54
  258. package/src/backends/mlx/rollback.ts +0 -92
  259. package/src/backends/mlx/row-sampling.ts +0 -49
  260. package/src/backends/mlx/serial-executor.ts +0 -251
  261. package/src/backends/mlx/speculative-group.ts +0 -438
  262. package/src/backends/mlx/speculative-round.ts +0 -161
  263. package/src/backends/mlx/speculative.ts +0 -131
  264. package/src/backends/mlx/state-rows.ts +0 -104
  265. package/src/backends/mlx/state-views.ts +0 -46
  266. package/src/backends/mlx/target-layout-capability.ts +0 -35
  267. package/src/backends/mlx/text-engine.ts +0 -73
  268. package/src/backends/mlx/transitioning-kv-rows.ts +0 -173
  269. package/src/chat-template.ts +0 -382
  270. package/src/cli.ts +0 -3490
  271. package/src/config.ts +0 -399
  272. package/src/contracts/completion.ts +0 -16
  273. package/src/contracts/execution.ts +0 -70
  274. package/src/contracts/generation.ts +0 -103
  275. package/src/contracts/host.ts +0 -7
  276. package/src/contracts/object-cache.ts +0 -8
  277. package/src/contracts/pi-provider.ts +0 -30
  278. package/src/contracts/pi.ts +0 -261
  279. package/src/contracts/prefix-cache.ts +0 -33
  280. package/src/contracts/resources.ts +0 -25
  281. package/src/contracts/sampling.ts +0 -23
  282. package/src/contracts/scheduling.ts +0 -54
  283. package/src/contracts/tsconfig.json +0 -14
  284. package/src/dataset/generators.ts +0 -1087
  285. package/src/dataset/index.ts +0 -12
  286. package/src/dataset/job.ts +0 -81
  287. package/src/dataset/llm.ts +0 -71
  288. package/src/dataset/registry.ts +0 -528
  289. package/src/diffusion/diffusion-generate.ts +0 -507
  290. package/src/download.ts +0 -505
  291. package/src/embed.ts +0 -77
  292. package/src/engine/admission.ts +0 -59
  293. package/src/engine/cancellation.ts +0 -38
  294. package/src/engine/engine.ts +0 -56
  295. package/src/engine/execution-coordinator.ts +0 -70
  296. package/src/engine/execution-plan.ts +0 -64
  297. package/src/engine/execution-tasks.ts +0 -44
  298. package/src/engine/namespaced-cache.ts +0 -13
  299. package/src/engine/resources.ts +0 -53
  300. package/src/engine/scheduler.ts +0 -92
  301. package/src/engine/session.ts +0 -247
  302. package/src/eval/README.md +0 -130
  303. package/src/eval/ifeval.ts +0 -81
  304. package/src/eval/kl-prompts.ts +0 -29
  305. package/src/eval/kl.ts +0 -590
  306. package/src/eval/perplexity.ts +0 -164
  307. package/src/eval/quality-db.ts +0 -92
  308. package/src/eval/reference-logits.ts +0 -207
  309. package/src/eval/runner.ts +0 -124
  310. package/src/eval/score.ts +0 -49
  311. package/src/eval/tasks/bfcl.ts +0 -535
  312. package/src/eval/tasks/gsm8k.ts +0 -98
  313. package/src/eval/tasks/hashhop.ts +0 -139
  314. package/src/eval/tasks/humaneval.ts +0 -275
  315. package/src/eval/tasks/ifeval.ts +0 -525
  316. package/src/eval/tasks/mmlu.ts +0 -157
  317. package/src/eval/ultrafeedback.ts +0 -60
  318. package/src/evaldb.ts +0 -199
  319. package/src/expert-io.ts +0 -393
  320. package/src/expert-offload-build.ts +0 -77
  321. package/src/expert-offload.ts +0 -76
  322. package/src/expert-residency.ts +0 -1134
  323. package/src/expert-usage.ts +0 -415
  324. package/src/fill/echo-index.ts +0 -188
  325. package/src/fill/fill-session.ts +0 -463
  326. package/src/fill/proposal.ts +0 -68
  327. package/src/fill/schema-rows.ts +0 -506
  328. package/src/fill/tool-boundary.ts +0 -103
  329. package/src/fit.ts +0 -249
  330. package/src/generate.ts +0 -1352
  331. package/src/grammar.ts +0 -604
  332. package/src/harness-pi.ts +0 -184
  333. package/src/hf-push.ts +0 -628
  334. package/src/hub-rest.ts +0 -242
  335. package/src/index.ts +0 -63
  336. package/src/inference/denoising.ts +0 -17
  337. package/src/inference/draft-acceptance.ts +0 -41
  338. package/src/inference/draft-output.ts +0 -49
  339. package/src/inference/graph.ts +0 -33
  340. package/src/inference/prefill.ts +0 -45
  341. package/src/inference/rollback.ts +0 -7
  342. package/src/jobs/index.ts +0 -31
  343. package/src/jobs/job-entry.ts +0 -107
  344. package/src/jobs/task-client.ts +0 -19
  345. package/src/jobs/types.ts +0 -2
  346. package/src/kv-scheme.ts +0 -279
  347. package/src/kv-store.ts +0 -1135
  348. package/src/lab/README.md +0 -22
  349. package/src/lab/curve/curve-designer.html +0 -303
  350. package/src/lab/curve/curve-sampler.ts +0 -122
  351. package/src/lab/expert-trace/expert-trace.ts +0 -69
  352. package/src/lab/paged-kv/paged-attention.ts +0 -117
  353. package/src/lab/paged-kv/paged-kv-rows.ts +0 -111
  354. package/src/lab/paged-kv/paged-kv.ts +0 -336
  355. package/src/library.ts +0 -40
  356. package/src/lora.ts +0 -408
  357. package/src/md-modules.d.ts +0 -54
  358. package/src/media-fetch.ts +0 -442
  359. package/src/memory/article.ts +0 -558
  360. package/src/memory/chunk-validation.ts +0 -150
  361. package/src/memory/chunk.ts +0 -318
  362. package/src/memory/cluster.ts +0 -351
  363. package/src/memory/coverage.ts +0 -134
  364. package/src/memory/crosslink.ts +0 -0
  365. package/src/memory/db.ts +0 -253
  366. package/src/memory/entity.ts +0 -203
  367. package/src/memory/gate.ts +0 -187
  368. package/src/memory/history.ts +0 -152
  369. package/src/memory/ingest.ts +0 -335
  370. package/src/memory/model.ts +0 -183
  371. package/src/memory/normalize.ts +0 -671
  372. package/src/memory/parse.ts +0 -22
  373. package/src/memory/pipeline.ts +0 -275
  374. package/src/memory/prompts.ts +0 -65
  375. package/src/memory/query.ts +0 -334
  376. package/src/memory/reconcile.ts +0 -734
  377. package/src/memory/redirect.ts +0 -108
  378. package/src/memory/reindex.ts +0 -325
  379. package/src/memory/resolve.ts +0 -307
  380. package/src/memory/rest.ts +0 -245
  381. package/src/memory/route.ts +0 -272
  382. package/src/memory/schedule.ts +0 -165
  383. package/src/memory/stages.ts +0 -777
  384. package/src/memory/synthesize.ts +0 -1638
  385. package/src/memory/tools.ts +0 -749
  386. package/src/memory/vault.ts +0 -1194
  387. package/src/memory/wikify.ts +0 -1064
  388. package/src/mlx/array.ts +0 -425
  389. package/src/mlx/autograd.ts +0 -304
  390. package/src/mlx/checkpoint.ts +0 -149
  391. package/src/mlx/compile.ts +0 -168
  392. package/src/mlx/custom-vjp.ts +0 -188
  393. package/src/mlx/ffi.ts +0 -531
  394. package/src/mlx/materialize.ts +0 -23
  395. package/src/mlx/metal-kernel.ts +0 -159
  396. package/src/mlx/normalized-argmax.ts +0 -116
  397. package/src/mlx/ops.ts +0 -1108
  398. package/src/mlx/quantized-rows.ts +0 -36
  399. package/src/mlx/token-bitmask.ts +0 -52
  400. package/src/mlx/turboquant-kv-decode.ts +0 -82
  401. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  402. package/src/mlx/turboquant-ops.ts +0 -861
  403. package/src/mlx/turboquant-tables.ts +0 -237
  404. package/src/mmap.ts +0 -90
  405. package/src/model/artifact-identity.ts +0 -73
  406. package/src/model/batched-kv.ts +0 -235
  407. package/src/model/batched-mask.ts +0 -279
  408. package/src/model/batched-quant.ts +0 -212
  409. package/src/model/batched-quantized-kv.ts +0 -144
  410. package/src/model/batched-rotating-quant.ts +0 -353
  411. package/src/model/batched-rotating-state.ts +0 -149
  412. package/src/model/batched-rotating.ts +0 -338
  413. package/src/model/batched-row-storage.ts +0 -179
  414. package/src/model/batched-ssm.ts +0 -52
  415. package/src/model/batched-turboquant-kv.ts +0 -94
  416. package/src/model/compiled-decode.ts +0 -712
  417. package/src/model/decoded-kv-donor.ts +0 -12
  418. package/src/model/delayed-quantized-kv.ts +0 -72
  419. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  420. package/src/model/delayed-turboquant-kv.ts +0 -57
  421. package/src/model/diffusion-gemma.ts +0 -1026
  422. package/src/model/factory.ts +0 -215
  423. package/src/model/fingerprint.ts +0 -45
  424. package/src/model/flash-attention.ts +0 -694
  425. package/src/model/full-prefill-padding.ts +0 -56
  426. package/src/model/full-prefill-row.ts +0 -105
  427. package/src/model/gemma4-base.ts +0 -2370
  428. package/src/model/gemma4.ts +0 -1395
  429. package/src/model/generated/gemma4-12b.ts +0 -429
  430. package/src/model/generated/gemma4-26b.ts +0 -448
  431. package/src/model/generated/gemma4-e4b.ts +0 -717
  432. package/src/model/generated/index.ts +0 -18
  433. package/src/model/glm52-atlas.ts +0 -511
  434. package/src/model/glm52-cache.ts +0 -792
  435. package/src/model/glm52-config.ts +0 -262
  436. package/src/model/glm52-container.ts +0 -305
  437. package/src/model/glm52-coupling.ts +0 -388
  438. package/src/model/glm52-dsa.ts +0 -606
  439. package/src/model/glm52-expert-layout.ts +0 -253
  440. package/src/model/glm52-layout.ts +0 -152
  441. package/src/model/glm52-memory.ts +0 -500
  442. package/src/model/glm52-mla.ts +0 -792
  443. package/src/model/glm52-moe.ts +0 -223
  444. package/src/model/glm52-pilot.ts +0 -537
  445. package/src/model/glm52-reference.ts +0 -445
  446. package/src/model/glm52-residency.ts +0 -557
  447. package/src/model/glm52-resident-weights.ts +0 -135
  448. package/src/model/glm52-streamed-experts.ts +0 -453
  449. package/src/model/glm52-streamed-metal.ts +0 -610
  450. package/src/model/glm52-weights.ts +0 -503
  451. package/src/model/glm52.ts +0 -1132
  452. package/src/model/implementation.ts +0 -69
  453. package/src/model/kv-attention-view.ts +0 -70
  454. package/src/model/minicpm5.ts +0 -376
  455. package/src/model/profile.ts +0 -468
  456. package/src/model/quantized-append-attention.ts +0 -31
  457. package/src/model/qwen-append.ts +0 -12
  458. package/src/model/qwen-conv.ts +0 -52
  459. package/src/model/qwen3-delta-state.ts +0 -74
  460. package/src/model/qwen3-delta.ts +0 -451
  461. package/src/model/qwen3-moe.ts +0 -404
  462. package/src/model/qwen3-mrope.ts +0 -250
  463. package/src/model/qwen3.ts +0 -277
  464. package/src/model/qwen3_5-checkpoint.ts +0 -164
  465. package/src/model/qwen3_5.ts +0 -881
  466. package/src/model/rotating-kv-donor.ts +0 -35
  467. package/src/model/rotating-kv-layout.ts +0 -262
  468. package/src/model/rotating-row-transaction.ts +0 -35
  469. package/src/model/speculative-rotating-kv.ts +0 -101
  470. package/src/model/ssm-prefill-padding.ts +0 -39
  471. package/src/model/support.ts +0 -110
  472. package/src/model/token-groups.ts +0 -66
  473. package/src/model/trellis-balanced-scatter.ts +0 -51
  474. package/src/model/trellis-linear.ts +0 -672
  475. package/src/model/trellis-shared-m.ts +0 -151
  476. package/src/model/trellis-shared-scatter.ts +0 -144
  477. package/src/model/trellis-splitk-prefill.ts +0 -158
  478. package/src/model/trellis-tiled-prefill.ts +0 -136
  479. package/src/model/trellis-vector-expand.ts +0 -62
  480. package/src/model/trellis-wide-prefill.ts +0 -94
  481. package/src/model/turboquant-codec.ts +0 -89
  482. package/src/model/universal/archs.ts +0 -332
  483. package/src/model/universal/dense.ts +0 -612
  484. package/src/model/universal/modules.ts +0 -170
  485. package/src/model/universal/rope.ts +0 -381
  486. package/src/model/whisper.ts +0 -435
  487. package/src/native/expert_io.c +0 -523
  488. package/src/native/frame_extract.swift +0 -140
  489. package/src/native/mic_capture.swift +0 -92
  490. package/src/native-pack.ts +0 -163
  491. package/src/pi-provider.ts +0 -53
  492. package/src/pi-session.ts +0 -81
  493. package/src/pi-terminal.ts +0 -274
  494. package/src/pi-web.ts +0 -1940
  495. package/src/preflight.ts +0 -158
  496. package/src/prompt-cache.ts +0 -682
  497. package/src/quantize/allocator.ts +0 -627
  498. package/src/quantize/atomic-output.ts +0 -38
  499. package/src/quantize/calibration.ts +0 -181
  500. package/src/quantize/config-writer.ts +0 -145
  501. package/src/quantize/index.ts +0 -84
  502. package/src/quantize/job.ts +0 -162
  503. package/src/quantize/quantizer.ts +0 -666
  504. package/src/quantize/rotate.ts +0 -603
  505. package/src/quantize/safetensors-writer.ts +0 -249
  506. package/src/quantize/sensitivity.ts +0 -216
  507. package/src/quantize/trellis.ts +0 -586
  508. package/src/quantize/weight-transform.ts +0 -292
  509. package/src/registry.ts +0 -602
  510. package/src/runtime-config.ts +0 -77
  511. package/src/safetensors.ts +0 -165
  512. package/src/sampler.ts +0 -1012
  513. package/src/serve/admin-routes.ts +0 -184
  514. package/src/serve/assets/curve-terrain.html +0 -105
  515. package/src/serve/assets/training-inference-map.html +0 -575
  516. package/src/serve/audio-routes.ts +0 -323
  517. package/src/serve/aux-routes.ts +0 -132
  518. package/src/serve/batch-scheduler.ts +0 -8
  519. package/src/serve/checkpoint-identity.ts +0 -64
  520. package/src/serve/generation-gateway.ts +0 -537
  521. package/src/serve/isolate.ts +0 -609
  522. package/src/serve/kv-budget.ts +0 -14
  523. package/src/serve/lab-routes.ts +0 -341
  524. package/src/serve/lane-registry.ts +0 -45
  525. package/src/serve/model-admin-routes.ts +0 -155
  526. package/src/serve/model-binding.ts +0 -49
  527. package/src/serve/model-host.ts +0 -511
  528. package/src/serve/parent-application.ts +0 -56
  529. package/src/serve/prompt-response-trace.ts +0 -161
  530. package/src/serve/response-lease.ts +0 -24
  531. package/src/serve/session-search.ts +0 -326
  532. package/src/serve/static-routes.ts +0 -101
  533. package/src/serve/transcription-server.ts +0 -64
  534. package/src/serve/transcription-service.ts +0 -502
  535. package/src/server.ts +0 -1282
  536. package/src/spec/assistant-rows.ts +0 -93
  537. package/src/spec/assistant-source.ts +0 -126
  538. package/src/spec/cache-identity.ts +0 -22
  539. package/src/spec/deepspec-rows.ts +0 -29
  540. package/src/spec/deepspec-source.ts +0 -145
  541. package/src/spec/dflash-source.ts +0 -149
  542. package/src/spec/drafter.ts +0 -304
  543. package/src/spec/dspark/ab-stats.ts +0 -136
  544. package/src/spec/dspark/calibration.ts +0 -112
  545. package/src/spec/dspark/data-dflash.ts +0 -156
  546. package/src/spec/dspark/data.ts +0 -201
  547. package/src/spec/dspark/deepspec-module.ts +0 -776
  548. package/src/spec/dspark/generate-dflash.ts +0 -245
  549. package/src/spec/dspark/generate.ts +0 -290
  550. package/src/spec/dspark/loader.ts +0 -23
  551. package/src/spec/dspark/loss.ts +0 -213
  552. package/src/spec/dspark/module-dflash.ts +0 -560
  553. package/src/spec/dspark/module.ts +0 -540
  554. package/src/spec/dspark/quantize-drafter.ts +0 -91
  555. package/src/spec/dspark/sample.ts +0 -112
  556. package/src/spec/generate.ts +0 -276
  557. package/src/spec/glm52-mtp-graph.ts +0 -29
  558. package/src/spec/glm52-mtp-rows.ts +0 -132
  559. package/src/spec/glm52-mtp-source.ts +0 -318
  560. package/src/spec/glm52-mtp-state.ts +0 -33
  561. package/src/spec/ngram-source.ts +0 -226
  562. package/src/spec/projected-draft-rows.ts +0 -73
  563. package/src/spec/qwen-mtp-module.ts +0 -171
  564. package/src/spec/qwen-mtp-rows.ts +0 -230
  565. package/src/spec/qwen-mtp-source.ts +0 -386
  566. package/src/spec/qwen-mtp-state.ts +0 -30
  567. package/src/spec/serve-loop.ts +0 -587
  568. package/src/spec/source.ts +0 -228
  569. package/src/spec/two-model-rows.ts +0 -62
  570. package/src/spec/two-model.ts +0 -113
  571. package/src/ssd-cache.ts +0 -431
  572. package/src/ssd-durability.ts +0 -209
  573. package/src/storage/host-buffer.ts +0 -24
  574. package/src/storage/kv-writer.ts +0 -83
  575. package/src/storage/kv-writer.worker.js +0 -264
  576. package/src/storage/retention-policy.ts +0 -37
  577. package/src/tiered-prompt-cache.ts +0 -45
  578. package/src/tokenizer.ts +0 -335
  579. package/src/tool-approvals.ts +0 -131
  580. package/src/tool-call.ts +0 -695
  581. package/src/train/dataset.ts +0 -484
  582. package/src/train/diffusion-lora.ts +0 -216
  583. package/src/train/export.ts +0 -32
  584. package/src/train/flash-cce.ts +0 -1234
  585. package/src/train/forward.ts +0 -266
  586. package/src/train/fuse.ts +0 -243
  587. package/src/train/index.ts +0 -46
  588. package/src/train/job.ts +0 -205
  589. package/src/train/lora-params.ts +0 -254
  590. package/src/train/loss.ts +0 -1591
  591. package/src/train/merge.ts +0 -195
  592. package/src/train/optimizer.ts +0 -183
  593. package/src/train/prefix-shared.ts +0 -490
  594. package/src/train/rank.ts +0 -137
  595. package/src/train/segmented.ts +0 -1828
  596. package/src/train/steel-qmm-header.ts +0 -1461
  597. package/src/train/trainer.ts +0 -1221
  598. package/src/train/watch.ts +0 -542
  599. package/src/vision/diffusion-vision.ts +0 -620
  600. package/src/vision/embedder.ts +0 -176
  601. package/src/vision/preprocess.ts +0 -247
  602. package/src/vision/prompt.ts +0 -409
  603. package/src/vision/qwen3vl-preprocess.ts +0 -409
  604. package/src/vision/qwen3vl-prompt.ts +0 -233
  605. package/src/vision/qwen3vl-tower.ts +0 -566
  606. package/src/vision/siglip.ts +0 -607
  607. package/src/vision/video-frames.ts +0 -148
  608. package/src/web/app.html +0 -2155
  609. package/src/web/app.js +0 -5863
  610. package/src/web/skills/memory/SKILL.md +0 -120
  611. package/src/web/skills/web-research/SKILL.md +0 -46
  612. package/src/web/skills.ts +0 -97
  613. package/src/web/src/adapters-panel.ts +0 -288
  614. package/src/web/src/assistant.ts +0 -451
  615. package/src/web/src/chat.ts +0 -1060
  616. package/src/web/src/composer.ts +0 -962
  617. package/src/web/src/dataset.ts +0 -132
  618. package/src/web/src/finetune.ts +0 -267
  619. package/src/web/src/hub.ts +0 -428
  620. package/src/web/src/main.ts +0 -98
  621. package/src/web/src/markdown.ts +0 -0
  622. package/src/web/src/memory-panel.ts +0 -572
  623. package/src/web/src/model-picker.ts +0 -159
  624. package/src/web/src/palette.ts +0 -346
  625. package/src/web/src/protocol.ts +0 -26
  626. package/src/web/src/quantize.ts +0 -167
  627. package/src/web/src/rag.ts +0 -274
  628. package/src/web/src/sessions.ts +0 -469
  629. package/src/web/src/shell.ts +0 -804
  630. package/src/web/src/ui-catalog.ts +0 -78
  631. package/src/web/src/voice.ts +0 -141
  632. package/src/web/vendor/README.md +0 -61
  633. package/src/web-assets.ts +0 -47
  634. package/src/web-tools.ts +0 -478
  635. package/src/weights.ts +0 -255
  636. /package/src/{serve → server}/completion-sink.ts +0 -0
  637. /package/src/{serve → server}/generated-token-history.ts +0 -0
  638. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  639. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  640. /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
@@ -1,1828 +0,0 @@
1
- // Segmented backward: long-context LoRA SFT that streams the backward pass
2
- // segment-by-segment so only ONE segment's activations are ever live, instead
3
- // of holding every layer's recompute activations at once (the spike that makes
4
- // naive gradient checkpointing crash at long context). Design + proof:
5
- // docs/design/orpo-training.md (mechanism validated on a toy in
6
- // scripts/ckpt-mem-test.ts, SEG mode: 0.000% grad error, peak below per-layer
7
- // checkpointing; bit-exact on MiniCPM5 under flash attention).
8
- //
9
- // Mechanism (B=1 SFT only — the responseOnlyCe path):
10
- // 1. Forward, saving boundaries. Run the layer stack; at each segment edge
11
- // materialize (eval) and DETACH (copy to a graph-free leaf) the residual
12
- // stream hidden into a boundary. Intra-segment interiors are discarded; the
13
- // boundaries are small ([1, T, hidden] per edge).
14
- // 2. Loss head. finalNorm + LM head + response-only CE over the LAST boundary,
15
- // differentiated w.r.t. that boundary -> the loss value and dh_out, the
16
- // gradient of the loss w.r.t. the final residual stream.
17
- // 3. Backward, reverse over segments. For each segment k (last -> first) an
18
- // mlx_vjp of `segment_forward(boundary_k, ...LoRA)` with the cotangent dh_out
19
- // returns [dh_in, ...dLoRA_segment]: dh_in seeds segment k-1, the LoRA grads
20
- // scatter into the global flat grad vector. Only segment k's activations are
21
- // live. (A vjp is exactly reverse-mode AD: vjps = (df/dx)^T (.) dh.)
22
- //
23
- // Why mlx_vjp and not a surrogate-loss value_and_grad: the equivalent
24
- // `value_and_grad(sum(stop_grad(dh) (.) segment_forward(...)))` is numerically
25
- // identical but LEAKS ~one activation buffer per segment per step at the mlx
26
- // level (measured; not GC/cache/synchronize/reuse reclaimable — see
27
- // docs/design/orpo-training.md §9.4). mlx_vjp takes the cotangent
28
- // directly, needs no surrogate, and does not leak.
29
- //
30
- // IMPORTANT lifetime fact: mlx `eval` does NOT detach — an eval'd array retains
31
- // its upstream graph, so each forward boundary must be copied into a fresh leaf
32
- // (detachLeaf) or it keeps a whole layer-stack's activations alive (~0.1 GB per
33
- // 4-layer segment of within-step retention otherwise). The vjp objects are built
34
- // ONCE and reused across steps; their closures read the per-step batch from a
35
- // mutable holder.
36
-
37
- import { MlxArray } from "../mlx/array";
38
- import * as ops from "../mlx/ops";
39
- import { peakMemory } from "../mlx/ffi";
40
- import { Vjp } from "../mlx/autograd";
41
- import { runtimeValue } from "../runtime-config";
42
-
43
- // MLX_BUN_SEG_MEM_LOG=1: log the within-step peak after each phase of the
44
- // segmented step (forward+boundaries / head vjp / each segment backward).
45
- // The trainer resets the peak at step start (MLX_BUN_MEM_LOG=1), so the phase
46
- // where the monotone peak JUMPS is where the step's memory actually lives.
47
- // (This is how the backlog-#2 refutation below was measured. An inter-segment
48
- // clearCache was also tried and had NO effect — the watermark is live memory,
49
- // not allocator cache retention.)
50
- const SEG_MEM = runtimeValue("MLX_BUN_SEG_MEM_LOG") === "1";
51
- const segMem = (label: string): void => {
52
- if (SEG_MEM) console.log(` [seg-mem] ${label}: peak=${(peakMemory() / 1e9).toFixed(2)} GB`);
53
- };
54
- import { MiniCPM5Model, setMiniCpmPrefixPlan } from "../model/minicpm5";
55
- import type { Gemma4Model } from "../model/gemma4";
56
- import { setGemmaPrefixPlan } from "../model/gemma4";
57
- import type { Cache, Mask, SharedKv } from "../model/gemma4-base";
58
- import { TrainingCache } from "./forward";
59
- import {
60
- responseOnlyLogpMean, fusedLogpMeanFromHidden, chunkedLogpMeanFromHidden,
61
- orpoLossFromLogps, spanLogpMeanFromHidden, respSpanFromMask, combineFullNll,
62
- type ChunkCtx, type SftScope, type HeadSpan,
63
- } from "./loss";
64
- import { prefixSharedCaches, prefixGatherIdx, branchLogpMeanGathered, blockSparsePrefixMaskGemma, prefixFullNll } from "./prefix-shared";
65
-
66
- // Experiment toggle: which bounded head to use inside the segmented mlx_vjp.
67
- // "checkpoint" = per-chunk Checkpoint recompute (bounds memory under nesting);
68
- // anything else (default) = the analytic fused CustomVjp. The fused head bounds
69
- // memory at the TOP level but not when nested in the segmented mlx_vjp (its
70
- // per-chunk graph isn't freed incrementally there); Checkpoint does. See
71
- // docs/design/orpo-training.md.
72
- const SEG_HEAD = runtimeValue("MLX_BUN_SEG_HEAD") ?? "checkpoint";
73
- function boundedHeadFromHidden(
74
- model: MiniCPM5Model | Gemma4Model, h: MlxArray, ids: number[], mask: number[],
75
- chunkSize: number, sink: Array<{ dispose(): void }>, span?: HeadSpan,
76
- ): MlxArray {
77
- return SEG_HEAD === "fused"
78
- ? fusedLogpMeanFromHidden(model, h, ids, mask, chunkSize, sink, 0, false, span)
79
- : chunkedLogpMeanFromHidden(model, h, ids, mask, chunkSize, sink, span);
80
- }
81
- import type { TrainableLora } from "./lora-params";
82
- import { rowLength, type SftBatch, type DpoBatch } from "./dataset";
83
-
84
- // Token-chunk size for the bounded SFT head (the ORPO paths take theirs from
85
- // cfg.orpoChunkSize; SFT has no per-run knob, 512 matches the ORPO default).
86
- const SFT_HEAD_CHUNK = Number(runtimeValue("MLX_BUN_SEG_HEAD_CHUNK") ?? "512");
87
-
88
- /** [M,V]-bounded response-only CE from post-finalNorm hidden — the SFT
89
- * segmented head (kernel-review backlog #8). Same span arithmetic and
90
- * token-mean reduction as loss.ts responseOnlyCe, but the logits only ever
91
- * exist per token-chunk ([chunk,V] Checkpoint recompute, or the fused
92
- * CustomVjp head via MLX_BUN_SEG_HEAD) instead of the full [1,M,V] + its
93
- * cotangent inside the head vjp. CE = −(mean response logp); per-position
94
- * grads are bit-exact vs the whole-vocab head (chunking is over TOKEN
95
- * positions; each position's full-V lse is computed unchanged — only the
96
- * scalar loss VALUE can differ in fp association across chunks). */
97
- export function boundedSftCe(
98
- model: MiniCPM5Model | Gemma4Model, hn: MlxArray, batch: SftBatch,
99
- sink: Array<{ dispose(): void }>, chunkSize = SFT_HEAD_CHUNK,
100
- ): MlxArray {
101
- const L = batch.ids[0]!.length;
102
- const startT = Math.max(0, batch.promptLens[0]! - 1); // first supervised input position
103
- const M = Math.min(rowLength(batch, 0), L) - 1 - startT;
104
- if (M <= 0) throw new Error("sftLoss: no response tokens to supervise");
105
- const mean = boundedHeadFromHidden(model, hn, batch.ids[0]!, [], chunkSize, sink, { startT, M });
106
- const neg = ops.mulScalar(mean, -1); // CE = −(mean logp); exact sign flip
107
- mean.dispose();
108
- const loss = ops.sumAxis(neg, 0, false); // [1] -> scalar, matches the vjp cotangent `one`
109
- neg.dispose();
110
- return loss;
111
- }
112
-
113
- /** Split `nLayers` into contiguous `[lo, hi)` ranges of at most `segSize`
114
- * layers each (the last range may be shorter). `segSize` is the memory knob:
115
- * fewer layers per segment -> lower peak, ~fixed total recompute time. */
116
- export function planSegmentsBySize(nLayers: number, segSize: number): [number, number][] {
117
- if (segSize < 1) throw new Error(`planSegmentsBySize: segSize must be >= 1 (got ${segSize})`);
118
- const ranges: [number, number][] = [];
119
- for (let lo = 0; lo < nLayers; lo += segSize) ranges.push([lo, Math.min(lo + segSize, nLayers)]);
120
- return ranges;
121
- }
122
-
123
- // NOTE (2026-07-02, kernel-review backlog #2 REFUTED BY MEASUREMENT): a
124
- // "full-attention isolation" planner (singleton segments for e4b's 7 full
125
- // layers) was built and A/B'd @8K — ZERO peak win (18.09 vs 18.02 GB). The
126
- // design §5 premise is false: mlx's sdpa BACKWARD materializes the O(L²)
127
- // scores for EVERY layer (~3.5 GB/layer @8K on e4b — sliding's window is just
128
- // an additive mask; measured: sliding pair +7.1 GB ≈ sliding+full +7.15). The
129
- // worst segment is set by LAYER COUNT alone, so segSize IS the whole knob:
130
- // segment_size 1 measures 14.59 GB @8K (+3% step time) vs 17.5 @seg2. Below
131
- // that needs the [M,V]-bounded SFT head (backlog #8, ~3 GB head-vjp spike)
132
- // and/or an O(L)-memory attention backward. Evidence:
133
- // seg-isolation-smoke.ts (deleted 2026-08-23; git history) + MLX_BUN_SEG_MEM_LOG=1 probes.
134
-
135
- /** Detach `a` into a graph-free LEAF holding its exact (evaluated) bytes, then
136
- * dispose `a`. mlx retains the upstream graph after `eval`, so a segment
137
- * boundary (or a value_and_grad output) would otherwise keep its whole layer
138
- * stack's activations alive. Copying the bytes into a fresh leaf frees that
139
- * graph. The leaf is a valid differentiable input for the per-segment vag. */
140
- function detachLeaf(a: MlxArray): MlxArray {
141
- // Force a row-major copy first: the buffer is read linearly, so a
142
- // NON-contiguous input (e.g. a transposed donor K/V view) would be
143
- // byte-scrambled. `ops.contiguous` is a no-op for already-contiguous arrays
144
- // (hidden boundaries) and materializes row-major for strided views.
145
- // detachCopy = ONE mlx→mlx copy (backlog #9: the old
146
- // fromBytesCopy(rawBytes()) route was two copies via the JS heap).
147
- const c = ops.contiguous(a);
148
- const leaf = c.detachCopy();
149
- c.dispose();
150
- a.dispose();
151
- return leaf;
152
- }
153
-
154
- /** Batch detach: ONE evalAll barrier for the whole set (backlog #9 — each
155
- * per-array eval was a separate blocking GPU drain; a segment's vjp returns
156
- * dh + 2·consumed-donors + 2·targets arrays, so the per-segment sync count
157
- * collapses from ~2+2n to 1), then one mlx→mlx copy each. Disposes inputs. */
158
- function detachLeaves(arrs: MlxArray[]): MlxArray[] {
159
- const cs = arrs.map((a) => ops.contiguous(a));
160
- ops.evalAll(cs);
161
- const leaves = cs.map((c) => c.detachCopy());
162
- for (const c of cs) c.dispose();
163
- for (const a of arrs) a.dispose();
164
- return leaves;
165
- }
166
-
167
- /** Layer index a target's modulePath belongs to, or -1 (e.g. embedding/head). */
168
- function targetLayer(modulePath: string): number {
169
- const m = modulePath.match(/\.layers\.(\d+)\./);
170
- return m ? Number(m[1]) : -1;
171
- }
172
-
173
- /** Reusable segmented-backward driver for one (model, lora, segmentation). Build
174
- * ONCE per training run and call `.step(batch)` each iteration — the per-segment
175
- * value_and_grads are created up front and reused (rebuilding them every step
176
- * leaks at the mlx level). Returns the loss scalar and the LoRA gradients in
177
- * `flatParams(lora)` order ([...A, ...B] over targets), drop-in for
178
- * `ValueAndGrad.apply(flatParams(lora))`. Caller owns the returned arrays.
179
- *
180
- * The active adapter must be attached (attachForTraining) so the layer forward
181
- * picks up the swapped LoRA leaves via loraState. B=1 SFT only. */
182
- export class SegmentedBackward {
183
- private readonly nLayers: number;
184
- private readonly n: number; // number of LoRA targets
185
- private readonly segIdxs: number[][]; // target indices per segment
186
- private readonly caches: Cache[];
187
- private readonly headVjp: Vjp;
188
- private readonly segVjps: Vjp[];
189
- private readonly one: MlxArray; // scalar cotangent 1.0 for the (scalar) head loss
190
- // Per-step context the (reused) closures read.
191
- private curBatch: SftBatch | null = null;
192
- // Bounded-head per-step transients (chunk slices + Checkpoint/CustomVjp
193
- // objects): the head backward recomputes through them, so they are disposed
194
- // only AFTER the head vjp's outputs are detached (which forces the eval).
195
- private headSink: Array<{ dispose(): void }> = [];
196
- private disposed = false;
197
-
198
- constructor(
199
- private readonly model: MiniCPM5Model,
200
- private readonly lora: TrainableLora,
201
- private readonly ranges: [number, number][],
202
- ) {
203
- this.nLayers = model.layers.length;
204
- const lastHi = ranges[ranges.length - 1]?.[1] ?? 0;
205
- if (ranges[0]?.[0] !== 0 || lastHi !== this.nLayers)
206
- throw new Error(`SegmentedBackward: ranges must cover [0, ${this.nLayers}) (got ${JSON.stringify(ranges)})`);
207
-
208
- this.n = lora.targets.length;
209
- const layerOf = lora.targets.map((t) => targetLayer(t.modulePath));
210
- this.segIdxs = ranges.map(([lo, hi]) =>
211
- layerOf.map((li, j) => ({ li, j })).filter((x) => x.li >= lo && x.li < hi).map((x) => x.j),
212
- );
213
- this.caches = Array.from({ length: this.nLayers }, () => new TrainingCache());
214
- this.one = MlxArray.fromFloat32(new Float32Array([1]), []); // d(loss)/d(loss) = 1
215
-
216
- // Loss head: vjp of finalNorm + the [M,V]-BOUNDED response CE over the last
217
- // boundary leaf (kernel-review backlog #8: the old responseOnlyCe here
218
- // materialized [1,M,V] logits + cotangents inside the head vjp — measured
219
- // ~3 GB @8K on e4b, the exact transient the segmentation exists to kill).
220
- // CE = −(mean response logp); span and reduction identical to
221
- // responseOnlyCe, per-position grads bit-exact (chunking is over token
222
- // positions — each position's full-V lse/softmax is computed unchanged).
223
- // outputs[0] = loss scalar; vjps[0] = dLoss/d(last residual stream) = dh.
224
- this.headVjp = new Vjp((p) => {
225
- const hn = model.finalNorm.forward(p[0]!);
226
- const loss = boundedSftCe(model, hn, this.curBatch!, this.headSink);
227
- hn.dispose();
228
- return [loss];
229
- }, 1);
230
-
231
- // One reused vjp per segment over [boundary, ...segment LoRA] -> [output].
232
- // apply(..., [dh]) returns vjps [dh_in, dA_0, dB_0, ...].
233
- this.segVjps = ranges.map(([lo, hi], k) => {
234
- const idxs = this.segIdxs[k]!;
235
- return new Vjp((p) => {
236
- // Swap this segment's LoRA primals into the live LoraWeights so the
237
- // forward graph differentiates them; restore the originals after (the
238
- // primal wrappers are disposed by the vjp closure).
239
- const saved: [MlxArray, MlxArray][] = idxs.map((j) => [lora.targets[j]!.lw.a, lora.targets[j]!.lw.b]);
240
- idxs.forEach((j, s) => {
241
- lora.targets[j]!.lw.a = p[1 + 2 * s]!;
242
- lora.targets[j]!.lw.b = p[1 + 2 * s + 1]!;
243
- });
244
- try {
245
- return [model.runLayerRange(p[0]!, lo, hi, this.caches)];
246
- } finally {
247
- idxs.forEach((j, s) => {
248
- lora.targets[j]!.lw.a = saved[s]![0];
249
- lora.targets[j]!.lw.b = saved[s]![1];
250
- });
251
- }
252
- }, 1);
253
- });
254
- }
255
-
256
- step(batch: SftBatch): { value: MlxArray; grads: MlxArray[] } {
257
- if (this.disposed) throw new Error("SegmentedBackward used after dispose");
258
- const { model, lora, ranges, caches, segIdxs, n } = this;
259
- const B = batch.ids.length;
260
- if (B !== 1) throw new Error("SegmentedBackward: only B=1 is supported (responseOnlyCe path)");
261
- const L = batch.ids[0]!.length;
262
- if (L < 2) throw new Error("SegmentedBackward: sequence too short (need >= 2 tokens)");
263
- this.curBatch = batch;
264
-
265
- const nSeg = ranges.length;
266
- const T = L - 1;
267
- const inputHost = new Int32Array(T);
268
- for (let t = 0; t < T; t++) inputHost[t] = batch.ids[0]![t]!;
269
- const inputIds = MlxArray.fromInt32(inputHost, [1, T]);
270
-
271
- const boundaries: (MlxArray | null)[] = [];
272
- let value: MlxArray | null = null;
273
- let dhOut: MlxArray | null = null;
274
- let transferred = false;
275
- const grads: (MlxArray | null)[] = new Array(2 * n).fill(null);
276
-
277
- try {
278
- // --- 1. Forward, saving detached boundaries at segment edges. --------
279
- boundaries.push(detachLeaf(model.embed.encode(inputIds))); // [1, T, hidden]
280
- for (let k = 0; k < nSeg; k++) {
281
- const [lo, hi] = ranges[k]!;
282
- boundaries.push(detachLeaf(model.runLayerRange(boundaries[k]!, lo, hi, caches)));
283
- }
284
-
285
- // --- 2. Loss head over the last boundary (vjp with cotangent 1.0). ---
286
- // The bounded head pushes per-step chunk slices + Checkpoints into
287
- // headSink; the head BACKWARD recomputes through them, so materialize
288
- // the full head-vjp graph BEFORE freeing the sink (lazy-eval UAF
289
- // discipline, same as the ORPO classes).
290
- this.headSink = [];
291
- const head = this.headVjp.apply([boundaries[nSeg]!], [this.one]);
292
- ops.evalAll([head.outputs[0]!, head.vjps[0]!]);
293
- value = detachLeaf(head.outputs[0]!); // scalar loss, detached from the head graph (frees logits)
294
- dhOut = detachLeaf(head.vjps[0]!); // dLoss / d(last residual stream), graph-free
295
- for (const d of this.headSink) d.dispose();
296
- this.headSink = [];
297
- boundaries[nSeg]!.dispose();
298
- boundaries[nSeg] = null;
299
-
300
- // --- 3. Backward, reverse over segments (vjp, cotangent = dh). --------
301
- for (let k = nSeg - 1; k >= 0; k--) {
302
- const idxs = segIdxs[k]!;
303
- const params: MlxArray[] = [boundaries[k]!];
304
- for (const j of idxs) params.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
305
-
306
- const res = this.segVjps[k]!.apply(params, [dhOut!]);
307
- res.outputs[0]!.dispose(); // segment output — unused (only its vjp matters)
308
- // vjps[0] = dh_in (grad w.r.t. this segment's input boundary) -> dh_out
309
- // for segment k-1; the rest scatter into the global flat vector. Detach
310
- // to graph-free leaves (eval doesn't detach; keeps peak bounded) — ONE
311
- // barrier for the segment's whole vjp set.
312
- const leaves = detachLeaves(res.vjps);
313
- idxs.forEach((j, s) => {
314
- grads[j] = leaves[1 + 2 * s]!; // dA
315
- grads[n + j] = leaves[1 + 2 * s + 1]!; // dB
316
- });
317
- dhOut!.dispose(); // consumed as this segment's cotangent
318
- dhOut = leaves[0]!;
319
- boundaries[k]!.dispose();
320
- boundaries[k] = null;
321
- }
322
-
323
- // Segment-0 dh_in is the gradient w.r.t. the embedding output — MiniCPM5's
324
- // embedding carries no LoRA, so it is discarded (disposed in finally).
325
- transferred = true;
326
- return { value: value!, grads: grads as MlxArray[] };
327
- } finally {
328
- inputIds.dispose();
329
- this.curBatch = null;
330
- for (const d of this.headSink) d.dispose(); // no-op unless the head threw mid-step
331
- this.headSink = [];
332
- dhOut?.dispose();
333
- for (const b of boundaries) b?.dispose();
334
- if (!transferred) {
335
- value?.dispose();
336
- for (const g of grads) g?.dispose();
337
- }
338
- }
339
- }
340
-
341
- dispose(): void {
342
- if (this.disposed) return;
343
- this.disposed = true;
344
- this.headVjp.dispose();
345
- for (const v of this.segVjps) v.dispose();
346
- for (const c of this.caches) c.dispose();
347
- this.one.dispose();
348
- }
349
- }
350
-
351
- // ===========================================================================
352
- // e4b (Gemma4Model): segmented backward with KV-shared donor threading.
353
- // ===========================================================================
354
- //
355
- // Same skeleton as SegmentedBackward (forward boundaries -> head vjp -> reverse
356
- // per-segment vjp), with the two e4b-specific pieces (docs §10):
357
- //
358
- // - per-layer-input is a PURE CONSTANT boundary: the per_layer_input_gate /
359
- // per_layer_projection LoRA live inside the layer (differentiated normally
360
- // within each segment); the [B,L,nLayers,width] tensor feeds in as a
361
- // constant (its grad would flow to the embed / per_layer_model_projection,
362
- // neither a LoRA target) — detached once, sliced per segment, cotangent
363
- // discarded.
364
- //
365
- // - the KV-shared donor K/V is a SECOND boundary stream WITH its own cotangent.
366
- // A reused donor d (e4b: 22 sliding, 23 full) is PRODUCED by the segment
367
- // containing d (saved as a detached boundary) and CONSUMED by later segments
368
- // whose sharers reuse it. In the backward, a consumer segment's vjp yields a
369
- // cotangent on the donor K/V (accumulated into dKV[d]); the producer segment
370
- // has a MULTI-OUTPUT forward [h, donorK, donorV] whose vjp takes cotangents
371
- // [dh, dKV[d].k, dKV[d].v] — folding the sharers' gradient into the donor.
372
-
373
- type KvPair = { keys: MlxArray; values: MlxArray };
374
-
375
- export class SegmentedBackwardGemma4 {
376
- private readonly nLayers: number;
377
- private readonly n: number;
378
- private readonly segIdxs: number[][];
379
- private readonly consumes: number[][]; // reused donors consumed as input, per segment
380
- private readonly produces: number[][]; // reused donors output for later segments, per segment
381
- private readonly caches: Cache[];
382
- private readonly headVjp: Vjp;
383
- private readonly segVjps: Vjp[];
384
- private readonly one: MlxArray;
385
- // Per-step constants the (reused) closures read.
386
- private curBatch: SftBatch | null = null;
387
- private curMasks: Map<string, Mask> | null = null;
388
- private curPerLayer: MlxArray | null = null;
389
- // Bounded-head per-step transients — see SegmentedBackward.headSink.
390
- private headSink: Array<{ dispose(): void }> = [];
391
- private disposed = false;
392
-
393
- constructor(
394
- private readonly model: Gemma4Model,
395
- private readonly lora: TrainableLora,
396
- private readonly ranges: [number, number][],
397
- ) {
398
- this.nLayers = model.layers.length;
399
- const lastHi = ranges[ranges.length - 1]?.[1] ?? 0;
400
- if (ranges[0]?.[0] !== 0 || lastHi !== this.nLayers)
401
- throw new Error(`SegmentedBackwardGemma4: ranges must cover [0, ${this.nLayers})`);
402
- this.n = lora.targets.length;
403
-
404
- const layerOf = lora.targets.map((t) => targetLayer(t.modulePath));
405
- this.segIdxs = ranges.map(([lo, hi]) =>
406
- layerOf.map((li, j) => ({ li, j })).filter((x) => x.li >= lo && x.li < hi).map((x) => x.j),
407
- );
408
-
409
- // Donor/sharer KV threading: for each reused donor d, the segment that holds
410
- // d PRODUCES its K/V; a later segment with a sharer of d CONSUMES it.
411
- const segOfLayer = new Map<number, number>();
412
- ranges.forEach(([lo, hi], k) => { for (let i = lo; i < hi; i++) segOfLayer.set(i, k); });
413
- const reused = model.reusedDonors;
414
- const producerSeg = new Map<number, number>();
415
- for (const d of reused) producerSeg.set(d, segOfLayer.get(d)!);
416
- this.consumes = ranges.map(() => []);
417
- this.produces = ranges.map(() => []);
418
- for (let i = 0; i < this.nLayers; i++) {
419
- if (model.cacheIndex[i] !== -1) continue; // donors don't consume
420
- const d = model.previousKvs[i]!; // this sharer's donor (a reused donor)
421
- const sSeg = segOfLayer.get(i)!;
422
- const pSeg = producerSeg.get(d)!;
423
- if (sSeg > pSeg) { // cross-segment: thread d's K/V from pSeg to sSeg
424
- if (!this.consumes[sSeg]!.includes(d)) this.consumes[sSeg]!.push(d);
425
- if (!this.produces[pSeg]!.includes(d)) this.produces[pSeg]!.push(d);
426
- }
427
- // sSeg === pSeg: runLayerRange threads d internally; no boundary needed.
428
- }
429
-
430
- this.caches = Array.from({ length: model.numDonors }, () => new TrainingCache());
431
- this.one = MlxArray.fromFloat32(new Float32Array([1]), []);
432
-
433
- // Loss head (identical to MiniCPM5): finalNorm + bounded response CE over the last
434
- // boundary, vjp with cotangent 1.0 -> [loss], [dh].
435
- this.headVjp = new Vjp((p) => {
436
- const hn = model.finalNorm.forward(p[0]!);
437
- const loss = boundedSftCe(model, hn, this.curBatch!, this.headSink);
438
- hn.dispose();
439
- return [loss];
440
- }, 1);
441
-
442
- // One reused vjp per segment. Inputs: [boundary, (k,v)*consumes, (a,b)*lora].
443
- // Outputs: [h, (k,v)*produces]. The closures read curMasks/curPerLayer.
444
- this.segVjps = ranges.map(([lo, hi], k) => {
445
- const idxs = this.segIdxs[k]!;
446
- const cons = this.consumes[k]!;
447
- const prod = this.produces[k]!;
448
- const base = 1 + 2 * cons.length; // first LoRA primal index
449
- const nOut = 1 + 2 * prod.length;
450
- const vjp = new Vjp((p) => {
451
- const boundary = p[0]!;
452
- const kvIn = new Map<number, SharedKv>();
453
- cons.forEach((d, ci) => kvIn.set(d, { kind: "plain", keys: p[1 + 2 * ci]!, values: p[1 + 2 * ci + 1]!, offset: 0 }));
454
- const saved: [MlxArray, MlxArray][] = idxs.map((j) => [lora.targets[j]!.lw.a, lora.targets[j]!.lw.b]);
455
- idxs.forEach((j, s) => {
456
- lora.targets[j]!.lw.a = p[base + 2 * s]!;
457
- lora.targets[j]!.lw.b = p[base + 2 * s + 1]!;
458
- });
459
- try {
460
- const { h, donorKvOut } = model.runLayerRange(
461
- boundary, lo, hi, this.caches, this.curMasks!, this.curPerLayer, kvIn,
462
- );
463
- const outs: MlxArray[] = [h];
464
- for (const d of prod) {
465
- const kv = donorKvOut.get(d)!;
466
- if (kv.kind !== "plain") throw new Error("segmented e4b: quantized donor KV unsupported in training");
467
- // Output the CONTIGUOUS donor K/V: the consumer segments received a
468
- // contiguous (detachLeaf'd) copy, so their accumulated cotangent dKV
469
- // is row-major. The producer's raw K/V are transposed views; matching
470
- // layouts keeps the vjp's cotangent indexing consistent.
471
- const ck = ops.contiguous(kv.keys), cv = ops.contiguous(kv.values);
472
- kv.keys.dispose(); kv.values.dispose();
473
- outs.push(ck, cv);
474
- }
475
- // in-segment-only reused donors (produced but not threaded forward) — free.
476
- for (const [d, kv] of donorKvOut)
477
- if (!prod.includes(d) && kv.kind === "plain") { kv.keys.dispose(); kv.values.dispose(); }
478
- return outs;
479
- } finally {
480
- idxs.forEach((j, s) => { lora.targets[j]!.lw.a = saved[s]![0]; lora.targets[j]!.lw.b = saved[s]![1]; });
481
- }
482
- }, nOut);
483
- return vjp;
484
- });
485
- }
486
-
487
- step(batch: SftBatch): { value: MlxArray; grads: MlxArray[] } {
488
- if (this.disposed) throw new Error("SegmentedBackwardGemma4 used after dispose");
489
- const { model, lora, ranges, caches, segIdxs, consumes, produces, n } = this;
490
- const B = batch.ids.length;
491
- if (B !== 1) throw new Error("SegmentedBackwardGemma4: only B=1 is supported");
492
- const L = batch.ids[0]!.length;
493
- if (L < 2) throw new Error("SegmentedBackwardGemma4: sequence too short");
494
- this.curBatch = batch;
495
-
496
- const nSeg = ranges.length;
497
- const T = L - 1;
498
- const inputHost = new Int32Array(T);
499
- for (let t = 0; t < T; t++) inputHost[t] = batch.ids[0]![t]!;
500
- const inputIds = MlxArray.fromInt32(inputHost, [1, T]);
501
-
502
- const boundaries: (MlxArray | null)[] = [];
503
- const donorBnd = new Map<number, KvPair>(); // detached reused-donor K/V boundaries
504
- const dKV = new Map<number, KvPair>(); // accumulated reused-donor cotangents
505
- let value: MlxArray | null = null;
506
- let dhOut: MlxArray | null = null;
507
- let transferred = false;
508
- const grads: (MlxArray | null)[] = new Array(2 * n).fill(null);
509
-
510
- try {
511
- // --- 1. Forward, saving detached boundaries (hidden + reused donor K/V). ---
512
- const { hScaled, perLayer } = model.embedForSegmented(inputIds);
513
- boundaries.push(detachLeaf(hScaled));
514
- this.curPerLayer = perLayer ? detachLeaf(perLayer) : null;
515
- this.curMasks = model.makeTrainingMasks(caches, T);
516
-
517
- for (let k = 0; k < nSeg; k++) {
518
- const [lo, hi] = ranges[k]!;
519
- const kvIn = new Map<number, SharedKv>();
520
- for (const d of consumes[k]!) {
521
- const b = donorBnd.get(d)!;
522
- kvIn.set(d, { kind: "plain", keys: b.keys, values: b.values, offset: 0 });
523
- }
524
- const { h, donorKvOut } = model.runLayerRange(boundaries[k]!, lo, hi, caches, this.curMasks, this.curPerLayer, kvIn);
525
- for (const d of produces[k]!) {
526
- const kv = donorKvOut.get(d)!;
527
- if (kv.kind !== "plain") throw new Error("segmented e4b: quantized donor KV unsupported in training");
528
- donorBnd.set(d, { keys: detachLeaf(kv.keys), values: detachLeaf(kv.values) });
529
- }
530
- for (const [d, kv] of donorKvOut)
531
- if (!produces[k]!.includes(d) && kv.kind === "plain") { kv.keys.dispose(); kv.values.dispose(); }
532
- boundaries.push(detachLeaf(h));
533
- segMem(`fwd seg ${k} [${lo},${hi})`);
534
- }
535
-
536
- // --- 2. Loss head (vjp, cotangent 1.0). Bounded-head sink discipline:
537
- // materialize the head-vjp graph BEFORE freeing the sink (see the
538
- // MiniCPM5 class / ORPO classes — lazy-eval UAF). ---
539
- this.headSink = [];
540
- const head = this.headVjp.apply([boundaries[nSeg]!], [this.one]);
541
- ops.evalAll([head.outputs[0]!, head.vjps[0]!]);
542
- value = detachLeaf(head.outputs[0]!);
543
- dhOut = detachLeaf(head.vjps[0]!);
544
- for (const d of this.headSink) d.dispose();
545
- this.headSink = [];
546
- boundaries[nSeg]!.dispose();
547
- boundaries[nSeg] = null;
548
- segMem("head vjp");
549
-
550
- // --- 3. Backward, reverse over segments (vjp, cotangents dh + dKV). ---
551
- for (let k = nSeg - 1; k >= 0; k--) {
552
- const idxs = segIdxs[k]!;
553
- const cons = consumes[k]!;
554
- const prod = produces[k]!;
555
- const dh = dhOut!;
556
-
557
- const primals: MlxArray[] = [boundaries[k]!];
558
- for (const d of cons) { const b = donorBnd.get(d)!; primals.push(b.keys, b.values); }
559
- for (const j of idxs) primals.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
560
-
561
- const cots: MlxArray[] = [dh];
562
- for (const d of prod) {
563
- const acc = dKV.get(d)!; // set by the consumer segments already processed (reverse order)
564
- cots.push(acc.keys, acc.values);
565
- }
566
-
567
- const res = this.segVjps[k]!.apply(primals, cots);
568
- for (const o of res.outputs) o.dispose();
569
-
570
- // vjps[0] = dh_in; then (dk,dv) per consumed donor; then (da,db) per
571
- // LoRA — detached in ONE barrier (backlog #9).
572
- const leaves = detachLeaves(res.vjps);
573
- const newDh = leaves[0]!;
574
- cons.forEach((d, ci) => {
575
- this.accumulateDKV(dKV, d, leaves[1 + 2 * ci]!, leaves[1 + 2 * ci + 1]!);
576
- });
577
- const base = 1 + 2 * cons.length;
578
- idxs.forEach((j, s) => {
579
- grads[j] = leaves[base + 2 * s]!;
580
- grads[n + j] = leaves[base + 2 * s + 1]!;
581
- });
582
-
583
- dh.dispose();
584
- dhOut = newDh;
585
- // produced donors' cotangents are now consumed -> free them + their boundary.
586
- for (const d of prod) {
587
- const acc = dKV.get(d)!; acc.keys.dispose(); acc.values.dispose(); dKV.delete(d);
588
- const b = donorBnd.get(d)!; b.keys.dispose(); b.values.dispose(); donorBnd.delete(d);
589
- }
590
- boundaries[k]!.dispose();
591
- boundaries[k] = null;
592
- segMem(`bwd seg ${k} [${ranges[k]![0]},${ranges[k]![1]})`);
593
- }
594
-
595
- transferred = true;
596
- return { value: value!, grads: grads as MlxArray[] };
597
- } finally {
598
- inputIds.dispose();
599
- this.curBatch = null;
600
- this.curPerLayer?.dispose();
601
- this.curPerLayer = null;
602
- if (this.curMasks) { for (const m of this.curMasks.values()) m.arr?.dispose(); this.curMasks = null; }
603
- for (const d of this.headSink) d.dispose(); // no-op unless the head threw mid-step
604
- this.headSink = [];
605
- dhOut?.dispose();
606
- for (const b of boundaries) b?.dispose();
607
- for (const kv of donorBnd.values()) { kv.keys.dispose(); kv.values.dispose(); }
608
- for (const kv of dKV.values()) { kv.keys.dispose(); kv.values.dispose(); }
609
- if (!transferred) {
610
- value?.dispose();
611
- for (const g of grads) g?.dispose();
612
- }
613
- }
614
- }
615
-
616
- /** Accumulate a reused-donor K/V cotangent (sum across consuming segments) in
617
- * bf16. The donor K/V grad ends up ~0.5-1% (relNorm) off the full backward —
618
- * bf16 NON-ASSOCIATIVITY, established (not a logic bug), via:
619
- * - single-consumer reuse is BIT-EXACT (no sum to reorder);
620
- * - the error is flat in consumer-SEGMENT count and tracks SUMMAND count
621
- * (donor 22's 15 sharers ≈0.46%, donor 23's 3 sharers ≈0.000%);
622
- * - it is grouping-DEPENDENT (donor-22 grad moves ~0.5% from a 2- vs
623
- * 9-consumer split) for BOTH bf16 AND fp32 accumulation.
624
- * fp32 cross-segment accumulation does NOT fix it (it's identical at the
625
- * natural cut and marginally worse at fine cuts): the dominant term is mlx's
626
- * WITHIN-vjp bf16 cotangent sum per consumer, which regrouping the sharers
627
- * changes — unfixable without an fp32 forward. bf16-class, fine for training.
628
- * `dk`/`dv` are detached bf16 leaves; the sum is re-detached. */
629
- private accumulateDKV(dKV: Map<number, KvPair>, d: number, dk: MlxArray, dv: MlxArray): void {
630
- const prev = dKV.get(d);
631
- if (!prev) { dKV.set(d, { keys: dk, values: dv }); return; }
632
- const sk = detachLeaf(ops.add(prev.keys, dk));
633
- const sv = detachLeaf(ops.add(prev.values, dv));
634
- prev.keys.dispose(); prev.values.dispose(); dk.dispose(); dv.dispose();
635
- dKV.set(d, { keys: sk, values: sv });
636
- }
637
-
638
- dispose(): void {
639
- if (this.disposed) return;
640
- this.disposed = true;
641
- this.headVjp.dispose();
642
- for (const v of this.segVjps) v.dispose();
643
- for (const c of this.caches) c.dispose();
644
- this.one.dispose();
645
- }
646
- }
647
-
648
- // ===========================================================================
649
- // SegmentedBackwardOrpo: segmented backward for ORPO (MiniCPM5 only, B=1).
650
- // ===========================================================================
651
- //
652
- // Same skeleton as SegmentedBackward, but operates over TWO sequences
653
- // (chosen + rejected) per step, accumulating LoRA gradients from both branches.
654
- // Memory profile: TWO sets of boundary buffers ([1,T,hidden] per segment edge)
655
- // are live during the forward phase; only one segment's activations are live per
656
- // backward sub-step (one branch at a time). Grads from chosen and rejected are
657
- // summed per parameter before being returned.
658
- //
659
- // The head VJP takes [h_chosen_last, h_rejected_last] → [loss scalar].
660
- // Its cotangents [dh_chosen, dh_rejected] seed the two backward passes separately.
661
-
662
- /** Segmented backward for ORPO (MiniCPM5 only, B=1). Runs chosen+rejected forwards
663
- * segment-by-segment, accumulating LoRA gradients from both branches. */
664
- export class SegmentedBackwardOrpo {
665
- private readonly nLayers: number;
666
- private readonly n: number;
667
- private readonly segIdxs: number[][];
668
- private readonly caches: Cache[];
669
- private readonly headVjp: Vjp;
670
- private readonly segVjps: Vjp[];
671
- private readonly one: MlxArray;
672
- // Per-step mutable context
673
- private curChosenIds: number[] | null = null;
674
- private curChosenMask: number[] | null = null;
675
- private curRejectedIds: number[] | null = null;
676
- private curRejectedMask: number[] | null = null;
677
- private disposed = false;
678
-
679
- // When > 0, the loss head runs the FUSED linear-CE path (fusedLogpMeanFromHidden:
680
- // token-chunked analytic CustomVjp) instead of the full-[M,V] responseOnlyLogpMean
681
- // — so the head term is bounded to [chunk,V] INSIDE the segmented backward,
682
- // compounding with the per-segment layer savings (ORPO pressure = layers +
683
- // response length + head). Per-step disposables (the CustomVjps + response slices)
684
- // live in headSink, disposed after the head vjp is eval'd.
685
- private headSink: Array<{ dispose(): void }> = [];
686
-
687
- constructor(
688
- private readonly model: MiniCPM5Model,
689
- private readonly lora: TrainableLora,
690
- private readonly ranges: [number, number][],
691
- private readonly lambda: number,
692
- private readonly fusedChunkSize = 0,
693
- private readonly sftScope: SftScope = "response",
694
- ) {
695
- this.nLayers = model.layers.length;
696
- const lastHi = ranges[ranges.length - 1]?.[1] ?? 0;
697
- if (ranges[0]?.[0] !== 0 || lastHi !== this.nLayers)
698
- throw new Error(`SegmentedBackwardOrpo: ranges must cover [0, ${this.nLayers})`);
699
- this.n = lora.targets.length;
700
- const layerOf = lora.targets.map((t) => targetLayer(t.modulePath));
701
- this.segIdxs = ranges.map(([lo, hi]) =>
702
- layerOf.map((li, j) => ({ li, j })).filter((x) => x.li >= lo && x.li < hi).map((x) => x.j),
703
- );
704
- this.caches = Array.from({ length: this.nLayers }, () => new TrainingCache());
705
- this.one = MlxArray.fromFloat32(new Float32Array([1]), []);
706
-
707
- // Head VJP: inputs = [h_chosen_last, h_rejected_last] (pre-finalNorm at last boundary).
708
- // Applies finalNorm to each, computes ℓw and ℓr, returns scalar orpo loss.
709
- // nOut = 1 (one scalar loss output, one cotangent needed from caller).
710
- // apply([hC, hR], [one]) returns vjps[0] = dh_chosen, vjps[1] = dh_rejected.
711
- this.headVjp = new Vjp((p) => {
712
- const hnC = model.finalNorm.forward(p[0]!); // [1, T, hidden]
713
- const hnR = model.finalNorm.forward(p[1]!);
714
- const cs = this.fusedChunkSize;
715
- const lw = cs > 0 // [1]
716
- ? boundedHeadFromHidden(model, hnC, this.curChosenIds!, this.curChosenMask!, cs, this.headSink)
717
- : responseOnlyLogpMean(model, hnC, this.curChosenIds!, this.curChosenMask!);
718
- const lr = cs > 0 // [1]
719
- ? boundedHeadFromHidden(model, hnR, this.curRejectedIds!, this.curRejectedMask!, cs, this.headSink)
720
- : responseOnlyLogpMean(model, hnR, this.curRejectedIds!, this.curRejectedMask!);
721
- // sft_scope:"full": the chosen head additionally runs over the PROMPT span
722
- // [0, startT) of the SAME hnC (same tier: bounded or whole-vocab), and the
723
- // full-scope chosen NLL replaces mean(-ℓw). The prompt-position gradient
724
- // rides into this vjp's dh_chosen automatically (the loss graph includes
725
- // the prompt-span head). ℓw/ℓr stay response-only for the odds ratio.
726
- let nllFull: MlxArray | null = null;
727
- if (this.sftScope === "full") {
728
- const { startT, M } = respSpanFromMask(this.curChosenMask!, this.curChosenIds!.length - 1);
729
- let pm: MlxArray | null = null;
730
- if (M > 0 && startT > 0)
731
- pm = cs > 0
732
- ? boundedHeadFromHidden(model, hnC, this.curChosenIds!, this.curChosenMask!, cs, this.headSink, { startT: 0, M: startT })
733
- : spanLogpMeanFromHidden(model, hnC, this.curChosenIds!, 0, startT);
734
- nllFull = combineFullNll(pm, lw, M > 0 ? startT : 0, M);
735
- pm?.dispose();
736
- }
737
- hnC.dispose(); hnR.dispose();
738
- const loss = orpoLossFromLogps(lw, lr, this.lambda, nllFull ?? undefined);
739
- lw.dispose(); lr.dispose();
740
- nllFull?.dispose();
741
- return [loss];
742
- }, 1);
743
-
744
- // One reused vjp per segment — same as SegmentedBackward; reused across both branches.
745
- this.segVjps = ranges.map(([lo, hi], k) => {
746
- const idxs = this.segIdxs[k]!;
747
- return new Vjp((p) => {
748
- const saved: [MlxArray, MlxArray][] = idxs.map((j) => [lora.targets[j]!.lw.a, lora.targets[j]!.lw.b]);
749
- idxs.forEach((j, s) => { lora.targets[j]!.lw.a = p[1 + 2 * s]!; lora.targets[j]!.lw.b = p[1 + 2 * s + 1]!; });
750
- try {
751
- return [model.runLayerRange(p[0]!, lo, hi, this.caches)];
752
- } finally {
753
- idxs.forEach((j, s) => { lora.targets[j]!.lw.a = saved[s]![0]; lora.targets[j]!.lw.b = saved[s]![1]; });
754
- }
755
- }, 1);
756
- });
757
- }
758
-
759
- step(batch: DpoBatch): { value: MlxArray; grads: MlxArray[] } {
760
- if (this.disposed) throw new Error("SegmentedBackwardOrpo used after dispose");
761
- const { model, lora, ranges, caches, segIdxs, n } = this;
762
- if (batch.chosenIds.length !== 1) throw new Error("SegmentedBackwardOrpo: only B=1 is supported");
763
- // Store per-step batch for the head VJP closure
764
- this.curChosenIds = batch.chosenIds[0]!;
765
- this.curChosenMask = batch.chosenMask[0]!;
766
- this.curRejectedIds = batch.rejectedIds[0]!;
767
- this.curRejectedMask = batch.rejectedMask[0]!;
768
-
769
- const Lc = batch.chosenIds[0]!.length;
770
- const Lr = batch.rejectedIds[0]!.length;
771
- if (Lc < 2 || Lr < 2) throw new Error("SegmentedBackwardOrpo: sequences too short");
772
- const nSeg = ranges.length;
773
- const gradsC: (MlxArray | null)[] = new Array(2 * n).fill(null);
774
- const gradsR: (MlxArray | null)[] = new Array(2 * n).fill(null);
775
- const bndC: (MlxArray | null)[] = [];
776
- const bndR: (MlxArray | null)[] = [];
777
- let value: MlxArray | null = null;
778
- let dhC: MlxArray | null = null;
779
- let dhR: MlxArray | null = null;
780
- let transferred = false;
781
-
782
- try {
783
- // --- 1. Forward chosen ---
784
- const Tc = Lc - 1;
785
- const chosenInput = new Int32Array(Tc);
786
- for (let t = 0; t < Tc; t++) chosenInput[t] = batch.chosenIds[0]![t]!;
787
- const chosenIds = MlxArray.fromInt32(chosenInput, [1, Tc]);
788
- bndC.push(detachLeaf(model.embed.encode(chosenIds)));
789
- chosenIds.dispose();
790
- for (let k = 0; k < nSeg; k++) {
791
- const [lo, hi] = ranges[k]!;
792
- bndC.push(detachLeaf(model.runLayerRange(bndC[k]!, lo, hi, caches)));
793
- }
794
-
795
- // --- 2. Forward rejected ---
796
- const Tr = Lr - 1;
797
- const rejInput = new Int32Array(Tr);
798
- for (let t = 0; t < Tr; t++) rejInput[t] = batch.rejectedIds[0]![t]!;
799
- const rejIds = MlxArray.fromInt32(rejInput, [1, Tr]);
800
- bndR.push(detachLeaf(model.embed.encode(rejIds)));
801
- rejIds.dispose();
802
- for (let k = 0; k < nSeg; k++) {
803
- const [lo, hi] = ranges[k]!;
804
- bndR.push(detachLeaf(model.runLayerRange(bndR[k]!, lo, hi, caches)));
805
- }
806
-
807
- // --- 3. Head VJP: [last_chosen, last_rejected] → [loss], cotangent 1.0 → [dh_c, dh_r] ---
808
- // The fused head pushes per-step CustomVjps + response slices into headSink;
809
- // they recompute during this apply's backward, so dispose only AFTER the
810
- // outputs/vjps are detached to leaves (which forces the eval).
811
- this.headSink = [];
812
- const head = this.headVjp.apply([bndC[nSeg]!, bndR[nSeg]!], [this.one]);
813
- // Materialize the full head-VJP graph (incl. the nested CustomVjp backward that
814
- // reads savedLse/savedBlockMax) BEFORE freeing headSink — avoids the lazy-eval
815
- // use-after-free segfault (see the segmented-prefix crash diagnosis).
816
- ops.evalAll([head.outputs[0]!, head.vjps[0]!, head.vjps[1]!]);
817
- value = detachLeaf(head.outputs[0]!);
818
- dhC = detachLeaf(head.vjps[0]!);
819
- dhR = detachLeaf(head.vjps[1]!);
820
- for (const d of this.headSink) d.dispose();
821
- this.headSink = [];
822
- bndC[nSeg]!.dispose(); bndC[nSeg] = null;
823
- bndR[nSeg]!.dispose(); bndR[nSeg] = null;
824
-
825
- // --- 4. Backward chosen ---
826
- for (let k = nSeg - 1; k >= 0; k--) {
827
- const idxs = segIdxs[k]!;
828
- const params: MlxArray[] = [bndC[k]!];
829
- for (const j of idxs) params.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
830
- const res = this.segVjps[k]!.apply(params, [dhC!]);
831
- res.outputs[0]!.dispose();
832
- const leaves = detachLeaves(res.vjps); // one barrier (backlog #9)
833
- idxs.forEach((j, s) => {
834
- gradsC[j] = leaves[1 + 2 * s]!; // dA
835
- gradsC[n + j] = leaves[1 + 2 * s + 1]!; // dB
836
- });
837
- dhC!.dispose();
838
- dhC = leaves[0]!;
839
- bndC[k]!.dispose(); bndC[k] = null;
840
- }
841
- dhC!.dispose(); dhC = null; // embedding grad, discarded (no LoRA on embed)
842
-
843
- // --- 5. Backward rejected ---
844
- for (let k = nSeg - 1; k >= 0; k--) {
845
- const idxs = segIdxs[k]!;
846
- const params: MlxArray[] = [bndR[k]!];
847
- for (const j of idxs) params.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
848
- const res = this.segVjps[k]!.apply(params, [dhR!]);
849
- res.outputs[0]!.dispose();
850
- const leaves = detachLeaves(res.vjps); // one barrier (backlog #9)
851
- idxs.forEach((j, s) => {
852
- gradsR[j] = leaves[1 + 2 * s]!; // dA
853
- gradsR[n + j] = leaves[1 + 2 * s + 1]!; // dB
854
- });
855
- dhR!.dispose();
856
- dhR = leaves[0]!;
857
- bndR[k]!.dispose(); bndR[k] = null;
858
- }
859
- dhR!.dispose(); dhR = null; // embedding grad, discarded
860
-
861
- // --- 6. Sum LoRA grads from both branches ---
862
- const grads: MlxArray[] = gradsC.map((gc, i) => {
863
- const gr = gradsR[i]!;
864
- const sum = ops.add(gc!, gr);
865
- gc!.dispose(); gr.dispose();
866
- gradsC[i] = null; gradsR[i] = null;
867
- return detachLeaf(sum);
868
- });
869
-
870
- transferred = true;
871
- return { value: value!, grads };
872
- } finally {
873
- this.curChosenIds = this.curChosenMask = this.curRejectedIds = this.curRejectedMask = null;
874
- dhC?.dispose(); dhR?.dispose();
875
- for (const b of bndC) b?.dispose();
876
- for (const b of bndR) b?.dispose();
877
- if (!transferred) {
878
- value?.dispose();
879
- for (const g of gradsC) g?.dispose();
880
- for (const g of gradsR) g?.dispose();
881
- }
882
- }
883
- }
884
-
885
- dispose(): void {
886
- if (this.disposed) return;
887
- this.disposed = true;
888
- this.headVjp.dispose();
889
- for (const v of this.segVjps) v.dispose();
890
- for (const c of this.caches) c.dispose();
891
- this.one.dispose();
892
- }
893
- }
894
-
895
- // ===========================================================================
896
- // SegmentedBackwardOrpoPrefix: segmented backward + PREFIX-SHARING (MiniCPM5, B=1).
897
- // ===========================================================================
898
- //
899
- // The composition of SegmentedBackwardOrpo (layer-streaming backward) with
900
- // prefix-sharing (one forward over [prompt; chosen; rejected], block-sparse mask
901
- // + block-wise RoPE). Instead of TWO separate sequences, this streams ONE concat
902
- // sequence segment-by-segment, so the shared prompt is encoded exactly once even
903
- // at long sequence length — the prefix-share saving now holds AT seq 8192.
904
- //
905
- // Differences from SegmentedBackwardOrpo:
906
- // - input = the single concat [prompt(P); chosen(Rc); rejected(Rr)] (T = P+Rc+Rr),
907
- // NOT two branches. One boundary stream, one backward walk.
908
- // - per-layer cache = PrefixSharedCache (its makeMask returns the block-sparse
909
- // mask: chosen & rejected each attend the prompt but NOT each other). Flows
910
- // through runLayerRange automatically (it calls cache[aIdx].makeMask).
911
- // - setMiniCpmPrefixPlan({P,Rc,Rr}) is ACTIVE for the whole step (set in step(),
912
- // cleared in finally) so block-wise RoPE rides through BOTH the streaming
913
- // forward AND every segment's recompute inside the per-segment vjp.
914
- // - the head gathers the chosen/rejected response hiddens from the final
915
- // [1,T,hidden] (via branchLogpMeanGathered — the same [M,V]-free per-branch
916
- // flash head the non-segmented prefix path uses) and ORPO-combines them. Its
917
- // vjp w.r.t. the last boundary is the full [1,T,hidden] dh (the gather's
918
- // takeAxis vjp scatter-adds each branch's response-position cotangent back
919
- // into the full sequence), which seeds the single segmented backward.
920
-
921
- export class SegmentedBackwardOrpoPrefix {
922
- private readonly nLayers: number;
923
- private readonly n: number;
924
- private readonly segIdxs: number[][];
925
- private readonly headVjp: Vjp;
926
- private readonly segVjps: Vjp[];
927
- private readonly one: MlxArray;
928
- // Per-step mutable context the (reused) closures read.
929
- private curP = 0;
930
- private curRc = 0;
931
- private curRr = 0;
932
- private curPromptIds: number[] | null = null;
933
- private curChosenResp: number[] | null = null;
934
- private curRejectedResp: number[] | null = null;
935
- private curChunk: ChunkCtx | undefined = undefined;
936
- // Per-step prefix caches (rebuilt each step — they capture P/Rc/Rr). The seg
937
- // vjps read them via this.curCaches so makeMask returns the block-sparse mask.
938
- private curCaches: Cache[] = [];
939
- private headSink: Array<{ dispose(): void }> = [];
940
- private disposed = false;
941
-
942
- constructor(
943
- private readonly model: MiniCPM5Model,
944
- private readonly lora: TrainableLora,
945
- private readonly ranges: [number, number][],
946
- private readonly lambda: number,
947
- private readonly chunkCtx?: ChunkCtx,
948
- private readonly sftScope: SftScope = "response",
949
- ) {
950
- this.nLayers = model.layers.length;
951
- const lastHi = ranges[ranges.length - 1]?.[1] ?? 0;
952
- if (ranges[0]?.[0] !== 0 || lastHi !== this.nLayers)
953
- throw new Error(`SegmentedBackwardOrpoPrefix: ranges must cover [0, ${this.nLayers})`);
954
- this.n = lora.targets.length;
955
- const layerOf = lora.targets.map((t) => targetLayer(t.modulePath));
956
- this.segIdxs = ranges.map(([lo, hi]) =>
957
- layerOf.map((li, j) => ({ li, j })).filter((x) => x.li >= lo && x.li < hi).map((x) => x.j),
958
- );
959
- this.one = MlxArray.fromFloat32(new Float32Array([1]), []);
960
-
961
- // Head VJP: input = the last boundary [1, T, hidden] (pre-finalNorm). Applies
962
- // finalNorm, gathers each branch's response hiddens, computes ℓw/ℓr through the
963
- // [M,V]-free per-branch head (branchLogpMeanGathered), returns the scalar orpo
964
- // loss. nOut = 1; apply([h_last], [one]) returns vjps[0] = dh [1, T, hidden]
965
- // (the gather's takeAxis vjp scatter-adds the per-branch dh into the full seq).
966
- this.headVjp = new Vjp((p) => {
967
- const hn = model.finalNorm.forward(p[0]!); // [1, T, hidden]
968
- const { chosenIdx, rejectedIdx } = prefixGatherIdx(this.curP, this.curRc, this.curRr);
969
- const lw = branchLogpMeanGathered(model, hn, chosenIdx, this.curChosenResp!, this.curChunk);
970
- const lr = branchLogpMeanGathered(model, hn, rejectedIdx, this.curRejectedResp!, this.curChunk);
971
- // sft_scope:"full": prompt predictions (H[0..P-2] → prompt[1..P-1]) from the
972
- // same concat hiddens; the gather's takeAxis vjp scatter-adds the prompt
973
- // cotangent into dh alongside the responses'. ℓw/ℓr stay response-only.
974
- const nllFull = this.sftScope === "full"
975
- ? prefixFullNll(model, hn, this.curPromptIds!, lw, this.curRc, this.curChunk)
976
- : null;
977
- hn.dispose();
978
- const loss = orpoLossFromLogps(lw, lr, this.lambda, nllFull ?? undefined);
979
- lw.dispose(); lr.dispose();
980
- nllFull?.dispose();
981
- return [loss];
982
- }, 1);
983
-
984
- // One reused vjp per segment — same shape as SegmentedBackwardOrpo, but the
985
- // forward calls runLayerRange with the PREFIX caches (block-sparse mask). The
986
- // prefix RoPE plan is active for the whole step (set in step), so the recompute
987
- // here ropes block-wise too. (branchLogpMeanGathered handles the head; these
988
- // only stream the transformer layers.)
989
- this.segVjps = ranges.map(([lo, hi], k) => {
990
- const idxs = this.segIdxs[k]!;
991
- return new Vjp((p) => {
992
- const saved: [MlxArray, MlxArray][] = idxs.map((j) => [lora.targets[j]!.lw.a, lora.targets[j]!.lw.b]);
993
- idxs.forEach((j, s) => { lora.targets[j]!.lw.a = p[1 + 2 * s]!; lora.targets[j]!.lw.b = p[1 + 2 * s + 1]!; });
994
- try {
995
- return [model.runLayerRange(p[0]!, lo, hi, this.curCaches)];
996
- } finally {
997
- idxs.forEach((j, s) => { lora.targets[j]!.lw.a = saved[s]![0]; lora.targets[j]!.lw.b = saved[s]![1]; });
998
- }
999
- }, 1);
1000
- });
1001
- }
1002
-
1003
- /** One ORPO step over the prefix-shared concat. `promptIds`/`chosenResp`/
1004
- * `rejectedResp` come from splitPrefixBatch. Returns the loss + LoRA grads in
1005
- * flatParams(lora) order (caller owns). */
1006
- stepPrefix(promptIds: number[], chosenResp: number[], rejectedResp: number[]): { value: MlxArray; grads: MlxArray[] } {
1007
- if (this.disposed) throw new Error("SegmentedBackwardOrpoPrefix used after dispose");
1008
- const { model, lora, ranges, segIdxs, n } = this;
1009
- const P = promptIds.length, Rc = chosenResp.length, Rr = rejectedResp.length;
1010
- if (P < 1 || Rc < 1 || Rr < 1) throw new Error("SegmentedBackwardOrpoPrefix: need P,Rc,Rr >= 1");
1011
- const T = P + Rc + Rr;
1012
- this.curP = P; this.curRc = Rc; this.curRr = Rr;
1013
- this.curPromptIds = promptIds;
1014
- this.curChosenResp = chosenResp; this.curRejectedResp = rejectedResp;
1015
- // Per-step chunk ctx pointing the head's CustomVjp/slice disposables at this
1016
- // step's headSink (the flash/fused head recompute runs during headVjp.apply;
1017
- // we dispose headSink only AFTER the outputs/vjps are detached -> eval forced).
1018
- this.headSink = [];
1019
- this.curChunk = this.chunkCtx ? { ...this.chunkCtx, sink: this.headSink } : undefined;
1020
- // One prefix cache per layer (captures P/Rc/Rr; makeMask -> block-sparse mask).
1021
- this.curCaches = prefixSharedCaches(model.layers.length, P, Rc, Rr);
1022
-
1023
- const nSeg = ranges.length;
1024
- const concat = new Int32Array(T);
1025
- concat.set(promptIds, 0);
1026
- concat.set(chosenResp, P);
1027
- concat.set(rejectedResp, P + Rc);
1028
- const inputIds = MlxArray.fromInt32(concat, [1, T]);
1029
-
1030
- const boundaries: (MlxArray | null)[] = [];
1031
- let value: MlxArray | null = null;
1032
- let dhOut: MlxArray | null = null;
1033
- let transferred = false;
1034
- const grads: (MlxArray | null)[] = new Array(2 * n).fill(null);
1035
-
1036
- // Block-wise RoPE active for the WHOLE step (forward + every segment recompute).
1037
- setMiniCpmPrefixPlan({ P, Rc, Rr });
1038
- try {
1039
- // --- 1. Forward over the concat, saving detached boundaries at seg edges. ---
1040
- boundaries.push(detachLeaf(model.embed.encode(inputIds))); // [1, T, hidden]
1041
- for (let k = 0; k < nSeg; k++) {
1042
- const [lo, hi] = ranges[k]!;
1043
- boundaries.push(detachLeaf(model.runLayerRange(boundaries[k]!, lo, hi, this.curCaches)));
1044
- }
1045
-
1046
- // --- 2. Head over the last boundary (vjp, cotangent 1.0) -> [dh 1,T,hidden]. ---
1047
- const head = this.headVjp.apply([boundaries[nSeg]!], [this.one]);
1048
- // Force the FULL head-VJP graph (incl. the nested flash-CCE CustomVjp backward,
1049
- // which reads savedLse/savedBlockMax) to materialize BEFORE the headSink dispose
1050
- // below — else that deferred backward can run AFTER its lse/blockMax are freed
1051
- // under MLX lazy eval (a use-after-free → segfault at a tiny address, ~step 100).
1052
- ops.evalAll([head.outputs[0]!, head.vjps[0]!]);
1053
- value = detachLeaf(head.outputs[0]!);
1054
- dhOut = detachLeaf(head.vjps[0]!); // dLoss / d(last residual stream), [1,T,hidden]
1055
- for (const d of this.headSink) d.dispose();
1056
- this.headSink = [];
1057
- boundaries[nSeg]!.dispose(); boundaries[nSeg] = null;
1058
-
1059
- // --- 3. Backward, reverse over segments (single stream, cotangent = dh). ---
1060
- for (let k = nSeg - 1; k >= 0; k--) {
1061
- const idxs = segIdxs[k]!;
1062
- const params: MlxArray[] = [boundaries[k]!];
1063
- for (const j of idxs) params.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
1064
- const res = this.segVjps[k]!.apply(params, [dhOut!]);
1065
- res.outputs[0]!.dispose();
1066
- const leaves = detachLeaves(res.vjps); // one barrier (backlog #9)
1067
- idxs.forEach((j, s) => {
1068
- grads[j] = leaves[1 + 2 * s]!; // dA
1069
- grads[n + j] = leaves[1 + 2 * s + 1]!; // dB
1070
- });
1071
- dhOut!.dispose();
1072
- dhOut = leaves[0]!;
1073
- boundaries[k]!.dispose(); boundaries[k] = null;
1074
- }
1075
- // Segment-0 dh_in = grad w.r.t. the embedding output (no LoRA on embed) — drop.
1076
-
1077
- transferred = true;
1078
- return { value: value!, grads: grads as MlxArray[] };
1079
- } finally {
1080
- setMiniCpmPrefixPlan(null);
1081
- inputIds.dispose();
1082
- for (const c of this.curCaches) c.dispose();
1083
- this.curCaches = [];
1084
- this.curPromptIds = this.curChosenResp = this.curRejectedResp = null;
1085
- for (const d of this.headSink) d.dispose();
1086
- this.headSink = [];
1087
- dhOut?.dispose();
1088
- for (const b of boundaries) b?.dispose();
1089
- if (!transferred) {
1090
- value?.dispose();
1091
- for (const g of grads) g?.dispose();
1092
- }
1093
- }
1094
- }
1095
-
1096
- dispose(): void {
1097
- if (this.disposed) return;
1098
- this.disposed = true;
1099
- this.headVjp.dispose();
1100
- for (const v of this.segVjps) v.dispose();
1101
- this.one.dispose();
1102
- }
1103
- }
1104
-
1105
- // ===========================================================================
1106
- // SegmentedBackwardOrpoGemma4: segmented backward for ORPO on e4b (B=1).
1107
- // ===========================================================================
1108
- //
1109
- // The cross of SegmentedBackwardGemma4 (KV-shared donor threading + per-layer-
1110
- // input boundary + per-segment vjp with consumes/produces) and
1111
- // SegmentedBackwardOrpo (TWO sequences — chosen + rejected — and the ORPO head).
1112
- //
1113
- // Per step: forward each branch segment-by-segment (saving detached hidden +
1114
- // reused-donor K/V boundaries, the branch's per-layer-input constant, and the
1115
- // branch's masks); the ORPO head vjp over [last_chosen, last_rejected] -> loss
1116
- // yields [dh_chosen, dh_rejected]; then each branch is walked backward in
1117
- // reverse over its segments (donor cotangents accumulated in dKV exactly as in
1118
- // SegmentedBackwardGemma4). LoRA grads from the two branches are summed.
1119
- //
1120
- // The donor consumes/produces TOPOLOGY is a function of the architecture and the
1121
- // segmentation only (not the data), so it is computed ONCE and reused for both
1122
- // branches. The masks and per-layer-input DIFFER per branch (different lengths),
1123
- // so the reused per-segment vjp closures read the active branch's masks/perLayer
1124
- // off curMasks/curPerLayer, swapped per backward pass.
1125
-
1126
- /** Per-branch forward state: the detached boundaries + reused-donor K/V
1127
- * boundaries, plus the masks and per-layer-input constant for THAT branch
1128
- * (held across its backward; the reused seg vjps read them via curMasks/
1129
- * curPerLayer). The step's finally disposes whatever survives. */
1130
- interface BranchState {
1131
- boundaries: (MlxArray | null)[];
1132
- donorBnd: Map<number, KvPair>;
1133
- masks: Map<string, Mask> | null;
1134
- perLayer: MlxArray | null;
1135
- }
1136
-
1137
- export class SegmentedBackwardOrpoGemma4 {
1138
- private readonly nLayers: number;
1139
- private readonly n: number;
1140
- private readonly segIdxs: number[][];
1141
- private readonly consumes: number[][];
1142
- private readonly produces: number[][];
1143
- private readonly caches: Cache[];
1144
- private readonly headVjp: Vjp;
1145
- private readonly segVjps: Vjp[];
1146
- private readonly one: MlxArray;
1147
- // Per-step / per-branch mutable context the (reused) closures read.
1148
- private curChosenIds: number[] | null = null;
1149
- private curChosenMask: number[] | null = null;
1150
- private curRejectedIds: number[] | null = null;
1151
- private curRejectedMask: number[] | null = null;
1152
- private curMasks: Map<string, Mask> | null = null; // active branch (set per backward pass)
1153
- private curPerLayer: MlxArray | null = null;
1154
- // Fused linear-CE head sink (see SegmentedBackwardOrpo): per-step CustomVjps +
1155
- // response slices, disposed after the head vjp is eval'd. Bounds the head term
1156
- // to [chunk,V] inside the e4b segmented backward.
1157
- private headSink: Array<{ dispose(): void }> = [];
1158
- private disposed = false;
1159
-
1160
- constructor(
1161
- private readonly model: Gemma4Model,
1162
- private readonly lora: TrainableLora,
1163
- private readonly ranges: [number, number][],
1164
- private readonly lambda: number,
1165
- private readonly fusedChunkSize = 0,
1166
- private readonly sftScope: SftScope = "response",
1167
- ) {
1168
- this.nLayers = model.layers.length;
1169
- const lastHi = ranges[ranges.length - 1]?.[1] ?? 0;
1170
- if (ranges[0]?.[0] !== 0 || lastHi !== this.nLayers)
1171
- throw new Error(`SegmentedBackwardOrpoGemma4: ranges must cover [0, ${this.nLayers})`);
1172
- this.n = lora.targets.length;
1173
-
1174
- const layerOf = lora.targets.map((t) => targetLayer(t.modulePath));
1175
- this.segIdxs = ranges.map(([lo, hi]) =>
1176
- layerOf.map((li, j) => ({ li, j })).filter((x) => x.li >= lo && x.li < hi).map((x) => x.j),
1177
- );
1178
-
1179
- // Donor/sharer KV threading topology (identical to SegmentedBackwardGemma4):
1180
- // architecture + segmentation only, so shared across both branches.
1181
- const segOfLayer = new Map<number, number>();
1182
- ranges.forEach(([lo, hi], k) => { for (let i = lo; i < hi; i++) segOfLayer.set(i, k); });
1183
- const reused = model.reusedDonors;
1184
- const producerSeg = new Map<number, number>();
1185
- for (const d of reused) producerSeg.set(d, segOfLayer.get(d)!);
1186
- this.consumes = ranges.map(() => []);
1187
- this.produces = ranges.map(() => []);
1188
- for (let i = 0; i < this.nLayers; i++) {
1189
- if (model.cacheIndex[i] !== -1) continue;
1190
- const d = model.previousKvs[i]!;
1191
- const sSeg = segOfLayer.get(i)!;
1192
- const pSeg = producerSeg.get(d)!;
1193
- if (sSeg > pSeg) {
1194
- if (!this.consumes[sSeg]!.includes(d)) this.consumes[sSeg]!.push(d);
1195
- if (!this.produces[pSeg]!.includes(d)) this.produces[pSeg]!.push(d);
1196
- }
1197
- }
1198
-
1199
- this.caches = Array.from({ length: model.numDonors }, () => new TrainingCache());
1200
- this.one = MlxArray.fromFloat32(new Float32Array([1]), []);
1201
-
1202
- // ORPO head (identical to SegmentedBackwardOrpo): inputs = [last_chosen,
1203
- // last_rejected] pre-finalNorm; finalNorm each, length-normalized response
1204
- // logp, orpo loss. vjp with cotangent 1.0 -> [dh_chosen, dh_rejected].
1205
- this.headVjp = new Vjp((p) => {
1206
- const hnC = this.model.finalNorm.forward(p[0]!);
1207
- const hnR = this.model.finalNorm.forward(p[1]!);
1208
- const cs = this.fusedChunkSize;
1209
- const lw = cs > 0
1210
- ? boundedHeadFromHidden(this.model, hnC, this.curChosenIds!, this.curChosenMask!, cs, this.headSink)
1211
- : responseOnlyLogpMean(this.model, hnC, this.curChosenIds!, this.curChosenMask!);
1212
- const lr = cs > 0
1213
- ? boundedHeadFromHidden(this.model, hnR, this.curRejectedIds!, this.curRejectedMask!, cs, this.headSink)
1214
- : responseOnlyLogpMean(this.model, hnR, this.curRejectedIds!, this.curRejectedMask!);
1215
- // sft_scope:"full": prompt-span head over the same hnC (see
1216
- // SegmentedBackwardOrpo); ℓw/ℓr stay response-only for the odds ratio.
1217
- let nllFull: MlxArray | null = null;
1218
- if (this.sftScope === "full") {
1219
- const { startT, M } = respSpanFromMask(this.curChosenMask!, this.curChosenIds!.length - 1);
1220
- let pm: MlxArray | null = null;
1221
- if (M > 0 && startT > 0)
1222
- pm = cs > 0
1223
- ? boundedHeadFromHidden(this.model, hnC, this.curChosenIds!, this.curChosenMask!, cs, this.headSink, { startT: 0, M: startT })
1224
- : spanLogpMeanFromHidden(this.model, hnC, this.curChosenIds!, 0, startT);
1225
- nllFull = combineFullNll(pm, lw, M > 0 ? startT : 0, M);
1226
- pm?.dispose();
1227
- }
1228
- hnC.dispose(); hnR.dispose();
1229
- const loss = orpoLossFromLogps(lw, lr, this.lambda, nllFull ?? undefined);
1230
- lw.dispose(); lr.dispose();
1231
- nllFull?.dispose();
1232
- return [loss];
1233
- }, 1);
1234
-
1235
- // One reused vjp per segment (identical to SegmentedBackwardGemma4); reused
1236
- // across both branches. Inputs: [boundary, (k,v)*consumes, (a,b)*lora].
1237
- // Outputs: [h, (k,v)*produces]. Reads curMasks/curPerLayer (active branch).
1238
- this.segVjps = ranges.map(([lo, hi], k) => {
1239
- const idxs = this.segIdxs[k]!;
1240
- const cons = this.consumes[k]!;
1241
- const prod = this.produces[k]!;
1242
- const base = 1 + 2 * cons.length;
1243
- const nOut = 1 + 2 * prod.length;
1244
- return new Vjp((p) => {
1245
- const boundary = p[0]!;
1246
- const kvIn = new Map<number, SharedKv>();
1247
- cons.forEach((d, ci) => kvIn.set(d, { kind: "plain", keys: p[1 + 2 * ci]!, values: p[1 + 2 * ci + 1]!, offset: 0 }));
1248
- const saved: [MlxArray, MlxArray][] = idxs.map((j) => [lora.targets[j]!.lw.a, lora.targets[j]!.lw.b]);
1249
- idxs.forEach((j, s) => {
1250
- lora.targets[j]!.lw.a = p[base + 2 * s]!;
1251
- lora.targets[j]!.lw.b = p[base + 2 * s + 1]!;
1252
- });
1253
- try {
1254
- const { h, donorKvOut } = this.model.runLayerRange(
1255
- boundary, lo, hi, this.caches, this.curMasks!, this.curPerLayer, kvIn,
1256
- );
1257
- const outs: MlxArray[] = [h];
1258
- for (const d of prod) {
1259
- const kv = donorKvOut.get(d)!;
1260
- if (kv.kind !== "plain") throw new Error("segmented e4b orpo: quantized donor KV unsupported in training");
1261
- const ck = ops.contiguous(kv.keys), cv = ops.contiguous(kv.values);
1262
- kv.keys.dispose(); kv.values.dispose();
1263
- outs.push(ck, cv);
1264
- }
1265
- for (const [d, kv] of donorKvOut)
1266
- if (!prod.includes(d) && kv.kind === "plain") { kv.keys.dispose(); kv.values.dispose(); }
1267
- return outs;
1268
- } finally {
1269
- idxs.forEach((j, s) => { lora.targets[j]!.lw.a = saved[s]![0]; lora.targets[j]!.lw.b = saved[s]![1]; });
1270
- }
1271
- }, nOut);
1272
- });
1273
- }
1274
-
1275
- /** Forward one branch segment-by-segment, populating `st` (boundaries +
1276
- * reused-donor K/V boundaries + this branch's masks + per-layer constant).
1277
- * Mutates `st` incrementally so the step's finally always sees partial state
1278
- * on throw. The last boundary (st.boundaries[nSeg]) feeds the ORPO head. */
1279
- private forwardBranch(ids: number[], st: BranchState): void {
1280
- const { model, ranges, caches, consumes, produces } = this;
1281
- const T = ids.length - 1;
1282
- const inputHost = new Int32Array(T);
1283
- for (let t = 0; t < T; t++) inputHost[t] = ids[t]!;
1284
- const inputIds = MlxArray.fromInt32(inputHost, [1, T]);
1285
- try {
1286
- const { hScaled, perLayer } = model.embedForSegmented(inputIds);
1287
- st.boundaries.push(detachLeaf(hScaled));
1288
- st.perLayer = perLayer ? detachLeaf(perLayer) : null;
1289
- st.masks = model.makeTrainingMasks(caches, T);
1290
-
1291
- for (let k = 0; k < ranges.length; k++) {
1292
- const [lo, hi] = ranges[k]!;
1293
- const kvIn = new Map<number, SharedKv>();
1294
- for (const d of consumes[k]!) {
1295
- const b = st.donorBnd.get(d)!;
1296
- kvIn.set(d, { kind: "plain", keys: b.keys, values: b.values, offset: 0 });
1297
- }
1298
- const { h, donorKvOut } = model.runLayerRange(st.boundaries[k]!, lo, hi, caches, st.masks, st.perLayer, kvIn);
1299
- for (const d of produces[k]!) {
1300
- const kv = donorKvOut.get(d)!;
1301
- if (kv.kind !== "plain") throw new Error("segmented e4b orpo: quantized donor KV unsupported in training");
1302
- st.donorBnd.set(d, { keys: detachLeaf(kv.keys), values: detachLeaf(kv.values) });
1303
- }
1304
- for (const [d, kv] of donorKvOut)
1305
- if (!produces[k]!.includes(d) && kv.kind === "plain") { kv.keys.dispose(); kv.values.dispose(); }
1306
- st.boundaries.push(detachLeaf(h));
1307
- }
1308
- } finally {
1309
- inputIds.dispose();
1310
- }
1311
- }
1312
-
1313
- /** Backward one branch in reverse over segments. Takes ownership of `dhSeed`
1314
- * (the head's cotangent for this branch's last boundary) and fills `grads`
1315
- * (length 2n) in place with the branch's LoRA gradients. Reused-donor
1316
- * cotangents are accumulated in a per-branch dKV (bf16, as in
1317
- * SegmentedBackwardGemma4). Sets curMasks/curPerLayer to this branch for the
1318
- * duration so the reused seg vjps see the right constants. */
1319
- private backwardBranch(st: BranchState, dhSeed: MlxArray, grads: (MlxArray | null)[]): void {
1320
- const { lora, ranges, segIdxs, consumes, produces, n } = this;
1321
- this.curMasks = st.masks;
1322
- this.curPerLayer = st.perLayer;
1323
- const dKV = new Map<number, KvPair>();
1324
- let dhOut: MlxArray | null = dhSeed;
1325
- try {
1326
- for (let k = ranges.length - 1; k >= 0; k--) {
1327
- const idxs = segIdxs[k]!;
1328
- const cons = consumes[k]!;
1329
- const prod = produces[k]!;
1330
- const dh = dhOut!;
1331
-
1332
- const primals: MlxArray[] = [st.boundaries[k]!];
1333
- for (const d of cons) { const b = st.donorBnd.get(d)!; primals.push(b.keys, b.values); }
1334
- for (const j of idxs) primals.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
1335
-
1336
- const cots: MlxArray[] = [dh];
1337
- for (const d of prod) { const acc = dKV.get(d)!; cots.push(acc.keys, acc.values); }
1338
-
1339
- const res = this.segVjps[k]!.apply(primals, cots);
1340
- for (const o of res.outputs) o.dispose();
1341
-
1342
- const leaves = detachLeaves(res.vjps); // one barrier (backlog #9)
1343
- const newDh = leaves[0]!;
1344
- cons.forEach((d, ci) => {
1345
- this.accumulateDKV(dKV, d, leaves[1 + 2 * ci]!, leaves[1 + 2 * ci + 1]!);
1346
- });
1347
- const base = 1 + 2 * cons.length;
1348
- idxs.forEach((j, s) => {
1349
- grads[j] = leaves[base + 2 * s]!;
1350
- grads[n + j] = leaves[base + 2 * s + 1]!;
1351
- });
1352
-
1353
- dh.dispose();
1354
- dhOut = newDh;
1355
- for (const d of prod) {
1356
- const acc = dKV.get(d)!; acc.keys.dispose(); acc.values.dispose(); dKV.delete(d);
1357
- const b = st.donorBnd.get(d)!; b.keys.dispose(); b.values.dispose(); st.donorBnd.delete(d);
1358
- }
1359
- st.boundaries[k]!.dispose();
1360
- st.boundaries[k] = null;
1361
- }
1362
- } finally {
1363
- dhOut?.dispose(); // segment-0 dh_in (embed / per-layer grad) — no LoRA there
1364
- for (const kv of dKV.values()) { kv.keys.dispose(); kv.values.dispose(); }
1365
- this.curMasks = null;
1366
- this.curPerLayer = null;
1367
- }
1368
- }
1369
-
1370
- /** Accumulate a reused-donor K/V cotangent (sum across consuming segments) —
1371
- * identical to SegmentedBackwardGemma4 (bf16, non-associative ~0.5% class). */
1372
- private accumulateDKV(dKV: Map<number, KvPair>, d: number, dk: MlxArray, dv: MlxArray): void {
1373
- const prev = dKV.get(d);
1374
- if (!prev) { dKV.set(d, { keys: dk, values: dv }); return; }
1375
- const sk = detachLeaf(ops.add(prev.keys, dk));
1376
- const sv = detachLeaf(ops.add(prev.values, dv));
1377
- prev.keys.dispose(); prev.values.dispose(); dk.dispose(); dv.dispose();
1378
- dKV.set(d, { keys: sk, values: sv });
1379
- }
1380
-
1381
- private disposeBranch(st: BranchState | null): void {
1382
- if (!st) return;
1383
- for (const b of st.boundaries) b?.dispose();
1384
- for (const kv of st.donorBnd.values()) { kv.keys.dispose(); kv.values.dispose(); }
1385
- st.perLayer?.dispose();
1386
- if (st.masks) for (const m of st.masks.values()) m.arr?.dispose();
1387
- }
1388
-
1389
- step(batch: DpoBatch): { value: MlxArray; grads: MlxArray[] } {
1390
- if (this.disposed) throw new Error("SegmentedBackwardOrpoGemma4 used after dispose");
1391
- const { n } = this;
1392
- if (batch.chosenIds.length !== 1) throw new Error("SegmentedBackwardOrpoGemma4: only B=1 is supported");
1393
- this.curChosenIds = batch.chosenIds[0]!;
1394
- this.curChosenMask = batch.chosenMask[0]!;
1395
- this.curRejectedIds = batch.rejectedIds[0]!;
1396
- this.curRejectedMask = batch.rejectedMask[0]!;
1397
- const Lc = batch.chosenIds[0]!.length;
1398
- const Lr = batch.rejectedIds[0]!.length;
1399
- if (Lc < 2 || Lr < 2) throw new Error("SegmentedBackwardOrpoGemma4: sequences too short");
1400
-
1401
- const nSeg = this.ranges.length;
1402
- const stC: BranchState = { boundaries: [], donorBnd: new Map(), masks: null, perLayer: null };
1403
- const stR: BranchState = { boundaries: [], donorBnd: new Map(), masks: null, perLayer: null };
1404
- const gradsC: (MlxArray | null)[] = new Array(2 * n).fill(null);
1405
- const gradsR: (MlxArray | null)[] = new Array(2 * n).fill(null);
1406
- let value: MlxArray | null = null;
1407
- let dhC: MlxArray | null = null;
1408
- let dhR: MlxArray | null = null;
1409
- let transferred = false;
1410
-
1411
- try {
1412
- // --- 1. Forward both branches (boundaries + donor K/V + masks/perLayer). ---
1413
- this.forwardBranch(batch.chosenIds[0]!, stC);
1414
- this.forwardBranch(batch.rejectedIds[0]!, stR);
1415
-
1416
- // --- 2. ORPO head over the two last boundaries (cotangent 1.0). The fused
1417
- // head pushes per-step CustomVjps + slices into headSink; dispose only after
1418
- // outputs/vjps are detached to leaves (which forces the head eval). ---
1419
- this.headSink = [];
1420
- const head = this.headVjp.apply([stC.boundaries[nSeg]!, stR.boundaries[nSeg]!], [this.one]);
1421
- // Materialize the full head-VJP graph (incl. the nested CustomVjp backward that
1422
- // reads savedLse/savedBlockMax) BEFORE freeing headSink — avoids the lazy-eval
1423
- // use-after-free segfault (see the segmented-prefix crash diagnosis).
1424
- ops.evalAll([head.outputs[0]!, head.vjps[0]!, head.vjps[1]!]);
1425
- value = detachLeaf(head.outputs[0]!);
1426
- dhC = detachLeaf(head.vjps[0]!);
1427
- dhR = detachLeaf(head.vjps[1]!);
1428
- for (const d of this.headSink) d.dispose();
1429
- this.headSink = [];
1430
- stC.boundaries[nSeg]!.dispose(); stC.boundaries[nSeg] = null;
1431
- stR.boundaries[nSeg]!.dispose(); stR.boundaries[nSeg] = null;
1432
-
1433
- // --- 3. Backward each branch (ownership of dh transfers to backwardBranch). ---
1434
- const seedC = dhC; dhC = null;
1435
- this.backwardBranch(stC, seedC, gradsC);
1436
- const seedR = dhR; dhR = null;
1437
- this.backwardBranch(stR, seedR, gradsR);
1438
-
1439
- // --- 4. Sum LoRA grads from both branches. ---
1440
- const grads: MlxArray[] = gradsC.map((gc, i) => {
1441
- const gr = gradsR[i]!;
1442
- const sum = ops.add(gc!, gr);
1443
- gc!.dispose(); gr.dispose();
1444
- gradsC[i] = null; gradsR[i] = null;
1445
- return detachLeaf(sum);
1446
- });
1447
-
1448
- transferred = true;
1449
- return { value: value!, grads };
1450
- } finally {
1451
- this.curChosenIds = this.curChosenMask = this.curRejectedIds = this.curRejectedMask = null;
1452
- dhC?.dispose(); dhR?.dispose();
1453
- this.disposeBranch(stC);
1454
- this.disposeBranch(stR);
1455
- if (!transferred) {
1456
- value?.dispose();
1457
- for (const g of gradsC) g?.dispose();
1458
- for (const g of gradsR) g?.dispose();
1459
- }
1460
- }
1461
- }
1462
-
1463
- dispose(): void {
1464
- if (this.disposed) return;
1465
- this.disposed = true;
1466
- this.headVjp.dispose();
1467
- for (const v of this.segVjps) v.dispose();
1468
- for (const c of this.caches) c.dispose();
1469
- this.one.dispose();
1470
- }
1471
- }
1472
-
1473
- // ===========================================================================
1474
- // SegmentedBackwardOrpoPrefixGemma4: segmented backward + PREFIX-SHARING on e4b.
1475
- // ===========================================================================
1476
- //
1477
- // The e4b (Gemma4) analogue of SegmentedBackwardOrpoPrefix (MiniCPM5): the
1478
- // composition of SegmentedBackwardOrpoGemma4's SEGMENT machinery (donor-KV
1479
- // threading across segment boundaries, per-layer-input boundary, sliding-window
1480
- // vs full-attention per-layer-type masks) with prefix-sharing (one forward over
1481
- // the single concat [prompt; chosen; rejected]).
1482
- //
1483
- // It is SegmentedBackwardOrpoGemma4 reduced to ONE stream (not two branches),
1484
- // with three substitutions — exactly the MiniCPM5 prefix recipe lifted to e4b:
1485
- // - the streamed sequence is the single concat [prompt(P); chosen(Rc); rejected(Rr)]
1486
- // (T = P+Rc+Rr), so one boundary stream, one backward walk, one donor-KV thread.
1487
- // - the per-(donor-)layer caches are Gemma4PrefixSharedSegCache (a LOCAL copy of
1488
- // the private Gemma4PrefixSharedCache in prefix-shared.ts): makeMask(T, window)
1489
- // returns the LOGICAL-position block-sparse + sliding-window prefix mask.
1490
- // makeTrainingMasks(caches,T) builds one such mask per layer-type (sliding gets
1491
- // the window AND; full gets the plain block-sparse causal), exactly like
1492
- // orpoLossPrefixSharedGemma's forward.
1493
- // - setGemmaPrefixPlan({P,Rc,Rr}) is ACTIVE for the whole step (set in stepPrefix,
1494
- // cleared in finally) so block-wise RoPE rides through BOTH the streaming forward
1495
- // AND every segment's mlx_vjp recompute, through donor AND sharer layers
1496
- // identically to the non-segmented orpoLossPrefixSharedGemma.
1497
- // - the head gathers the chosen/rejected response hiddens from the final
1498
- // [1,T,hidden] (branchLogpMeanGathered — the [M,V]-free per-branch flash head)
1499
- // and ORPO-combines; its vjp w.r.t. the last boundary is the full [1,T,hidden] dh
1500
- // (the gather's takeAxis vjp scatter-adds each branch's response-position
1501
- // cotangent back into the full sequence), seeding the single segmented backward.
1502
- //
1503
- // The caches count = model.numDonors (like orpoLossPrefixSharedGemma), NOT
1504
- // model.layers.length: sharers reuse the donors' fetched K/V. The donor
1505
- // consumes/produces topology is architecture+segmentation only (data-independent),
1506
- // computed ONCE — identical to SegmentedBackwardOrpoGemma4. All the hard-won
1507
- // lessons hold: mlx_vjp per segment (NOT surrogate value_and_grad), detachLeaf at
1508
- // boundaries, reused vjp objects, head disposables into a per-step sink, the
1509
- // CONTIGUOUS-donor-K/V layout matching between producer output and consumer
1510
- // cotangent, and the bf16-class accumulateDKV cross-segment donor cotangent sum.
1511
-
1512
- /** Pass-through cache (offset 0) for the e4b prefix-shared SEGMENTED forward whose
1513
- * makeMask returns the block-sparse + logical-window mask. A LOCAL copy of the
1514
- * (private) Gemma4PrefixSharedCache in prefix-shared.ts — replicated here rather
1515
- * than exporting it (file-ownership boundary). makeTrainingMasks calls
1516
- * makeMask(T, window) with window = the model's sliding window for sliding layers
1517
- * and null for full layers; the window arg selects the sliding term. One per DONOR
1518
- * layer (sharers reuse donors' fetched KV inside runLayerRange). */
1519
- class Gemma4PrefixSharedSegCache implements Cache {
1520
- /** Training-only adapter; never admitted, merged, or persisted. */
1521
- signature(): string { return "train:gemma4-prefix-shared-seg"; }
1522
- offset = 0;
1523
- constructor(private readonly P: number, private readonly Rc: number, private readonly Rr: number) {}
1524
- updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
1525
- return [k.slice([0, 0, 0, 0], k.shape), v.slice([0, 0, 0, 0], v.shape)];
1526
- }
1527
- makeMask(N: number, windowSize: number | null): Mask {
1528
- if (N !== this.P + this.Rc + this.Rr)
1529
- throw new Error(`Gemma4PrefixSharedSegCache: N=${N} != P+Rc+Rr=${this.P + this.Rc + this.Rr}`);
1530
- return { mode: "array", arr: blockSparsePrefixMaskGemma(this.P, this.Rc, this.Rr, windowSize) };
1531
- }
1532
- state(): MlxArray[] { return []; }
1533
- isTrimmable(): boolean { return true; }
1534
- trim(_n: number): void { /* offset pinned at 0 */ }
1535
- dispose(): void { /* owns no arrays */ }
1536
- }
1537
-
1538
- export class SegmentedBackwardOrpoPrefixGemma4 {
1539
- private readonly nLayers: number;
1540
- private readonly n: number;
1541
- private readonly segIdxs: number[][];
1542
- private readonly consumes: number[][];
1543
- private readonly produces: number[][];
1544
- private readonly caches: Cache[]; // numDonors TrainingCache (the running KV the seg vjps fetch into)
1545
- private readonly headVjp: Vjp;
1546
- private readonly segVjps: Vjp[];
1547
- private readonly one: MlxArray;
1548
- // Per-step mutable context the (reused) closures read.
1549
- private curP = 0;
1550
- private curRc = 0;
1551
- private curRr = 0;
1552
- private curPromptIds: number[] | null = null;
1553
- private curChosenResp: number[] | null = null;
1554
- private curRejectedResp: number[] | null = null;
1555
- private curChunk: ChunkCtx | undefined = undefined;
1556
- private curMasks: Map<string, Mask> | null = null; // prefix masks for this step
1557
- private curPerLayer: MlxArray | null = null;
1558
- private headSink: Array<{ dispose(): void }> = [];
1559
- private disposed = false;
1560
-
1561
- constructor(
1562
- private readonly model: Gemma4Model,
1563
- private readonly lora: TrainableLora,
1564
- private readonly ranges: [number, number][],
1565
- private readonly lambda: number,
1566
- private readonly chunkCtx?: ChunkCtx,
1567
- private readonly sftScope: SftScope = "response",
1568
- ) {
1569
- this.nLayers = model.layers.length;
1570
- const lastHi = ranges[ranges.length - 1]?.[1] ?? 0;
1571
- if (ranges[0]?.[0] !== 0 || lastHi !== this.nLayers)
1572
- throw new Error(`SegmentedBackwardOrpoPrefixGemma4: ranges must cover [0, ${this.nLayers})`);
1573
- this.n = lora.targets.length;
1574
-
1575
- const layerOf = lora.targets.map((t) => targetLayer(t.modulePath));
1576
- this.segIdxs = ranges.map(([lo, hi]) =>
1577
- layerOf.map((li, j) => ({ li, j })).filter((x) => x.li >= lo && x.li < hi).map((x) => x.j),
1578
- );
1579
-
1580
- // Donor/sharer KV threading topology (identical to SegmentedBackwardGemma4 /
1581
- // SegmentedBackwardOrpoGemma4): architecture + segmentation only — independent
1582
- // of the data AND of prefix-sharing (the donor structure is the same concat).
1583
- const segOfLayer = new Map<number, number>();
1584
- ranges.forEach(([lo, hi], k) => { for (let i = lo; i < hi; i++) segOfLayer.set(i, k); });
1585
- const reused = model.reusedDonors;
1586
- const producerSeg = new Map<number, number>();
1587
- for (const d of reused) producerSeg.set(d, segOfLayer.get(d)!);
1588
- this.consumes = ranges.map(() => []);
1589
- this.produces = ranges.map(() => []);
1590
- for (let i = 0; i < this.nLayers; i++) {
1591
- if (model.cacheIndex[i] !== -1) continue;
1592
- const d = model.previousKvs[i]!;
1593
- const sSeg = segOfLayer.get(i)!;
1594
- const pSeg = producerSeg.get(d)!;
1595
- if (sSeg > pSeg) {
1596
- if (!this.consumes[sSeg]!.includes(d)) this.consumes[sSeg]!.push(d);
1597
- if (!this.produces[pSeg]!.includes(d)) this.produces[pSeg]!.push(d);
1598
- }
1599
- }
1600
-
1601
- this.caches = Array.from({ length: model.numDonors }, () => new TrainingCache());
1602
- this.one = MlxArray.fromFloat32(new Float32Array([1]), []);
1603
-
1604
- // Head VJP: input = the last boundary [1, T, hidden] (pre-finalNorm). Applies
1605
- // finalNorm, gathers each branch's response hiddens (prefixGatherIdx layout),
1606
- // computes ℓw/ℓr through the [M,V]-free per-branch head (branchLogpMeanGathered),
1607
- // returns the scalar orpo loss. nOut = 1; apply([h_last],[one]) returns
1608
- // vjps[0] = dh [1,T,hidden] (the gather's takeAxis vjp scatter-adds the
1609
- // per-branch dh into the full sequence) — same as the MiniCPM5 prefix head.
1610
- this.headVjp = new Vjp((p) => {
1611
- const hn = model.finalNorm.forward(p[0]!); // [1, T, hidden]
1612
- const { chosenIdx, rejectedIdx } = prefixGatherIdx(this.curP, this.curRc, this.curRr);
1613
- const lw = branchLogpMeanGathered(model, hn, chosenIdx, this.curChosenResp!, this.curChunk);
1614
- const lr = branchLogpMeanGathered(model, hn, rejectedIdx, this.curRejectedResp!, this.curChunk);
1615
- // sft_scope:"full": prompt predictions from the same concat hiddens (see
1616
- // SegmentedBackwardOrpoPrefix); ℓw/ℓr stay response-only.
1617
- const nllFull = this.sftScope === "full"
1618
- ? prefixFullNll(model, hn, this.curPromptIds!, lw, this.curRc, this.curChunk)
1619
- : null;
1620
- hn.dispose();
1621
- const loss = orpoLossFromLogps(lw, lr, this.lambda, nllFull ?? undefined);
1622
- lw.dispose(); lr.dispose();
1623
- nllFull?.dispose();
1624
- return [loss];
1625
- }, 1);
1626
-
1627
- // One reused vjp per segment — same SHAPE as SegmentedBackwardOrpoGemma4's
1628
- // (Inputs: [boundary, (k,v)*consumes, (a,b)*lora]; Outputs: [h, (k,v)*produces]),
1629
- // reading the active step's PREFIX masks/perLayer off curMasks/curPerLayer. The
1630
- // prefix RoPE plan is active for the whole step (set in stepPrefix), so the
1631
- // recompute here ropes block-wise too. (branchLogpMeanGathered handles the head;
1632
- // these only stream the transformer layers + thread the donor KV.)
1633
- this.segVjps = ranges.map(([lo, hi], k) => {
1634
- const idxs = this.segIdxs[k]!;
1635
- const cons = this.consumes[k]!;
1636
- const prod = this.produces[k]!;
1637
- const base = 1 + 2 * cons.length;
1638
- const nOut = 1 + 2 * prod.length;
1639
- return new Vjp((p) => {
1640
- const boundary = p[0]!;
1641
- const kvIn = new Map<number, SharedKv>();
1642
- cons.forEach((d, ci) => kvIn.set(d, { kind: "plain", keys: p[1 + 2 * ci]!, values: p[1 + 2 * ci + 1]!, offset: 0 }));
1643
- const saved: [MlxArray, MlxArray][] = idxs.map((j) => [lora.targets[j]!.lw.a, lora.targets[j]!.lw.b]);
1644
- idxs.forEach((j, s) => {
1645
- lora.targets[j]!.lw.a = p[base + 2 * s]!;
1646
- lora.targets[j]!.lw.b = p[base + 2 * s + 1]!;
1647
- });
1648
- try {
1649
- const { h, donorKvOut } = model.runLayerRange(
1650
- boundary, lo, hi, this.caches, this.curMasks!, this.curPerLayer, kvIn,
1651
- );
1652
- const outs: MlxArray[] = [h];
1653
- for (const d of prod) {
1654
- const kv = donorKvOut.get(d)!;
1655
- if (kv.kind !== "plain") throw new Error("segmented e4b prefix: quantized donor KV unsupported in training");
1656
- const ck = ops.contiguous(kv.keys), cv = ops.contiguous(kv.values);
1657
- kv.keys.dispose(); kv.values.dispose();
1658
- outs.push(ck, cv);
1659
- }
1660
- for (const [d, kv] of donorKvOut)
1661
- if (!prod.includes(d) && kv.kind === "plain") { kv.keys.dispose(); kv.values.dispose(); }
1662
- return outs;
1663
- } finally {
1664
- idxs.forEach((j, s) => { lora.targets[j]!.lw.a = saved[s]![0]; lora.targets[j]!.lw.b = saved[s]![1]; });
1665
- }
1666
- }, nOut);
1667
- });
1668
- }
1669
-
1670
- /** Accumulate a reused-donor K/V cotangent (sum across consuming segments) —
1671
- * identical to SegmentedBackwardGemma4 (bf16, non-associative ~0.5% class). */
1672
- private accumulateDKV(dKV: Map<number, KvPair>, d: number, dk: MlxArray, dv: MlxArray): void {
1673
- const prev = dKV.get(d);
1674
- if (!prev) { dKV.set(d, { keys: dk, values: dv }); return; }
1675
- const sk = detachLeaf(ops.add(prev.keys, dk));
1676
- const sv = detachLeaf(ops.add(prev.values, dv));
1677
- prev.keys.dispose(); prev.values.dispose(); dk.dispose(); dv.dispose();
1678
- dKV.set(d, { keys: sk, values: sv });
1679
- }
1680
-
1681
- /** One ORPO step over the prefix-shared concat. `promptIds`/`chosenResp`/
1682
- * `rejectedResp` come from splitPrefixBatch. Returns the loss + LoRA grads in
1683
- * flatParams(lora) order (caller owns). Mirrors SegmentedBackwardOrpoPrefix.stepPrefix. */
1684
- stepPrefix(promptIds: number[], chosenResp: number[], rejectedResp: number[]): { value: MlxArray; grads: MlxArray[] } {
1685
- if (this.disposed) throw new Error("SegmentedBackwardOrpoPrefixGemma4 used after dispose");
1686
- const { model, lora, ranges, caches, segIdxs, consumes, produces, n } = this;
1687
- const P = promptIds.length, Rc = chosenResp.length, Rr = rejectedResp.length;
1688
- if (P < 1 || Rc < 1 || Rr < 1) throw new Error("SegmentedBackwardOrpoPrefixGemma4: need P,Rc,Rr >= 1");
1689
- const T = P + Rc + Rr;
1690
- this.curP = P; this.curRc = Rc; this.curRr = Rr;
1691
- this.curPromptIds = promptIds;
1692
- this.curChosenResp = chosenResp; this.curRejectedResp = rejectedResp;
1693
- this.headSink = [];
1694
- this.curChunk = this.chunkCtx ? { ...this.chunkCtx, sink: this.headSink } : undefined;
1695
-
1696
- const nSeg = ranges.length;
1697
- const concat = new Int32Array(T);
1698
- concat.set(promptIds, 0);
1699
- concat.set(chosenResp, P);
1700
- concat.set(rejectedResp, P + Rc);
1701
- const inputIds = MlxArray.fromInt32(concat, [1, T]);
1702
-
1703
- // Per-step prefix caches (one per DONOR layer): makeMask -> block-sparse +
1704
- // logical-window mask. makeTrainingMasks calls makeMask(T, window) per layer-type.
1705
- const prefixCaches: Cache[] = Array.from({ length: model.numDonors }, () => new Gemma4PrefixSharedSegCache(P, Rc, Rr));
1706
-
1707
- const boundaries: (MlxArray | null)[] = [];
1708
- const donorBnd = new Map<number, KvPair>(); // detached reused-donor K/V boundaries
1709
- const dKV = new Map<number, KvPair>(); // accumulated reused-donor cotangents
1710
- let value: MlxArray | null = null;
1711
- let dhOut: MlxArray | null = null;
1712
- let transferred = false;
1713
- const grads: (MlxArray | null)[] = new Array(2 * n).fill(null);
1714
-
1715
- // Block-wise RoPE active for the WHOLE step (forward + every segment recompute).
1716
- setGemmaPrefixPlan({ P, Rc, Rr });
1717
- try {
1718
- // --- 1. Forward over the concat, saving detached boundaries (hidden + donor K/V). ---
1719
- const { hScaled, perLayer } = model.embedForSegmented(inputIds);
1720
- boundaries.push(detachLeaf(hScaled));
1721
- this.curPerLayer = perLayer ? detachLeaf(perLayer) : null;
1722
- // Prefix masks (block-sparse + logical-window) for this step, built from the
1723
- // prefix caches (NOT model.makeTrainingMasks' offset-0 causal masks).
1724
- this.curMasks = model.makeTrainingMasks(prefixCaches, T);
1725
-
1726
- for (let k = 0; k < nSeg; k++) {
1727
- const [lo, hi] = ranges[k]!;
1728
- const kvIn = new Map<number, SharedKv>();
1729
- for (const d of consumes[k]!) {
1730
- const b = donorBnd.get(d)!;
1731
- kvIn.set(d, { kind: "plain", keys: b.keys, values: b.values, offset: 0 });
1732
- }
1733
- const { h, donorKvOut } = model.runLayerRange(boundaries[k]!, lo, hi, caches, this.curMasks, this.curPerLayer, kvIn);
1734
- for (const d of produces[k]!) {
1735
- const kv = donorKvOut.get(d)!;
1736
- if (kv.kind !== "plain") throw new Error("segmented e4b prefix: quantized donor KV unsupported in training");
1737
- donorBnd.set(d, { keys: detachLeaf(kv.keys), values: detachLeaf(kv.values) });
1738
- }
1739
- for (const [d, kv] of donorKvOut)
1740
- if (!produces[k]!.includes(d) && kv.kind === "plain") { kv.keys.dispose(); kv.values.dispose(); }
1741
- boundaries.push(detachLeaf(h));
1742
- }
1743
-
1744
- // --- 2. Head over the last boundary (vjp, cotangent 1.0) -> [dh 1,T,hidden]. ---
1745
- const head = this.headVjp.apply([boundaries[nSeg]!], [this.one]);
1746
- // Force the FULL head-VJP graph (incl. the nested flash-CCE CustomVjp backward,
1747
- // which reads savedLse/savedBlockMax) to materialize BEFORE the headSink dispose
1748
- // below — else that deferred backward can run AFTER its lse/blockMax are freed
1749
- // under MLX lazy eval (a use-after-free → segfault at a tiny address, ~step 100).
1750
- ops.evalAll([head.outputs[0]!, head.vjps[0]!]);
1751
- value = detachLeaf(head.outputs[0]!);
1752
- dhOut = detachLeaf(head.vjps[0]!); // dLoss / d(last residual stream), [1,T,hidden]
1753
- for (const d of this.headSink) d.dispose();
1754
- this.headSink = [];
1755
- boundaries[nSeg]!.dispose(); boundaries[nSeg] = null;
1756
-
1757
- // --- 3. Backward, reverse over segments (single stream; cotangents dh + dKV). ---
1758
- for (let k = nSeg - 1; k >= 0; k--) {
1759
- const idxs = segIdxs[k]!;
1760
- const cons = consumes[k]!;
1761
- const prod = produces[k]!;
1762
- const dh = dhOut!;
1763
-
1764
- const primals: MlxArray[] = [boundaries[k]!];
1765
- for (const d of cons) { const b = donorBnd.get(d)!; primals.push(b.keys, b.values); }
1766
- for (const j of idxs) primals.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
1767
-
1768
- const cots: MlxArray[] = [dh];
1769
- for (const d of prod) { const acc = dKV.get(d)!; cots.push(acc.keys, acc.values); }
1770
-
1771
- const res = this.segVjps[k]!.apply(primals, cots);
1772
- for (const o of res.outputs) o.dispose();
1773
-
1774
- const leaves = detachLeaves(res.vjps); // one barrier (backlog #9)
1775
- const newDh = leaves[0]!;
1776
- cons.forEach((d, ci) => {
1777
- this.accumulateDKV(dKV, d, leaves[1 + 2 * ci]!, leaves[1 + 2 * ci + 1]!);
1778
- });
1779
- const base = 1 + 2 * cons.length;
1780
- idxs.forEach((j, s) => {
1781
- grads[j] = leaves[base + 2 * s]!;
1782
- grads[n + j] = leaves[base + 2 * s + 1]!;
1783
- });
1784
-
1785
- dh.dispose();
1786
- dhOut = newDh;
1787
- for (const d of prod) {
1788
- const acc = dKV.get(d)!; acc.keys.dispose(); acc.values.dispose(); dKV.delete(d);
1789
- const b = donorBnd.get(d)!; b.keys.dispose(); b.values.dispose(); donorBnd.delete(d);
1790
- }
1791
- boundaries[k]!.dispose(); boundaries[k] = null;
1792
- }
1793
- // Segment-0 dh_in = grad w.r.t. the scaled embedding / per-layer-input (no LoRA
1794
- // target there: per_layer_input_gate/projection are differentiated INSIDE the
1795
- // segments) — dropped (disposed in finally).
1796
-
1797
- transferred = true;
1798
- return { value: value!, grads: grads as MlxArray[] };
1799
- } finally {
1800
- setGemmaPrefixPlan(null);
1801
- inputIds.dispose();
1802
- for (const c of prefixCaches) c.dispose();
1803
- this.curPerLayer?.dispose();
1804
- this.curPerLayer = null;
1805
- if (this.curMasks) { for (const m of this.curMasks.values()) m.arr?.dispose(); this.curMasks = null; }
1806
- this.curPromptIds = this.curChosenResp = this.curRejectedResp = null;
1807
- for (const d of this.headSink) d.dispose();
1808
- this.headSink = [];
1809
- dhOut?.dispose();
1810
- for (const b of boundaries) b?.dispose();
1811
- for (const kv of donorBnd.values()) { kv.keys.dispose(); kv.values.dispose(); }
1812
- for (const kv of dKV.values()) { kv.keys.dispose(); kv.values.dispose(); }
1813
- if (!transferred) {
1814
- value?.dispose();
1815
- for (const g of grads) g?.dispose();
1816
- }
1817
- }
1818
- }
1819
-
1820
- dispose(): void {
1821
- if (this.disposed) return;
1822
- this.disposed = true;
1823
- this.headVjp.dispose();
1824
- for (const v of this.segVjps) v.dispose();
1825
- for (const c of this.caches) c.dispose();
1826
- this.one.dispose();
1827
- }
1828
- }