@mlx-bun/inference 0.0.0-stage → 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 (467) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1475 -2
  3. package/THIRD_PARTY_NOTICES.md +3404 -0
  4. package/dist/native/libmlx_bun_ane.dylib +0 -0
  5. package/dist/native/libmlx_bun_expert_io.dylib +0 -0
  6. package/dist/native/mlx-bun-frame-extract +0 -0
  7. package/examples/fit-model.ts +25 -0
  8. package/examples/qwen3-forward.ts +42 -0
  9. package/examples/qwen3-generate.ts +28 -0
  10. package/examples/trellis-expand.ts +23 -0
  11. package/package.json +129 -4
  12. package/src/adapters/available.ts +110 -0
  13. package/src/adapters/index.ts +3 -0
  14. package/src/adapters/manager.ts +316 -0
  15. package/src/artifacts/auxiliary-files.ts +45 -0
  16. package/src/artifacts/config-dialects.ts +94 -0
  17. package/src/artifacts/config.ts +341 -0
  18. package/src/artifacts/draft-vocabulary.ts +30 -0
  19. package/src/artifacts/expert-offload-build.ts +75 -0
  20. package/src/artifacts/expert-offload.ts +89 -0
  21. package/src/artifacts/experts/index.ts +3 -0
  22. package/src/artifacts/experts/io.ts +386 -0
  23. package/src/artifacts/experts/residency.ts +1134 -0
  24. package/src/artifacts/experts/usage.ts +415 -0
  25. package/src/artifacts/fingerprint.ts +44 -0
  26. package/src/artifacts/glm52/container.ts +305 -0
  27. package/src/artifacts/glm52/expert-layout.ts +249 -0
  28. package/src/artifacts/glm52/index.ts +7 -0
  29. package/src/artifacts/glm52/kernel-layout.ts +124 -0
  30. package/src/artifacts/glm52/layout.ts +152 -0
  31. package/src/artifacts/glm52/memory.ts +500 -0
  32. package/src/artifacts/glm52/resident-weights.ts +135 -0
  33. package/src/artifacts/glm52/weights.ts +503 -0
  34. package/src/artifacts/glm52-config.ts +262 -0
  35. package/src/artifacts/identity.ts +94 -0
  36. package/src/artifacts/index.ts +15 -0
  37. package/src/artifacts/mmap.ts +90 -0
  38. package/src/artifacts/safetensors-writer.ts +249 -0
  39. package/src/artifacts/safetensors.ts +164 -0
  40. package/src/artifacts/turboquant-fold.ts +42 -0
  41. package/src/artifacts/weight-audit.ts +43 -0
  42. package/src/artifacts/weights.ts +255 -0
  43. package/src/contracts/index.ts +2 -0
  44. package/src/contracts/mlx/attention.ts +8 -0
  45. package/src/contracts/mlx/cache.ts +510 -0
  46. package/src/contracts/mlx/checkpoint.ts +10 -0
  47. package/src/contracts/mlx/draft-checkpoint.ts +14 -0
  48. package/src/contracts/mlx/draft-projection.ts +27 -0
  49. package/src/contracts/mlx/draft-target.ts +84 -0
  50. package/src/contracts/mlx/drafter.ts +176 -0
  51. package/src/contracts/mlx/forward-work.ts +23 -0
  52. package/src/contracts/mlx/glm52-weights.ts +25 -0
  53. package/src/contracts/mlx/graph.ts +309 -0
  54. package/src/contracts/mlx/head.ts +6 -0
  55. package/src/contracts/mlx/index.ts +15 -0
  56. package/src/contracts/mlx/kv-codec.ts +9 -0
  57. package/src/contracts/mlx/media.ts +113 -0
  58. package/src/contracts/mlx/positions.ts +16 -0
  59. package/src/contracts/mlx/row-table.ts +2 -0
  60. package/src/contracts/mlx/trainable.ts +103 -0
  61. package/src/contracts/mlx/whisper.ts +96 -0
  62. package/src/contracts/portable/composition.ts +106 -0
  63. package/src/contracts/portable/denoising.ts +17 -0
  64. package/src/contracts/portable/execution.ts +79 -0
  65. package/src/contracts/portable/generation.ts +103 -0
  66. package/src/contracts/portable/graph.ts +80 -0
  67. package/src/contracts/portable/index.ts +15 -0
  68. package/src/contracts/portable/memory-plan.ts +26 -0
  69. package/src/contracts/portable/object-cache.ts +8 -0
  70. package/src/contracts/portable/prefill.ts +63 -0
  71. package/src/contracts/portable/prefix-cache.ts +33 -0
  72. package/src/contracts/portable/prompt.ts +2 -0
  73. package/src/contracts/portable/resources.ts +25 -0
  74. package/src/contracts/portable/rollback.ts +7 -0
  75. package/src/contracts/portable/sampling.ts +44 -0
  76. package/src/contracts/portable/scheduling.ts +54 -0
  77. package/src/contracts/portable/token-delivery.ts +4 -0
  78. package/src/contracts/portable/tsconfig.json +14 -0
  79. package/src/embeddings/index.ts +1 -0
  80. package/src/embeddings/text.ts +85 -0
  81. package/src/execution/admission.ts +59 -0
  82. package/src/execution/batch-group.ts +1240 -0
  83. package/src/execution/batch-types.ts +225 -0
  84. package/src/execution/cancellation.ts +38 -0
  85. package/src/execution/continuation-request.ts +45 -0
  86. package/src/execution/continuation-types.ts +10 -0
  87. package/src/execution/coordinator.ts +70 -0
  88. package/src/execution/denoising-group.ts +180 -0
  89. package/src/execution/engine.ts +56 -0
  90. package/src/execution/fill-group.ts +505 -0
  91. package/src/execution/fit.ts +184 -0
  92. package/src/execution/gateway-binding.ts +178 -0
  93. package/src/execution/grammar-group.ts +224 -0
  94. package/src/execution/index.ts +18 -0
  95. package/src/execution/methods.ts +75 -0
  96. package/src/execution/mixed-iteration.ts +96 -0
  97. package/src/execution/plan.ts +65 -0
  98. package/src/execution/prefill-cohort.ts +100 -0
  99. package/src/execution/prefill-rows.ts +263 -0
  100. package/src/execution/request-policy.ts +19 -0
  101. package/src/execution/row-sampling.ts +49 -0
  102. package/src/execution/scheduler.ts +92 -0
  103. package/src/execution/session.ts +247 -0
  104. package/src/execution/speculative-group.ts +797 -0
  105. package/src/execution/tasks.ts +44 -0
  106. package/src/generation/autoregressive.ts +840 -0
  107. package/src/generation/bindings/autoregressive.ts +104 -0
  108. package/src/generation/bindings/denoising.ts +60 -0
  109. package/src/generation/bindings/prefill-policy.ts +36 -0
  110. package/src/generation/checkpoint-identity.ts +60 -0
  111. package/src/generation/denoising.ts +85 -0
  112. package/src/generation/diffusion.ts +583 -0
  113. package/src/generation/fill/echo-index.ts +188 -0
  114. package/src/generation/fill/index.ts +3 -0
  115. package/src/generation/fill/proposal.ts +69 -0
  116. package/src/generation/fill/schema-rows.ts +506 -0
  117. package/src/generation/fill/session.ts +464 -0
  118. package/src/generation/fill/tool-boundary.ts +103 -0
  119. package/src/generation/fill-append.ts +58 -0
  120. package/src/generation/generate.ts +44 -0
  121. package/src/generation/grammar-step.ts +39 -0
  122. package/src/generation/index.ts +14 -0
  123. package/src/generation/phase-dispatch.ts +115 -0
  124. package/src/generation/prefill.ts +33 -0
  125. package/src/generation/result.ts +49 -0
  126. package/src/generation/scopes.ts +157 -0
  127. package/src/generation/speculative/acceptance.ts +41 -0
  128. package/src/generation/speculative/adaptive-draft-gate.ts +166 -0
  129. package/src/generation/speculative/bindings/assistant-rows.ts +102 -0
  130. package/src/generation/speculative/bindings/autoregressive-rows.ts +124 -0
  131. package/src/generation/speculative/bindings/binding.ts +106 -0
  132. package/src/generation/speculative/bindings/deepspec-rows.ts +33 -0
  133. package/src/generation/speculative/bindings/draft-target.ts +10 -0
  134. package/src/generation/speculative/bindings/glm52-mtp-rows.ts +128 -0
  135. package/src/generation/speculative/bindings/projected-context-rows.ts +78 -0
  136. package/src/generation/speculative/bindings/projected-draft-rows.ts +95 -0
  137. package/src/generation/speculative/bindings/qwen-mtp-rows.ts +336 -0
  138. package/src/generation/speculative/bindings/two-model-rows.ts +63 -0
  139. package/src/generation/speculative/cache-identity.ts +16 -0
  140. package/src/generation/speculative/draft-kind.ts +149 -0
  141. package/src/generation/speculative/draft-policy.ts +41 -0
  142. package/src/generation/speculative/draft-registry.ts +118 -0
  143. package/src/generation/speculative/draft-vocab.ts +110 -0
  144. package/src/generation/speculative/index.ts +17 -0
  145. package/src/generation/speculative/output.ts +49 -0
  146. package/src/generation/speculative/round.ts +307 -0
  147. package/src/generation/speculative/source.ts +144 -0
  148. package/src/generation/speculative/sources/assistant-source.ts +64 -0
  149. package/src/generation/speculative/sources/deepspec-source.ts +70 -0
  150. package/src/generation/speculative/sources/dflash-source.ts +84 -0
  151. package/src/generation/speculative/sources/dflash2-source.ts +86 -0
  152. package/src/generation/speculative/sources/glm52-mtp-source.ts +66 -0
  153. package/src/generation/speculative/sources/ngram-source.ts +165 -0
  154. package/src/generation/speculative/sources/qwen-mtp-source.ts +154 -0
  155. package/src/generation/speculative/sources/two-model.ts +75 -0
  156. package/src/generation/types.ts +194 -0
  157. package/src/index.ts +20 -0
  158. package/src/input/audio/audiotoolbox.ts +112 -0
  159. package/src/input/audio/decode.ts +133 -0
  160. package/src/input/audio/features.ts +202 -0
  161. package/src/input/audio/index.ts +6 -0
  162. package/src/input/audio/transcode.ts +67 -0
  163. package/src/input/audio/whisper-mel.ts +185 -0
  164. package/src/input/audio/whisper-tokenizer.ts +238 -0
  165. package/src/input/chat-template.ts +389 -0
  166. package/src/input/index.ts +6 -0
  167. package/src/input/media-fetch.ts +439 -0
  168. package/src/input/packed-mixed.ts +43 -0
  169. package/src/input/special-tokens.ts +75 -0
  170. package/src/input/token-groups.ts +49 -0
  171. package/src/input/tokenizer.ts +336 -0
  172. package/src/input/tool-call.ts +689 -0
  173. package/src/input/vision/index.ts +3 -0
  174. package/src/input/vision/preprocess.ts +247 -0
  175. package/src/input/vision/prompt.ts +398 -0
  176. package/src/input/vision/qwen3vl-preprocess.ts +409 -0
  177. package/src/input/vision/qwen3vl-prompt.ts +237 -0
  178. package/src/input/vision/video-frames.ts +100 -0
  179. package/src/kernels/ane/fill.ts +101 -0
  180. package/src/kernels/ane/linear.ts +416 -0
  181. package/src/kernels/attention/flash.ts +694 -0
  182. package/src/kernels/attention/kvq4-decode.ts +155 -0
  183. package/src/kernels/attention/masks.ts +40 -0
  184. package/src/kernels/attention/multi-query.ts +193 -0
  185. package/src/kernels/attention/paged.ts +130 -0
  186. package/src/kernels/delta/gated.ts +306 -0
  187. package/src/kernels/delta/index.ts +3 -0
  188. package/src/kernels/delta/silu-rms-heads.ts +69 -0
  189. package/src/kernels/delta/state.ts +74 -0
  190. package/src/kernels/gated-rms-norm.ts +64 -0
  191. package/src/kernels/glm52/layout.ts +37 -0
  192. package/src/kernels/glm52/streamed-experts.ts +360 -0
  193. package/src/kernels/logits.ts +38 -0
  194. package/src/kernels/quantization/affine-row.ts +154 -0
  195. package/src/kernels/quantization/affine-rows.ts +36 -0
  196. package/src/kernels/quantization/affine-split-mma.ts +144 -0
  197. package/src/kernels/quantization/affine3-mma.ts +117 -0
  198. package/src/kernels/quantization/affine3-rows.ts +103 -0
  199. package/src/kernels/quantization/affine4-mma.ts +113 -0
  200. package/src/kernels/quantization/affine4-rows.ts +102 -0
  201. package/src/kernels/residual-rms-norm.ts +65 -0
  202. package/src/kernels/sampling/index.ts +2 -0
  203. package/src/kernels/sampling/normalized-argmax.ts +116 -0
  204. package/src/kernels/sampling/token-bitmask.ts +52 -0
  205. package/src/kernels/softcap.ts +35 -0
  206. package/src/kernels/trellis/codebook.ts +123 -0
  207. package/src/kernels/trellis/down-codebook.ts +184 -0
  208. package/src/kernels/trellis/down-factored.ts +180 -0
  209. package/src/kernels/trellis/down-k3-interleaved-factored.ts +146 -0
  210. package/src/kernels/trellis/down-mma-wide.ts +195 -0
  211. package/src/kernels/trellis/down-mma.ts +145 -0
  212. package/src/kernels/trellis/expand.ts +42 -0
  213. package/src/kernels/trellis/gate-up-codebook-mma.ts +141 -0
  214. package/src/kernels/trellis/gate-up-codebook.ts +137 -0
  215. package/src/kernels/trellis/gate-up-factored.ts +197 -0
  216. package/src/kernels/trellis/gate-up-mma-wide.ts +147 -0
  217. package/src/kernels/trellis/gate-up-mma.ts +148 -0
  218. package/src/kernels/trellis/gate-up.ts +248 -0
  219. package/src/kernels/trellis/geometry.ts +22 -0
  220. package/src/kernels/trellis/index.ts +103 -0
  221. package/src/kernels/trellis/launch.ts +12 -0
  222. package/src/kernels/trellis/mixed-gate-up-factored.ts +119 -0
  223. package/src/kernels/trellis/mixed-gate-up.ts +146 -0
  224. package/src/kernels/trellis/qualified.ts +34 -0
  225. package/src/kernels/trellis/reduce.ts +150 -0
  226. package/src/kernels/trellis/scatter.ts +407 -0
  227. package/src/kernels/trellis/splitk-prefill.ts +152 -0
  228. package/src/kernels/trellis/tiled-prefill.ts +131 -0
  229. package/src/kernels/trellis/vector-expand.ts +58 -0
  230. package/src/kernels/trellis/wide-prefill.ts +89 -0
  231. package/src/kernels/turboquant/decode.ts +82 -0
  232. package/src/kernels/turboquant/index.ts +5 -0
  233. package/src/kernels/turboquant/inverse.ts +122 -0
  234. package/src/kernels/turboquant/ops.ts +860 -0
  235. package/src/kernels/turboquant/signs.ts +130 -0
  236. package/src/kernels/turboquant/tables.ts +124 -0
  237. package/src/layers/affine-verify-linear.ts +75 -0
  238. package/src/layers/ane-prefill-split.ts +283 -0
  239. package/src/layers/compiled-normalization.ts +41 -0
  240. package/src/layers/delta-heads.ts +23 -0
  241. package/src/layers/dense-embedding.ts +29 -0
  242. package/src/layers/dense-linear.ts +32 -0
  243. package/src/layers/dequantized-linear.ts +72 -0
  244. package/src/layers/folded-attention.ts +42 -0
  245. package/src/layers/geglu.ts +130 -0
  246. package/src/layers/helpers.ts +7 -0
  247. package/src/layers/index.ts +36 -0
  248. package/src/layers/kv4-decode-attention.ts +15 -0
  249. package/src/layers/layer-norm.ts +24 -0
  250. package/src/layers/loading.ts +92 -0
  251. package/src/layers/lora.ts +150 -0
  252. package/src/layers/normalization.ts +9 -0
  253. package/src/layers/prefix-layout.ts +33 -0
  254. package/src/layers/quantized-append-attention.ts +32 -0
  255. package/src/layers/quantized-attention.ts +353 -0
  256. package/src/layers/quantized-embedding.ts +39 -0
  257. package/src/layers/quantized-linear.ts +137 -0
  258. package/src/layers/quantized-switch-linear.ts +37 -0
  259. package/src/layers/qwen-append.ts +12 -0
  260. package/src/layers/qwen-mrope.ts +238 -0
  261. package/src/layers/rope-2d.ts +110 -0
  262. package/src/layers/rope.ts +389 -0
  263. package/src/layers/swiglu.ts +23 -0
  264. package/src/layers/trellis-codebook-down.ts +52 -0
  265. package/src/layers/trellis-codebook-gate-up.ts +42 -0
  266. package/src/layers/trellis-down.ts +117 -0
  267. package/src/layers/trellis-gate-up.ts +71 -0
  268. package/src/layers/trellis-linear.ts +211 -0
  269. package/src/layers/trellis-prompt.ts +102 -0
  270. package/src/layers/trellis-reduce-gate-up.ts +73 -0
  271. package/src/layers/trellis-scatter-down.ts +98 -0
  272. package/src/models/ane-install.ts +74 -0
  273. package/src/models/audio/conformer.ts +656 -0
  274. package/src/models/cache-composition.ts +44 -0
  275. package/src/models/capabilities.ts +89 -0
  276. package/src/models/chat-template.ts +10 -0
  277. package/src/models/composition.ts +107 -0
  278. package/src/models/diffusion-gemma/experts.ts +59 -0
  279. package/src/models/diffusion-gemma/family.ts +21 -0
  280. package/src/models/diffusion-gemma/model.ts +900 -0
  281. package/src/models/diffusion-gemma/vision.ts +549 -0
  282. package/src/models/drafter-loaders.ts +49 -0
  283. package/src/models/drafters.ts +39 -0
  284. package/src/models/factory.ts +351 -0
  285. package/src/models/families.ts +70 -0
  286. package/src/models/family.ts +125 -0
  287. package/src/models/gemma4/assistant-target.ts +46 -0
  288. package/src/models/gemma4/assistant.ts +327 -0
  289. package/src/models/gemma4/blocks.ts +755 -0
  290. package/src/models/gemma4/compiled-decode.ts +452 -0
  291. package/src/models/gemma4/family.ts +39 -0
  292. package/src/models/gemma4/gemma4-12b.ts +677 -0
  293. package/src/models/gemma4/gemma4-26b.ts +544 -0
  294. package/src/models/gemma4/gemma4-e4b.ts +599 -0
  295. package/src/models/gemma4/graphs.ts +147 -0
  296. package/src/models/gemma4/media.ts +88 -0
  297. package/src/models/gemma4/model.ts +860 -0
  298. package/src/models/gemma4/serving-blocks.ts +72 -0
  299. package/src/models/gemma4/trainable.ts +186 -0
  300. package/src/models/gemma4/written-blocks.ts +232 -0
  301. package/src/models/gemma4/written-forward.ts +159 -0
  302. package/src/models/gemma4/written-input.ts +29 -0
  303. package/src/models/glm52/atlas.ts +507 -0
  304. package/src/models/glm52/coupling.ts +388 -0
  305. package/src/models/glm52/dsa.ts +601 -0
  306. package/src/models/glm52/family.ts +20 -0
  307. package/src/models/glm52/mla.ts +812 -0
  308. package/src/models/glm52/model.ts +1127 -0
  309. package/src/models/glm52/moe.ts +223 -0
  310. package/src/models/glm52/mtp.ts +60 -0
  311. package/src/models/glm52/pilot.ts +537 -0
  312. package/src/models/glm52/reference.ts +445 -0
  313. package/src/models/glm52/residency.ts +557 -0
  314. package/src/models/glm52/streamed-experts.ts +447 -0
  315. package/src/models/graph.ts +59 -0
  316. package/src/models/implementation.ts +230 -0
  317. package/src/models/index.ts +15 -0
  318. package/src/models/media-input.ts +15 -0
  319. package/src/models/memory-plan.ts +39 -0
  320. package/src/models/minicpm5/family.ts +29 -0
  321. package/src/models/minicpm5/model.ts +394 -0
  322. package/src/models/minicpm5/trainable.ts +90 -0
  323. package/src/models/profile.ts +337 -0
  324. package/src/models/qwen/blocks.ts +658 -0
  325. package/src/models/qwen/checkpoint.ts +190 -0
  326. package/src/models/qwen/families.ts +57 -0
  327. package/src/models/qwen/media-input.ts +161 -0
  328. package/src/models/qwen/mtp.ts +101 -0
  329. package/src/models/qwen/qwen3-moe.ts +355 -0
  330. package/src/models/qwen/qwen3.ts +323 -0
  331. package/src/models/qwen/qwen38-27b-trellis-m1max.ts +1532 -0
  332. package/src/models/qwen/qwen38-27b-trellis-m4pro.ts +1092 -0
  333. package/src/models/qwen/qwen3_5.ts +433 -0
  334. package/src/models/qwen/written-layers.ts +285 -0
  335. package/src/models/row-table.ts +1 -0
  336. package/src/models/runtime.ts +73 -0
  337. package/src/models/speculative/deepspec-artifact.ts +10 -0
  338. package/src/models/speculative/deepspec.ts +744 -0
  339. package/src/models/speculative/dflash.ts +553 -0
  340. package/src/models/speculative/dflash2-artifact.ts +9 -0
  341. package/src/models/speculative/dflash2-operations.ts +1 -0
  342. package/src/models/speculative/dflash2.ts +576 -0
  343. package/src/models/speculative/loader.ts +29 -0
  344. package/src/models/support.ts +79 -0
  345. package/src/models/universal/archs.ts +365 -0
  346. package/src/models/universal/blocks.ts +479 -0
  347. package/src/models/universal/dense.ts +255 -0
  348. package/src/models/universal/family.ts +17 -0
  349. package/src/models/vision/qwen3vl.ts +566 -0
  350. package/src/models/vision/siglip.ts +520 -0
  351. package/src/models/vision/unified.ts +176 -0
  352. package/src/models/whisper/family.ts +19 -0
  353. package/src/models/whisper/fast.ts +734 -0
  354. package/src/models/whisper/model.ts +430 -0
  355. package/src/runtime/compiled-trace.ts +16 -0
  356. package/src/runtime/config.ts +77 -0
  357. package/src/runtime/expert-trace.ts +71 -0
  358. package/src/runtime/graph-forward.ts +65 -0
  359. package/src/runtime/integers.ts +6 -0
  360. package/src/runtime/native.ts +16 -0
  361. package/src/runtime/resources.ts +53 -0
  362. package/src/runtime/row-operations.ts +42 -0
  363. package/src/runtime/trace.ts +161 -0
  364. package/src/sampling/curve.ts +122 -0
  365. package/src/sampling/draft.ts +55 -0
  366. package/src/sampling/extras.ts +45 -0
  367. package/src/sampling/filters.ts +134 -0
  368. package/src/sampling/grammar.ts +630 -0
  369. package/src/sampling/hlg.ts +290 -0
  370. package/src/sampling/index.ts +9 -0
  371. package/src/sampling/processors.ts +142 -0
  372. package/src/sampling/sampler.ts +120 -0
  373. package/src/sampling/step.ts +343 -0
  374. package/src/sampling/types.ts +161 -0
  375. package/src/sampling/window-filters.ts +46 -0
  376. package/src/scoring/decisions.ts +96 -0
  377. package/src/scoring/full-sequence.ts +103 -0
  378. package/src/scoring/index.ts +9 -0
  379. package/src/scoring/kl.ts +29 -0
  380. package/src/scoring/perplexity.ts +165 -0
  381. package/src/scoring/response-logps.ts +65 -0
  382. package/src/state/affine-attention.ts +180 -0
  383. package/src/state/attachment.ts +68 -0
  384. package/src/state/attention-read.ts +215 -0
  385. package/src/state/batched-kv.ts +294 -0
  386. package/src/state/batched-mask.ts +445 -0
  387. package/src/state/batched-quant.ts +389 -0
  388. package/src/state/batched-quantized-kv.ts +209 -0
  389. package/src/state/batched-rotating-quant.ts +457 -0
  390. package/src/state/batched-rotating-state.ts +149 -0
  391. package/src/state/batched-rotating.ts +430 -0
  392. package/src/state/batched-row-storage.ts +179 -0
  393. package/src/state/batched-ssm.ts +54 -0
  394. package/src/state/batched-turboquant-kv.ts +144 -0
  395. package/src/state/bf16-first-kv.ts +663 -0
  396. package/src/state/cache-service.ts +592 -0
  397. package/src/state/capabilities.ts +65 -0
  398. package/src/state/checkpoint.ts +46 -0
  399. package/src/state/continuation-persistence.ts +126 -0
  400. package/src/state/decode-step-inputs.ts +31 -0
  401. package/src/state/decoded-kv-donor.ts +12 -0
  402. package/src/state/delayed-quantized-kv.ts +141 -0
  403. package/src/state/delayed-rotating-quantized-kv.ts +178 -0
  404. package/src/state/delayed-turboquant-kv.ts +123 -0
  405. package/src/state/durability.ts +214 -0
  406. package/src/state/encoder-cache.ts +26 -0
  407. package/src/state/full-kv-row-append.ts +33 -0
  408. package/src/state/full-kv-row-donor.ts +54 -0
  409. package/src/state/full-prefill-padding.ts +56 -0
  410. package/src/state/full-prefill-row.ts +112 -0
  411. package/src/state/full-transitioning-kv-rows.ts +70 -0
  412. package/src/state/glm52-cache.ts +783 -0
  413. package/src/state/grouped-heads-kv4-cache.ts +26 -0
  414. package/src/state/index.ts +38 -0
  415. package/src/state/kv-attention-view.ts +77 -0
  416. package/src/state/kv-budget.ts +14 -0
  417. package/src/state/kv-scheme.ts +315 -0
  418. package/src/state/kv-storage.ts +57 -0
  419. package/src/state/kv-tensor-rows.ts +212 -0
  420. package/src/state/kv.ts +324 -0
  421. package/src/state/kv4-head256-cache.ts +46 -0
  422. package/src/state/latent-legos.ts +895 -0
  423. package/src/state/layout.ts +277 -0
  424. package/src/state/leases.ts +26 -0
  425. package/src/state/paged/cache.ts +689 -0
  426. package/src/state/paged/codec.ts +36 -0
  427. package/src/state/paged/rows.ts +223 -0
  428. package/src/state/persistence-types.ts +173 -0
  429. package/src/state/persistence-worker.ts +83 -0
  430. package/src/state/persistence.ts +1036 -0
  431. package/src/state/persistence.worker.d.ts +3 -0
  432. package/src/state/persistence.worker.js +264 -0
  433. package/src/state/prefix-cache.ts +685 -0
  434. package/src/state/quantized-kv.ts +369 -0
  435. package/src/state/quantized-tensor.ts +26 -0
  436. package/src/state/request-policy.ts +48 -0
  437. package/src/state/retention-policy.ts +37 -0
  438. package/src/state/rollback.ts +131 -0
  439. package/src/state/rotating-kv-donor.ts +35 -0
  440. package/src/state/rotating-kv-layout.ts +341 -0
  441. package/src/state/rotating-kv.ts +529 -0
  442. package/src/state/rotating-quantized-kv.ts +616 -0
  443. package/src/state/rotating-row-transaction.ts +35 -0
  444. package/src/state/rows.ts +104 -0
  445. package/src/state/softcap-attention.ts +118 -0
  446. package/src/state/speculative/glm52-mtp-state.ts +38 -0
  447. package/src/state/speculative/qwen-mtp-state.ts +34 -0
  448. package/src/state/speculative-rotating-kv.ts +113 -0
  449. package/src/state/ssd-cache.ts +434 -0
  450. package/src/state/ssm-prefill-padding.ts +39 -0
  451. package/src/state/ssm.ts +422 -0
  452. package/src/state/target-layout.ts +7 -0
  453. package/src/state/tiered-prefix-cache.ts +45 -0
  454. package/src/state/training-cache.ts +300 -0
  455. package/src/state/transitioning-kv-rows.ts +267 -0
  456. package/src/state/turboquant-attention.ts +97 -0
  457. package/src/state/turboquant-codec.ts +90 -0
  458. package/src/state/turboquant-kv.ts +442 -0
  459. package/src/state/views.ts +26 -0
  460. package/src/transcription/index.ts +3 -0
  461. package/src/transcription/silero-vad.ts +379 -0
  462. package/src/transcription/whisper/decode.ts +773 -0
  463. package/src/transcription/whisper/format.ts +74 -0
  464. package/src/transcription/whisper/index.ts +4 -0
  465. package/src/transcription/whisper/timing.ts +274 -0
  466. package/src/transcription/whisper/transcribe.ts +394 -0
  467. package/src/transcription/whisper/types.ts +22 -0
@@ -0,0 +1,860 @@
1
+ import type { MlxCacheLayer, MlxMixedRange, MlxSequenceRange } from "../../contracts/mlx/graph";
2
+ // Gemma 4 text model — line-for-line port of mlx-lm's gemma4_text.py
3
+ // (oracle venv site-packages), covering the paths our target models
4
+ // exercise: 12B (dense), e4b (per-layer-input embeddings + KV-shared
5
+ // layers), 26B-A4B (MoE block: router + gather_qmm experts).
6
+ //
7
+ // Parity notes:
8
+ // - SDPA scale is 1.0 (Gemma4 normalizes q/k instead).
9
+ // - Full-attention layers: global_head_dim 512, 1 global KV head,
10
+ // attention_k_eq_v (V = same projection as K, with un-scaled RMS norm);
11
+ // ProportionalRoPE rotates only partial_rotary_factor·dims dims.
12
+ // - Python-float scalars promote weakly to the array dtype.
13
+ // - Replicate mlx python helper implementations exactly (x**3 is
14
+ // mx.power, not x·x·x — they round differently in bf16).
15
+ //
16
+ // Reads: every attention block reads its layer's cache through the read the
17
+ // caller's phase names (contracts/mlx/cache.ts AttentionCache), so the cache
18
+ // builds the mask (sliding layers compose RotatingKVCache, whose reads own the
19
+ // window) and attends with the kernel it was composed with. The graph never
20
+ // inspects the query count, builds a mask or chooses a kernel, the training
21
+ // forward (gradient checkpointing, prefix-shared and segmented training)
22
+ // included: it reads its stateless training caches the same way. The blocks
23
+ // (attention, feed-forward, per-layer inputs, KV sharing, decoder layer) live in
24
+ // blocks.ts, which the written graph files compose too.
25
+
26
+ import { disposeResources } from "../../runtime/resources";
27
+ import type { MlxArray } from "@mlx-bun/mlx/array";
28
+ import * as ops from "@mlx-bun/mlx/ops";
29
+ import type { ModelConfig } from "../../artifacts/config";
30
+ import type { Weights } from "../../artifacts/weights";
31
+ import { Checkpoint } from "@mlx-bun/mlx/checkpoint";
32
+ import { LoraState } from "../../layers/lora";
33
+ import type { GraphDeclaration } from "../../contracts/portable/graph";
34
+ import type { MlxCapturingForward, MlxRowTable, MlxCompiledDecodeStep, MlxDeclaredGraph, MlxHeadForward, MlxLayerCapture, MlxMixedForward, MlxPhaseForward, MlxKnownOperations, MlxPromptRange, MlxTokenPhases, MlxVerifyForward } from "../../contracts/mlx/graph";
35
+ import { CompiledDecode } from "./compiled-decode";
36
+ import type { TargetView } from "../../contracts/mlx/draft-target";
37
+ import type { TrainableGraph } from "../../contracts/mlx/trainable";
38
+ import { gemma4Trainable, type GradCheckpointCtx, type LayerLoras } from "./trainable";
39
+ import type { MediaEncoders, MediaSidecarProbes, MlxPromptInput, Vision } from "../../contracts/mlx/media";
40
+ import { declareGraph } from "../capabilities";
41
+ import { DEFAULT_PHASE_ROWS, rowTable } from "../graph";
42
+ import { bindGemma4MediaInput, gemma4DraftTarget, gemma4MediaEncoders } from "./media";
43
+ import type { AttentionRead, Cache, SharedKv, TrainingAttentionCache } from "../../contracts/mlx/cache";
44
+ import { CompiledGegluMLP, PlainGegluMLP, CompiledGeglu } from "../../layers/geglu";
45
+ import { ServingDenseLayer, ServingMoeLayer } from "./serving-blocks";
46
+ import { FixedExperts, SortedSwitchGLU, UnsortedSwitchGLU } from "./blocks";
47
+ import type { MlxEmbeddingsRange } from "../../contracts/mlx/media";
48
+ import type { MlxSeparateMixed, MlxPhaseGroup } from "../../contracts/mlx/graph";
49
+ import { mapPackedTokens } from "../../input/token-groups";
50
+ import { disposing } from "../../layers/helpers";
51
+ import { RMSNorm } from "../../layers/normalization";
52
+ import { QuantizedEmbedding } from "../../layers/quantized-embedding";
53
+ import type { QuantizedLinear } from "../../layers/quantized-linear";
54
+ import { logitSoftcap } from "../../kernels/logits";
55
+ import { compiledLogitSoftcap } from "../../kernels/softcap";
56
+ import { AttentionMasks } from "../../state/attention-read";
57
+ import { composedFullAttentionCache, composedSlidingAttentionCache } from "../../state/bf16-first-kv";
58
+ import { deferredTurboQuantKernels } from "../../state/turboquant-attention";
59
+ import {
60
+ DecoderLayer, kvDonors, PerLayerInputs, type Attention, type DonorRead, type Gemma4Phase,
61
+ } from "./blocks";
62
+
63
+ /** A donor layer's keys and values in the training forward: the keys (RoPE
64
+ * applied) and values it appended to its training cache, which its sharers read
65
+ * through that cache and which cross checkpoint and segment boundaries as
66
+ * tensors. */
67
+ type DecoderOperation = Pick<DecoderLayer,"forward"|"forwardAttn"|"forwardMlp">;
68
+
69
+ type PlainSharedKv = Extract<SharedKv, { kind: "plain" }>;
70
+
71
+ /** The training forward's attention (`Gemma4Model.forwardTrainingHidden`,
72
+ * gradient checkpointing and the segmented backward) over `attn`'s
73
+ * projections, roped block-wise under a prefix-shared layout. `cache` is the
74
+ * training cache of this layer, or of its donor for a KV-sharing layer; it
75
+ * stores nothing (`TrainingCache`, `PrefixSharedCache`), so its window read
76
+ * of the keys and values handed to it owns the mask (causal, windowed, padded
77
+ * or block-sparse) and the kernel the run composed, and a sharer reads its
78
+ * donor's keys and values (`sharedIn`) by appending them to the donor's cache
79
+ * again. Returns the attention output and the donor's keys and values (a
80
+ * sharer returns `sharedIn`); the caller owns those of a donor and disposes
81
+ * them after the pass. */
82
+ function attentionTraining(
83
+ attn: Attention, x: MlxArray, cache: TrainingAttentionCache, sharedIn: PlainSharedKv | null,
84
+ ): { out: MlxArray; shared: PlainSharedKv } {
85
+ const [B, L] = x.shape as [number, number, number];
86
+
87
+ let q = attn.qProj.forward(x);
88
+ q = disposing(q, ops.reshape(q, [B, L, attn.nHeads, attn.headDim]));
89
+ q = disposing(q, attn.qNorm.forward(q));
90
+
91
+ let shared: PlainSharedKv;
92
+ let read: AttentionRead;
93
+ if (!attn.hasKv) {
94
+ if (!sharedIn) throw new Error("KV-shared layer received no shared KV");
95
+ shared = sharedIn;
96
+ read = cache.appendWindow(shared.keys, shared.values);
97
+ } else {
98
+ let k = attn.kProj!.forward(x);
99
+ k = disposing(k, ops.reshape(k, [B, L, attn.nKvHeads, attn.headDim]));
100
+ let v: MlxArray;
101
+ if (attn.vProj) {
102
+ v = attn.vProj.forward(x);
103
+ v = disposing(v, ops.reshape(v, [B, L, attn.nKvHeads, attn.headDim]));
104
+ } else {
105
+ v = k; // attention_k_eq_v: shared projection; norms differ below
106
+ }
107
+
108
+ const kNormed = attn.kNorm!.forward(k);
109
+ const kT = ops.transposeAxes(kNormed, [0, 2, 1, 3]);
110
+ kNormed.dispose();
111
+ const kRoped = cache.ropePositions(kT, (block, offset) => attn.rope(block, offset));
112
+ kT.dispose();
113
+
114
+ const vNormed = attn.vNorm!.forward(v);
115
+ const vT = ops.transposeAxes(vNormed, [0, 2, 1, 3]);
116
+ vNormed.dispose();
117
+ if (v !== k) v.dispose();
118
+ k.dispose();
119
+
120
+ try { read = cache.appendWindow(kRoped, vT); }
121
+ catch (error) { kRoped.dispose(); vT.dispose(); throw error; }
122
+ shared = { kind: "plain", keys: kRoped, values: vT, offset: cache.offset };
123
+ }
124
+
125
+ let out: MlxArray;
126
+ try {
127
+ q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
128
+ q = disposing(q, cache.ropePositions(q, (block, offset) => attn.rope(block, offset)));
129
+ out = read.attend(q, 1.0);
130
+ } finally { read.dispose(); }
131
+ q.dispose();
132
+ const attnT = ops.transposeAxes(out, [0, 2, 1, 3]);
133
+ out.dispose();
134
+ const merged = ops.reshape(attnT, [B, L, -1]);
135
+ attnT.dispose();
136
+ return { out: disposing(merged, attn.oProj.forward(merged)), shared };
137
+ }
138
+
139
+ /** The training forward's attention sub-block of `layer` (see
140
+ * `DecoderLayer.forwardAttn`): inputNorm → `attentionTraining` → postAttnNorm →
141
+ * residual add. */
142
+ function layerAttnTraining(
143
+ layer: DecoderLayer, x: MlxArray, cache: TrainingAttentionCache, sharedIn: PlainSharedKv | null,
144
+ ): { h: MlxArray; shared: PlainSharedKv } {
145
+ let h = layer.inputNorm.forward(x);
146
+ const { out, shared } = attentionTraining(layer.attn, h, cache, sharedIn);
147
+ h.dispose();
148
+ h = out;
149
+ h = disposing(h, layer.postAttnNorm.forward(h));
150
+ h = disposing(h, ops.add(x, h));
151
+ return { h, shared };
152
+ }
153
+
154
+ /** The training forward's layer (see `attentionTraining`). The split into two
155
+ * sub-blocks lets the checkpoint path rematerialize attention and MLP
156
+ * separately (peak attn+MLP → max(attn,MLP)+hMid). */
157
+ function layerTraining(
158
+ layer: DecoderLayer, x: MlxArray, cache: TrainingAttentionCache,
159
+ sharedIn: PlainSharedKv | null, perLayerInput: MlxArray | null,
160
+ ): { h: MlxArray; shared: PlainSharedKv } {
161
+ const { h: hMid, shared } = layerAttnTraining(layer, x, cache, sharedIn);
162
+ const h = layer.forwardMlp(hMid, perLayerInput);
163
+ hMid.dispose();
164
+ return { h, shared };
165
+ }
166
+
167
+ export class Gemma4Model implements MlxDeclaredGraph, MlxTokenPhases {
168
+ #disposed = false;
169
+ readonly config: ModelConfig;
170
+ /** Total weight-shard bytes (for the conditional wired-limit scope). */
171
+ readonly weightsBytes: number;
172
+ /** Weight-name prefix ("language_model.model" or "model"). */
173
+ readonly prefixBase: string;
174
+ /** Active LoRA adapters for the current generation (see LoraState). */
175
+ readonly loraState = new LoraState();
176
+ /** Prepared image/audio embeddings, layer taps for drafts, and a compiled
177
+ * decode step, except through experts: shapeless replay
178
+ * retraces their growing windows. */
179
+ get graphCapabilities(): GraphDeclaration {
180
+ return declareGraph({
181
+ media: { input: "embeddings", video: false }, hiddenLayerTaps: true,
182
+ compiledDecode: this.config?.modelType?.startsWith("gemma4") === true && !this.config.text.enableMoeBlock,
183
+ });
184
+ }
185
+ #trainable: TrainableGraph | undefined;
186
+ /** Segmented backward with KV-shared donor threading, prefix-shared ORPO,
187
+ * gradient checkpointing, and the quantized tied head. Flash training
188
+ * attention is refused: e4b crashes natively on it at long sequences. */
189
+ get trainable(): TrainableGraph { return this.#trainable ??= gemma4Trainable(this); }
190
+ bindMediaInput(input: Vision): MlxPromptInput { return bindGemma4MediaInput(this, input); }
191
+ mediaEncoders(modelDir: string, probes: MediaSidecarProbes): Promise<MediaEncoders> {
192
+ return gemma4MediaEncoders(this, modelDir, probes);
193
+ }
194
+ draftTarget(caches: Cache[]): TargetView { return gemma4DraftTarget(this, caches); }
195
+ /** The compiled decode step declared above: the step records this graph's own
196
+ * forward, so it shares the graph's weights and constants. */
197
+ compiledDecodeStep(): MlxCompiledDecodeStep { return CompiledDecode.for(this); }
198
+ releaseCompiledDecode(): void { CompiledDecode.release(this); }
199
+ readonly embed: QuantizedEmbedding;
200
+ readonly layers: DecoderLayer[];
201
+ readonly finalNorm: RMSNorm;
202
+ readonly embedScale: number;
203
+ readonly windowSize: number;
204
+ /** Donor count: layers [0, numDonors) own caches; the rest share. */
205
+ readonly numDonors: number;
206
+ /** layer idx → donor layer idx whose fetched KV it consumes (self for donors). */
207
+ readonly previousKvs: number[];
208
+ /** layer idx → cache index (donors only; -1 for sharers). */
209
+ readonly cacheIndex: number[];
210
+ /** Set by the trainer around its training forwards to checkpoint each layer
211
+ * (null = off). While set, the scoring and training forward
212
+ * (`sequenceHidden`) runs the training forward (`forwardTrainingHidden`'s),
213
+ * whose donors hand their sharers keys and values as checkpoint tensors. */
214
+ gradCkpt: GradCheckpointCtx | null = null;
215
+ // e2b/e4b per-layer input machinery (the block and its parts)
216
+ readonly perLayerInputs: PerLayerInputs | null;
217
+ readonly perLayerEmbed: QuantizedEmbedding | null;
218
+ readonly perLayerModelProjection: QuantizedLinear | null;
219
+ readonly perLayerProjectionNorm: RMSNorm | null;
220
+ readonly perLayerWidth: number;
221
+ readonly #servingLayers: readonly ServingDenseLayer[];
222
+ readonly #sortedLayers: readonly ServingDenseLayer[];
223
+ readonly traceLayers: readonly ServingDenseLayer[];
224
+ readonly traceDecode: MlxCapturingForward;
225
+ readonly #ordinarySlice: PerLayerInputs["ordinaryLayer"] | undefined;
226
+ readonly #traceSlice: PerLayerInputs["traceLayer"] | undefined;
227
+ readonly traceLogits = (hidden: MlxArray) => this.#head(hidden, logitSoftcap);
228
+ readonly mediaPrefill: readonly MlxEmbeddingsRange[];
229
+ readonly embeddingPrompt: readonly MlxEmbeddingsRange[];
230
+ readonly mixed: readonly MlxMixedRange[];
231
+ readonly mixedSeparate: MlxSeparateMixed;
232
+ constructor(weights: Weights, config: ModelConfig, maxPhaseRows = DEFAULT_PHASE_ROWS) {
233
+ const t = config.text;
234
+ if (t.hiddenSize === 0 || !config.text.tieWordEmbeddings)
235
+ throw new Error("only tied-embedding text configs are supported");
236
+
237
+ this.config = config;
238
+ this.weightsBytes = [...weights.shards.files.values()]
239
+ .reduce((a, f) => a + f.mmap.size, 0);
240
+ // gemma4_unified prefixes weights with language_model.; gemma4_text doesn't
241
+ const prefixBase = weights.has("language_model.model.embed_tokens.weight")
242
+ ? "language_model.model" : "model";
243
+ this.prefixBase = prefixBase;
244
+ this.embed = QuantizedEmbedding.load(weights, `${prefixBase}.embed_tokens`, config);
245
+
246
+ this.numDonors = t.numHiddenLayers - t.numKvSharedLayers;
247
+ this.layers = Array.from(
248
+ { length: t.numHiddenLayers },
249
+ (_, i) => new DecoderLayer(weights, config, prefixBase, i, i < this.numDonors, true),
250
+ );
251
+ // previous_kvs (reference Gemma4TextModel): sharer j uses the LAST
252
+ // donor of its own layer type
253
+ this.previousKvs = kvDonors(this.layers.map(layer => layer.layerType), this.numDonors);
254
+ this.cacheIndex = this.layers.map((_, i) => (i < this.numDonors ? i : -1));
255
+
256
+ this.finalNorm = new RMSNorm(
257
+ weights.tensor(`${prefixBase}.norm.weight`),
258
+ t.rmsNormEps,
259
+ );
260
+ this.embedScale = Math.sqrt(t.hiddenSize);
261
+ this.windowSize = t.slidingWindow;
262
+
263
+ this.perLayerInputs = t.hiddenSizePerLayerInput > 0 ? new PerLayerInputs(weights, config, prefixBase) : null;
264
+ this.perLayerEmbed = this.perLayerInputs?.embed ?? null;
265
+ this.perLayerModelProjection = this.perLayerInputs?.modelProjection ?? null;
266
+ this.perLayerProjectionNorm = this.perLayerInputs?.projectionNorm ?? null;
267
+ this.perLayerWidth = this.perLayerInputs?.width ?? 0;
268
+ this.#servingLayers = this.layers.map(layer => layer.experts
269
+ ? new ServingMoeLayer(layer, new CompiledGegluMLP(layer.mlp), new FixedExperts(new UnsortedSwitchGLU(layer.experts.switchGlu, new CompiledGeglu())))
270
+ : new ServingDenseLayer(layer, new CompiledGegluMLP(layer.mlp)));
271
+ this.#sortedLayers = this.layers.map(layer => layer.experts
272
+ ? new ServingMoeLayer(layer, new CompiledGegluMLP(layer.mlp), new FixedExperts(new SortedSwitchGLU(layer.experts.switchGlu, new CompiledGeglu())))
273
+ : new ServingDenseLayer(layer, new CompiledGegluMLP(layer.mlp)));
274
+ this.#ordinarySlice = this.perLayerInputs?.ordinaryLayer.bind(this.perLayerInputs);
275
+ this.#traceSlice = this.perLayerInputs?.traceLayer.bind(this.perLayerInputs);
276
+ this.traceLayers = this.layers.map(layer => new ServingDenseLayer(layer, new PlainGegluMLP(layer.mlp)));
277
+ this.traceDecode = (ids,cache,capture) => {
278
+ const h = this.forwardDecoder(this.embedTokens(ids),cache,"decode",ids,capture,this.traceLayers,this.#traceSlice);
279
+ capture?.(this.layers.length,h); return h;
280
+ };
281
+
282
+ // Artifact top-k determines the reference gather-sort boundary. Each
283
+ // declared operation already owns its concrete expert and activation form.
284
+ const boundary = t.enableMoeBlock ? Math.ceil(64 / t.topKExperts) : Infinity;
285
+ const views = [{from:1,to:boundary-1,layers:this.#servingLayers}, {from:boundary,to:Infinity,layers:this.#sortedLayers}].filter(view=>Number.isFinite(view.from) && view.from<=view.to);
286
+ this.prompt = views.map(view => ({from:view.from,to:view.to,operation:(ids,cache,capture)=>this.#window(ids,cache,capture,view.layers)}));
287
+ this.promptChunk = this.prompt;
288
+ this.multiSequencePrompt = this.prompt;
289
+ this.multiSequencePromptChunk = this.multiSequencePrompt;
290
+ const decode: Record<number,MlxCapturingForward> = {}, verify: Record<number,MlxCapturingForward> = {};
291
+ for (let rows=1; rows<=maxPhaseRows; rows++) {
292
+ const layers = rows < boundary ? this.#servingLayers : this.#sortedLayers;
293
+ decode[rows] = (ids,cache,capture) => {
294
+ const hidden = this.forwardDecoder(this.embedTokens(ids),cache,"decode",ids,capture,layers);
295
+ capture?.(this.layers.length,hidden); return hidden;
296
+ };
297
+ verify[rows] = (ids,cache,capture) => this.#window(ids,cache,capture,layers);
298
+ }
299
+ this.decode = decode; this.verify = verify; this.appendKnown = views.map(view=>({from:view.from,to:view.to,operation:(ids,cache)=>this.#window(ids,cache,null,view.layers)}));
300
+ this.logits = rowTable<MlxHeadForward>(hidden=>this.logitsFromHidden(hidden),maxPhaseRows);
301
+ this.sequence = views.map(view=>({from:view.from,to:view.to,operation:(ids,cache)=>this.gradCkpt
302
+ ? this.#forwardTraining(this.embedTokens(ids),cache,ids)
303
+ : this.forwardDecoder(this.embedTokens(ids),cache,"window",ids,null,view.layers)}));
304
+ this.mediaPrefill = views.map(view=>({from:view.from,to:view.to,operation:(embeddings,cache,imageMask,ids,multimodalMask)=>
305
+ this.#prefillEmbeddings(embeddings,cache,imageMask,ids,multimodalMask,view.layers)}));
306
+ this.embeddingPrompt = this.mediaPrefill;
307
+ this.mixed = views.map(view=>({from:view.from,to:view.to,operation:groups=>this.#mixed(groups,view.layers)}));
308
+ const separate = views.map(view=>({from:view.from,to:view.to,operation:(group:MlxPhaseGroup)=>{
309
+ const hidden = this.forwardDecoder(this.embedTokens(group.ids),group.state,group.phase === "decode" ? "decode" : "window",group.ids,group.capture,view.layers);
310
+ group.capture?.(this.layers.length,hidden); return hidden;
311
+ }}));
312
+ this.mixedSeparate = {decode:separate,window:separate,verify:separate};
313
+
314
+ this.cacheLayers = this.layers.slice(0,this.numDonors).map(layer=>layer.layerType === "sliding_attention" ? {kind:"sliding" as const,window:this.windowSize}:{kind:"full" as const,turboQuant:"deferred" as const});
315
+ }
316
+
317
+ /** LoRA-mountable linears, keyed by weight-file module path: optiq
318
+ * mount.py's 7 target suffixes PLUS the e2b/e4b per-layer-input
319
+ * projections — mlx-lm's trainer targets those on e4b and optiq's
320
+ * mount silently drops their trained weights (we apply every adapter
321
+ * weight we can map).
322
+ * Expert pools stay non-targets (LoRASwitchLinear is future work). */
323
+ loraTargets(): Map<string, QuantizedLinear> {
324
+ const out = new Map<string, QuantizedLinear>();
325
+ for (let i = 0; i < this.layers.length; i++) {
326
+ const l = this.layers[i]!;
327
+ const p = `${this.prefixBase}.layers.${i}`;
328
+ const add = (suffix: string, lin: QuantizedLinear | null) => {
329
+ if (lin) out.set(`${p}.${suffix}`, lin);
330
+ };
331
+ add("self_attn.q_proj", l.attn.qProj);
332
+ add("self_attn.k_proj", l.attn.kProj);
333
+ add("self_attn.v_proj", l.attn.vProj);
334
+ add("self_attn.o_proj", l.attn.oProj);
335
+ add("mlp.gate_proj", l.mlp.gate);
336
+ add("mlp.up_proj", l.mlp.up);
337
+ add("mlp.down_proj", l.mlp.down);
338
+ add("per_layer_input_gate", l.perLayerGate);
339
+ add("per_layer_projection", l.perLayerProjection);
340
+ }
341
+ return out;
342
+ }
343
+
344
+ /** Caches for donor layers only (sharers attend their donors' reads), all
345
+ * sharing one mask memo, so each forward builds each mask once. Sliding
346
+ * layers compose a ring of the sliding window, whose reads own the window
347
+ * mask, and stay bf16 under TurboQuant. The full-attention caches' TurboQuant
348
+ * read is deferred (`deferredTurboQuantKernels`): values stay rotated through
349
+ * the fused SDPA and the read un-rotates the output. */
350
+ readonly cacheLayers: readonly MlxCacheLayer[];
351
+ readonly sequence: readonly MlxSequenceRange[];
352
+
353
+
354
+ /** Release model-owned graph constants after all executions and compiled
355
+ * runners have stopped. Weight tensors and hidden taps are borrowed. */
356
+ dispose(): void {
357
+ if (this.#disposed) return;
358
+ this.#disposed = true;
359
+ disposeResources(this.layers.flatMap(layer => layer.attn.ropeFreqs ? [layer.attn.ropeFreqs] : []));
360
+ }
361
+
362
+ /** Computed model constants must retain their constructor arithmetic when
363
+ * a restored request enters compiled decode without a prefill first. */
364
+ materializeGraphConstants(): void {
365
+ const constants = this.layers.flatMap(layer => layer.attn.ropeFreqs ? [layer.attn.ropeFreqs] : []);
366
+ if (constants.length) ops.evalAll(constants);
367
+ }
368
+
369
+ /** The scaled token embeddings [B, L, hidden] of `ids` (owned). */
370
+ protected embedTokens(ids: MlxArray): MlxArray {
371
+ const h = this.embed.encode(ids);
372
+ return disposing(h, ops.mulScalar(h, this.embedScale));
373
+ }
374
+
375
+ /** One whole chunk of a prompt: tokens per sequence equal the composition's
376
+ * prefill chunk size; rows are sequences times that size. Token ids only: a
377
+ * prompt with prepared media enters through the `MlxPromptInput` that
378
+ * `bindMediaInput` returns. The GPU is this graph's one unit. */
379
+ readonly prompt: readonly MlxPromptRange[];
380
+ readonly promptChunk: typeof this.prompt;
381
+ readonly multiSequencePromptChunk: typeof this.multiSequencePrompt;
382
+ readonly multiSequencePrompt: readonly MlxPromptRange[];
383
+
384
+ /** A prompt's positions that are not a whole chunk: the window read at every width. */
385
+
386
+ /** Known tokens: this graph declares no committed spans, so they read as a window. */
387
+ readonly appendKnown: MlxKnownOperations;
388
+
389
+ /** A causal window of token ids over every layer, `capture` seeing every layer and then the final hidden. */
390
+ #window(ids: MlxArray, cache: Cache[], capture: MlxLayerCapture | null, layers = this.#servingLayers): MlxArray {
391
+ const hidden = this.forwardDecoder(this.embedTokens(ids), cache, "window", ids, capture, layers);
392
+ capture?.(this.layers.length, hidden);
393
+ return hidden;
394
+ }
395
+
396
+ /** The one output head at every width. */
397
+ readonly logits: MlxRowTable<MlxHeadForward>;
398
+
399
+ /** One new token per sequence: `ids` [sequences, 1], so rows are sequences. */
400
+ readonly decode: MlxRowTable<MlxCapturingForward>;
401
+
402
+ /** A speculative window: each sequence's last accepted token and its drafted
403
+ * tokens, right-padded by the caller to the round's depth, `ids`
404
+ * [sequences, depth + 1], so rows are sequences times (depth + 1). The graph
405
+ * appends all depth + 1 positions; dropping the rejected suffix is the
406
+ * caller's speculative transaction, begun before this call. */
407
+ readonly verify: MlxRowTable<MlxCapturingForward>;
408
+
409
+ /** Per-layer inputs (reference _get_per_layer_inputs +
410
+ * _project_per_layer_inputs): [B, L, nLayers, perLayerWidth] (`PerLayerInputs.compute`). */
411
+ protected computePerLayerInputs(ids: MlxArray, hScaled: MlxArray): MlxArray {
412
+ return this.perLayerInputs!.compute(ids, hScaled);
413
+ }
414
+
415
+ /** The vision prefill phase: pre-merged (unscaled) input embeddings → hidden states. Used by the
416
+ * multimodal (vision/audio) path; `bidir` (bool [L]) marks image tokens,
417
+ * which attend bidirectionally among themselves (use_bidirectional_attention:
418
+ * "vision" — text stays causal; audio prompts pass NO bidir mask, and a
419
+ * mixed image+audio prompt drops the image overlay too,
420
+ * `02d723a:docs/design/generic-model-support.md` §6.6). `ids` ([1, L], the spliced token ids)
421
+ * is required for per-layer-input models (e2b/e4b): multimodal soft-token
422
+ * positions get token 0's per-layer embedding, and their tower content
423
+ * enters only through the projection term (the merged hidden). Matches
424
+ * optiq's frontend: `zeroed = where(text_mask, input_ids, 0)`.
425
+ *
426
+ * `multimodal` (bool [L]) is the per-layer-input ZEROING mask — the UNION
427
+ * of multimodal soft tokens (image | audio, §3.3 Q2), decoupled from the
428
+ * attention overlay: an audio-only prompt has no bidir mask but its audio
429
+ * positions must still be zeroed. When null (the legacy vision call
430
+ * shape), the zeroing mask falls back to `bidir` — vision passes the same
431
+ * image mask for both roles, so that path is unchanged. */
432
+ #prefillEmbeddings(
433
+ embeds: MlxArray, cache: Cache[], bidir: MlxArray | null,
434
+ ids: MlxArray | null = null, multimodal: MlxArray | null = null,
435
+ layers: readonly DecoderOperation[] = this.#servingLayers,
436
+ ): MlxArray {
437
+ const h = ops.mulScalar(embeds, this.embedScale);
438
+ const phase: Gemma4Phase = bidir ? { bidirectional: bidir } : "window";
439
+ if (this.perLayerWidth === 0) return this.forwardDecoder(h, cache, phase, null, null, layers);
440
+
441
+ if (!ids)
442
+ throw new Error("per-layer-input multimodal models require token ids");
443
+ const zeroing = multimodal ?? bidir;
444
+ let plIds = ids;
445
+ if (zeroing) {
446
+ const cond = ops.reshape(zeroing, ids.shape);
447
+ const zero = ops.zeros(ids.shape, ids.dtype);
448
+ plIds = ops.where(cond, zero, ids);
449
+ cond.dispose();
450
+ zero.dispose();
451
+ }
452
+ const out = this.forwardDecoder(h, cache, phase, plIds, null, layers);
453
+ if (plIds !== ids) plIds.dispose();
454
+ return out;
455
+ }
456
+
457
+ /** The decoder over `h0` [B, L, hidden] (consumed), every donor layer's
458
+ * attention reading its cache for `phase` and every KV-sharing layer
459
+ * attending its donor's read. `ids` [B, L] feed the per-layer inputs
460
+ * (e2b/e4b). `capture` receives each layer's residual output after the layer
461
+ * runs (see `MlxLayerCapture`); the caller hands it the final hidden. */
462
+ protected forwardDecoder(
463
+ h0: MlxArray, cache: Cache[], phase: Gemma4Phase, ids: MlxArray | null,
464
+ capture: MlxLayerCapture | null = null, layers: readonly DecoderOperation[] = this.#servingLayers, slice = this.#ordinarySlice,
465
+ ): MlxArray {
466
+ const B = h0.shape[0]!;
467
+ const L = h0.shape[1]!;
468
+ let h: MlxArray | null = h0;
469
+ let perLayer: MlxArray | null = null;
470
+ const donors: (DonorRead | null)[] = Array(this.numDonors).fill(null);
471
+ try {
472
+ // per-layer inputs (e2b/e4b)
473
+ if (this.perLayerWidth > 0) perLayer = this.computePerLayerInputs(ids!, h);
474
+ for (let i = 0; i < this.layers.length; i++) {
475
+ const ci = this.cacheIndex[i]!;
476
+ const pls = perLayer ? slice!(perLayer,i,B,L) : null;
477
+ let next: { h: MlxArray; donor: DonorRead | null };
478
+ try {
479
+ next = layers[i]!.forward(h, ci === -1 ? null : cache[ci]!, phase,
480
+ ci === -1 ? donors[this.previousKvs[i]!]! : null, pls);
481
+ } finally { pls?.dispose(); }
482
+ h.dispose();
483
+ h = next.h;
484
+ if (ci !== -1) donors[ci] = next.donor;
485
+ capture?.(i, h);
486
+ }
487
+ const out = this.finalNorm.forward(h);
488
+ h.dispose();
489
+ h = null;
490
+ return out;
491
+ } finally {
492
+ h?.dispose();
493
+ perLayer?.dispose();
494
+ // each donor's read exactly once, after its last sharer
495
+ for (const donor of donors) donor?.dispose();
496
+ }
497
+ }
498
+
499
+ /** Layer `i`'s per-layer input [B, L, width] (owned) from the model's
500
+ * [B, L, nLayers, width] tensor (`PerLayerInputs.layer`). */
501
+ #perLayerInput(perLayer: MlxArray, i: number, B: number, L: number): MlxArray {
502
+ return this.perLayerInputs!.layer(perLayer, i, B, L);
503
+ }
504
+
505
+ /** The training forward over stateless training caches (`TrainingCache`,
506
+ * `PrefixSharedCache`), one per donor layer: ids [1, T] to the final-norm
507
+ * hidden [1, T, hidden]. Every attention reads through its training cache's
508
+ * window read (a KV-sharing layer through its donor's, with the donor's keys
509
+ * and values), and a prefix-shared layout ropes block-wise; under `gradCkpt`
510
+ * each layer is checkpointed with its donor keys and values as checkpoint
511
+ * tensors. */
512
+ forwardTrainingHidden(ids: MlxArray, cache: Cache[]): MlxArray {
513
+ return this.#forwardTraining(this.embedTokens(ids), cache, ids);
514
+ }
515
+
516
+ /** The training cache layer `i` reads: its own, or its donor's for a
517
+ * KV-sharing layer (`cache` holds the donors' training caches). */
518
+ #trainingCache(cache: Cache[], i: number): TrainingAttentionCache {
519
+ const ci = this.cacheIndex[i]!;
520
+ return cache[ci === -1 ? this.cacheIndex[this.previousKvs[i]!]! : ci] as TrainingAttentionCache;
521
+ }
522
+
523
+ /** The training forward's decoder over `h0` (consumed). */
524
+ #forwardTraining(h0: MlxArray, cache: Cache[], ids: MlxArray | null): MlxArray {
525
+ const B = h0.shape[0]!;
526
+ const L = h0.shape[1]!;
527
+ let h = h0;
528
+
529
+ // per-layer inputs (e2b/e4b)
530
+ let perLayer: MlxArray | null = null;
531
+ if (this.perLayerWidth > 0) {
532
+ if (!ids) throw new Error("per-layer-input models require token ids");
533
+ perLayer = this.computePerLayerInputs(ids, h);
534
+ }
535
+
536
+ const intermediates: (PlainSharedKv | null)[] = Array(this.layers.length).fill(null);
537
+ for (let i = 0; i < this.layers.length; i++) {
538
+ const layer = this.layers[i]!;
539
+ const ci = this.cacheIndex[i]!;
540
+ const sharedIn = ci === -1 ? (intermediates[this.previousKvs[i]!] ?? null) : null;
541
+ const pls = perLayer ? this.#perLayerInput(perLayer, i, B, L) : null;
542
+ const layerCache = this.#trainingCache(cache, i);
543
+ const { h: next, shared } = this.gradCkpt
544
+ ? this.runCheckpointedLayer(i, layer, h, layerCache, sharedIn, pls)
545
+ : layerTraining(layer, h, layerCache, sharedIn, pls);
546
+ pls?.dispose();
547
+ h.dispose();
548
+ h = next;
549
+ intermediates[i] = shared;
550
+ }
551
+ perLayer?.dispose();
552
+ // dispose donor KV (each donor's shared entry exactly once)
553
+ for (let i = 0; i < this.numDonors; i++) {
554
+ const s = intermediates[i];
555
+ if (!s) continue;
556
+ Gemma4Model.disposeSharedKv(s);
557
+ }
558
+
559
+ h = disposing(h, this.finalNorm.forward(h));
560
+ return h;
561
+ }
562
+
563
+ /** Mixed rows in one forward: each group's attention reads its own caches,
564
+ * positions and SDPA shape in its own phase (a verify group reads a window);
565
+ * feed-forward work packs only real tokens, when the composition packs mixed
566
+ * MLP and no group is a verify window. Kernel selection for packed matmuls
567
+ * can differ from a solo GEMV, so this is an explicit Lab port until its
568
+ * matching packed oracle and serving comparison are accepted. */
569
+ #mixed(groups: Parameters<MlxMixedForward>[0], layers: readonly ServingDenseLayer[]): MlxArray[] {
570
+ const runs: Array<{ h: MlxArray; perLayer: MlxArray | null; donors: (DonorRead | null)[] }> = [];
571
+ const results: MlxArray[] = [];
572
+
573
+ try {
574
+ for (const group of groups) {
575
+ const run = { h: this.embedTokens(group.ids), perLayer: null as MlxArray | null,
576
+ donors: Array<DonorRead | null>(this.numDonors).fill(null) };
577
+ runs.push(run);
578
+ if (this.perLayerWidth > 0) run.perLayer = this.computePerLayerInputs(group.ids, run.h);
579
+ }
580
+ for (let i = 0; i < this.layers.length; i++) {
581
+ const layer = layers[i]!, ci = this.cacheIndex[i]!;
582
+ const mids: MlxArray[] = [], inputs: MlxArray[] = [];
583
+ try {
584
+ for (const [row, group] of groups.entries()) {
585
+ const run = runs[row]!;
586
+ const { h, donor } = layer.forwardAttn(run.h, ci === -1 ? null : group.state[ci]!,
587
+ group.phase === "decode" ? "decode" : "window", ci === -1 ? run.donors[this.previousKvs[i]!]! : null);
588
+ mids.push(h);
589
+ if (ci !== -1) run.donors[ci] = donor;
590
+ if (run.perLayer) {
591
+ const [B, L] = group.ids.shape;
592
+ using part = run.perLayer.slice([0, 0, i, 0], [B!, L!, i + 1, this.perLayerWidth]);
593
+ inputs.push(ops.reshape(part, [B!, L!, this.perLayerWidth]));
594
+ }
595
+ }
596
+ // Per-layer inputs use the same token order as the hidden states.
597
+ let packedInput: MlxArray | null = null;
598
+ if (inputs.length) {
599
+ const flat = inputs.map(input => ops.reshape(input, [1, -1, this.perLayerWidth]));
600
+ try { packedInput = ops.concatAxis(flat, 1); }
601
+ finally { for (const input of flat) input.dispose(); }
602
+ }
603
+ using pli = packedInput;
604
+ const outputs: MlxArray[] = [];
605
+ try {
606
+ outputs.push(...mapPackedTokens(mids, packed => layer.forwardMlp(packed, pli)));
607
+ } catch (error) { for (const output of outputs) output.dispose(); throw error; }
608
+ for (const [row, run] of runs.entries()) {
609
+ run.h.dispose(); run.h = outputs[row]!;
610
+ }
611
+ for (const [row, group] of groups.entries()) group.capture?.(i, runs[row]!.h);
612
+ } finally {
613
+ for (const mid of mids) mid.dispose();
614
+ for (const input of inputs) input.dispose();
615
+ }
616
+ }
617
+ for (const [row, group] of groups.entries()) {
618
+ const hidden = this.finalNorm.forward(runs[row]!.h); results.push(hidden);
619
+ group.capture?.(this.layers.length, hidden);
620
+ }
621
+ return results;
622
+ } catch (error) { for (const result of results) result.dispose(); throw error; }
623
+ finally {
624
+ for (const run of runs) {
625
+ run.h.dispose(); run.perLayer?.dispose();
626
+ for (const donor of run.donors) donor?.dispose();
627
+ }
628
+ }
629
+ }
630
+
631
+ // --- Segmented-backward support (`02d723a:docs/design/orpo-training.md`
632
+ // §5.3). These run the training forward's layers (`layerTraining`). The
633
+ // segmented driver builds the training caches + per-layer inputs ONCE, then
634
+ // drives runLayerRange per segment, threading the KV-shared donor K/V across
635
+ // segment boundaries.
636
+
637
+ /** Donor layer indices that KV-sharing sharers reuse (e4b: {22,23} — the last
638
+ * donor of each attention type; empty for dense models). Their K/V must
639
+ * survive across segment boundaries (saved as detached boundaries). */
640
+ get reusedDonors(): Set<number> {
641
+ return new Set(this.previousKvs.filter((d, i) => d !== i));
642
+ }
643
+
644
+ /** e2b/e4b training entry: the SCALED input embedding (the first segment
645
+ * boundary) plus the per-layer-input tensor [B,L,nLayers,width] (a detached
646
+ * boundary, sliced per layer inside runLayerRange). perLayer is null for dense
647
+ * models. Caller owns both. */
648
+ embedForSegmented(ids: MlxArray): { hScaled: MlxArray; perLayer: MlxArray | null } {
649
+ let h = this.embed.encode(ids);
650
+ h = disposing(h, ops.mulScalar(h, this.embedScale));
651
+ const perLayer = this.perLayerWidth > 0 ? this.computePerLayerInputs(ids, h) : null;
652
+ return { hScaled: h, perLayer };
653
+ }
654
+
655
+ /** Dispose a training donor's K/V. */
656
+ private static disposeSharedKv(s: PlainSharedKv): void {
657
+ s.offsetArr?.dispose();
658
+ s.keys.dispose();
659
+ s.values.dispose();
660
+ }
661
+
662
+ /** Run decoder layers `[aIdx, bIdx)` on hidden `h`, returning the residual
663
+ * stream after the last layer in the range (NO finalNorm) plus the K/V of
664
+ * any reused-donor layers in the range (`donorKvOut`, for later segments).
665
+ * The input `h` is NEVER disposed (the caller / vjp owns the boundary leaf);
666
+ * intra-range hidden intermediates and per-layer slices ARE disposed.
667
+ * Non-reused donors' K/V are disposed; reused donors' K/V are returned
668
+ * (caller threads + disposes). NOT gradient-checkpointed — the segmentation
669
+ * IS the memory strategy.
670
+ *
671
+ * @param cache the pass's training caches, one per donor layer.
672
+ * @param perLayer the [B,L,nLayers,width] tensor (caller-owned) or null.
673
+ * @param donorKvIn K/V of reused donors from EARLIER segments, keyed by donor
674
+ * layer index (caller-owned; not disposed here). */
675
+ runLayerRange(
676
+ h: MlxArray, aIdx: number, bIdx: number, cache: Cache[],
677
+ perLayer: MlxArray | null, donorKvIn: Map<number, SharedKv>,
678
+ ): { h: MlxArray; donorKvOut: Map<number, SharedKv> } {
679
+ const donorKvOut = new Map<number, SharedKv>();
680
+ if (aIdx >= bIdx) return { h, donorKvOut };
681
+ const B = h.shape[0]!;
682
+ const L = h.shape[1]!;
683
+ const reused = this.reusedDonors;
684
+ let cur = h;
685
+ for (let i = aIdx; i < bIdx; i++) {
686
+ const layer = this.layers[i]!;
687
+ const ci = this.cacheIndex[i]!;
688
+ const isDonor = ci !== -1;
689
+ let sharedIn: PlainSharedKv | null = null;
690
+ if (!isDonor) {
691
+ const donor = this.previousKvs[i]!;
692
+ // The segment donors hand on the plain keys and values they appended.
693
+ sharedIn = (donorKvOut.get(donor) ?? donorKvIn.get(donor) ?? null) as PlainSharedKv | null;
694
+ if (!sharedIn) throw new Error(`runLayerRange: donor ${donor} K/V not available for sharer ${i}`);
695
+ }
696
+ let pls: MlxArray | null = null;
697
+ if (perLayer) {
698
+ const sl = perLayer.slice([0, 0, i, 0], [B, L, i + 1, this.perLayerWidth]);
699
+ pls = ops.reshape(sl, [B, L, this.perLayerWidth]);
700
+ sl.dispose();
701
+ }
702
+ const { h: next, shared } = layerTraining(layer, cur, this.#trainingCache(cache, i), sharedIn, pls);
703
+ pls?.dispose();
704
+ if (i > aIdx) cur.dispose(); // keep the input boundary leaf; drop interiors
705
+ cur = next;
706
+ if (isDonor) {
707
+ if (reused.has(i)) donorKvOut.set(i, shared);
708
+ else Gemma4Model.disposeSharedKv(shared);
709
+ }
710
+ // sharer: `shared` === sharedIn (owned by donorKvIn/Out) — do not dispose.
711
+ }
712
+ return { h: cur, donorKvOut };
713
+ }
714
+
715
+ /** Run one layer under gradient checkpointing: its interior activations are
716
+ * dropped and recomputed in the backward pass. The layer's LoRA params, h,
717
+ * pls, and a sharer's donor KV are threaded as explicit checkpoint inputs
718
+ * (the closure must be pure and self-contained — it re-runs during the
719
+ * backward, and captured autograd primals would be stale); the training
720
+ * cache it reads stores nothing, so the recompute reads exactly as the
721
+ * forward did. A donor layer also emits its K/V as checkpoint outputs so the
722
+ * (retained, small) tensors reach the sharer layers downstream. */
723
+ private runCheckpointedLayer(
724
+ i: number, layer: DecoderLayer, h: MlxArray,
725
+ layerCache: TrainingAttentionCache, sharedIn: PlainSharedKv | null, pls: MlxArray | null,
726
+ ): { h: MlxArray; shared: PlainSharedKv | null } {
727
+ const ctx = this.gradCkpt!;
728
+ const ll = ctx.byLayer.get(i) ?? { attn: [], mlp: [] };
729
+ return ctx.splitMlp
730
+ ? this.runSplitCheckpointedLayer(layer, h, layerCache, sharedIn, pls, ll, ctx)
731
+ : this.runSingleCheckpointedLayer(layer, h, layerCache, sharedIn, pls, ll, ctx);
732
+ }
733
+
734
+ /** One Checkpoint over the whole layer (the original path). */
735
+ private runSingleCheckpointedLayer(
736
+ layer: DecoderLayer, h: MlxArray,
737
+ layerCache: TrainingAttentionCache, sharedIn: PlainSharedKv | null, pls: MlxArray | null,
738
+ ll: LayerLoras, ctx: GradCheckpointCtx,
739
+ ): { h: MlxArray; shared: PlainSharedKv | null } {
740
+ const lws = [...ll.attn, ...ll.mlp];
741
+ const isSharer = !layer.attn.hasKv;
742
+ const sIn = isSharer && sharedIn && sharedIn.kind === "plain" ? sharedIn : null;
743
+ const hasPls = pls !== null;
744
+
745
+ // Input order: [h, pls?, sharedInK?, sharedInV?, (lora_a, lora_b)*]
746
+ const inputs: MlxArray[] = [h];
747
+ if (hasPls) inputs.push(pls!);
748
+ if (sIn) inputs.push(sIn.keys, sIn.values);
749
+ for (const lw of lws) inputs.push(lw.a, lw.b);
750
+
751
+ const ck = new Checkpoint((ins) => {
752
+ let k = 0;
753
+ const hIn = ins[k++]!;
754
+ const plsIn = hasPls ? ins[k++]! : null;
755
+ const sharedInRebuilt: PlainSharedKv | null = sIn
756
+ ? { kind: "plain", keys: ins[k++]!, values: ins[k++]!, offset: 0 }
757
+ : null;
758
+ const saved = lws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
759
+ for (const lw of lws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
760
+ try {
761
+ const out = layerTraining(layer, hIn, layerCache, sharedInRebuilt, plsIn);
762
+ if (!isSharer && out.shared && out.shared.kind === "plain")
763
+ return [out.h, out.shared.keys, out.shared.values];
764
+ return [out.h];
765
+ } finally {
766
+ lws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
767
+ }
768
+ });
769
+ ctx.keepAlive.push(ck);
770
+
771
+ const outs = ck.apply(inputs);
772
+ const shared: PlainSharedKv | null =
773
+ !isSharer && outs.length === 3
774
+ ? { kind: "plain", keys: outs[1]!, values: outs[2]!, offset: 0 }
775
+ : null;
776
+ return { h: outs[0]!, shared };
777
+ }
778
+
779
+ /** Intra-layer MLP split (lever 4): TWO checkpoints — the attention sub-block
780
+ * (forwardAttn: inputNorm → attn → residual add, threading the donor K/V) and
781
+ * the MLP sub-block (forwardMlp: FFN + per-layer gate) — with the post-attn
782
+ * residual `hMid` as the boundary. The backward recompute holds only one
783
+ * sub-block's activations at a time, dropping per-layer peak from `attn+MLP`
784
+ * to `max(attn,MLP)+hMid`. Attn LoRA / pls / donor-KV go to the attn ck; MLP
785
+ * LoRA to the MLP ck. */
786
+ private runSplitCheckpointedLayer(
787
+ layer: DecoderLayer, h: MlxArray,
788
+ layerCache: TrainingAttentionCache, sharedIn: PlainSharedKv | null, pls: MlxArray | null,
789
+ ll: LayerLoras, ctx: GradCheckpointCtx,
790
+ ): { h: MlxArray; shared: PlainSharedKv | null } {
791
+ const isSharer = !layer.attn.hasKv;
792
+ const sIn = isSharer && sharedIn && sharedIn.kind === "plain" ? sharedIn : null;
793
+ const hasPls = pls !== null;
794
+ const attnLws = ll.attn;
795
+ const mlpLws = ll.mlp;
796
+
797
+ // --- attn sub-block checkpoint: [h, sharedInK?, sharedInV?, attnLora*]
798
+ const attnInputs: MlxArray[] = [h];
799
+ if (sIn) attnInputs.push(sIn.keys, sIn.values);
800
+ for (const lw of attnLws) attnInputs.push(lw.a, lw.b);
801
+
802
+ const attnCk = new Checkpoint((ins) => {
803
+ let k = 0;
804
+ const hIn = ins[k++]!;
805
+ const sharedInRebuilt: PlainSharedKv | null = sIn
806
+ ? { kind: "plain", keys: ins[k++]!, values: ins[k++]!, offset: 0 }
807
+ : null;
808
+ const saved = attnLws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
809
+ for (const lw of attnLws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
810
+ try {
811
+ const out = layerAttnTraining(layer, hIn, layerCache, sharedInRebuilt);
812
+ if (!isSharer && out.shared && out.shared.kind === "plain")
813
+ return [out.h, out.shared.keys, out.shared.values];
814
+ return [out.h];
815
+ } finally {
816
+ attnLws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
817
+ }
818
+ });
819
+ ctx.keepAlive.push(attnCk);
820
+ const attnOuts = attnCk.apply(attnInputs);
821
+ const hMid = attnOuts[0]!;
822
+ const shared: PlainSharedKv | null =
823
+ !isSharer && attnOuts.length === 3
824
+ ? { kind: "plain", keys: attnOuts[1]!, values: attnOuts[2]!, offset: 0 }
825
+ : null;
826
+
827
+ // --- MLP sub-block checkpoint: [hMid, pls?, mlpLora*] -> [hOut]
828
+ const mlpInputs: MlxArray[] = [hMid];
829
+ if (hasPls) mlpInputs.push(pls!);
830
+ for (const lw of mlpLws) mlpInputs.push(lw.a, lw.b);
831
+
832
+ const mlpCk = new Checkpoint((ins) => {
833
+ let k = 0;
834
+ const hMidIn = ins[k++]!;
835
+ const plsIn = hasPls ? ins[k++]! : null;
836
+ const saved = mlpLws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
837
+ for (const lw of mlpLws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
838
+ try {
839
+ return [layer.forwardMlp(hMidIn, plsIn)];
840
+ } finally {
841
+ mlpLws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
842
+ }
843
+ });
844
+ ctx.keepAlive.push(mlpCk);
845
+ const mlpOuts = mlpCk.apply(mlpInputs);
846
+ hMid.dispose(); // boundary leaf consumed by the MLP ck (mlx retains it in-graph)
847
+
848
+ return { h: mlpOuts[0]!, shared };
849
+ }
850
+
851
+ /** hidden [1, L, hidden] → softcapped logits [1, L, vocab]. */
852
+ logitsFromHidden(h: MlxArray): MlxArray { return this.#head(h, compiledLogitSoftcap); }
853
+
854
+ #head(h: MlxArray, softcapOperation: typeof logitSoftcap): MlxArray {
855
+ let logits = this.embed.asLinear(h);
856
+ const softcap = this.config.text.finalLogitSoftcapping;
857
+ if (softcap !== null) logits = disposing(logits, softcapOperation(logits, softcap));
858
+ return logits;
859
+ }
860
+ }