mlx-bun 0.2.0 → 0.3.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.
- package/README.md +71 -588
- package/docs/README.md +91 -0
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/design/batching.md +569 -0
- package/docs/design/decode-speed-program.md +3178 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +579 -334
- package/docs/design/kv-cache.md +680 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1627 -0
- package/docs/design/turboquant.md +2102 -0
- package/docs/design/unified-engine-frontier-plan.md +1831 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +34 -1
- package/docs/planning/release-notes-v0.3.0.md +35 -0
- package/docs/planning/video-series.md +2 -2
- package/docs/reference/benchmarks.md +1162 -0
- package/docs/reference/cli.md +394 -159
- package/docs/reference/distribution.md +321 -182
- package/docs/reference/environment.md +348 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +566 -168
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +104 -6
- package/docs/reference/server-api.md +886 -425
- package/docs/reference/server-config.md +638 -330
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +166 -0
- package/package.json +9 -4
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1646 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +47 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/gateway-binding.ts +71 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/memory-client.ts +189 -0
- package/src/backends/mlx/memory-guard.ts +40 -0
- package/src/backends/mlx/methods.ts +90 -0
- package/src/backends/mlx/model-prompt.ts +164 -0
- package/src/backends/mlx/model-serving.ts +79 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/rollback.ts +51 -0
- package/src/backends/mlx/serial-executor.ts +273 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-views.ts +26 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/cli.ts +58 -25
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +49 -0
- package/src/contracts/generation.ts +103 -0
- package/src/contracts/host.ts +7 -0
- package/src/contracts/jobs.ts +66 -0
- package/src/contracts/pi-provider.ts +30 -0
- package/src/contracts/pi.ts +259 -0
- package/src/contracts/resources.ts +18 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/contracts/tsconfig.json +14 -0
- package/src/diffusion/diffusion-generate.ts +218 -153
- package/src/embed.ts +1 -1
- package/src/engine/admission.ts +59 -0
- package/src/engine/cancellation.ts +38 -0
- package/src/engine/engine.ts +56 -0
- package/src/engine/execution-coordinator.ts +70 -0
- package/src/engine/execution-plan.ts +51 -0
- package/src/engine/resources.ts +42 -0
- package/src/engine/scheduler.ts +67 -0
- package/src/engine/session.ts +247 -0
- package/src/eval/README.md +44 -1
- package/src/eval/kl.ts +236 -3
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +8 -96
- package/src/fill/echo-index.ts +188 -0
- package/src/fill/fill-session.ts +452 -0
- package/src/fill/proposal.ts +68 -0
- package/src/fill/schema-rows.ts +506 -0
- package/src/fill/tool-boundary.ts +103 -0
- package/src/fit.ts +1 -1
- package/src/generate.ts +656 -268
- package/src/grammar.ts +12 -7
- package/src/harness-pi.ts +14 -33
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +7 -0
- package/src/inference/denoising.ts +17 -0
- package/src/inference/graph.ts +33 -0
- package/src/inference/prefill.ts +39 -0
- package/src/inference/rollback.ts +7 -0
- package/src/jobs/db.ts +4 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-entry.ts +12 -12
- package/src/jobs/runner.ts +98 -13
- package/src/jobs/task-client.ts +19 -0
- package/src/jobs/types.ts +2 -58
- package/src/kv-store.ts +109 -49
- package/src/lab/README.md +18 -0
- package/src/{assets → lab/curve}/curve-designer.html +2 -2
- package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
- package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
- package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
- package/src/library.ts +40 -0
- package/src/lora.ts +18 -0
- package/src/media-fetch.ts +1 -1
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +30 -169
- package/src/memory/prompts.ts +1 -1
- package/src/memory/rest.ts +1 -1
- package/src/memory/schedule.ts +1 -1
- package/src/memory/stages.ts +4 -1
- package/src/memory/vault.ts +1 -3
- package/src/mlx/array.ts +9 -10
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +58 -11
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +33 -13
- package/src/mlx/quantized-rows.ts +36 -0
- package/src/mlx/token-bitmask.ts +52 -0
- package/src/mlx/turboquant-kv-decode.ts +82 -0
- package/src/mlx/turboquant-kv-inverse.ts +122 -0
- package/src/mlx/turboquant-ops.ts +33 -3
- package/src/mlx/turboquant-tables.ts +2 -2
- package/src/model/batched-mask.ts +8 -2
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +2 -2
- package/src/model/batched-rotating.ts +1 -1
- package/src/model/compiled-decode.ts +1 -1
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +76 -33
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/gemma4-base.ts +26 -9
- package/src/model/gemma4.ts +6 -6
- package/src/model/generated/gemma4-12b.ts +2 -2
- package/src/model/generated/gemma4-26b.ts +2 -2
- package/src/model/generated/gemma4-e4b.ts +2 -2
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +1 -1
- package/src/model/implementation.ts +69 -0
- package/src/model/minicpm5.ts +6 -8
- package/src/model/profile.ts +12 -2
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +140 -56
- package/src/model/support.ts +1 -1
- package/src/model/trellis-balanced-scatter.ts +51 -0
- package/src/model/trellis-linear.ts +672 -0
- package/src/model/trellis-shared-m.ts +151 -0
- package/src/model/trellis-shared-scatter.ts +144 -0
- package/src/model/trellis-splitk-prefill.ts +158 -0
- package/src/model/trellis-tiled-prefill.ts +136 -0
- package/src/model/trellis-vector-expand.ts +62 -0
- package/src/model/trellis-wide-prefill.ts +94 -0
- package/src/model/universal/dense.ts +1 -1
- package/src/model/universal/modules.ts +8 -1
- package/src/native-pack.ts +6 -5
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +8 -315
- package/src/prompt-cache.ts +96 -53
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/config-writer.ts +10 -3
- package/src/quantize/rotate.ts +4 -4
- package/src/quantize/trellis.ts +586 -0
- package/src/responses.ts +28 -1
- package/src/runtime-config.ts +18 -4
- package/src/sampler.ts +4 -4
- package/src/serve/batch-scheduler.ts +8 -1671
- package/src/serve/chat-request.ts +391 -0
- package/src/serve/chat-stage.ts +195 -0
- package/src/serve/checkpoint-identity.ts +64 -0
- package/src/serve/completion-executor.ts +15 -7
- package/src/serve/discovery-routes.ts +12 -13
- package/src/serve/generation-gateway.ts +154 -193
- package/src/serve/http.ts +159 -0
- package/src/serve/inference-request.ts +79 -0
- package/src/serve/isolate.ts +297 -105
- package/src/serve/lab-routes.ts +3 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +7 -5
- package/src/serve/model-binding.ts +43 -0
- package/src/serve/model-host.ts +512 -0
- package/src/serve/openai-wire.ts +103 -0
- package/src/serve/parent-application.ts +56 -0
- package/src/serve/pipeline.ts +33 -0
- package/src/serve/preparation.ts +33 -0
- package/src/serve/request-plan.ts +19 -15
- package/src/serve/request-prep.ts +346 -0
- package/src/serve/response-lease.ts +24 -0
- package/src/serve/responses-client.ts +69 -0
- package/src/serve/session-completion-engine.ts +110 -0
- package/src/serve/session-search.ts +1 -1
- package/src/serve/static-routes.ts +5 -5
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +317 -2649
- package/src/spec/assistant-source.ts +10 -44
- package/src/spec/deepspec-source.ts +6 -6
- package/src/spec/dflash-source.ts +8 -8
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +2 -2
- package/src/spec/dspark/generate-dflash.ts +2 -2
- package/src/spec/dspark/generate.ts +2 -2
- package/src/spec/dspark/loader.ts +1 -1
- package/src/spec/dspark/module-dflash.ts +4 -3
- package/src/spec/dspark/module.ts +3 -3
- package/src/spec/dspark/quantize-drafter.ts +1 -1
- package/src/spec/generate.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +1 -1
- package/src/spec/ngram-source.ts +1 -1
- package/src/spec/prefix-state.ts +45 -0
- package/src/spec/qwen-mtp-source.ts +166 -69
- package/src/spec/serve-loop.ts +154 -156
- package/src/spec/source.ts +42 -10
- package/src/spec/two-model.ts +1 -1
- package/src/ssd-cache.ts +80 -12
- package/src/ssd-durability.ts +25 -13
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +2 -2
- package/src/train/forward.ts +6 -0
- package/src/train/loss.ts +1 -1
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +5 -3
- package/src/train/trainer.ts +6 -6
- package/src/vision/prompt.ts +1 -1
- package/src/vision/qwen3vl-tower.ts +52 -10
- package/src/web/app.html +1 -1
- package/src/web/app.js +5 -3
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +4 -4
- package/src/web/src/composer.ts +4 -4
- package/src/web/src/hub.ts +1 -1
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +1 -1
- package/src/web/src/model-picker.ts +1 -1
- package/src/web/src/palette.ts +1 -1
- package/src/web/src/protocol.ts +3 -39
- package/src/web/src/sessions.ts +1 -1
- package/src/web/src/ui-catalog.ts +4 -18
- package/src/web-assets.ts +47 -0
- package/src/weights.ts +55 -9
- package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/archive/investigations/hlg-ab.html +0 -34
- package/docs/archive/investigations/hlg-explorer.html +0 -238
- package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
- package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/archive/investigations/hlg-report.html +0 -262
- package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/archive/mlx-bun-lab-report.html +0 -275
- package/docs/archive/wwdc-mlx-bun.html +0 -848
- package/docs/design/adapters-end-to-end.md +0 -205
- package/docs/design/batching-perf-path.md +0 -255
- package/docs/design/batching-v2-plan.md +0 -529
- package/docs/design/bucketing-stage.md +0 -197
- package/docs/design/compat-cli-surface-design.md +0 -230
- package/docs/design/docs-reorg-plan.md +0 -165
- package/docs/design/dspark-serving-program.md +0 -371
- package/docs/design/dspark-speculative-decoding.md +0 -318
- package/docs/design/grammar-spec-batching-integration.md +0 -301
- package/docs/design/memory-inference-path.md +0 -422
- package/docs/design/memory-synthesis.md +0 -287
- package/docs/design/memory-system.md +0 -237
- package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
- package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
- package/docs/design/omlx-adoption-map.md +0 -98
- package/docs/design/orpo-dynamic-lambda.md +0 -220
- package/docs/design/orpo-future-enhancements.md +0 -340
- package/docs/design/paged-kv-cache.md +0 -214
- package/docs/design/parallel-slots.md +0 -567
- package/docs/design/parity-tier-dag.md +0 -161
- package/docs/design/repo-cleanup-plan.md +0 -231
- package/docs/design/runtime-isolation.md +0 -263
- package/docs/design/segmented-backward-training.md +0 -543
- package/docs/design/spec-decode-larger-targets.md +0 -229
- package/docs/design/ssd-kv-cold-tier.md +0 -415
- package/docs/design/structured-output.md +0 -484
- package/docs/design/tauri-desktop-app.md +0 -127
- package/docs/design/the-dreaming-master-plan.md +0 -445
- package/docs/design/turboquant-kv.md +0 -235
- package/docs/design/turboquant-weights.md +0 -467
- package/docs/design/w4a16-compute-precision-spike.md +0 -232
- package/docs/design/web-chat-beat-matrix.md +0 -315
- package/docs/design/write-pipeline-entity.md +0 -468
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -101
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -11
- /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
- /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
- /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
- /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
- /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
- /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
|
@@ -1,1246 +1,680 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
flash-CCE head
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
> hiddens before judging** (it's plausibly a free speedup to enable). Standing Lab
|
|
39
|
-
> gates (filter-on-real-data, teacher-forced grad fidelity, end-to-end quality)
|
|
40
|
-
> + the host-buffer pin-leak post-mortem:
|
|
41
|
-
> [orpo-flash-cce-pin-leak.md](../archive/investigations/orpo-flash-cce-pin-leak.md).
|
|
42
|
-
|
|
43
|
-
## Implementation status
|
|
44
|
-
|
|
45
|
-
**Landed (naïve correctness path — the shippable unit + the oracle everything
|
|
46
|
-
else validates against):**
|
|
47
|
-
- ✅ Mixed-precision rank scaling wired (`bitsMapFromModel` / `readPerLayerKl` →
|
|
48
|
-
`resolveRanks`); unit-tested ([`tests/train-rank.test.ts`](../../tests/train-rank.test.ts)).
|
|
49
|
-
- ✅ ORPO loss: `branchLogpMean` (with the **response-only B=1 forward** — no
|
|
50
|
-
full `[1,T,V]` logits, mirroring the SFT path), `log1mexp`, `orpoLogOdds`,
|
|
51
|
-
`orpoLossFromLogps`, `orpoLoss`, `orpoMetrics` in
|
|
52
|
-
[`loss.ts`](../../src/train/loss.ts). Math validated vs a standalone JS
|
|
53
|
-
reference + autograd differentiability
|
|
54
|
-
([`tests/train-orpo.test.ts`](../../tests/train-orpo.test.ts)).
|
|
55
|
-
- ✅ `orpoLoop` + dispatch + `orpoLambda`/`orpoWarmupIters`/`orpoLrSchedule`
|
|
56
|
-
config; `job.ts` submit surface + `1e-5` LR default; `/finetune` UI method
|
|
57
|
-
picker + λ/schedule panel.
|
|
58
|
-
- ✅ End-to-end verified on MiniCPM5-1B (gated
|
|
59
|
-
[`tests/train-orpo-e2e.test.ts`](../../tests/train-orpo-e2e.test.ts)): loss
|
|
60
|
-
improves, preference accuracy reported, adapter saved + mountable, adapted
|
|
61
|
-
generations differ.
|
|
62
|
-
- ✅ Docs: [training.md](../reference/training.md) updated.
|
|
63
|
-
|
|
64
|
-
**Also landed:**
|
|
65
|
-
- ✅ Dataset format renamed `"dpo"` → `"preference"` (with `"dpo"` kept as alias in
|
|
66
|
-
the type); [`tests/train-dataset.test.ts`](../../tests/train-dataset.test.ts) updated.
|
|
67
|
-
- ✅ Parity oracle script
|
|
68
|
-
[`scripts/experiments/parity-orpo.ts`](../../scripts/experiments/parity-orpo.ts):
|
|
69
|
-
10/10 checks pass — scalar loss, log1mexp stability, gradient checks (finite
|
|
70
|
-
differences vs autograd) at B=1 and B=3, edge cases.
|
|
71
|
-
- ✅ `fusedGegluDifferentiable` in `src/model/fused-geglu-kernel.ts`
|
|
72
|
-
(**kernel DELETED 2026-07-05** in the Phase-1 deletion pass —
|
|
73
|
-
[unified-engine-frontier-plan.md §6](unified-engine-frontier-plan.md);
|
|
74
|
-
the compiled activations are the only geglu now, bit-exact, and training
|
|
75
|
-
needs no flag sanitization): `CustomVjp`-backed version with hand-derived
|
|
76
|
-
backward (grad\_b = dc·gelu(a), grad\_a = dc·b·gelu'(a)); was tested in
|
|
77
|
-
`tests/fused-geglu-vjp.test.ts` (deleted with it).
|
|
78
|
-
- ✅ **Fused GeGLU wired into the e4b training path.** A training-mode flag
|
|
79
|
-
(`setFusedGeluTraining`, set by the trainer around the loop, cleared in its
|
|
80
|
-
finally) makes BOTH Gemma GeGLU sites — the MLP (`gelu(gate)·up`) and the
|
|
81
|
-
per-layer-input gate (`gelu(gate)·pli`) — use the differentiable wrapper during
|
|
82
|
-
training and the plain `fusedGeglu` (byte-for-byte unchanged) at inference.
|
|
83
|
-
**This removes the `MLX_BUN_FUSED_GELU=0` requirement for training** (the plain
|
|
84
|
-
kernel is a bare `CustomKernel` with no vjp; feeding it to autograd threw
|
|
85
|
-
`[Primitive::vjp] Not implemented for CustomKernel`). Bonus: the backward
|
|
86
|
-
recomputes the gelu from the primal instead of retaining the ~9 spelled-out
|
|
87
|
-
intermediates, lowering the MLP activation memory the backward holds (the
|
|
88
|
-
dominant resident at long context under `ops.sdpa`). **Validated** on real e4b
|
|
89
|
-
([`scripts/experiments/fused-geglu-train-parity.ts`](../../scripts/experiments/fused-geglu-train-parity.ts)):
|
|
90
|
-
forward loss bit-identical to the spelled-out path (kernel is kl=0), both sites
|
|
91
|
-
dispatch (168 = 42 layers × 2 sites × 2 ORPO branches), spelled arm fused-free,
|
|
92
|
-
grads match in the kernel's bf16 class (~2%; the forward is exact, the backward
|
|
93
|
-
is two bf16 computations of the same gelu derivative — the hand-vjp is validated
|
|
94
|
-
to the closed form at tol 0.05 in the vjp test). Composes with the segmented
|
|
95
|
-
path (the fused `CustomVjp` nests inside the segmented `mlx_vjp`: segmented ORPO
|
|
96
|
-
still PASSES with fused on, peak 11.25 GB full → 7.58 GB segmented). Unit guard
|
|
97
|
-
(`tests/fused-geglu-vjp.test.ts`, deleted with the kernel): the
|
|
98
|
-
flag toggles and the differentiable forward bit-matches the plain kernel.
|
|
99
|
-
(Historical: training at the time also needed `MLX_BUN_PERF_KERNEL=0` — that
|
|
100
|
-
perf kernel was likewise deleted 2026-07-05, so no training flag sanitization
|
|
101
|
-
remains.)
|
|
102
|
-
- ✅ `responseOnlyLogpMean` helper in `loss.ts` (post-finalNorm h → mean response
|
|
103
|
-
logp; used by the ORPO head VJP).
|
|
104
|
-
- ✅ `SegmentedBackwardOrpo` in
|
|
105
|
-
[`src/train/segmented.ts`](../../src/train/segmented.ts): MiniCPM5 segmented
|
|
106
|
-
backward for ORPO — runs chosen+rejected forwards segment-by-segment, two-input
|
|
107
|
-
head VJP (`new Vjp(fn, 1)` with `apply([hC, hR], [one])` → vjps[0]=dh\_c,
|
|
108
|
-
vjps[1]=dh\_r), backward passes sum LoRA grads from both branches. Wired into
|
|
109
|
-
`orpoLoop` behind the existing `segmentSize > 0` gate.
|
|
110
|
-
- ✅ **Token-chunked LM head (the large-vocab moat):** `chunkedLogpMeanB1` +
|
|
111
|
-
`ChunkCtx` in [`loss.ts`](../../src/train/loss.ts), gated by `orpoChunkSize`
|
|
112
|
-
(`TrainConfig` / `orpo_chunk_size`). Each token-chunk's head is wrapped in a
|
|
113
|
-
`Checkpoint` so its `[chunk, vocab]` logits are recomputed in the backward
|
|
114
|
-
instead of all `[M, vocab]` retained — bounding the dominant large-vocab term
|
|
115
|
-
to `[chunkSize, vocab]`. The per-chunk checkpoints, plus the hidden/slices they
|
|
116
|
-
read (a Checkpoint *drops* its input activations, so freeing them early
|
|
117
|
-
corrupts the result — found and fixed during bring-up), live in a `sink`
|
|
118
|
-
disposed by `orpoLoop` via `accumulateStep`'s `afterMicroEval` hook (after the
|
|
119
|
-
grads are eval'd). Verified gated
|
|
120
|
-
([`tests/train-orpo-chunked.test.ts`](../../tests/train-orpo-chunked.test.ts)):
|
|
121
|
-
forward parity vs non-chunked within **bf16 tolerance** (the only divergence is
|
|
122
|
-
occasional single-ULP head-matmul rounding that tiles differently for `[M,V]`
|
|
123
|
-
vs `[chunk,V]` — sub-bf16, the established class), **exact** for a single
|
|
124
|
-
chunk, and the chunked backward trains + improves loss. This is **token-axis**
|
|
125
|
-
chunking — the high-impact win for our case (`M` up to thousands × 262k vocab).
|
|
126
|
-
**Vocab-axis** chunking + online-LSE (CCE-style) is unnecessary at 262k
|
|
127
|
-
(`[1, 262k]` bf16 ≈ 0.5 MB) and remains a future tier.
|
|
128
|
-
|
|
129
|
-
- ✅ **Fused linear-CE head (Liger `FusedLinearCrossEntropy` ported to MLX)** —
|
|
130
|
-
`fusedLogpMeanB1` (`loss.ts`), gated by `orpo_fused_ce`. A single `CustomVjp`
|
|
131
|
-
with an ANALYTIC `softmax−onehot` backward (no autograd through logsumexp+gather,
|
|
132
|
-
no retained `[M,V]` logits in either direction); `dh` via mlx's own
|
|
133
|
-
quantized-matmul x-vjp (`transpose=false`); Gemma softcap matched bit-exactly to
|
|
134
|
-
`logitSoftcap`. Value BIT-EXACT vs the full-logits head on MiniCPM5 **and** e4b;
|
|
135
|
-
grads 0.51%/1.97% bf16-class; peak `[chunk,V]`-bounded. See
|
|
136
|
-
[lever 1](#ranked-levers-with-our-setup-verdicts) for the full write-up. This is
|
|
137
|
-
the deeper engine the token-chunked Checkpoint head approximated.
|
|
138
|
-
|
|
139
|
-
- ✅ **Benchmark + parity harness** (the perf-tracking discipline):
|
|
140
|
-
[`scripts/bench-orpo.ts`](../../scripts/bench-orpo.ts) (ms/step + peak GB
|
|
141
|
-
across head/segment configs on synthetic long-response data) and
|
|
142
|
-
[`scripts/bench-attn-backward.ts`](../../scripts/bench-attn-backward.ts)
|
|
143
|
-
(attention-backward dQ/dK/dV parity vs a materialized reference + timing).
|
|
144
|
-
MiniCPM5-1B @ SEQ=1024: baseline 3191 ms / 9.34 GB · chunked-head(256) **2758
|
|
145
|
-
ms / 8.62 GB** (faster + lower) · segmented(4) 3633 ms / **2.45 GB** (the
|
|
146
|
-
dominant memory lever — layer activations, not the head, dominate at this
|
|
147
|
-
scale/vocab; the head win grows with vocab, e.g. Gemma's 262k).
|
|
148
|
-
- ✅ **SDPA backward — flash kernel FIXED to L2 parity (kept, not deleted).**
|
|
149
|
-
Parity oracle, made explicit: the default `ops.sdpa` training path is **L1**
|
|
150
|
-
(mlx-lm's tuner differentiates the same `mx.fast.sdpa`); the flash kernel is
|
|
151
|
-
**L2** — a port of mlx-optiq's `flash_attention_metal`, so **optiq is its
|
|
152
|
-
oracle**. Confirmed end to end: optiq's flash dK == `ops.sdpa` to f16
|
|
153
|
-
(`scripts/flash-optiq-check.py`, rel **0.0%**), and mlx-bun's fixed flash ==
|
|
154
|
-
`ops.sdpa` (`tests/flash-attention.test.ts`, ≤f16) ⟹ **mlx-bun flash == optiq
|
|
155
|
-
(L2 parity)**; `ops.sdpa` itself is finite-difference verified. The original
|
|
156
|
-
kernel diverged ~100% from optiq on dK — a genuine **port bug**: a spurious dK
|
|
157
|
-
transpose in `flashBackward` (corrupted dK for Tkv≠D) + a per-thread
|
|
158
|
-
(divergent) `threadgroup_barrier` in the dQ causal tile-skip; both fixed. The
|
|
159
|
-
real `ops.sdpa`-vs-flash tradeoff is **memory**: `ops.sdpa` backward is
|
|
160
|
-
**O(L²)** (0.02→0.33→1.40 GB at T=512/2048/4096), flash is **O(L)**
|
|
161
|
-
(0.03→0.23→0.90 GB) but ~30× slower → flash is the opt-in long-context path.
|
|
162
|
-
**Lessons:** (1) state parity *to a named oracle* (L1 mlx-lm / L2 mlx-optiq),
|
|
163
|
-
not "correct"; (2) "fix, don't delete" — the kernel had a localized, fixable
|
|
164
|
-
port bug + a real memory purpose; (3) a parity reference is trustworthy only if
|
|
165
|
-
read **contiguously** (`ops.contiguous` before `toFloat32`) with a
|
|
166
|
-
**non-uniform cotangent** — both pitfalls produced false "kernel is wrong"
|
|
167
|
-
signals; finite differences settled it.
|
|
168
|
-
- ✅ **Regularization knobs (the small-set overfitting toolkit):** **LoRA-input
|
|
169
|
-
dropout** (`lora_dropout`) — recompute-safe (mask keyed by step+layer so the
|
|
170
|
-
segmented/checkpoint backward reproduces it; verified by a finite+improving
|
|
171
|
-
loss through the segmented recompute path); **rsLoRA** (`rs_lora`, α/√rank,
|
|
172
|
-
wired train→save→load so inference applies the same per-layer scale); **LoRA+**
|
|
173
|
-
(`lora_plus_ratio`, per-param LR in AdamW — B leaves get the higher LR).
|
|
174
|
-
Unit-tested ([`tests/train-regularization.test.ts`](../../tests/train-regularization.test.ts))
|
|
175
|
-
+ gated e2e with all three on through segmented recompute
|
|
176
|
-
([`tests/train-regularization-e2e.test.ts`](../../tests/train-regularization-e2e.test.ts)).
|
|
177
|
-
The dangling `/finetune` dropout field is now wired (`lora_dropout`).
|
|
178
|
-
- ✅ **Segmented backward for ORPO on e4b** (`SegmentedBackwardOrpoGemma4`,
|
|
179
|
-
[`src/train/segmented.ts`](../../src/train/segmented.ts)): the cross of
|
|
180
|
-
`SegmentedBackwardGemma4` (per-layer-input boundary + cross-segment KV-shared
|
|
181
|
-
donor cotangent threading) and `SegmentedBackwardOrpo` (two branches +
|
|
182
|
-
reference-free ORPO head). Each branch streams forward saving detached
|
|
183
|
-
hidden + reused-donor K/V boundaries + its own masks/per-layer constant; the
|
|
184
|
-
ORPO head vjp over `[last_chosen, last_rejected]` yields `[dh_c, dh_r]`; each
|
|
185
|
-
branch walks backward in reverse over its segments (donor cotangents
|
|
186
|
-
accumulated in a per-branch `dKV`), and the two branches' LoRA grads are
|
|
187
|
-
summed. Wired into `orpoLoop` (`segment_size > 0`, B=1) alongside the MiniCPM5
|
|
188
|
-
path. **Validated** ([`scripts/experiments/segmented-grad-test-orpo-e4b.ts`](../../scripts/experiments/segmented-grad-test-orpo-e4b.ts),
|
|
189
|
-
grad-parity vs the monolithic ORPO `value_and_grad` on real e4b, 2026-06-18):
|
|
190
|
-
loss matches to ~6 decimals; at **λ=0 the path is byte-identical** to the SFT
|
|
191
|
-
`SegmentedBackwardGemma4` (relNorm 1.1341%, maxAbs 3.79e-3 — same numbers,
|
|
192
|
-
proving zero added error); the donor-KV recipients (layers 22/23 k/v_proj,
|
|
193
|
-
the direct dKV recipients) stay clean (<0.5%), proving the cross-segment donor
|
|
194
|
-
threading; overall match sits in the e4b segmented bf16-class (~1.1% flash /
|
|
195
|
-
~2.0–2.3% sdpa, the same level the SFT path shows here). **Peak**: full
|
|
196
|
-
two-branch ORPO backward **12.67 GB → segmented 7.66 GB** at L=256 (saved
|
|
197
|
-
5 GB — a bigger win than SFT's 2.4 GB, since the two-branch full backward
|
|
198
|
-
holds both branches at once); 8.71 GB at L=1024 where the full backward
|
|
199
|
-
won't fit.
|
|
200
|
-
- ✅ **Shared prompt-prefix reference (lever 7, MiniCPM5)** in
|
|
201
|
-
[`src/train/prefix-shared.ts`](../../src/train/prefix-shared.ts): one forward
|
|
202
|
-
over `[prompt; chosen; rejected]` with a block-sparse mask + block-wise RoPE.
|
|
203
|
-
Validated 2026-06-18 — **forward BIT-EXACT** vs the two-forward path, grads in
|
|
204
|
-
the bf16 class (~1.05%), **1.78× fewer token-passes**, peak 5.40 → 3.80 GB. See
|
|
205
|
-
[lever 7](#ranked-levers-with-our-setup-verdicts) for the full write-up.
|
|
206
|
-
**LANDED (2026-06-19):** wired into the trainer (`orpo_prefix_shared`), composed
|
|
207
|
-
with the flash-CCE head per branch AND with the segmented backward
|
|
208
|
-
(`SegmentedBackwardOrpoPrefix`); the e4b port is composed too (next entry).
|
|
209
|
-
- ✅ **e4b prefix-shared port — LANDED + composed with the segmented backward
|
|
210
|
-
(`SegmentedBackwardOrpoPrefixGemma4`, 2026-06-19); the length-sensitivity below is an
|
|
211
|
-
accepted numerical caveat for a fine-tune, not a blocker** (seg-vs-non-seg-same-prefix
|
|
212
|
-
grads match to 1.7–2.3%, loss bit-exact-class). Ported to Gemma e4b
|
|
213
|
-
(`setGemmaPrefixPlan` + block-wise RoPE in [`gemma4.ts`](../../src/model/gemma4.ts)
|
|
214
|
-
`Attention.ropeBlocks`; `blockSparsePrefixMaskGemma` with a LOGICAL-position
|
|
215
|
-
sliding-window cut + `Gemma4PrefixSharedCache` + `orpoLossPrefixSharedGemma` in
|
|
216
|
-
[`prefix-shared.ts`](../../src/train/prefix-shared.ts), reusing
|
|
217
|
-
`forwardHidden` for per-layer-input + donor-KV). **The construction is proven
|
|
218
|
-
correct**: the prefix-shared forward is BIT-EXACT to a plain forward over the
|
|
219
|
-
same concatenated sequence (`scripts/experiments/prefix-shared-e4b-localize.ts`:
|
|
220
|
-
block-wise-RoPE+block-mask path `(c)` == plain-concat path `(b)` to 0.00% at
|
|
221
|
-
every chosen position; per-layer-inputs bit-exact across lengths). **But it does
|
|
222
|
-
NOT numerically reproduce the two-forward path on e4b**: prefix-shared forward
|
|
223
|
-
loss diverges ~0.08% and the chosen/rejected hiddens up to ~3-14% from the two
|
|
224
|
-
separate `[prompt;resp]` forwards — because **the e4b full-forward is itself
|
|
225
|
-
length-sensitive**. Demonstrated model-only, independent of prefix-sharing:
|
|
226
|
-
`forward([prompt])` vs `forward([prompt;extra])` differ **1.7% at position 0**
|
|
227
|
-
(one attention key — softmax=1.0, cannot reassociate) up to **~14%** at later
|
|
228
|
-
prompt positions, even though those positions are causally independent of the
|
|
229
|
-
appended tokens, and even through the **production `TrainingCache`** path (fused
|
|
230
|
-
causal). Root cause = e4b's **scale=1.0 attention** (headDim 256, q/k normed →
|
|
231
|
-
q·k ~N(0,256) → sharply peaked softmax) amplifying sub-bf16 matmul-tiling
|
|
232
|
-
roundings that vary with the row count — the SAME amplification class documented
|
|
233
|
-
for the e4b SigLIP vision tower ([[siglip-vision-parity-cross-build]]). MiniCPM5
|
|
234
|
-
(standard `1/√d` scale) shows 0.00% length-sensitivity, which is why its
|
|
235
|
-
prefix-shared path is bit-exact to two-forward. **Consequence:** on e4b,
|
|
236
|
-
prefix-sharing is a *self-consistent* computation (correct gradients of a valid
|
|
237
|
-
single-concat loss; 1.78× fewer prompt-token passes, peak proportionally lower)
|
|
238
|
-
but is NOT a drop-in numerical equivalent of two-forward training the way it is
|
|
239
|
-
on MiniCPM5. **Decision pending** (Josh): land it framed via the
|
|
240
|
-
single-concat-forward oracle / defer / investigate the length-sensitivity
|
|
241
|
-
further. Code is committed-ready; the parity script's two-forward comparison is
|
|
242
|
-
expected to show the e4b-class divergence, not bit-exactness.
|
|
243
|
-
|
|
244
|
-
**Follow-on (each validated against the naïve path via the
|
|
245
|
-
[oracle ladder](#parity)):** **optimize** the (now-correct) flash kernel — it's
|
|
246
|
-
~30× slower than `ops.sdpa` because it's a naive scalar Metal kernel (no
|
|
247
|
-
`simdgroup_matrix`, 32-thread threadgroups → ~0.5% of GPU peak), whereas
|
|
248
|
-
`ops.sdpa` is MLX's hand-tuned simdgroup-matrix flash. Its O(L) memory only pays
|
|
249
|
-
off when `ops.sdpa`'s O(L²) won't fit (≳16K context). **Note (future research
|
|
250
|
-
spike):** MLX is open source — we can read exactly how
|
|
251
|
-
`mx.fast.scaled_dot_product_attention` is implemented (simdgroup-matrix tiling),
|
|
252
|
-
match it, and then potentially **beat it for OUR specific use case** (fixed
|
|
253
|
-
head-dims, training-only backward, our seq-len/precision regime, the O(L)
|
|
254
|
-
memory constraint they don't optimize for). Not now — `ops.sdpa` is the fast
|
|
255
|
-
default and the current training (`ft-e4b-v2.sh`, no `MLX_BUN_TRAIN_ATTN`) uses
|
|
256
|
-
it for both forward and backward; the flash kernel is the dormant opt-in
|
|
257
|
-
fallback. Then: fused GeGLU wired into model training path,
|
|
258
|
-
intra-layer MLP split, shared prompt-prefix.
|
|
259
|
-
**End goal:** a before/after ORPO quality experiment on Gemma e4b or Qwen-4B
|
|
260
|
-
(curated preference set + a benchmark eval).
|
|
261
|
-
|
|
262
|
-
## The objective
|
|
1
|
+
---
|
|
2
|
+
status: landed
|
|
3
|
+
axis: USING
|
|
4
|
+
canonical-for: training
|
|
5
|
+
plan-anchor: "Phase: Steel flash-CCE ORPO head + full ORPO training stack"
|
|
6
|
+
last-verified: 2026-08-23
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Training — design (ORPO, flash-CCE head, prefix sharing, segmented backward)
|
|
10
|
+
|
|
11
|
+
The canonical design record for mlx-bun's LoRA trainer: the **ORPO** objective
|
|
12
|
+
(Hong et al. 2024) as a third `method` beside `sft`/`dpo`, and the long-context
|
|
13
|
+
machinery it runs on — the `[M,V]`-free **flash-CCE** head, the **prefix-shared**
|
|
14
|
+
two-branch forward, and the **segmented backward**. All of it is landed and
|
|
15
|
+
default-on in `scripts/train-orpo.ts` and `mlx-bun train`.
|
|
16
|
+
|
|
17
|
+
- Usage, flags, measured run tables: [training.md](../reference/training.md),
|
|
18
|
+
[docs/reference/training.md](../reference/training.md).
|
|
19
|
+
- Build narratives (frozen): [steel-flash-cce-handoff.md](../archive/investigations/steel-flash-cce-handoff.md),
|
|
20
|
+
[segmented-backward-handoff.md](../archive/investigations/segmented-backward-handoff.md),
|
|
21
|
+
[orpo-flash-cce-pin-leak.md](../archive/investigations/orpo-flash-cce-pin-leak.md),
|
|
22
|
+
[orpo-base-uf-experiment-and-directions.md](../archive/investigations/orpo-base-uf-experiment-and-directions.md),
|
|
23
|
+
[orpo-uf-testing-handoff.md](../archive/investigations/orpo-uf-testing-handoff.md).
|
|
24
|
+
- Open training work is tracked in PLAN.md under the plan-anchor above; status
|
|
25
|
+
prose lives there, not here.
|
|
26
|
+
|
|
27
|
+
Code: [`src/train/loss.ts`](../../src/train/loss.ts) (objective + heads),
|
|
28
|
+
[`trainer.ts`](../../src/train/trainer.ts) (loops, config), [`segmented.ts`](../../src/train/segmented.ts),
|
|
29
|
+
[`prefix-shared.ts`](../../src/train/prefix-shared.ts), [`flash-cce.ts`](../../src/train/flash-cce.ts)
|
|
30
|
+
(+ `steel-qmm-header.ts`), [`job.ts`](../../src/train/job.ts) (submit surface),
|
|
31
|
+
[`lora-params.ts`](../../src/train/lora-params.ts), [`rank.ts`](../../src/train/rank.ts).
|
|
32
|
+
Entry points: [`scripts/train-orpo.ts`](../../scripts/train-orpo.ts) (env launcher),
|
|
33
|
+
`mlx-bun train` (`src/cli.ts`), the `/finetune` job runner. SFT examples:
|
|
34
|
+
`scripts/examples/chunk-finetune.ts`, `scripts/examples/ft-e4b-v2.sh`,
|
|
35
|
+
`scripts/examples/chunk-eval.ts`.
|
|
36
|
+
|
|
37
|
+
## 1. The objective
|
|
263
38
|
|
|
264
39
|
$$\mathcal{L}_{ORPO} = \mathcal{L}_{NLL}(y_w) + \lambda \cdot \mathcal{L}_{OR}$$
|
|
265
40
|
|
|
266
|
-
- $\mathcal{L}_{NLL}(y_w)$ —
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
`
|
|
273
|
-
xfactlab/orpo
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
`
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
`
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
`
|
|
376
|
-
`
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
- `
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
-
|
|
389
|
-
preference shape for `method: "orpo"`. Optionally introduce a user-facing
|
|
390
|
-
`"preference"` label with `dpo` kept as an alias, so the format name is not
|
|
391
|
-
method-specific.
|
|
392
|
-
|
|
393
|
-
### Surface
|
|
394
|
-
|
|
395
|
-
- [`docs/reference/training.md`](../reference/training.md): add ORPO to the
|
|
396
|
-
"SFT vs DPO" section (→ "SFT / DPO / ORPO"), the data-format table (the
|
|
397
|
-
preference format serves both), and the config table (`orpo_lambda`).
|
|
398
|
-
- `/finetune` web UI: add `orpo` to the method picker; show λ when selected.
|
|
399
|
-
|
|
400
|
-
## Recommended defaults
|
|
401
|
-
|
|
402
|
-
From a hyperparameter spike on ORPO-for-LoRA-on-small-models (paper, TRL
|
|
403
|
-
`ORPOConfig`, and Labonne's Llama-3 ORPO-LoRA recipe). The one trap: the
|
|
404
|
-
paper's `8e-6` LR is for **full** fine-tuning — do not inherit it for LoRA, and
|
|
405
|
-
do not inherit DPO-LoRA's higher `5e-5` either, because ORPO's loss carries a
|
|
406
|
-
full SFT NLL term that a high LR destabilizes.
|
|
41
|
+
- $\mathcal{L}_{NLL}(y_w)$ — cross-entropy on the **chosen** sequence. Scope is
|
|
42
|
+
`sft_scope` (`TrainConfig.sftScope`, default **`full`**):
|
|
43
|
+
- `full` (paper/TRL-faithful): token-mean CE over all non-pad positions,
|
|
44
|
+
prompt AND response, from the **same chosen forward** — the head runs once
|
|
45
|
+
over the prompt span and once over the response span (disjoint), combined
|
|
46
|
+
as $\text{NLL}_{full} = -\frac{(P{-}1)\,\text{pm} + M\,\ell_w}{(P{-}1)+M}$
|
|
47
|
+
(`combineFullNll`). Matches TRL `ORPOTrainer.chosen_nll_loss` and
|
|
48
|
+
xfactlab/orpo `outputs_pos.loss`.
|
|
49
|
+
- `response`: $-\ell_w$, the pre-2026-07 house convention (prompt masked),
|
|
50
|
+
kept bit-exact for reproducing older runs.
|
|
51
|
+
- $\mathcal{L}_{OR} = -\log\sigma(\text{log\_odds})$ with
|
|
52
|
+
$\text{log\_odds} = (\ell_w - \ell_r) - (\text{log1mexp}(\ell_w) - \text{log1mexp}(\ell_r))$,
|
|
53
|
+
$\text{log1mexp}(x) = \log(1-e^x)$. $\ell_w,\ell_r$ are **length-normalized
|
|
54
|
+
means** over response tokens in BOTH scope modes (also TRL's convention —
|
|
55
|
+
its `get_batch_logps` masks the prompt for the odds ratio; only the NLL term
|
|
56
|
+
is full-sequence). $\lambda$ weights only $\mathcal{L}_{OR}$; the NLL term is
|
|
57
|
+
unweighted.
|
|
58
|
+
|
|
59
|
+
Implementation (`loss.ts`): `log1mexp` (stable `log(clip(1 − exp(x), eps, 1))`
|
|
60
|
+
— mlx-bun has no `expm1`/`log1p`; per-token mean logp is comfortably negative
|
|
61
|
+
so the clip is safe), `orpoLogOdds`, `orpoLossFromLogps(lw, lr, λ, nllFull?)`
|
|
62
|
+
(model-free, unit-testable against a hand reference), `orpoLoss(model, batch,
|
|
63
|
+
λ, chunk?, sftScope)` (runs the two forwards), `orpoMetrics` (no-grad
|
|
64
|
+
`loss`/`nll`/`or`/`accuracy`/`margin`).
|
|
65
|
+
|
|
66
|
+
Two facts that shape everything downstream:
|
|
67
|
+
|
|
68
|
+
1. **Mean, not sum.** The one substantive difference from DPO's `seqLogp`
|
|
69
|
+
(per-row sum). Summing biases toward short responses and makes the OR
|
|
70
|
+
gradient length-dependent.
|
|
71
|
+
2. **The chosen forward does double duty.** ORPO is **2 forwards/step**
|
|
72
|
+
(chosen + rejected, both with grad) vs DPO's 4 (2 reference + 2 policy). No
|
|
73
|
+
reference model, no `setLoraScale(0)` dance, no detached reference tensors.
|
|
74
|
+
Never forward chosen twice; never compute a rejected NLL.
|
|
75
|
+
|
|
76
|
+
Two unpadded sequential B=1 forwards are the *right* shape, not a limitation:
|
|
77
|
+
TRL's padded `concatenated_forward` exists to amortize FSDP all-gathers we
|
|
78
|
+
don't have; on unified memory the padding is pure waste.
|
|
79
|
+
|
|
80
|
+
## 2. Oracles and the parity ladder
|
|
81
|
+
|
|
82
|
+
Neither `mlx_lm` nor `optiq` ships ORPO, so — unlike DPO (`loss.ts` ported
|
|
83
|
+
`optiq/lora/dpo.py`) — ORPO is a **no-oracle mlx-bun original (Lab-class)**.
|
|
84
|
+
The current MLX SOTA (`mlx-lm-lora`) ships a non-canonical ORPO
|
|
85
|
+
(`log_sigmoid(chosen − rejected)`, no SFT-NLL term); do not copy it.
|
|
86
|
+
|
|
87
|
+
What IS oracle-backed, and where each tier bottoms out:
|
|
88
|
+
|
|
89
|
+
| Tier | Path | Oracle / gate |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| L1 | Non-segmented SFT `value_and_grad` through `ops.sdpa` | **Bit-exact vs `mlx_lm.lora`** — loss on 4/6 mask spans, 0.5 f32-ulp on 2; `dB` over all 42 e4b q_proj layers `0.000e+0` (2026-06-16). Response-only CE == full-masked CE on every span. |
|
|
92
|
+
| L2 | `MLX_BUN_TRAIN_ATTN=flash` (port of optiq's `flash_attention_metal`) | optiq flash dK == `ops.sdpa` to f16 (`scripts/oracle/flash-optiq-check.py`); ours == `ops.sdpa` (`tests/flash-attention.test.ts`). |
|
|
93
|
+
| Lab | ORPO loss math | Standalone JS/numpy reference + finite differences (`tests/train-orpo.test.ts`; the deleted `parity-orpo.ts` passed 10/10 incl. FD grads at B=1/B=3). |
|
|
94
|
+
| Lab | Each memory tier (chunked → fused → flash head; segmented; prefix) | Validated against the tier below: value bit-exact or bf16-class, grads bf16-class, bottoming out at the naive full-logits two-forward path. |
|
|
95
|
+
|
|
96
|
+
Rules learned the hard way:
|
|
97
|
+
|
|
98
|
+
- A parity reference is trustworthy only if read **contiguously**
|
|
99
|
+
(`ops.contiguous` before `toFloat32`) with a **non-uniform cotangent** — both
|
|
100
|
+
pitfalls produced false "kernel is wrong" signals; finite differences settled it.
|
|
101
|
+
- "0.28% dh" for the flash-CCE head is fp-reassociation vs a full-logits
|
|
102
|
+
*proxy*, not error (two correct reductions in different orders aren't
|
|
103
|
+
bit-identical). Correctness is FD + the math audit; fidelity is
|
|
104
|
+
**teacher-forced on real hiddens**, not synthetic random logits (whose flat
|
|
105
|
+
softmax is the coeff filter's worst case). Standing regression:
|
|
106
|
+
`tests/train-orpo-fused-ce.test.ts` "teacher-forced fidelity" (cos > 0.999,
|
|
107
|
+
relnorm < 5% vs a `Vjp` full-logits reference).
|
|
108
|
+
- Validate every custom `.vjp` against `mx.vjp`/autograd, never a hand formula.
|
|
109
|
+
|
|
110
|
+
## 3. Trainer architecture
|
|
111
|
+
|
|
112
|
+
`trainLora` dispatches on `cfg.method`: `sftLoop` / `dpoLoop` / `orpoLoop`. All
|
|
113
|
+
share `resolveRanks` → `buildTrainableLora` → `AdamW` → a closure that swaps the
|
|
114
|
+
LoRA A/B primals so only those leaves differentiate. ORPO consumes the
|
|
115
|
+
**preference** dataset format `{prompt, chosen, rejected}` (`"dpo"` kept as an
|
|
116
|
+
alias) via `loadDpoDataset` / `iterateDpoBatches`.
|
|
117
|
+
|
|
118
|
+
`orpoLoop` path selection (verified in `trainer.ts`):
|
|
119
|
+
|
|
120
|
+
- `segmentSize > 0` → segmented classes (MiniCPM5 or Gemma4 only; else throws).
|
|
121
|
+
With `orpoPrefixShared` the composed `SegmentedBackwardOrpoPrefix[Gemma4]` is
|
|
122
|
+
built alongside a plain `SegmentedBackwardOrpo[Gemma4]` for per-row fallback.
|
|
123
|
+
The segmented head is `boundedHeadFromHidden` (Checkpoint-chunked or fused)
|
|
124
|
+
when `orpoFusedCe || orpoFlashCe`, else full `responseOnlyLogpMean`.
|
|
125
|
+
- Otherwise a `ValueAndGrad` over `orpoLossPrefixShared[Gemma]` (when the row's
|
|
126
|
+
prompts are byte-identical; `splitPrefixBatch`) or `orpoLoss`, with a
|
|
127
|
+
`ChunkCtx` selecting the head: `{fused, flash}` (chunk = `orpoChunkSize` or
|
|
128
|
+
512) / Checkpoint token-chunked (`orpoChunkSize > 0`) / full logits.
|
|
129
|
+
- Every micro-step goes through `accumulateStep(gradAccumSteps, …)`; the
|
|
130
|
+
per-chunk Checkpoint sink is disposed in its `afterMicroEval` hook.
|
|
131
|
+
- Dropout: `model.loraState.dropoutSeed` is set once per micro-step so the
|
|
132
|
+
segmented/checkpoint recompute replays the same mask; cleared before
|
|
133
|
+
metrics/val.
|
|
134
|
+
- `MLX_BUN_TRAIN_ATTN=flash` is **refused for Gemma models** (e4b SIGTRAPed at
|
|
135
|
+
≥2K and has not been re-validated since the kernel fixes); MiniCPM5 allowed.
|
|
136
|
+
|
|
137
|
+
### Config surface (TrainConfig ↔ job submit ↔ CLI/launcher)
|
|
138
|
+
|
|
139
|
+
| TrainConfig | `FinetuneSubmit` | `mlx-bun train` / `train-orpo.ts` env | Default | Notes |
|
|
140
|
+
|---|---|---|---|---|
|
|
141
|
+
| `orpoLambda` | `orpo_lambda` | `LAMBDA` | 0.1 | weights only L_OR |
|
|
142
|
+
| `orpoWarmupIters` / `orpoLrSchedule` | `orpo_warmup_iters` / `orpo_lr_schedule` | — | 0 / `cosine` | launcher sets warmup = min(10, iters/10) |
|
|
143
|
+
| `orpoChunkSize` | `orpo_chunk_size` | — | 0 (job: 512 for orpo) | token-chunk for all head variants |
|
|
144
|
+
| `orpoFusedCe` | `orpo_fused_ce` | `FLASH=0` selects it | false | analytic MLX fused head |
|
|
145
|
+
| `orpoFlashCe` | `orpo_flash_ce` | `FLASH` (default on) | false (job: true for orpo) | implies fused |
|
|
146
|
+
| `orpoPrefixShared` | `orpo_prefix_shared` | `PREFIX` (default on) | false (job: true for orpo) | MiniCPM5 + Gemma4, B=1 |
|
|
147
|
+
| `sftScope` | `sft_scope` | `SFT_SCOPE` | `full` | |
|
|
148
|
+
| `segmentSize` | `segment_size` | `SEG` / `SEGOFF=1`; `--seg` / `--no-segment` | 0 (job/CLI: 2 for orpo) | |
|
|
149
|
+
| `gradAccumSteps` | `grad_accumulation_steps` | `--grad-accum` (CLI only; no env knob in `train-orpo.ts`) | 1 | |
|
|
150
|
+
| `loraDropout` / `rsLora` / `loraPlusRatio` | `lora_dropout` / `rs_lora` / `lora_plus_ratio` | — | 0 / false / 1 | |
|
|
151
|
+
| `warmStartAdapter` | `warm_start_adapter` | `RESUME` / `--resume` | "" | weights only; optimizer + schedule restart |
|
|
152
|
+
| `gradCheckpoint` + `mlpSplit` | `grad_checkpoint` / `mlp_split` | — | false | mutually exclusive with `segmentSize`; `mlpSplit` Gemma4-only |
|
|
153
|
+
| `learningRate` | `learning_rate` | `LR` | 1e-5 for orpo (job/CLI/launcher) | `DEFAULT_TRAIN_CONFIG` is 2e-4 (SFT) |
|
|
154
|
+
| `rank` / `scale` | `rank` / `scale` | `RANK` / `SCALE` | orpo: 16 / **2.0** (CLI + launcher) | `DEFAULT_TRAIN_CONFIG`: 8 / 1.0; SFT example scripts use 20 |
|
|
155
|
+
|
|
156
|
+
Method-prefixed knobs (`orpo_*` / `dpo_*`) are deliberate: no shared or renamed
|
|
157
|
+
fields.
|
|
158
|
+
|
|
159
|
+
## 4. Recommended defaults and regularization geometry
|
|
160
|
+
|
|
161
|
+
From the hyperparameter spike (paper, TRL `ORPOConfig`, Labonne's Llama-3
|
|
162
|
+
ORPO-LoRA). The trap: the paper's `8e-6` is for *full* fine-tuning and
|
|
163
|
+
DPO-LoRA's `5e-5` is too hot because ORPO carries a full SFT-NLL term.
|
|
407
164
|
|
|
408
165
|
| Knob | Default | Why |
|
|
409
166
|
|---|---|---|
|
|
410
|
-
| `learning_rate` |
|
|
411
|
-
| `orpo_lambda`
|
|
412
|
-
|
|
|
413
|
-
| `
|
|
414
|
-
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
Sources: [ORPO paper (EMNLP 2024)](https://aclanthology.org/2024.emnlp-main.626/),
|
|
167
|
+
| `learning_rate` | 1e-5 | matches our SFT recipe; sweep 5e-6–2e-5 |
|
|
168
|
+
| `orpo_lambda` | 0.1 | paper's primary value + TRL default; stable 0.1–0.5 (λ=1.0 depresses chosen logp) |
|
|
169
|
+
| schedule / warmup | cosine, ≈3% of iters | the OR gradient's `1/(1−P)` factor amplifies low-likelihood updates, so cold high LR hurts more than in SFT |
|
|
170
|
+
| `rank` | 16 | |
|
|
171
|
+
| epochs | 1 to start | eval loss rises fast past epoch 1–2; early-stop on val margin |
|
|
172
|
+
|
|
173
|
+
Sources: [ORPO paper](https://aclanthology.org/2024.emnlp-main.626/),
|
|
419
174
|
[TRL `orpo_config.py`](https://github.com/huggingface/trl/blob/main/trl/trainer/orpo_config.py),
|
|
420
|
-
[Labonne
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
- `
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
and
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
**
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
- **`mx.fast` ships fused fwd+bwd VJP kernels for `rms_norm`, `layer_norm`,
|
|
927
|
-
`rope`** (`RMSNormVJP` / `LayerNormVJP`; `RoPE::vjp` cleverly re-runs the same
|
|
928
|
-
kernel inverted — zero extra kernel). **Use these; don't hand-roll them.**
|
|
929
|
-
- **SDPA is the exception, and it matters:** the forward is fused, but the
|
|
930
|
-
**backward is NYI on Metal** — `ScaledDotProductAttentionVJP::use_fallback()`
|
|
931
|
-
returns true unconditionally and `eval_gpu` throws, so MLX **decomposes** the
|
|
932
|
-
SDPA backward into matmul→softmax→matmul and **materializes the L×L attention
|
|
933
|
-
matrix** (the fused backward exists only for CUDA/cuDNN). This is precisely why
|
|
934
|
-
our `ops.sdpa` training path has **O(L²) backward memory** (see
|
|
935
|
-
[training.md](../reference/training.md)) and why segmentation is doing the
|
|
936
|
-
compensating. → A **fused FA2-style SDPA backward** (store only the per-row
|
|
937
|
-
`logsumexp` `L`; recompute `S=QKᵀ`, `P=exp(S−L)` in the backward) is therefore
|
|
938
|
-
a genuine, high-value custom-kernel target, not a free win.
|
|
939
|
-
- **Not covered by `mx.fast`** (need custom kernels): SDPA backward (above),
|
|
940
|
-
GeGLU/SwiGLU, cross-entropy / log-softmax+gather, the LoRA epilogue.
|
|
941
|
-
- **`mx.fast.metal_kernel` wrapped in `mx.custom_function` with a `.vjp` is
|
|
942
|
-
fully differentiable** (verified) — the mechanism for every custom fused
|
|
943
|
-
fwd+bwd kernel below. Validate each `.vjp` against `mx.vjp`/autograd, never a
|
|
944
|
-
hand-derived formula (our standing rule).
|
|
945
|
-
|
|
946
|
-
### Ranked fusion opportunities
|
|
947
|
-
|
|
948
|
-
**Tier 0 — free, do first (table-stakes):**
|
|
949
|
-
1. **Compile the outermost unrolled forward+grad.** One `mx.compile` region over
|
|
950
|
-
the straight-line DAG fuses the norm/RoPE/residual/LoRA-add/activation glue
|
|
951
|
-
and the matching backward, and merges duplicate work. The unrolling is what
|
|
952
|
-
makes the region large; compiling the outermost `value_and_grad` is what puts
|
|
953
|
-
the backward inside it.
|
|
954
|
-
2. **Route training through `mx.fast.rms_norm` / `layer_norm` / `rope`** —
|
|
955
|
-
already fused both directions. (Note: `sdpa` forward only — its backward is
|
|
956
|
-
the custom-kernel target in Tier 1.)
|
|
957
|
-
|
|
958
|
-
**Tier 1 — custom fused fwd+bwd Metal kernels (the cutting-edge; no-oracle originals, Lab-gated):**
|
|
959
|
-
3. **Fused linear + log-softmax + gather loss (CCE/Liger-style) — THE moat.**
|
|
960
|
-
Lever 1 of the memory plan as a single kernel: fuse the (quantized,
|
|
961
|
-
vocab-chunked) head matmul + `logsumexp` + target gather so the `[T, 262k]`
|
|
962
|
-
logits never touch memory; the backward reconstructs `softmax − onehot` from a
|
|
963
|
-
saved per-token LSE (Liger writes the grad in-place; CCE skips near-zero
|
|
964
|
-
softmax elements). Returns the per-token log-prob ORPO needs — **and the
|
|
965
|
-
ORPO odds-ratio gradient must be folded into this kernel**, since the grad is
|
|
966
|
-
produced inside the forward, not via autograd over materialized logits.
|
|
967
|
-
**No MLX project has this** (see landscape) — biggest differentiator.
|
|
968
|
-
4. **Fused SDPA backward (FA2-style).** Fills the Metal NYI gap above: store the
|
|
969
|
-
per-row `logsumexp` `L` in the forward, recompute `S=QKᵀ` / `P=exp(S−L)` in
|
|
970
|
-
the backward (5 matmuls, no stored L×L matrix) → O(L) attention-backward
|
|
971
|
-
memory instead of today's O(L²). High value because it removes the term
|
|
972
|
-
segmentation is currently compensating for. (FA3's extra tricks are
|
|
973
|
-
Hopper-specific; mirror FA2 on Metal.)
|
|
974
|
-
5. **Fused GeGLU/SwiGLU with a vjp.** (The custom fused-GeGLU/SwiGLU kernels
|
|
975
|
-
were **DELETED 2026-07-05** — compiled activations own the fusion win
|
|
976
|
-
bit-exactly; see
|
|
977
|
-
[unified-engine-frontier-plan.md §6](unified-engine-frontier-plan.md). Kept
|
|
978
|
-
here as the backward-math record.) The backward math is known (Liger): for
|
|
979
|
-
`c = act(a)⊙b`, save `a`,`b`, recompute the activation — `db = dc·act(a)`,
|
|
980
|
-
`da = dc·b·act'(a)`. This `.vjp` was written and validated before the
|
|
981
|
-
deletion; the memory angle still matters since the MLP intermediate is our
|
|
982
|
-
**dominant resident tensor** (the intra-layer split target, lever 4).
|
|
983
|
-
6. **Fused dequant-GEMM + LoRA epilogue (+ manual backward).** MLX's
|
|
984
|
-
`quantized_matmul` already fuses dequant into the *base* GEMM; the missing
|
|
985
|
-
piece is folding the LoRA epilogue `scale·(x@A)@B` into that kernel and a
|
|
986
|
-
hand-derived fused backward — `h=x@A`; `dB=s·hᵀ@dy`; `dA=s·xᵀ@(dy@Bᵀ)`;
|
|
987
|
-
`dx=dy@Wᵀ + s·(dy@Bᵀ)@Aᵀ` (Unsloth's `matmul_lora` is the blueprint; rank
|
|
988
|
-
r≪d so `h` is cheap to recompute). MLX's stock LoRA is **three separate ops**
|
|
989
|
-
+ generic autograd — no fused path exists in the ecosystem.
|
|
990
|
-
|
|
991
|
-
**Tier 2 — DAG-level passes the flat graph enables:**
|
|
992
|
-
7. **Recompute↔backward fusion** in the segmented path: the recompute-forward
|
|
993
|
-
and its per-segment vjp share one compiled fused region (the reason to do
|
|
994
|
-
this in a compiler vs eager `mlx_vjp`). Plus the classic passes over the
|
|
995
|
-
static DAG — TVM-style anchor pattern-fusion (matmul/SDPA absorbs an
|
|
996
|
-
elementwise epilogue; two anchors never chain), Inductor-style vertical +
|
|
997
|
-
horizontal fusion (fuse QKV projections, share one RoPE angle), an
|
|
998
|
-
XLA-style don't-duplicate-expensive-ops guard, and cross-layer CSE/global
|
|
999
|
-
value numbering (the unrolled N-layer graph repeats RoPE tables, masks,
|
|
1000
|
-
scale constants).
|
|
1001
|
-
8. **Min-cut rematerialization for segment boundaries.** AOTAutograd's
|
|
1002
|
-
`min_cut_rematerialization_partition` is the directly-portable algorithm:
|
|
1003
|
-
build a flow network (internal edge capacity = tensor byte-size), bias toward
|
|
1004
|
-
recomputing fusion-friendly ops, and `min_cut` picks save-vs-recompute — a
|
|
1005
|
-
formal replacement for hand-chosen `segment_size` that *is* fusion-aware.
|
|
1006
|
-
[Checkmate](https://arxiv.org/abs/1910.02653)'s MILP is the exact-budget
|
|
1007
|
-
upgrade when we must hit the 24 GB ceiling precisely.
|
|
1008
|
-
|
|
1009
|
-
### Competitive landscape (why this is a flagship)
|
|
1010
|
-
|
|
1011
|
-
An ecosystem survey found **no MLX preference trainer fuses or chunks the
|
|
1012
|
-
loss** — every one (`mlx-lm-lora`, `optiq`'s `dpo.py`, `mlx-tune`, SiLLM, the
|
|
1013
|
-
`mlx-examples` DPO gist) materializes the full `[B, T, V]` logits and calls
|
|
1014
|
-
stock `log_softmax`/`cross_entropy`. CCE and Liger are Triton/CUDA-only with **no
|
|
1015
|
-
Metal path**. Two openings stand out:
|
|
1016
|
-
|
|
1017
|
-
- **Correctness:** the current MLX SOTA, `mlx-lm-lora`, ships a **non-canonical
|
|
1018
|
-
ORPO** — it uses `log_sigmoid(chosen_logp − rejected_logp)` and **omits the
|
|
1019
|
-
additive SFT-NLL term** ORPO requires. Our loss (true log-odds + λ·NLL,
|
|
1020
|
-
length-normalized; one known scope divergence — see the note under "The
|
|
1021
|
-
objective") is *already* closer to the paper than anything shipping.
|
|
1022
|
-
**Do not copy their loss.**
|
|
1023
|
-
- **Efficiency:** a **Metal fused/chunked CE + fused LoRA** would be the first
|
|
1024
|
-
fused preference loss in the MLX world, on a quantized base, with grad
|
|
1025
|
-
checkpointing / segmented backward. Other gaps nobody fills: cached DPO
|
|
1026
|
-
reference log-probs, a concatenated/prefix-shared chosen+rejected forward.
|
|
1027
|
-
|
|
1028
|
-
These fused kernels are **no-oracle originals (Lab-gated)** — validated
|
|
1029
|
-
against the naïve path via the [oracle ladder](#parity), consistent with the
|
|
1030
|
-
fidelity model (external-oracle tiers L1/L2 plus the no-oracle Lab class).
|
|
1031
|
-
|
|
1032
|
-
## Model coverage, quantization, batching, dropout
|
|
1033
|
-
|
|
1034
|
-
Four cross-cutting requirements, each grounded in what the code already does.
|
|
1035
|
-
|
|
1036
|
-
### Mixed-precision quantized base
|
|
1037
|
-
|
|
1038
|
-
**Already handled in the layer stack — ORPO inherits it free.** Each
|
|
1039
|
-
[`QuantizedLinear`](../../src/model/gemma4-base.ts) carries its **own**
|
|
1040
|
-
`QuantSpec` (bits, group size) from `quantFor(config.quantization, path)`, and
|
|
1041
|
-
its `forward` is `quantizedMatmul(x, w, scales, biases, spec)` + the f32 LoRA
|
|
1042
|
-
residual. mlx's `quantized_matmul` has a working vjp **w.r.t. `x`** (the entire
|
|
1043
|
-
project trains LoRA on a 4-bit base today), and the base weights are frozen
|
|
1044
|
-
(grads flow only through the f32 A/B). "Mixed precision" just means a different
|
|
1045
|
-
`spec` per call — nothing special on the layer-stack backward.
|
|
1046
|
-
|
|
1047
|
-
Two things ORPO must get right for mixed precision:
|
|
1048
|
-
|
|
1049
|
-
1. **Vocab-chunked head over a quantized, possibly-mixed head.** The fused
|
|
1050
|
-
log-prob engine (lever 1) tiles the LM head along the vocab axis. The head is
|
|
1051
|
-
quantized in every model — tied `QuantizedEmbedding.asLinear`
|
|
1052
|
-
(Gemma, tied Qwen) or a separate `QuantizedLinear` `lmHead` (MiniCPM5,
|
|
1053
|
-
untied Qwen) — so a vocab chunk is a **row-slice of the quantized
|
|
1054
|
-
`w`/`scales`/`biases`** fed to `quantizedMatmul` per chunk. One spec covers
|
|
1055
|
-
the whole head, so chunking is a clean slice; this is the only quant-aware
|
|
1056
|
-
piece of new code.
|
|
1057
|
-
2. **Wire the `bitsMap` so `by_bits`/`by_kl` actually fire. ✅ DONE.** This is
|
|
1058
|
-
optiq's mixed-precision mechanism, confirmed from its source: a calibration
|
|
1059
|
-
**sensitivity pass** measures each layer's KL divergence when quantized
|
|
1060
|
-
(`optiq/core/sensitivity.py`), the most-sensitive layers are kept at 8-bit,
|
|
1061
|
-
and **the same KL signal also sets the LoRA rank** — `by_bits`:
|
|
1062
|
-
`rank·(bits/4)` (4-bit→r8, 8-bit→r16), `by_kl`: `rank·clip(kl/median,0.5,2)`.
|
|
1063
|
-
optiq's words: *"the same layers kept at 8-bit also get more adapter rank —
|
|
1064
|
-
one signal, two optimizations."* It is **rank scaling, not LR/loss/gradient
|
|
1065
|
-
weighting** — "more signal to the 8-bit layers" = more adapter capacity
|
|
1066
|
-
there. Our [`rank.ts`](../../src/train/rank.ts) was already a faithful port of
|
|
1067
|
-
optiq's `sensitivity_rank.py`, but the trainer never supplied the maps, so it
|
|
1068
|
-
silently went uniform. Fixed: `bitsMapFromModel` reads each target's
|
|
1069
|
-
`linear.spec.bits` (authoritative, always present), `readPerLayerKl` reads
|
|
1070
|
-
`optiq_metadata.json` for `by_kl`; the trainer builds both and passes them to
|
|
1071
|
-
`resolveRanks`, and emits the resulting rank spread. Benefits SFT/DPO too;
|
|
1072
|
-
ORPO inherits it. (Unit-tested weight-free in
|
|
1073
|
-
[`tests/train-rank.test.ts`](../../tests/train-rank.test.ts).)
|
|
1074
|
-
|
|
1075
|
-
### Per-model specialized loops vs the monolith
|
|
1076
|
-
|
|
1077
|
-
**Preference: a per-model loop-unrolled, branch-deleted training path, with a
|
|
1078
|
-
monolithic fallback** — and the codebase already has this exact pattern for the
|
|
1079
|
-
forward path. [`scripts/gen-model.ts`](../../scripts/gen-model.ts) generates
|
|
1080
|
-
specialized files in [`src/model/generated/`](../../src/model/generated/)
|
|
1081
|
-
(`gemma4-12b/e4b/26b`) that unroll the per-layer loop, bake per-layer constants
|
|
1082
|
-
(cache index, sliding/full window, **per-layer quant bits/group size**), and
|
|
1083
|
-
delete `instanceof`/conditional branches; [`factory.ts`](../../src/model/factory.ts)
|
|
1084
|
-
dispatches `GENERATED.get(configFingerprint(config)) ?? Gemma4Model` — generated
|
|
1085
|
-
if the fingerprint matches, **monolith otherwise**.
|
|
1086
|
-
|
|
1087
|
-
Plan, mirroring that structure for training:
|
|
1088
|
-
|
|
1089
|
-
- **Monolith first.** The generic `SegmentedBackward` (MiniCPM5) /
|
|
1090
|
-
`SegmentedBackwardGemma4` call stock `runLayerRange` — they cover Gemma + CPM5
|
|
1091
|
-
today. Qwen has no segmented backward yet (needs `SSMCache` + a gated-delta
|
|
1092
|
-
custom-kernel vjp), so **Qwen ORPO lands first on the full (un-segmented)
|
|
1093
|
-
monolithic backward**; segmented Qwen is a follow-on.
|
|
1094
|
-
- **Then generate the specialized segment-forward** per model (Qwen / Gemma /
|
|
1095
|
-
CPM5) — the unrolled, branch-deleted function the per-segment vjp calls —
|
|
1096
|
-
emitted by extending `gen-model.ts`, fingerprint-dispatched, with the monolith
|
|
1097
|
-
as the miss fallback. Same proven mechanism, applied to the training segment
|
|
1098
|
-
forward instead of the decode forward. (The `mx.compile` flat-DAG path in
|
|
1099
|
-
[`compiled-decode.ts`](../../src/model/compiled-decode.ts) is the other
|
|
1100
|
-
specialization lever, for reference.)
|
|
1101
|
-
|
|
1102
|
-
All three target families exist: [`gemma4.ts`](../../src/model/gemma4.ts),
|
|
1103
|
-
[`minicpm5.ts`](../../src/model/minicpm5.ts),
|
|
1104
|
-
[`qwen3_5.ts`](../../src/model/qwen3_5.ts) (gated-delta hybrid).
|
|
1105
|
-
|
|
1106
|
-
### Batching
|
|
1107
|
-
|
|
1108
|
-
DPO batching already exists — [`iterateDpoBatches`](../../src/train/dataset.ts)
|
|
1109
|
-
B>1 pads each branch to its own batch-max with response masks, and
|
|
1110
|
-
[`forward.ts`](../../src/train/forward.ts) has the padding-aware `[B,1,L,L]`
|
|
1111
|
-
batched attention mask. ORPO reuses all of it: the **chunked fused log-prob
|
|
1112
|
-
engine tiles over the `(B · response-token)` axis**, so B>1 falls out, with the
|
|
1113
|
-
length-normalization done per row.
|
|
1114
|
-
|
|
1115
|
-
The real constraint is the **regime matrix** — the memory-optimized paths
|
|
1116
|
-
(segmented backward, response-only head) are **B=1 only** today:
|
|
1117
|
-
|
|
1118
|
-
| Regime | Path | Use when |
|
|
1119
|
-
|---|---|---|
|
|
1120
|
-
| **B>1 monolithic** | batched padded forward + chunked loss, no segmentation | short context, headroom for B rows |
|
|
1121
|
-
| **B=1 segmented** | segmented/intra-layer backward, response-only/chunked head | long context (the memory-bound case) |
|
|
1122
|
-
|
|
1123
|
-
At long context the intended lever is **`grad_accumulation_steps`** (raise
|
|
1124
|
-
effective batch with no extra peak) rather than true B>1. ✅ **Wired** (all three
|
|
1125
|
-
loops, sft/dpo/orpo): the shared `accumulateStep` helper in
|
|
1126
|
-
[`trainer.ts`](../../src/train/trainer.ts) runs `grad_accumulation_steps`
|
|
1127
|
-
micro-batches per optimizer step and mean-accumulates their grads (each scaled by
|
|
1128
|
-
`1/N`) before a single `opt.step`. Each micro-batch's grads (and the running
|
|
1129
|
-
accumulator) are eval'd before the next micro-batch's forward builds, so only one
|
|
1130
|
-
micro-batch's activations are ever live — **peak memory does not grow with `N`**
|
|
1131
|
-
(measured ~1.02× at N=3 for both SFT and ORPO on MiniCPM5; composes with the
|
|
1132
|
-
segmented and gradient-checkpoint paths). `N=1` is a byte-for-byte pass-through.
|
|
1133
|
-
The accumulation math is proven deterministically in
|
|
1134
|
-
[`tests/train-gradaccum.test.ts`](../../tests/train-gradaccum.test.ts) and
|
|
1135
|
-
demonstrated end-to-end in
|
|
1136
|
-
[`scripts/experiments/parity-gradaccum.ts`](../../scripts/experiments/parity-gradaccum.ts).
|
|
1137
|
-
True batched segmented backward is a larger effort (pad-aware boundaries); not
|
|
1138
|
-
required for a first landing.
|
|
1139
|
-
|
|
1140
|
-
### Dropout
|
|
1141
|
-
|
|
1142
|
-
**Currently a dangling stub:** the `/finetune` UI has an `f-dropout` field that
|
|
1143
|
-
is sent in the submit payload, but it is **not** in `FinetuneSubmit` / the
|
|
1144
|
-
trainer, and the saved config hardcodes `lora_dropout: 0.0`
|
|
1145
|
-
([`lora-params.ts`](../../src/train/lora-params.ts)). Wire it:
|
|
1146
|
-
`lora_dropout` → `FinetuneSubmit` → `TrainConfig` → a **training-only** dropout
|
|
1147
|
-
on the LoRA input in the adapter forward (PEFT applies dropout to `x` before
|
|
1148
|
-
`A`). Default `0.0`; Labonne's ORPO-LoRA uses `0.05`.
|
|
1149
|
-
|
|
1150
|
-
- **ORPO note:** TRL sets `disable_dropout=True` on the *base* model so
|
|
1151
|
-
chosen/rejected log-probs are deterministic — our quantized base has no
|
|
1152
|
-
dropout modules, so only the **LoRA-adapter dropout** is in play. Keep it.
|
|
1153
|
-
- **Correctness constraint that must be flagged:** our segmented backward and
|
|
1154
|
-
gradient checkpointing **recompute the forward inside the vjp**. If dropout
|
|
1155
|
-
draws a fresh mask on the recompute, the gradient is **wrong**. So when
|
|
1156
|
-
dropout > 0, the dropout mask must be **deterministic per step** — draw an RNG
|
|
1157
|
-
key once per step and **replay the same key** in every recompute (forward and
|
|
1158
|
-
each per-segment vjp). Default `0.0` sidesteps this entirely; enabling dropout
|
|
1159
|
-
requires the key-replay plumbing. This also interacts with the
|
|
1160
|
-
chunked-loss recompute. Note it in the implementation.
|
|
1161
|
-
|
|
1162
|
-
## Experiment: before/after ORPO on Gemma e4b (IFEval)
|
|
1163
|
-
|
|
1164
|
-
The end-goal quality experiment — does ORPO *meaningfully improve* the model?
|
|
1165
|
-
Train on **UltraFeedback** preference pairs, measure **IFEval**
|
|
1166
|
-
(instruction-following) before vs after. IFEval is the primary metric because
|
|
1167
|
-
its checks are **programmatically verifiable** — no LLM judge, fully
|
|
1168
|
-
reproducible (AlpacaEval/MT-Bench would need a GPT-4-class judge; a follow-on).
|
|
1169
|
-
|
|
1170
|
-
Pieces built (all unit-tested, judge-free):
|
|
1171
|
-
- [`src/eval/ifeval.ts`](../../src/eval/ifeval.ts) — verifiable-instruction
|
|
1172
|
-
scorer (keywords / length / format / case / start-end / punctuation; prompt-
|
|
1173
|
-
and instruction-level accuracy). [`tests/ifeval.test.ts`](../../tests/ifeval.test.ts).
|
|
1174
|
-
- [`src/eval/ultrafeedback.ts`](../../src/eval/ultrafeedback.ts) +
|
|
1175
|
-
[`scripts/curate-ultrafeedback.ts`](../../scripts/curate-ultrafeedback.ts) —
|
|
1176
|
-
binarized UltraFeedback → `{prompt, chosen, rejected}` with length filtering.
|
|
1177
|
-
[`tests/ultrafeedback.test.ts`](../../tests/ultrafeedback.test.ts).
|
|
1178
|
-
- [`scripts/run-ifeval.ts`](../../scripts/run-ifeval.ts) — generate + score, with
|
|
1179
|
-
or without an adapter (the before/after runner).
|
|
1180
|
-
- [`scripts/bench-orpo.ts`](../../scripts/bench-orpo.ts) — track step time/peak.
|
|
1181
|
-
|
|
1182
|
-
Runbook (the generate/train steps are yours to run — they're long / download):
|
|
1183
|
-
1. **Curate:** export `HuggingFaceH4/ultrafeedback_binarized` to JSONL, then
|
|
1184
|
-
`bun scripts/curate-ultrafeedback.ts uf.jsonl data/uf-pref 2048`.
|
|
1185
|
-
2. **Baseline:** `MODEL=<e4b> bun scripts/run-ifeval.ts ifeval.jsonl` → record.
|
|
1186
|
-
3. **Fit check:** `MODEL=<e4b> CHUNK=512 bun scripts/bench-orpo.ts` to confirm
|
|
1187
|
-
the config fits 24 GB (e4b's 262k vocab makes the chunked head matter).
|
|
1188
|
-
4. **Train ORPO** (no env-flag sanitization needed — the no-vjp kernels were
|
|
1189
|
-
deleted 2026-07-05): `method:"orpo"`,
|
|
1190
|
-
`learning_rate 1e-5`, `orpo_lambda 0.1`, `orpo_chunk_size 512`,
|
|
1191
|
-
`max_seq_length ~1024`, `rank 16`. ⚠️ Segmented backward for ORPO is
|
|
1192
|
-
**MiniCPM5-only** today (`SegmentedBackwardOrpoGemma4` is a follow-on), so
|
|
1193
|
-
e4b ORPO runs the non-segmented path — rely on the **chunked head** + a modest
|
|
1194
|
-
`max_seq_length` to fit; longer context needs the Gemma segmented port.
|
|
1195
|
-
5. **After:** `MODEL=<e4b> bun scripts/run-ifeval.ts ifeval.jsonl --adapter <out>`
|
|
1196
|
-
→ compare prompt/instruction accuracy to the baseline.
|
|
1197
|
-
|
|
1198
|
-
The curated set should bias toward instruction-following pairs so the IFEval
|
|
1199
|
-
signal moves (UltraFeedback is broad; a topic/length filter focused on
|
|
1200
|
-
formatting/constraint-following prompts sharpens the before/after delta).
|
|
1201
|
-
|
|
1202
|
-
**Prompt masking (verified):** the NLL / odds-ratio is computed on the
|
|
1203
|
-
**completion only** — `respMask` zeroes the prompt and the loss sums only
|
|
1204
|
-
`mask[t+1]==1` positions. This holds even for a huge prompt + tiny completion
|
|
1205
|
-
(e.g. a 13 KB policy/transcript prompt + a ~100-char JSON answer): left-
|
|
1206
|
-
truncation preserves the boundary (`promptLen = maxLen − completionLen`). The
|
|
1207
|
-
*only* way the prompt leaks in is a completion that is itself ≥ `max_seq_length`
|
|
1208
|
-
— `loadDpoDataset` warns loudly if that happens. Regression-tested in
|
|
1209
|
-
[`tests/dpo-masking.test.ts`](../../tests/dpo-masking.test.ts).
|
|
1210
|
-
|
|
1211
|
-
**Small sets over-fit fast** (~450 pairs): keep `orpo_lambda` modest (default
|
|
1212
|
-
`0.1`), carve a ~50-pair `valid.jsonl` (curate with `valFrac ≈ 0.1`), and watch
|
|
1213
|
-
the **val margin** (mean chosen−rejected log-odds) — it's emitted per
|
|
1214
|
-
`steps_per_eval` and persisted to `metrics.json` as `bestMargin` (a better
|
|
1215
|
-
checkpoint signal than val loss). `save_checkpoints: true` keeps every eval
|
|
1216
|
-
checkpoint so you can mount the best-margin one.
|
|
1217
|
-
|
|
1218
|
-
## Resolved decisions
|
|
1219
|
-
|
|
1220
|
-
- **LR / λ defaults** — `learning_rate=1e-5`, `orpo_lambda=0.1`, cosine
|
|
1221
|
-
schedule, warmup ≈3% of iters (see Recommended defaults). Revisit after a
|
|
1222
|
-
real run.
|
|
1223
|
-
- **Schedule knobs** — **method-prefixed**: `orpo_*` for ORPO, `dpo_*` for DPO
|
|
1224
|
-
(no shared fields).
|
|
1225
|
-
- **Format naming** — rename the `dpo` dataset format to `preference` with
|
|
1226
|
-
`dpo` kept as an alias, now that two methods consume it.
|
|
1227
|
-
- **Mixed-precision quant** — inherited free in the layer stack (per-linear
|
|
1228
|
-
`QuantSpec` + working `quantized_matmul` vjp). New quant-aware code is limited
|
|
1229
|
-
to the vocab-chunked head. Also wire `bitsMap` from `linear.spec.bits` so
|
|
1230
|
-
`by_bits` rank scaling fires (fixes a known silent fallback; helps SFT/DPO).
|
|
1231
|
-
- **Per-model paths** — monolith first (Gemma + CPM5 segmented; Qwen full
|
|
1232
|
-
backward), then generated per-model unrolled segment-forwards via
|
|
1233
|
-
`gen-model.ts`, fingerprint-dispatched with the monolith as fallback.
|
|
1234
|
-
- **Batching** — reuse DPO batching; chunked loss makes B>1 fall out.
|
|
1235
|
-
Memory-optimized paths stay B=1; `grad_accumulation_steps` is the intended
|
|
1236
|
-
long-context lever but is **not yet consumed by any loop** (tracked follow-on).
|
|
1237
|
-
True batched segmented backward is a follow-on.
|
|
1238
|
-
- **Dropout** — wire the existing UI stub through to a training-only LoRA-input
|
|
1239
|
-
dropout (default `0.0`). If > 0, the dropout key must be drawn per step and
|
|
1240
|
-
**replayed** in every recompute (segmented/checkpoint/chunked) or gradients
|
|
1241
|
-
diverge.
|
|
1242
|
-
- **Regularization (open — small sweep)** — rank is the primary regularizer;
|
|
1243
|
-
adopt **rsLoRA** (`α/√r`) so `rank_scaling` regularizes capacity, not step
|
|
1244
|
-
size; consider **LoRA+** (higher LR for B, which does the early learning under
|
|
1245
|
-
zero-init); default **weight decay lower than SFT** (the SFT-NLL term already
|
|
1246
|
-
anchors); cosine + warmup. See [Regularization & optimizer geometry](#regularization--optimizer-geometry).
|
|
175
|
+
[Labonne](https://huggingface.co/blog/mlabonne/orpo-llama-3).
|
|
176
|
+
|
|
177
|
+
**Regularization, ordered by leverage** (ORPO is ~1 epoch on small sets, so
|
|
178
|
+
overfitting and step geometry matter more than capacity):
|
|
179
|
+
|
|
180
|
+
- **Rank is the primary regularizer**; `rank_scaling` (`by_bits`/`by_kl`) is how
|
|
181
|
+
capacity is distributed. `bitsMapFromModel` (reads `linear.spec.bits`) and
|
|
182
|
+
`readPerLayerKl` (`optiq_metadata.json`) feed `resolveRanks` — before this was
|
|
183
|
+
wired the policy silently went uniform. `by_bits`: `rank·(bits/4)`; `by_kl`:
|
|
184
|
+
`rank·clip(kl/median, 0.5, 2)` — optiq's "one signal, two optimizations"
|
|
185
|
+
(`tests/train-rank.test.ts`).
|
|
186
|
+
- **Rank ↔ scale ↔ init.** Update is `α·BA` with B zero-init, A uniform(±1/√in).
|
|
187
|
+
At step 0 `dA ∝ dy·Bᵀ = 0`: B does all the early learning → **LoRA+**
|
|
188
|
+
(`lora_plus_ratio`, higher LR on B leaves) helps. A fixed α makes the step
|
|
189
|
+
size scale with per-layer rank, defeating rank scaling → **rsLoRA**
|
|
190
|
+
(`rs_lora`, α/√r; saved into the adapter config so inference applies the same
|
|
191
|
+
per-layer scale).
|
|
192
|
+
- **Weight decay** on A and B independently is a nuclear-norm-style penalty on
|
|
193
|
+
`ΔW`, not Frobenius; the SFT-NLL term already anchors, so default lower than
|
|
194
|
+
SFT's 0.01 and tune.
|
|
195
|
+
- **LoRA-input dropout** (`lora_dropout`, PEFT-style, default 0, ~0.05 for small
|
|
196
|
+
sets). The base has no dropout modules (TRL's `disable_dropout` is moot).
|
|
197
|
+
Recompute determinism is mandatory: mask keyed by (step, layer) so segmented
|
|
198
|
+
/ checkpoint / chunked recompute reproduces it (`tests/train-regularization*.test.ts`).
|
|
199
|
+
|
|
200
|
+
The remaining work here is empirical: sweep `rs_lora` / `lora_plus_ratio` /
|
|
201
|
+
`lora_dropout` / `weight_decay` on a real run and judge by held-out val margin
|
|
202
|
+
(`bestMargin` in `metrics.json`).
|
|
203
|
+
|
|
204
|
+
## 5. Memory and speed
|
|
205
|
+
|
|
206
|
+
Two terms dominate at long context, and neither is the LoRA grads: (1) the LM
|
|
207
|
+
head logits `[1,T,V]` + gradient (~4.3/8.6 GB bf16 at T=8192, V=262k), doubled
|
|
208
|
+
by ORPO's two branches; (2) per-layer activations held for the backward. The
|
|
209
|
+
ordering principle for Apple Silicon: **only reducing total live footprint
|
|
210
|
+
moves the constraint — relocation does not.** CPU and GPU share one pool, so
|
|
211
|
+
activation offloading frees zero bytes, risks double residency, and burns the
|
|
212
|
+
scarce bandwidth. We do not implement it.
|
|
213
|
+
|
|
214
|
+
### 5.1 The head: from token-chunking to the flash-CCE kernel
|
|
215
|
+
|
|
216
|
+
CE and the per-token target logp ORPO needs are the same quantity
|
|
217
|
+
(`logp_t = h_t·W_{y_t} − logsumexp_v(h_t·W_v)`), so one memory-efficient engine
|
|
218
|
+
serves the NLL and the odds-ratio terms. Four tiers were built; each is
|
|
219
|
+
validated against the one below.
|
|
220
|
+
|
|
221
|
+
1. **Token-chunked Checkpoint head** (`chunkedLogpMeanB1`, `orpoChunkSize`).
|
|
222
|
+
Each `[chunk,V]` head is wrapped in a `Checkpoint` and recomputed in the
|
|
223
|
+
backward. A Checkpoint *drops* its input activations, so the hidden slices
|
|
224
|
+
it reads must live in the `sink` until grads are eval'd (found during
|
|
225
|
+
bring-up). Forward parity within bf16 (single-ULP head-matmul tiling
|
|
226
|
+
differences), exact for one chunk (`tests/train-orpo-chunked.test.ts`).
|
|
227
|
+
2. **Fused linear-CE head** (`fusedLogpMeanB1`, `orpo_fused_ce`; Liger
|
|
228
|
+
`FusedLinearCrossEntropy` on MLX). One `CustomVjp`: forward materializes
|
|
229
|
+
`[chunk,V]` per chunk (`quantizedMatmul(h, W, transpose=true)` + Gemma
|
|
230
|
+
`logitSoftcap`), reduces to `target − lse`, drops the logits; the analytic
|
|
231
|
+
backward recomputes and forms `g·(onehot − softmax)·sech²` directly, then
|
|
232
|
+
`dh = quantizedMatmul(grad, W, transpose=false)` — mlx's own x-vjp,
|
|
233
|
+
contracting vocab without dequantizing the head. Value **bit-exact** vs full
|
|
234
|
+
logits on MiniCPM5 and e4b (softcap=30 matched by reusing `logitSoftcap`'s
|
|
235
|
+
div-by-bf16-cap formula); grads 0.51% / 1.97% bf16-class
|
|
236
|
+
(`tests/train-orpo-fused-ce.test.ts`). The head is frozen (not a LoRA
|
|
237
|
+
target), so only `dh` flows back.
|
|
238
|
+
3. **Vocab-blocked online-softmax in pure MLX** — implemented, numerically
|
|
239
|
+
correct, **does not bound memory**, gated off. The `(m,d)` recurrence and
|
|
240
|
+
the cross-block `dh` accumulation must be f32 (bf16 gave 17→45% grad error
|
|
241
|
+
scaling with block count). But the online softmax is a sequentially
|
|
242
|
+
dependent chain and MLX's lazy eval retains the whole chain: head-isolated
|
|
243
|
+
peak went 3.4 GB → 14.1 GB at 512-wide blocks (MiniCPM5 M=2048, 2026-06-18).
|
|
244
|
+
Forcing an eval mid-chain breaks autodiff. Conclusion: the CCE residency
|
|
245
|
+
bound needs the vocab loop INSIDE a kernel.
|
|
246
|
+
4. **flash-CCE Metal kernel** (`flash-cce.ts`, `orpo_flash_ce`) — Apple
|
|
247
|
+
Cut-Cross-Entropy + Liger transpiled Triton → Metal with three divergences:
|
|
248
|
+
the classifier is the **quantized** head (in-Metal 4/8-bit affine dequant,
|
|
249
|
+
mlx `quantized.h` qdot pattern); only `dE`/`dh` is needed (frozen head, no
|
|
250
|
+
`dC`); the cross-block LSE merge is cheap MLX ops on per-block partials, not
|
|
251
|
+
atomic logaddexp. Evolution, all 2026-06-18/19 on the M1 Max 32 GB dev box:
|
|
252
|
+
- v1 (one threadgroup per token, whole vocab serially): memory bound
|
|
253
|
+
achieved (MiniCPM5 M=8192 fwd 2.45 → 0.34 GB; e4b M=512 bwd 2.06 → 1.17
|
|
254
|
+
GB) but e4b ≥1024 tripped the GPU watchdog.
|
|
255
|
+
- Vocab-parallel grid `[TG, nVocabBlocks, M]`, `VOCAB_BLOCK=8192`; backward
|
|
256
|
+
atomic-adds partial `dh` into `[M,H]`. e4b M=1024 deterministic, logp
|
|
257
|
+
0.20% / dh 0.40%, bwd peak 3.41 → 1.46 GB.
|
|
258
|
+
- `BLOCK_B` token tiling (dequant a vocab row once, reuse across 8 tokens
|
|
259
|
+
fwd / 4 bwd — the backward is register-bound by `dacc`): 2.5× total.
|
|
260
|
+
- **steel GEMM** (`steel-qmm-header.ts`, MLX's verbatim `BlockMMA`) for
|
|
261
|
+
forward and backward, with a temp-BlockMMA-per-H-tile + lane-local
|
|
262
|
+
fragment accumulation: e4b bwd **3687 → 754 ms**, fwd 180 ms, peak
|
|
263
|
+
**0.93 GB flat @ M=8192**; dh 0.40% e4b / 0.28% cpm.
|
|
264
|
+
`MLX_BUN_CCE_NOSTEEL` / `MLX_BUN_CCE_BWD_NOSTEEL` fall back to the
|
|
265
|
+
simdgroup / scalar kernels (also the non-32-divisible-shape fallback).
|
|
266
|
+
- Apple-CCE backward skips: coeff filter (`MLX_BUN_CCE_BWD_FILTER_EPS`) and
|
|
267
|
+
blockMax skip (`MLX_BUN_CCE_BWD_BLOCK_EPS`), **both default `1e-5` since
|
|
268
|
+
2026-07-02** (`=0` compiles them out). Real-data teacher-forced
|
|
269
|
+
measurement (M1 Max, chunk-ORPO data): filter 0.343%/1.41× (CPM5),
|
|
270
|
+
0.158%/1.70× (e4b); block skip ≤0.004% dh; combined **1.71× / 3.16×**
|
|
271
|
+
backward vs exact. Run-to-run dh is not byte-stable at any eps (atomic-add
|
|
272
|
+
reassociation). Note: docs/reference/training.md still describes the skips as
|
|
273
|
+
off by default; `flash-cce.ts` is the truth.
|
|
274
|
+
- Operand precision today is **W4A32**: 4-bit weights dequantized into fp32
|
|
275
|
+
tiles and multiplied with fp32 simdgroup matrices; the `dh` accumulator is
|
|
276
|
+
already fp32. See §8.1 (W4A16 spike).
|
|
277
|
+
|
|
278
|
+
Composition finding: the fused `CustomVjp` **does not bound memory when nested
|
|
279
|
+
inside the segmented `mlx_vjp` head** (its per-chunk graph is not freed
|
|
280
|
+
incrementally there; +0.8–1.1 GB at L=3072–4096 on MiniCPM5), so the segmented
|
|
281
|
+
bounded head uses the Checkpoint variant `chunkedLogpMeanFromHidden` (bit-exact
|
|
282
|
+
recompute). And bounding the head did not reduce the segmented peak at any
|
|
283
|
+
reachable scale (MiniCPM5 L=2048–4096 × SEG 2/6; e4b L=2048: 11.95 GB both) —
|
|
284
|
+
the head `[M,V]` is transient and never stacks with the per-segment backward
|
|
285
|
+
peak. The trainer still routes flash/fused into the segmented head so e4b never
|
|
286
|
+
reintroduces `[M,V]`.
|
|
287
|
+
|
|
288
|
+
Host-buffer lesson (2026-06-21): `u32()` kernel-arg arrays built with the
|
|
289
|
+
zero-copy `fromView` and disposed before the lazy kernel evaluated leaked pins
|
|
290
|
+
(+32/step) → use-after-free that ran clean on the 32 GB M1 Max and crashed on
|
|
291
|
+
the 24 GB M4 Pro. Fix: `fromBytesCopy`; `MetalKernel.apply` `ptr()` lifetimes
|
|
292
|
+
hardened; a `pinned` canary on the train metric. `fromView` is only for
|
|
293
|
+
process-lifetime memory. A related UAF in the segmented ORPO classes (the flash
|
|
294
|
+
head's `headSink` freed before the lazy backward read `lse`/`blockMax`) is fixed
|
|
295
|
+
by `ops.evalAll` on the head-VJP roots before dispose.
|
|
296
|
+
|
|
297
|
+
### 5.2 Attention backward: `ops.sdpa` vs the flash kernel
|
|
298
|
+
|
|
299
|
+
Default training attention is `ops.sdpa` (`mx.fast.scaled_dot_product_attention`)
|
|
300
|
+
— exact (dQ/dK/dV FD-verified 0.00%) and fast, but its Metal backward is NYI
|
|
301
|
+
(`ScaledDotProductAttentionVJP::use_fallback()` returns true), so MLX
|
|
302
|
+
decomposes it and **materializes the L×L scores for every layer**: O(L²)
|
|
303
|
+
backward memory (0.02 → 0.33 → 1.40 GB at T=512/2048/4096 in the isolated
|
|
304
|
+
attention-backward bench; ~3.5 GB/layer @8K on e4b). The hand-rolled flash
|
|
305
|
+
kernel (`src/model/flash-attention.ts`, `MLX_BUN_TRAIN_ATTN=flash`) is O(L)
|
|
306
|
+
(0.03 → 0.23 → 0.90 GB) but ~30× slower — a naive scalar kernel at ~0.5% of GPU
|
|
307
|
+
peak. Two genuine port bugs were fixed rather than deleting it (a spurious dK
|
|
308
|
+
transpose for Tkv≠D; a divergent `threadgroup_barrier` in the dQ causal
|
|
309
|
+
tile-skip), restoring L2 parity with optiq. It remains the dormant long-context
|
|
310
|
+
fallback for MiniCPM5; the trainer refuses it for Gemma.
|
|
311
|
+
|
|
312
|
+
The sliding-window layers are NOT cheaper in the backward — the window is only
|
|
313
|
+
an additive mask on the materialized scores (measured 2026-07-02: a sliding
|
|
314
|
+
pair +7.1 GB ≈ sliding+full +7.15 GB @8K). This falsified the earlier
|
|
315
|
+
full-attention-isolation planner premise (§5.3).
|
|
316
|
+
|
|
317
|
+
### 5.3 Segmented backward
|
|
318
|
+
|
|
319
|
+
Naive gradient checkpointing (ours and Python mlx's) does not stream: the
|
|
320
|
+
checkpointed backward holds every layer's recompute activations at once, so
|
|
321
|
+
e4b spiked to 23 GB live @2048 (resting 6.95 GB) and crashed at 4096
|
|
322
|
+
(`peak(L) ≈ 7.7 GB + 7.6 MB/token`). Measurement note that unlocked this:
|
|
323
|
+
`mlx_get_peak_memory` is peak **live**, not live+cache; use
|
|
324
|
+
`activeMemory()`/`cacheMemory()`/`clearCache()` to separate them, and eval the
|
|
325
|
+
loss and grads together (`evalAll([value, ...grads])`) — evaluating `value`
|
|
326
|
+
separately frees the forward and forces the backward to recompute-and-hold
|
|
327
|
+
everything.
|
|
328
|
+
|
|
329
|
+
**Mechanism** (`SegmentedBackward*`, `planSegmentsBySize`):
|
|
330
|
+
|
|
331
|
+
1. Forward, saving boundaries: run the layer stack; at each segment edge
|
|
332
|
+
(`segmentSize` layers) eval and **detach** the residual hidden into a leaf
|
|
333
|
+
(`detachLeaf` = `ops.contiguous` + `detachCopy`; `detachLeaves` batches the
|
|
334
|
+
eval barrier). mlx `eval` does not detach — without the copy each boundary
|
|
335
|
+
drags a layer-stack of activations.
|
|
336
|
+
2. Head: `finalNorm` + LM head + loss over the last boundary as a `Vjp` with
|
|
337
|
+
scalar cotangent 1.0 → loss value and `dh_out`.
|
|
338
|
+
3. Backward, reverse over segments: `vjp.apply([boundary_k, ...LoRA_k], [dh_out])`
|
|
339
|
+
→ `[dh_in, ...dLoRA_k]`; `dh_in` seeds segment k−1; LoRA grads scatter into
|
|
340
|
+
the flat grad vector. Only one segment's activations are live. Recompute
|
|
341
|
+
overhead ≈ 1× forward regardless of segment count, so `segmentSize` is a
|
|
342
|
+
peak knob at ~fixed time.
|
|
343
|
+
|
|
344
|
+
**The `mlx_vjp` finding.** The first implementation used optiq's
|
|
345
|
+
surrogate-loss trick — `value_and_grad` of
|
|
346
|
+
`sum(stop_grad(dh) ⊙ segment_forward(...))` per segment. Numerically identical,
|
|
347
|
+
but it **leaked ~32 MB of live memory per segment per step** (linear, no
|
|
348
|
+
plateau; 1.12 → 6.61 GB over 30 iters at SEG=4/2048), unreclaimable by
|
|
349
|
+
`clearCache`/GC/`synchronize`/disposal. A minimal repro pinned it to
|
|
350
|
+
`value_and_grad` (`MODE=layers` leaks ~6 MB/iter; `MODE=vjp` flat). Driving
|
|
351
|
+
each segment with **`mlx_vjp`** (`Vjp` in `src/mlx/autograd.ts`) is the
|
|
352
|
+
production path: active flat (1.04 GB × 12 iters end-to-end at SEQ=4096). Vjp
|
|
353
|
+
objects are built once per run and reused; rebuilding per step leaks.
|
|
354
|
+
|
|
355
|
+
**Numbers.** MiniCPM5-1B, SEG=4, M1 Max 32 GB, 2026-06-16, peak live:
|
|
356
|
+
|
|
357
|
+
| seq | full backward | segmented | note |
|
|
358
|
+
|---|---|---|---|
|
|
359
|
+
| 512 | 3.15 GB | 1.87 GB | |
|
|
360
|
+
| 2048 | 10.91 GB | **3.29 GB** | post-vjp fix (3.44 pre) |
|
|
361
|
+
| 4096 | 21–26 GB (spikes) | 6–8 GB | |
|
|
362
|
+
|
|
363
|
+
Under flash attention the segmented grads bit-match the full `value_and_grad`
|
|
364
|
+
(relNorm 0.0000%, maxAbs 0.0). Under `ops.sdpa` they differ ~6% relNorm — not a
|
|
365
|
+
segmentation bug: mlx's fused sdpa gives a different bf16 result in its eager
|
|
366
|
+
forward (12.0712) than its autograd-decomposed forward (12.0568), so boundaries
|
|
367
|
+
saved eagerly and recomputed under autograd disagree. The non-segmented trainer
|
|
368
|
+
already trained against the autograd forward while inference used the eager
|
|
369
|
+
one. A real 300-iter run (SEG=4, SEQ=4096) peaked 6.51 GB (non-seg 25.47 GB),
|
|
370
|
+
no leak, and its adapter scored 95.10 on the chunk holdout vs 91.70
|
|
371
|
+
non-segmented.
|
|
372
|
+
|
|
373
|
+
**e4b (Gemma4) additions** (`SegmentedBackwardGemma4`, `Gemma4Model.runLayerRange`
|
|
374
|
+
/ `makeTrainingMasks` / `embedForSegmented` / `reusedDonors`):
|
|
375
|
+
|
|
376
|
+
- Per-layer-input is a pure constant boundary (its grad would flow to the
|
|
377
|
+
embed / `per_layer_model_projection`, neither a LoRA target): detached once,
|
|
378
|
+
sliced per segment, cotangent discarded.
|
|
379
|
+
- KV-shared donor K/V (donors 22 sliding / 23 full feed all sharers in
|
|
380
|
+
[24,42)) is a **second boundary stream with its own cotangent**: sharer
|
|
381
|
+
segments differentiate `[h, donorK, donorV, ...LoRA]` and accumulate the
|
|
382
|
+
donor vjps into `dKV[d]`; the donor segment's multi-output forward
|
|
383
|
+
`[h_out, k, v]` takes `[dh, dKV.k, dKV.v]`. Two contiguity fixes were
|
|
384
|
+
essential: `detachLeaf` must force row-major (donor K/V are transposed
|
|
385
|
+
views) and the producer must output contiguous donor K/V.
|
|
386
|
+
- Grads: single-consumer donor reuse is bit-exact (2-segment split at 24: 0/258
|
|
387
|
+
off); the natural 7-segment cut is ~0.97% relNorm from bf16
|
|
388
|
+
non-associativity of the within-vjp cotangent sums — flat in consumer count,
|
|
389
|
+
tracks summand count (donor 22 ≈0.46%, donor 23 ≈0.000%), not helped by fp32
|
|
390
|
+
cross-segment accumulation. **The lever is the segment cut, not the dtype**:
|
|
391
|
+
keep a donor's sharers in one consumer segment for tighter grads at higher
|
|
392
|
+
peak. Vs the `mlx_lm.lora` bit-exact reference: segmented q_proj dB ~2.3%
|
|
393
|
+
(bf16-class).
|
|
394
|
+
- Memory vs the honest reference (`mlx_lm.lora --train --grad-checkpoint`,
|
|
395
|
+
which checkpoints per layer, 42 groups of 1; oracle venv, M1 Max 32 GB,
|
|
396
|
+
2026-06-16), matched grouping:
|
|
397
|
+
|
|
398
|
+
| L | mlx-lm (per-layer) | ours segSize=1 | ours segSize=6 |
|
|
399
|
+
|---|---|---|---|
|
|
400
|
+
| 2048 | 12.84 GB | **8.76 GB** | 11.05 |
|
|
401
|
+
| 4096 | 20.87 GB | **10.93 GB** | 16.06 |
|
|
402
|
+
| 8192 | OOM (25.72 @16 layers) | **15.29 GB** | 17.5 (seg2) |
|
|
403
|
+
|
|
404
|
+
Two unisolated confounds: mlx-lm's `default_loss` materializes full
|
|
405
|
+
`[1,L,262144]` logits (~4 GB @4K) vs our response-only CE; and full streaming
|
|
406
|
+
vs per-layer checkpoint inside one backward. The headline is part
|
|
407
|
+
CE-optimization, part mechanism.
|
|
408
|
+
|
|
409
|
+
**Segmentation strategy — corrected 2026-07-02.** The original plan (isolate the
|
|
410
|
+
7 O(L²) full-attention layers in singleton segments, `planSegments` with a
|
|
411
|
+
memory cost model) was built and A/B'd @8K: **zero peak win** (18.09 vs 18.02
|
|
412
|
+
GB seg2), because every layer's sdpa backward is O(L²) (§5.2). The worst
|
|
413
|
+
segment is set by **layer count alone** — `segment_size` is the whole knob:
|
|
414
|
+
seg1 = 14.59 GB @8K on e4b, +3% step time, loss identical. Going lower needs an
|
|
415
|
+
O(L)-memory attention backward, not smarter segmentation. `planSegmentsBySize`
|
|
416
|
+
stays; the donor/boundary constraints from the plan still hold. Rotor/min-cut
|
|
417
|
+
planners (§8.1) would need a corrected cost model.
|
|
418
|
+
|
|
419
|
+
**ORPO variants.** `SegmentedBackwardOrpo[Gemma4]` run chosen and rejected
|
|
420
|
+
forwards segment-by-segment, a two-input head `Vjp` over `[hC, hR]` yields
|
|
421
|
+
`[dh_c, dh_r]`, each branch walks backward over its segments (donor cotangents
|
|
422
|
+
in a per-branch `dKV`), LoRA grads summed. e4b at L=256: full two-branch 12.67
|
|
423
|
+
GB → segmented 7.66 GB; 8.71 GB at L=1024 where the full backward doesn't fit
|
|
424
|
+
(2026-06-18, M1 Max 32 GB). At λ=0 the e4b path is byte-identical to SFT
|
|
425
|
+
`SegmentedBackwardGemma4` (relNorm 1.1341%, maxAbs 3.79e-3 — the same numbers),
|
|
426
|
+
proving zero added error; donor recipients (layers 22/23 k/v_proj) stay <0.5%.
|
|
427
|
+
|
|
428
|
+
**Intra-layer MLP split** (`mlpSplit`, gradCheckpoint path, Gemma4 only):
|
|
429
|
+
`forwardAttn` / `forwardMlp` checkpointed separately with the post-attn
|
|
430
|
+
residual `hMid` as the boundary. Bit-exact (0.000000%). Honest peak: L=2048
|
|
431
|
+
no-ckpt 28.60 → single 23.40 → split 23.35 GB — the per-layer checkpoint
|
|
432
|
+
already serializes layers, so the split saves one layer's attn-sized window;
|
|
433
|
+
under `ops.sdpa` the MLP intermediate dominates. Off by default.
|
|
434
|
+
|
|
435
|
+
### 5.4 Shared prompt-prefix forward
|
|
436
|
+
|
|
437
|
+
Chosen and rejected share the prompt; the causal mask makes the prefix's
|
|
438
|
+
hidden states independent of the continuation. One forward over
|
|
439
|
+
`[prompt(P); chosen(Rc); rejected(Rr)]` with (a) a **block-sparse mask**
|
|
440
|
+
(causal AND NOT rejected→chosen, `blockSparseMask`, carried by
|
|
441
|
+
`PrefixSharedCache`), (b) **block-wise RoPE** resetting each response to offset
|
|
442
|
+
P (`ropeBlocks`, `setMiniCpmPrefixPlan` / `setGemmaPrefixPlan`), and the head
|
|
443
|
+
gathering `chosen[k] ← H[P−1+k]`, `rejected[0] ← H[P−1]`, `rejected[k≥1] ←
|
|
444
|
+
H[P+Rc+k−1]` (`prefixGatherIdx`). Token cost `2(P+R) → P+2R`; reverse-mode AD
|
|
445
|
+
sums the two branches' cotangents into the prefix for free. Published for DPO
|
|
446
|
+
(Wang & Hegde, arXiv 2410.20305); no mainstream trainer does it at training
|
|
447
|
+
time. `splitPrefixBatch` falls back to two-forward for rows whose prompts
|
|
448
|
+
differ (logged once, counted).
|
|
449
|
+
|
|
450
|
+
- **MiniCPM5**: forward **bit-exact** vs two-forward (P=512/Rc=64/Rr=80), grads
|
|
451
|
+
~1.05% (f32-over-bf16 reassociation in LoRA grad matmuls over the merged
|
|
452
|
+
length; holds at λ=0), 1.78× fewer token-passes, peak 5.40 → 3.80 GB
|
|
453
|
+
(2026-06-18).
|
|
454
|
+
- **e4b** (`blockSparsePrefixMaskGemma` with a LOGICAL-position sliding-window
|
|
455
|
+
cut, `Gemma4PrefixSharedCache`, `orpoLossPrefixSharedGemma`): the
|
|
456
|
+
construction is proven — block-RoPE+mask == a plain forward over the same
|
|
457
|
+
concat to 0.00% at every chosen position — but it does NOT reproduce the
|
|
458
|
+
two-forward numbers (loss ~0.08%, hiddens 3–14%) because **the e4b forward is
|
|
459
|
+
itself length-sensitive**: `forward([prompt])` vs `forward([prompt; extra])`
|
|
460
|
+
differ 1.7% at position 0 up to ~14% later, through the production
|
|
461
|
+
`TrainingCache`. Root cause is e4b's scale=1.0 attention (headDim 256, normed
|
|
462
|
+
q/k → sharply peaked softmax) amplifying sub-bf16 matmul-tiling roundings
|
|
463
|
+
that vary with row count — the same class as the SigLIP tower. MiniCPM5
|
|
464
|
+
(1/√d) shows 0.00%. Accepted as a numerical caveat for a fine-tune; the
|
|
465
|
+
single-concat forward is its oracle. Composed with the segmented backward
|
|
466
|
+
(`SegmentedBackwardOrpoPrefixGemma4`, donor-KV + logical-window mask threaded
|
|
467
|
+
through segments): seg-vs-non-seg grads 1.7–2.3%, peak 30–39% lower
|
|
468
|
+
(2026-06-19).
|
|
469
|
+
- Measured effect on e4b @8192 (M1 Max 32 GB, prompt-dominant data, short
|
|
470
|
+
probe): 16.1 GB / ~175 s/step → **13.3 GB / ~70 s/step** with prefix-share.
|
|
471
|
+
|
|
472
|
+
### 5.5 What composes with what
|
|
473
|
+
|
|
474
|
+
| | flash/fused head | segmented | prefix-shared | grad-accum | B>1 |
|
|
475
|
+
|---|---|---|---|---|---|
|
|
476
|
+
| flash/fused head | — | yes (Checkpoint-bounded head inside segments) | yes (per branch) | yes | no (B=1) |
|
|
477
|
+
| segmented | | — | yes (MiniCPM5 + Gemma4) | yes | no |
|
|
478
|
+
| prefix-shared | | | — | yes | no |
|
|
479
|
+
| grad-accum | | | | — | n/a |
|
|
480
|
+
|
|
481
|
+
The memory-optimized paths are **B=1 only**; `grad_accumulation_steps` is the
|
|
482
|
+
effective-batch lever at B=1 memory (`accumulateStep` evals and frees each
|
|
483
|
+
micro-graph; measured ~1.02× peak at N=3 on MiniCPM5, both SFT and ORPO;
|
|
484
|
+
`tests/train-gradaccum.test.ts`). B>1 falls out on the monolithic path via
|
|
485
|
+
`iterateDpoBatches` padding + the batched pad mask (`forward.ts`). True batched
|
|
486
|
+
segmented backward is not built. Qwen has no segmented backward (needs
|
|
487
|
+
`SSMCache` + a differentiable gated-delta kernel) and would land on the full
|
|
488
|
+
backward first.
|
|
489
|
+
|
|
490
|
+
Explicitly dropped: activation offloading (no-op on unified memory), TRL's
|
|
491
|
+
padded concatenated forward, sequence packing / length-grouped batching
|
|
492
|
+
(SFT-only wins, moot at B=1).
|
|
493
|
+
|
|
494
|
+
### 5.6 Quantized base
|
|
495
|
+
|
|
496
|
+
Each `QuantizedLinear` carries its own `QuantSpec`; `quantized_matmul` has a
|
|
497
|
+
working x-vjp and the base is frozen, so mixed precision costs nothing in the
|
|
498
|
+
layer stack. The only quant-aware training code is the head: a vocab chunk is
|
|
499
|
+
an axis-0 row slice of the quantized `w`/`scales`/`biases` (one spec per head),
|
|
500
|
+
and the flash-CCE kernel dequantizes in-kernel.
|
|
501
|
+
|
|
502
|
+
## 6. Kernel-fusion notes (the flat-DAG payoff)
|
|
503
|
+
|
|
504
|
+
Verified MLX facts (re-grep against the pinned 0.31.2 before relying):
|
|
505
|
+
`mx.compile` fuses contiguous elementwise chains in fwd AND bwd, bounded by
|
|
506
|
+
matmul/SDPA/reductions/reshape — compile the *outermost* `value_and_grad` or
|
|
507
|
+
the backward lands outside the region; `mx.fast` ships fused fwd+bwd for
|
|
508
|
+
`rms_norm`/`layer_norm`/`rope` (use them); SDPA backward is the Metal
|
|
509
|
+
exception (§5.2); `mx.fast.metal_kernel` inside `mx.custom_function` with a
|
|
510
|
+
`.vjp` is fully differentiable (the mechanism every custom head uses).
|
|
511
|
+
|
|
512
|
+
Remaining custom-kernel targets, ranked: fused SDPA backward (FA2-style,
|
|
513
|
+
store per-row LSE, recompute `P` — removes the O(L²) term segmentation
|
|
514
|
+
compensates for); fused dequant-GEMM + LoRA epilogue with the Unsloth-style
|
|
515
|
+
manual backward (`h=x@A; dB=s·hᵀ@dy; dA=s·xᵀ@(dy@Bᵀ); dx=dy@Wᵀ + s·(dy@Bᵀ)@Aᵀ`);
|
|
516
|
+
recompute↔backward fusion in the segmented path; min-cut rematerialization
|
|
517
|
+
(AOTAutograd's partitioner) as a fusion-aware replacement for hand-chosen cuts.
|
|
518
|
+
The GeGLU/SwiGLU `CustomVjp` (grad_b = dc·gelu(a), grad_a = dc·b·gelu'(a)) was
|
|
519
|
+
written and validated, then deleted 2026-07-05 with the fused kernels —
|
|
520
|
+
compiled activations own that fusion bit-exactly
|
|
521
|
+
([unified-engine-frontier-plan.md §6](unified-engine-frontier-plan.md)), and
|
|
522
|
+
training needs no env-flag sanitization.
|
|
523
|
+
|
|
524
|
+
Per-model specialized training paths would mirror `scripts/gen-model.ts`'s
|
|
525
|
+
fingerprint-dispatched generated forwards with the monolith as fallback; not
|
|
526
|
+
built.
|
|
527
|
+
|
|
528
|
+
## 7. Quality experiment pipeline
|
|
529
|
+
|
|
530
|
+
Pieces (judge-free, unit-tested): `src/eval/ifeval.ts` (verifiable-instruction
|
|
531
|
+
scorer), `src/eval/ultrafeedback.ts` + `scripts/curate-ultrafeedback.ts`
|
|
532
|
+
(binarized UF → preference JSONL with length filtering), `scripts/run-ifeval.ts`
|
|
533
|
+
(before/after runner), `scripts/bench-orpo.ts` (ms/step + peak across head /
|
|
534
|
+
segment configs). Prompt masking is verified (`tests/dpo-masking.test.ts`):
|
|
535
|
+
left-truncation preserves the prompt/response boundary; the only leak is a
|
|
536
|
+
completion ≥ `max_seq_length`, which `loadDpoDataset` warns on.
|
|
537
|
+
|
|
538
|
+
Result so far: an 800-step MiniCPM5 UltraFeedback run left IFEval flat
|
|
539
|
+
(22.5%) — general data + a tiny run on a 1B is the wrong lever. The
|
|
540
|
+
load-bearing target is the **chunk segmenter** (distilled segmentation with
|
|
541
|
+
boundary/label accuracy vs gold). Small sets (~450 pairs) overfit fast: keep λ
|
|
542
|
+
modest, carve a ~50-pair `valid.jsonl`, watch val margin, `save_checkpoints`
|
|
543
|
+
and mount the best-margin checkpoint.
|
|
544
|
+
|
|
545
|
+
## 8. Future
|
|
546
|
+
|
|
547
|
+
### 8.1 Deferred performance levers
|
|
548
|
+
|
|
549
|
+
- **Grad-accumulation A/B.** `--grad-accum` is exposed in `mlx-bun train`
|
|
550
|
+
(`scripts/train-orpo.ts` has no env knob for it). What's open is the
|
|
551
|
+
measurement: at hard B=1 every update is one pair (the 0.4↔3.3 per-step loss
|
|
552
|
+
swing is that noise); A/B G=4/8 on val margin. It is not a speedup and not a
|
|
553
|
+
guaranteed win (the prior run converged at B=1, val 1.66→1.50). Generic
|
|
554
|
+
N-way batching is parked — prefix-share is already a hand-built
|
|
555
|
+
batch-of-2 (1F+1B over `[P; Rc; Rr]`), strictly cheaper than a generic
|
|
556
|
+
2-batch's 2F+2B.
|
|
557
|
+
- **W4A16 head GEMM (compute precision) — proposed 2026-06-19, not started.**
|
|
558
|
+
The steel head runs W4A32 (`BlockMMA<float,float>` fwd and both bwd phases;
|
|
559
|
+
fp32 staging tiles; dequant into fp32). Apple GPUs run fp16 FMA at ~2× fp32,
|
|
560
|
+
and the kernel is **ALU-bound, measured live** (M1 Max, MiniCPM5 ORPO
|
|
561
|
+
`--no-segment` seq 4096: GPU 100% @1296 MHz, memory BW 129 GB/s ≈ ⅓ of ~400,
|
|
562
|
+
17.4/32 GB) — the 4-bit storage already did its job and the GPU spends
|
|
563
|
+
cycles on fp32 FMAs. Plan: (0) confirm which `BlockMMA`/`BlockLoader`
|
|
564
|
+
template params are operand vs accumulator; (1b) profile the dequant-ALU vs
|
|
565
|
+
FMA split — if FMA < ~40% of the kernel, reset expectations; (2) add a gate
|
|
566
|
+
(`CCE_A_HALF` define + env flag, fp32 stays the exact parity mode for the
|
|
567
|
+
1e-5 gate, half graded by greedy-prefix logit parity); (3) forward to half
|
|
568
|
+
operands first, land alone; (4) backward range probe — the survivors after
|
|
569
|
+
the skips are near-zero coeff tails where fp16 underflows, and `h` can carry
|
|
570
|
+
residual outliers (M1 Max has no bf16 simdgroup_matrix, so fp16 is the only
|
|
571
|
+
half); (5) backward to half only on a go; (6) re-sweep `BLOCK_B`/tiles —
|
|
572
|
+
half staging halves threadgroup memory, the constraint that capped the
|
|
573
|
+
current tiling. fp32 accumulate is non-negotiable; validate grads vs
|
|
574
|
+
autograd. Context measurement (M1 Max, MiniCPM5 seq 4096): `--no-segment`
|
|
575
|
+
~1.25 s/step @9.26 GB vs `--seg 2` ~1.58 s/step @2.62 GB — bit-identical
|
|
576
|
+
output, ~20% faster for 3.5× peak.
|
|
577
|
+
- **Sequence-chunk the MLP sub-block** inside segments: MLP is per-token
|
|
578
|
+
independent, so its `d_ff` intermediate can drop from `O(T·d_ff)` to
|
|
579
|
+
`O(chunk·d_ff)` at ~zero recompute — the bigger MLP lever than `mlpSplit`.
|
|
580
|
+
- **Budget-aware segment planner** (rotor DP / min-cut): only worth building
|
|
581
|
+
with the corrected cost model (every layer O(L²) under `ops.sdpa`); today
|
|
582
|
+
`segment_size` is the whole knob.
|
|
583
|
+
- **Faster flash-attention backward**: simdgroup tiling, larger threadgroups,
|
|
584
|
+
FA2-style LSE-only storage — matters only where `ops.sdpa`'s O(L²) is the
|
|
585
|
+
blocker (≳16K). MLX's own `mx.fast.sdpa` source is the blueprint.
|
|
586
|
+
- **Fused LoRA epilogue** (§6) — benefits SFT/DPO/ORPO and adapter serving.
|
|
587
|
+
- **Shape-specialized compiled training graphs**: each row has concrete
|
|
588
|
+
`(P, Rc, Rr)`, so ORPO can compile exact or bucketed shapes keyed on model
|
|
589
|
+
checksum + flags + segment plan + chunk sizes, eager fallback for misses.
|
|
590
|
+
Custom Metal kernels stay outside compiled regions.
|
|
591
|
+
- **ORPO-grad folding inside the head kernel** (speed-only; autograd composes
|
|
592
|
+
the odds-ratio grad outside it today) and a head-LoRA `∇A/∇B` path if the
|
|
593
|
+
head ever becomes a target.
|
|
594
|
+
- Optimize the flash SDPA kernel with simdgroup_matrix, or beat `mx.fast.sdpa`
|
|
595
|
+
for our fixed head-dims / training-only backward / O(L) constraint —
|
|
596
|
+
research spike, not scheduled.
|
|
597
|
+
|
|
598
|
+
### 8.2 Dynamic λ controller (proposed 2026-06-20, reframed 2026-07-01; not built)
|
|
599
|
+
|
|
600
|
+
Josh's framing: *λ is not a hyperparameter to tune but a process variable to
|
|
601
|
+
control* — push preference pressure to the maximum the model can bear
|
|
602
|
+
(self-discovering), retreat on confirmed coherence damage (self-stabilizing),
|
|
603
|
+
hold inside the healthy band. Motivation: the SFT/NLL half learns easily (val
|
|
604
|
+
loss falls while accuracy/margin barely move); over-weighting OR triggers
|
|
605
|
+
**likelihood displacement** (margin widened by crushing `logp(rejected)` and
|
|
606
|
+
sometimes `logp(chosen)`, cf. arXiv 2410.08847). Closest prior is β-DPO
|
|
607
|
+
(arXiv 2407.08639, per-batch calibration); the claim here would be closed-loop
|
|
608
|
+
trajectory control with a displacement-aware signal — the controller must beat
|
|
609
|
+
the *best constant* λ from a sweep, not λ=0.1.
|
|
610
|
+
|
|
611
|
+
- **Signals**: `lw` and `lr` separately (today `orpoMetrics` returns only
|
|
612
|
+
`loss/nll/or/accuracy/margin` — surfacing `lw`/`lr` is the load-bearing
|
|
613
|
+
instrumentation), on **val** each `stepsPerEval` (per-step B=1 train metrics
|
|
614
|
+
are too noisy). Control on the **response-only** `lw` — full-scope NLL mixes
|
|
615
|
+
prompt modeling into the level. Margin↑ via `lw`↑ is healthy; margin↑ via
|
|
616
|
+
`lr`↓ with `lw` flat/falling is displacement.
|
|
617
|
+
- **Law**: phase machine (SFT warmup at `LAMBDA_MIN` until `dLw` plateaus →
|
|
618
|
+
ramp `+RAMP` per eval while `dLw ≥ −EPS_DISP` → back off on displacement,
|
|
619
|
+
hysteresis over K evals; hard stop if `lw` falls below its start by a
|
|
620
|
+
threshold) — i.e. AIMD; generalize to P → PD → PID incrementally, each term
|
|
621
|
+
earning its place (D on a robust EMA slope for the leading indicator; I with
|
|
622
|
+
anti-windup for chronic offset). Bias toward pushing with a safety margin
|
|
623
|
+
below the cliff; the degenerate failure is collapsing to pure SFT.
|
|
624
|
+
- **Wiring**: λ becomes a mutable ref read per step (today it is a constant
|
|
625
|
+
threaded into `orpoLoss` and baked into the segmented classes' constructors
|
|
626
|
+
→ add `setLambda`); controller lives in `orpoLoop` after each val eval;
|
|
627
|
+
gated `orpoLambdaSchedule: "static" | "adaptive"`, static = today exactly.
|
|
628
|
+
- **Validation**: same data/seed, Arm A static 0.1 vs Arm B adaptive; compare
|
|
629
|
+
val accuracy/margin and `lw` trajectories plus downstream win-rate; validate
|
|
630
|
+
the controller on the prefer-uppercase positive control first; set EPS from
|
|
631
|
+
observed val-`lw` noise, not hand-fit. Report both arms (adaptive is our
|
|
632
|
+
method, not "standard ORPO").
|
|
633
|
+
|
|
634
|
+
### 8.3 Objective variants and data
|
|
635
|
+
|
|
636
|
+
Once the shared logp engine exists these are cheap siblings: **SimPO**
|
|
637
|
+
(`softplus(−β((lw − lr) − γ))`), **IPO**, **hinge/SLiC** margin loss, label
|
|
638
|
+
smoothing / per-example weights / loss clipping / ambiguity filtering. Data
|
|
639
|
+
quality likely beats objective cleverness on small runs: filter UltraFeedback
|
|
640
|
+
by score gap, bounded length ratio, semantic contrast, and instruction-following
|
|
641
|
+
categories if IFEval is the metric.
|
|
642
|
+
|
|
643
|
+
## History
|
|
644
|
+
|
|
645
|
+
- 2026-06-15 — e4b LoRA enablement diagnosis: the wall is backward memory
|
|
646
|
+
(full-vocab logits + un-streamed activations), not attention; response-only
|
|
647
|
+
CE landed.
|
|
648
|
+
- 2026-06-16 — Segmented backward Phase A (MiniCPM5, bit-exact under flash,
|
|
649
|
+
10.91 → 3.29 GB @2048) and Phase B (e4b, donor-KV + per-layer-input
|
|
650
|
+
threading, all 42 layers @8K where mlx-lm OOMs); surrogate `value_and_grad`
|
|
651
|
+
leak found, replaced by `mlx_vjp`; non-segmented SFT proven bit-exact vs
|
|
652
|
+
`mlx_lm.lora`.
|
|
653
|
+
- 2026-06-17 — ORPO landed as a method: loss math + tests, `orpoLoop`, job
|
|
654
|
+
submit, `/finetune` picker, e2e on MiniCPM5; `bitsMap` rank scaling wired;
|
|
655
|
+
dataset format renamed `preference`; flash SDPA kernel fixed to L2 parity.
|
|
656
|
+
- 2026-06-18 — Token-chunked head; fused linear-CE head; vocab-blocked pure-MLX
|
|
657
|
+
CCE (correct, unbounded, gated off); flash-CCE kernel v1 → vocab-parallel →
|
|
658
|
+
`BLOCK_B`; `SegmentedBackwardOrpo` + `SegmentedBackwardOrpoGemma4`; MiniCPM5
|
|
659
|
+
prefix-shared reference (forward bit-exact); LoRA dropout / rsLoRA / LoRA+;
|
|
660
|
+
`mlpSplit`.
|
|
661
|
+
- 2026-06-19 — Steel GEMM flash head fwd+bwd in production (e4b bwd 3687 → 754
|
|
662
|
+
ms, 0.93 GB flat); prefix-sharing wired and composed with the flash head and
|
|
663
|
+
segmented backward for MiniCPM5 and e4b; warm-start (`RESUME`);
|
|
664
|
+
`scripts/train-orpo.ts` launcher; W4A16 spike proposed; grad-accum A/B
|
|
665
|
+
deferred.
|
|
666
|
+
- 2026-06-20/23 — Dynamic λ controller proposed, PID/AIMD framing added.
|
|
667
|
+
- 2026-06-21/22 — Host-buffer pin-leak post-mortem (`fromView` → `fromBytesCopy`);
|
|
668
|
+
Lab gates: FD + teacher-forced fidelity replace "parity" for the no-oracle
|
|
669
|
+
head; coeff filter measured near-free on real data.
|
|
670
|
+
- 2026-07-01 — `sft_scope: full | response` (default `full`), implemented on
|
|
671
|
+
every ORPO path; dynamic-λ reframed as band control.
|
|
672
|
+
- 2026-07-02 — Apple-CCE skips default 1e-5 (1.71× / 3.16× backward);
|
|
673
|
+
teacher-forced fidelity regression test; full-attention isolation planner
|
|
674
|
+
refuted (every layer O(L²)); `segment_size` 1 = 14.59 GB @8K on e4b.
|
|
675
|
+
- 2026-07-05 — Fused GeGLU/SwiGLU and perf kernels deleted; no training flag
|
|
676
|
+
sanitization remains.
|
|
677
|
+
- 2026-08-23 — This doc consolidated `docs/design/orpo-training.md`,
|
|
678
|
+
`docs/design/orpo-training.md`, `docs/design/orpo-training.md`, and
|
|
679
|
+
`docs/design/orpo-training.md`; examples scripts moved to
|
|
680
|
+
`scripts/examples/`.
|