mlx-bun 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (601) hide show
  1. package/README.md +1453 -88
  2. package/THIRD_PARTY_NOTICES.md +882 -0
  3. package/bin/mlx-bun.convert.mjs +4 -0
  4. package/bin/mlx-bun.fuse.mjs +4 -0
  5. package/bin/mlx-bun.generate.mjs +4 -0
  6. package/bin/mlx-bun.lora.mjs +4 -0
  7. package/bin/mlx-bun.mjs +13 -21
  8. package/bin/mlx-bun.server.mjs +4 -0
  9. package/bin/mlx-bun.upload.mjs +4 -0
  10. package/dist/web/app.js +9584 -0
  11. package/package.json +50 -53
  12. package/src/cli/ane-install.ts +76 -0
  13. package/src/cli/args.ts +179 -0
  14. package/src/cli/engine-entry.ts +68 -0
  15. package/src/cli/inference.ts +214 -0
  16. package/src/cli/job-entry.ts +97 -0
  17. package/src/cli/library-host.ts +117 -0
  18. package/src/cli/main.ts +57 -0
  19. package/src/cli/memory-engine.ts +163 -0
  20. package/src/cli/memory.ts +36 -0
  21. package/src/cli/mlx-lm-aliases.ts +309 -0
  22. package/src/cli/model-choice.ts +33 -0
  23. package/src/cli/model-selection.ts +84 -0
  24. package/src/cli/module-host.ts +11 -0
  25. package/src/cli/module-verbs.ts +50 -0
  26. package/src/cli/numerical-policy.ts +12 -0
  27. package/src/cli/perplexity.ts +89 -0
  28. package/src/cli/serve-host.ts +423 -0
  29. package/src/cli/serve-isolated.ts +217 -0
  30. package/src/cli/serve-options.ts +106 -0
  31. package/src/cli/serve-state.ts +205 -0
  32. package/src/cli/serve.ts +500 -0
  33. package/src/cli/served-model-host.ts +102 -0
  34. package/src/cli/server-entry.ts +148 -0
  35. package/src/cli/serving-unit.ts +177 -0
  36. package/src/{tui.ts → cli/terminal.ts} +13 -8
  37. package/src/cli/worker-adapters.ts +34 -0
  38. package/src/cli/worker-entry.ts +272 -0
  39. package/src/cli/worker-unit.ts +179 -0
  40. package/src/engine/adapter-operation.ts +26 -0
  41. package/src/engine/cache-services.ts +114 -0
  42. package/src/engine/completion.ts +39 -0
  43. package/src/engine/generation-gateway.ts +358 -0
  44. package/src/engine/index.ts +59 -0
  45. package/src/engine/kv-budget.ts +120 -0
  46. package/src/engine/media-preparation.ts +232 -0
  47. package/src/engine/model-binding.ts +24 -0
  48. package/src/engine/model-host.ts +513 -0
  49. package/src/engine/model-serving.ts +89 -0
  50. package/src/{serve → engine}/preparation.ts +10 -8
  51. package/src/{serve → engine}/session-completion-engine.ts +6 -6
  52. package/src/engine/telemetry.ts +109 -0
  53. package/src/hub/downloads.ts +116 -0
  54. package/src/jobs/db.ts +16 -7
  55. package/src/jobs/events.ts +29 -0
  56. package/src/jobs/executable.ts +5 -0
  57. package/src/jobs/host.ts +51 -0
  58. package/src/jobs/in-process.ts +55 -0
  59. package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
  60. package/src/jobs/runner.ts +150 -231
  61. package/src/jobs/service.ts +75 -0
  62. package/src/jobs/sse.ts +59 -23
  63. package/src/jobs/worker-memory.ts +46 -0
  64. package/src/jobs/worker-process.ts +163 -0
  65. package/src/jobs/worker-supervisor.ts +258 -0
  66. package/src/modules.ts +58 -0
  67. package/src/publishing/catalog-hub.ts +18 -0
  68. package/src/publishing/credentials.ts +31 -0
  69. package/src/publishing/upload.ts +37 -0
  70. package/src/residency/leased-adapters.ts +21 -0
  71. package/src/residency/local-records.ts +16 -0
  72. package/src/residency/model-residency.ts +405 -0
  73. package/src/residency/record-index.ts +28 -0
  74. package/src/residency/resident-estimate.ts +50 -0
  75. package/src/{anthropic.ts → server/anthropic.ts} +3 -81
  76. package/src/server/cache-routes.ts +36 -0
  77. package/src/{serve → server}/chat-request.ts +12 -10
  78. package/src/{serve → server}/chat-stage.ts +27 -22
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +42 -42
  81. package/src/server/decision-body.ts +55 -0
  82. package/src/server/decision-routes.ts +134 -0
  83. package/src/server/decision-wire.ts +228 -0
  84. package/src/{serve → server}/discovery-routes.ts +42 -83
  85. package/src/server/generated-token-history.ts +57 -0
  86. package/src/{serve → server}/http.ts +25 -13
  87. package/src/{serve → server}/inference-request.ts +5 -5
  88. package/src/server/job-routes.ts +20 -0
  89. package/src/server/media-prompt.ts +63 -0
  90. package/src/server/memory-completion-client.ts +46 -0
  91. package/src/server/model-routes.ts +192 -0
  92. package/src/{serve → server}/openai-wire.ts +3 -2
  93. package/src/{serve → server}/pipeline.ts +1 -1
  94. package/src/server/prompt-builder.ts +13 -0
  95. package/src/server/prompt-contracts.ts +17 -0
  96. package/src/server/proxy-routes.ts +187 -0
  97. package/src/server/publishing-routes.ts +41 -0
  98. package/src/{serve → server}/request-plan.ts +8 -8
  99. package/src/{serve → server}/request-prep.ts +82 -68
  100. package/src/{serve → server}/responses-client.ts +30 -13
  101. package/src/{responses.ts → server/responses.ts} +12 -93
  102. package/src/server/routes.ts +205 -0
  103. package/src/server/start.ts +72 -0
  104. package/src/server/status-routes.ts +108 -0
  105. package/src/{serve → server}/text-completion-stage.ts +25 -4
  106. package/src/server/text-prompt.ts +12 -0
  107. package/src/{serve → server}/token-streams.ts +36 -38
  108. package/src/server/worker-routes.ts +311 -0
  109. package/src/storage/paths.ts +61 -0
  110. package/src/web/assets.ts +37 -0
  111. package/src/web/{src → browser}/api.ts +5 -23
  112. package/src/web/browser/chat-host.ts +17 -0
  113. package/src/web/browser/installed-panels.ts +6 -0
  114. package/src/web/browser/main.ts +55 -0
  115. package/src/web/browser/markdown.ts +279 -0
  116. package/src/web/browser/memory-panel.ts +9 -0
  117. package/src/web/browser/palette.ts +64 -0
  118. package/src/web/browser/protocol.ts +13 -0
  119. package/src/web/browser/shell.ts +467 -0
  120. package/src/web/{src → browser}/status.ts +32 -36
  121. package/src/web/build.ts +51 -0
  122. package/src/web/public/app.html +1054 -0
  123. package/src/web/{sw.js → public/sw.js} +4 -3
  124. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  125. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  126. package/THIRD_PARTY_LICENSES.md +0 -51
  127. package/docs/README.md +0 -94
  128. package/docs/archive/hlg-sampling.md +0 -350
  129. package/docs/archive/investigations/audio-input-plan.md +0 -356
  130. package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
  131. package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
  132. package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
  133. package/docs/archive/investigations/curve-bisector-routing.md +0 -123
  134. package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
  135. package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
  136. package/docs/archive/investigations/dspark-handoff.md +0 -224
  137. package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
  138. package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
  139. package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
  140. package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
  141. package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
  142. package/docs/archive/investigations/lab-build-journal.md +0 -157
  143. package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
  144. package/docs/archive/investigations/openjarvis-fit.md +0 -198
  145. package/docs/archive/investigations/openwiki-evaluation.md +0 -249
  146. package/docs/archive/investigations/optimization_plan.md +0 -346
  147. package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
  148. package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
  149. package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
  150. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
  151. package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
  152. package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
  153. package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
  154. package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
  155. package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
  156. package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
  157. package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
  158. package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
  159. package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
  160. package/docs/archive/planning/chat-ui-vision.md +0 -50
  161. package/docs/archive/planning/chunk-task-roadmap.md +0 -77
  162. package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
  163. package/docs/archive/planning/journal.md +0 -316
  164. package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
  165. package/docs/archive/planning/pi-integration-review.md +0 -178
  166. package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
  167. package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
  168. package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
  169. package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
  170. package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
  171. package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
  172. package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
  173. package/docs/archive/planning/web-ui-pass-plan.md +0 -213
  174. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  175. package/docs/design/batching.md +0 -1841
  176. package/docs/design/decode-speed-program.md +0 -2349
  177. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  178. package/docs/design/generic-model-support.md +0 -705
  179. package/docs/design/kv-cache.md +0 -824
  180. package/docs/design/orpo-training.md +0 -680
  181. package/docs/design/speculative-decoding.md +0 -1990
  182. package/docs/design/turboquant.md +0 -2212
  183. package/docs/design/unified-engine-frontier-plan.md +0 -1880
  184. package/docs/design/web-chat-redesign.md +0 -695
  185. package/docs/planning/PRODUCT_ROADMAP.md +0 -594
  186. package/docs/planning/ResearchTopics.md +0 -119
  187. package/docs/planning/release-notes-v0.4.0.md +0 -45
  188. package/docs/planning/video-series.md +0 -421
  189. package/docs/reference/benchmarks.md +0 -3613
  190. package/docs/reference/cli.md +0 -585
  191. package/docs/reference/distribution.md +0 -355
  192. package/docs/reference/environment.md +0 -398
  193. package/docs/reference/glossary.md +0 -39
  194. package/docs/reference/library-api.md +0 -674
  195. package/docs/reference/memory.md +0 -260
  196. package/docs/reference/models.md +0 -334
  197. package/docs/reference/server-api.md +0 -1775
  198. package/docs/reference/server-config.md +0 -846
  199. package/docs/reference/training.md +0 -462
  200. package/docs/reference/troubleshooting.md +0 -158
  201. package/src/admission-plan.ts +0 -18
  202. package/src/audio/conformer.ts +0 -647
  203. package/src/audio/decode.ts +0 -133
  204. package/src/audio/features.ts +0 -202
  205. package/src/audio/transcode.ts +0 -67
  206. package/src/backends/mlx/assistant-target.ts +0 -46
  207. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  208. package/src/backends/mlx/autoregressive.ts +0 -108
  209. package/src/backends/mlx/batch-group.ts +0 -1790
  210. package/src/backends/mlx/cache-attachment.ts +0 -67
  211. package/src/backends/mlx/cache-layout.ts +0 -31
  212. package/src/backends/mlx/checkpoint-state.ts +0 -53
  213. package/src/backends/mlx/continuation-persistence.ts +0 -106
  214. package/src/backends/mlx/continuation-request.ts +0 -43
  215. package/src/backends/mlx/continuation.ts +0 -77
  216. package/src/backends/mlx/diffusion.ts +0 -39
  217. package/src/backends/mlx/draft-target.ts +0 -32
  218. package/src/backends/mlx/evaluation.ts +0 -105
  219. package/src/backends/mlx/fill-append.ts +0 -45
  220. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  221. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  222. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  223. package/src/backends/mlx/gateway-binding.ts +0 -130
  224. package/src/backends/mlx/graph.ts +0 -38
  225. package/src/backends/mlx/kv-codec.ts +0 -9
  226. package/src/backends/mlx/kv-maintenance.ts +0 -133
  227. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  228. package/src/backends/mlx/memory-client.ts +0 -189
  229. package/src/backends/mlx/methods.ts +0 -90
  230. package/src/backends/mlx/model-prompt.ts +0 -164
  231. package/src/backends/mlx/model-serving.ts +0 -79
  232. package/src/backends/mlx/prefill-cohort.ts +0 -89
  233. package/src/backends/mlx/prefill-rows.ts +0 -200
  234. package/src/backends/mlx/prefill.ts +0 -33
  235. package/src/backends/mlx/projected-context-rows.ts +0 -72
  236. package/src/backends/mlx/request-policy.ts +0 -20
  237. package/src/backends/mlx/request-state-policy.ts +0 -41
  238. package/src/backends/mlx/rollback.ts +0 -89
  239. package/src/backends/mlx/row-sampling.ts +0 -49
  240. package/src/backends/mlx/serial-executor.ts +0 -242
  241. package/src/backends/mlx/speculative-group.ts +0 -359
  242. package/src/backends/mlx/speculative-round.ts +0 -161
  243. package/src/backends/mlx/speculative.ts +0 -114
  244. package/src/backends/mlx/state-rows.ts +0 -104
  245. package/src/backends/mlx/state-views.ts +0 -46
  246. package/src/backends/mlx/target-layout-capability.ts +0 -35
  247. package/src/backends/mlx/text-engine.ts +0 -73
  248. package/src/backends/mlx/transitioning-kv-rows.ts +0 -165
  249. package/src/chat-template.ts +0 -382
  250. package/src/cli.ts +0 -3168
  251. package/src/config.ts +0 -399
  252. package/src/contracts/completion.ts +0 -16
  253. package/src/contracts/execution.ts +0 -60
  254. package/src/contracts/generation.ts +0 -103
  255. package/src/contracts/host.ts +0 -7
  256. package/src/contracts/pi-provider.ts +0 -30
  257. package/src/contracts/pi.ts +0 -259
  258. package/src/contracts/prefix-cache.ts +0 -23
  259. package/src/contracts/resources.ts +0 -25
  260. package/src/contracts/sampling.ts +0 -23
  261. package/src/contracts/scheduling.ts +0 -45
  262. package/src/contracts/tsconfig.json +0 -14
  263. package/src/dataset/generators.ts +0 -1087
  264. package/src/dataset/index.ts +0 -12
  265. package/src/dataset/job.ts +0 -81
  266. package/src/dataset/llm.ts +0 -71
  267. package/src/dataset/registry.ts +0 -528
  268. package/src/diffusion/diffusion-generate.ts +0 -507
  269. package/src/download.ts +0 -505
  270. package/src/embed.ts +0 -77
  271. package/src/engine/admission.ts +0 -59
  272. package/src/engine/cancellation.ts +0 -38
  273. package/src/engine/engine.ts +0 -56
  274. package/src/engine/execution-coordinator.ts +0 -70
  275. package/src/engine/execution-plan.ts +0 -53
  276. package/src/engine/resources.ts +0 -53
  277. package/src/engine/scheduler.ts +0 -72
  278. package/src/engine/session.ts +0 -247
  279. package/src/eval/README.md +0 -130
  280. package/src/eval/ifeval.ts +0 -81
  281. package/src/eval/kl-prompts.ts +0 -29
  282. package/src/eval/kl.ts +0 -590
  283. package/src/eval/perplexity.ts +0 -164
  284. package/src/eval/quality-db.ts +0 -92
  285. package/src/eval/reference-logits.ts +0 -207
  286. package/src/eval/runner.ts +0 -124
  287. package/src/eval/score.ts +0 -49
  288. package/src/eval/tasks/bfcl.ts +0 -535
  289. package/src/eval/tasks/gsm8k.ts +0 -98
  290. package/src/eval/tasks/hashhop.ts +0 -139
  291. package/src/eval/tasks/humaneval.ts +0 -275
  292. package/src/eval/tasks/ifeval.ts +0 -525
  293. package/src/eval/tasks/mmlu.ts +0 -157
  294. package/src/eval/ultrafeedback.ts +0 -60
  295. package/src/evaldb.ts +0 -199
  296. package/src/expert-io.ts +0 -393
  297. package/src/expert-offload-build.ts +0 -77
  298. package/src/expert-offload.ts +0 -76
  299. package/src/expert-residency.ts +0 -1134
  300. package/src/expert-usage.ts +0 -415
  301. package/src/fill/echo-index.ts +0 -188
  302. package/src/fill/fill-session.ts +0 -452
  303. package/src/fill/proposal.ts +0 -68
  304. package/src/fill/schema-rows.ts +0 -506
  305. package/src/fill/tool-boundary.ts +0 -103
  306. package/src/fit.ts +0 -249
  307. package/src/generate.ts +0 -1346
  308. package/src/grammar.ts +0 -591
  309. package/src/harness-pi.ts +0 -184
  310. package/src/hf-push.ts +0 -628
  311. package/src/hub-rest.ts +0 -242
  312. package/src/index.ts +0 -63
  313. package/src/inference/denoising.ts +0 -17
  314. package/src/inference/draft-acceptance.ts +0 -41
  315. package/src/inference/draft-output.ts +0 -49
  316. package/src/inference/graph.ts +0 -33
  317. package/src/inference/prefill.ts +0 -39
  318. package/src/inference/rollback.ts +0 -7
  319. package/src/jobs/index.ts +0 -31
  320. package/src/jobs/job-entry.ts +0 -107
  321. package/src/jobs/task-client.ts +0 -19
  322. package/src/jobs/types.ts +0 -2
  323. package/src/kv-scheme.ts +0 -257
  324. package/src/kv-store.ts +0 -1099
  325. package/src/lab/README.md +0 -18
  326. package/src/lab/curve/curve-designer.html +0 -303
  327. package/src/lab/curve/curve-sampler.ts +0 -122
  328. package/src/lab/expert-trace/expert-trace.ts +0 -69
  329. package/src/lab/paged-kv/paged-kv-rows.ts +0 -83
  330. package/src/lab/paged-kv/paged-kv.ts +0 -270
  331. package/src/library.ts +0 -40
  332. package/src/lora.ts +0 -408
  333. package/src/md-modules.d.ts +0 -49
  334. package/src/media-fetch.ts +0 -442
  335. package/src/memory/article.ts +0 -558
  336. package/src/memory/chunk-validation.ts +0 -150
  337. package/src/memory/chunk.ts +0 -318
  338. package/src/memory/cluster.ts +0 -351
  339. package/src/memory/coverage.ts +0 -134
  340. package/src/memory/crosslink.ts +0 -0
  341. package/src/memory/db.ts +0 -253
  342. package/src/memory/entity.ts +0 -203
  343. package/src/memory/gate.ts +0 -187
  344. package/src/memory/history.ts +0 -152
  345. package/src/memory/ingest.ts +0 -335
  346. package/src/memory/model.ts +0 -183
  347. package/src/memory/normalize.ts +0 -671
  348. package/src/memory/parse.ts +0 -22
  349. package/src/memory/pipeline.ts +0 -275
  350. package/src/memory/prompts.ts +0 -65
  351. package/src/memory/query.ts +0 -334
  352. package/src/memory/reconcile.ts +0 -734
  353. package/src/memory/redirect.ts +0 -108
  354. package/src/memory/reindex.ts +0 -325
  355. package/src/memory/resolve.ts +0 -307
  356. package/src/memory/rest.ts +0 -245
  357. package/src/memory/route.ts +0 -272
  358. package/src/memory/schedule.ts +0 -165
  359. package/src/memory/stages.ts +0 -777
  360. package/src/memory/synthesize.ts +0 -1638
  361. package/src/memory/tools.ts +0 -749
  362. package/src/memory/vault.ts +0 -1194
  363. package/src/memory/wikify.ts +0 -1064
  364. package/src/mlx/array.ts +0 -388
  365. package/src/mlx/autograd.ts +0 -304
  366. package/src/mlx/checkpoint.ts +0 -149
  367. package/src/mlx/compile.ts +0 -168
  368. package/src/mlx/custom-vjp.ts +0 -188
  369. package/src/mlx/ffi.ts +0 -512
  370. package/src/mlx/materialize.ts +0 -23
  371. package/src/mlx/metal-kernel.ts +0 -159
  372. package/src/mlx/ops.ts +0 -1021
  373. package/src/mlx/quantized-rows.ts +0 -36
  374. package/src/mlx/token-bitmask.ts +0 -52
  375. package/src/mlx/turboquant-kv-decode.ts +0 -82
  376. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  377. package/src/mlx/turboquant-ops.ts +0 -861
  378. package/src/mlx/turboquant-tables.ts +0 -237
  379. package/src/mmap.ts +0 -90
  380. package/src/model/artifact-identity.ts +0 -73
  381. package/src/model/batched-kv.ts +0 -235
  382. package/src/model/batched-mask.ts +0 -279
  383. package/src/model/batched-quant.ts +0 -212
  384. package/src/model/batched-quantized-kv.ts +0 -144
  385. package/src/model/batched-rotating-quant.ts +0 -353
  386. package/src/model/batched-rotating-state.ts +0 -149
  387. package/src/model/batched-rotating.ts +0 -338
  388. package/src/model/batched-row-storage.ts +0 -179
  389. package/src/model/batched-ssm.ts +0 -52
  390. package/src/model/batched-turboquant-kv.ts +0 -93
  391. package/src/model/compiled-decode.ts +0 -712
  392. package/src/model/decoded-kv-donor.ts +0 -12
  393. package/src/model/delayed-quantized-kv.ts +0 -72
  394. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  395. package/src/model/delayed-turboquant-kv.ts +0 -55
  396. package/src/model/diffusion-gemma.ts +0 -1026
  397. package/src/model/factory.ts +0 -189
  398. package/src/model/fingerprint.ts +0 -33
  399. package/src/model/flash-attention.ts +0 -694
  400. package/src/model/full-prefill-padding.ts +0 -56
  401. package/src/model/full-prefill-row.ts +0 -105
  402. package/src/model/gemma4-base.ts +0 -2351
  403. package/src/model/gemma4.ts +0 -1315
  404. package/src/model/generated/gemma4-12b.ts +0 -429
  405. package/src/model/generated/gemma4-26b.ts +0 -448
  406. package/src/model/generated/gemma4-e4b.ts +0 -717
  407. package/src/model/generated/index.ts +0 -18
  408. package/src/model/glm52-atlas.ts +0 -511
  409. package/src/model/glm52-cache.ts +0 -792
  410. package/src/model/glm52-config.ts +0 -262
  411. package/src/model/glm52-container.ts +0 -305
  412. package/src/model/glm52-coupling.ts +0 -388
  413. package/src/model/glm52-dsa.ts +0 -606
  414. package/src/model/glm52-expert-layout.ts +0 -253
  415. package/src/model/glm52-layout.ts +0 -152
  416. package/src/model/glm52-memory.ts +0 -500
  417. package/src/model/glm52-mla.ts +0 -792
  418. package/src/model/glm52-moe.ts +0 -223
  419. package/src/model/glm52-pilot.ts +0 -537
  420. package/src/model/glm52-reference.ts +0 -445
  421. package/src/model/glm52-residency.ts +0 -557
  422. package/src/model/glm52-resident-weights.ts +0 -135
  423. package/src/model/glm52-streamed-experts.ts +0 -453
  424. package/src/model/glm52-streamed-metal.ts +0 -610
  425. package/src/model/glm52-weights.ts +0 -503
  426. package/src/model/glm52.ts +0 -1132
  427. package/src/model/implementation.ts +0 -69
  428. package/src/model/kv-attention-view.ts +0 -67
  429. package/src/model/minicpm5.ts +0 -376
  430. package/src/model/profile.ts +0 -443
  431. package/src/model/qwen-append.ts +0 -12
  432. package/src/model/qwen-conv.ts +0 -52
  433. package/src/model/qwen3-delta-state.ts +0 -74
  434. package/src/model/qwen3-delta.ts +0 -451
  435. package/src/model/qwen3-moe.ts +0 -404
  436. package/src/model/qwen3-mrope.ts +0 -252
  437. package/src/model/qwen3.ts +0 -277
  438. package/src/model/qwen3_5-checkpoint.ts +0 -164
  439. package/src/model/qwen3_5.ts +0 -819
  440. package/src/model/rotating-kv-donor.ts +0 -35
  441. package/src/model/rotating-kv-layout.ts +0 -262
  442. package/src/model/rotating-row-transaction.ts +0 -35
  443. package/src/model/speculative-rotating-kv.ts +0 -101
  444. package/src/model/ssm-prefill-padding.ts +0 -39
  445. package/src/model/support.ts +0 -102
  446. package/src/model/trellis-balanced-scatter.ts +0 -51
  447. package/src/model/trellis-linear.ts +0 -672
  448. package/src/model/trellis-shared-m.ts +0 -151
  449. package/src/model/trellis-shared-scatter.ts +0 -144
  450. package/src/model/trellis-splitk-prefill.ts +0 -158
  451. package/src/model/trellis-tiled-prefill.ts +0 -136
  452. package/src/model/trellis-vector-expand.ts +0 -62
  453. package/src/model/trellis-wide-prefill.ts +0 -94
  454. package/src/model/turboquant-codec.ts +0 -83
  455. package/src/model/universal/archs.ts +0 -332
  456. package/src/model/universal/dense.ts +0 -612
  457. package/src/model/universal/modules.ts +0 -170
  458. package/src/model/universal/rope.ts +0 -381
  459. package/src/native/expert_io.c +0 -523
  460. package/src/native/frame_extract.swift +0 -140
  461. package/src/native-pack.ts +0 -162
  462. package/src/pi-provider.ts +0 -53
  463. package/src/pi-session.ts +0 -81
  464. package/src/pi-terminal.ts +0 -274
  465. package/src/pi-web.ts +0 -1932
  466. package/src/preflight.ts +0 -158
  467. package/src/prompt-cache.ts +0 -460
  468. package/src/quantize/allocator.ts +0 -627
  469. package/src/quantize/atomic-output.ts +0 -38
  470. package/src/quantize/calibration.ts +0 -181
  471. package/src/quantize/config-writer.ts +0 -145
  472. package/src/quantize/index.ts +0 -84
  473. package/src/quantize/job.ts +0 -162
  474. package/src/quantize/quantizer.ts +0 -666
  475. package/src/quantize/rotate.ts +0 -603
  476. package/src/quantize/safetensors-writer.ts +0 -249
  477. package/src/quantize/sensitivity.ts +0 -216
  478. package/src/quantize/trellis.ts +0 -586
  479. package/src/quantize/weight-transform.ts +0 -292
  480. package/src/registry.ts +0 -602
  481. package/src/runtime-config.ts +0 -81
  482. package/src/safetensors.ts +0 -158
  483. package/src/sampler.ts +0 -1011
  484. package/src/serve/admin-routes.ts +0 -184
  485. package/src/serve/assets/curve-terrain.html +0 -105
  486. package/src/serve/assets/training-inference-map.html +0 -575
  487. package/src/serve/aux-routes.ts +0 -132
  488. package/src/serve/batch-scheduler.ts +0 -8
  489. package/src/serve/checkpoint-identity.ts +0 -64
  490. package/src/serve/generation-gateway.ts +0 -496
  491. package/src/serve/isolate.ts +0 -609
  492. package/src/serve/kv-budget.ts +0 -14
  493. package/src/serve/lab-routes.ts +0 -341
  494. package/src/serve/lane-registry.ts +0 -45
  495. package/src/serve/model-admin-routes.ts +0 -155
  496. package/src/serve/model-binding.ts +0 -43
  497. package/src/serve/model-host.ts +0 -511
  498. package/src/serve/parent-application.ts +0 -56
  499. package/src/serve/prompt-response-trace.ts +0 -147
  500. package/src/serve/response-lease.ts +0 -24
  501. package/src/serve/session-search.ts +0 -326
  502. package/src/serve/static-routes.ts +0 -101
  503. package/src/server.ts +0 -1314
  504. package/src/spec/assistant-rows.ts +0 -92
  505. package/src/spec/assistant-source.ts +0 -126
  506. package/src/spec/cache-identity.ts +0 -22
  507. package/src/spec/deepspec-rows.ts +0 -29
  508. package/src/spec/deepspec-source.ts +0 -145
  509. package/src/spec/dflash-source.ts +0 -149
  510. package/src/spec/drafter.ts +0 -304
  511. package/src/spec/dspark/ab-stats.ts +0 -136
  512. package/src/spec/dspark/calibration.ts +0 -112
  513. package/src/spec/dspark/data-dflash.ts +0 -156
  514. package/src/spec/dspark/data.ts +0 -201
  515. package/src/spec/dspark/deepspec-module.ts +0 -776
  516. package/src/spec/dspark/generate-dflash.ts +0 -245
  517. package/src/spec/dspark/generate.ts +0 -290
  518. package/src/spec/dspark/loader.ts +0 -23
  519. package/src/spec/dspark/loss.ts +0 -213
  520. package/src/spec/dspark/module-dflash.ts +0 -560
  521. package/src/spec/dspark/module.ts +0 -540
  522. package/src/spec/dspark/quantize-drafter.ts +0 -91
  523. package/src/spec/dspark/sample.ts +0 -112
  524. package/src/spec/generate.ts +0 -276
  525. package/src/spec/glm52-mtp-graph.ts +0 -29
  526. package/src/spec/glm52-mtp-rows.ts +0 -132
  527. package/src/spec/glm52-mtp-source.ts +0 -317
  528. package/src/spec/glm52-mtp-state.ts +0 -33
  529. package/src/spec/ngram-source.ts +0 -204
  530. package/src/spec/projected-draft-rows.ts +0 -73
  531. package/src/spec/qwen-mtp-module.ts +0 -171
  532. package/src/spec/qwen-mtp-rows.ts +0 -219
  533. package/src/spec/qwen-mtp-source.ts +0 -383
  534. package/src/spec/qwen-mtp-state.ts +0 -30
  535. package/src/spec/serve-loop.ts +0 -585
  536. package/src/spec/source.ts +0 -215
  537. package/src/spec/two-model-rows.ts +0 -61
  538. package/src/spec/two-model.ts +0 -113
  539. package/src/ssd-cache.ts +0 -372
  540. package/src/ssd-durability.ts +0 -211
  541. package/src/tokenizer.ts +0 -335
  542. package/src/tool-approvals.ts +0 -131
  543. package/src/tool-call.ts +0 -695
  544. package/src/train/dataset.ts +0 -484
  545. package/src/train/diffusion-lora.ts +0 -216
  546. package/src/train/export.ts +0 -32
  547. package/src/train/flash-cce.ts +0 -1234
  548. package/src/train/forward.ts +0 -266
  549. package/src/train/fuse.ts +0 -243
  550. package/src/train/index.ts +0 -46
  551. package/src/train/job.ts +0 -205
  552. package/src/train/lora-params.ts +0 -254
  553. package/src/train/loss.ts +0 -1591
  554. package/src/train/merge.ts +0 -195
  555. package/src/train/optimizer.ts +0 -183
  556. package/src/train/prefix-shared.ts +0 -490
  557. package/src/train/rank.ts +0 -137
  558. package/src/train/segmented.ts +0 -1828
  559. package/src/train/steel-qmm-header.ts +0 -1461
  560. package/src/train/trainer.ts +0 -1221
  561. package/src/train/watch.ts +0 -542
  562. package/src/vision/diffusion-vision.ts +0 -620
  563. package/src/vision/embedder.ts +0 -168
  564. package/src/vision/preprocess.ts +0 -247
  565. package/src/vision/prompt.ts +0 -373
  566. package/src/vision/qwen3vl-preprocess.ts +0 -409
  567. package/src/vision/qwen3vl-prompt.ts +0 -196
  568. package/src/vision/qwen3vl-tower.ts +0 -555
  569. package/src/vision/siglip.ts +0 -599
  570. package/src/vision/video-frames.ts +0 -148
  571. package/src/web/app.html +0 -2143
  572. package/src/web/app.js +0 -5703
  573. package/src/web/skills/memory/SKILL.md +0 -120
  574. package/src/web/skills/web-research/SKILL.md +0 -46
  575. package/src/web/skills.ts +0 -97
  576. package/src/web/src/adapters-panel.ts +0 -288
  577. package/src/web/src/assistant.ts +0 -451
  578. package/src/web/src/chat.ts +0 -1057
  579. package/src/web/src/composer.ts +0 -959
  580. package/src/web/src/dataset.ts +0 -132
  581. package/src/web/src/finetune.ts +0 -267
  582. package/src/web/src/hub.ts +0 -428
  583. package/src/web/src/main.ts +0 -98
  584. package/src/web/src/markdown.ts +0 -0
  585. package/src/web/src/memory-panel.ts +0 -572
  586. package/src/web/src/model-picker.ts +0 -159
  587. package/src/web/src/palette.ts +0 -346
  588. package/src/web/src/protocol.ts +0 -26
  589. package/src/web/src/quantize.ts +0 -167
  590. package/src/web/src/rag.ts +0 -274
  591. package/src/web/src/sessions.ts +0 -469
  592. package/src/web/src/shell.ts +0 -804
  593. package/src/web/src/ui-catalog.ts +0 -78
  594. package/src/web/vendor/README.md +0 -61
  595. package/src/web-assets.ts +0 -47
  596. package/src/web-tools.ts +0 -478
  597. package/src/weights.ts +0 -255
  598. /package/src/{serve → server}/completion-sink.ts +0 -0
  599. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  600. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  601. /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
@@ -1,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
- }