mlx-bun 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1453 -88
- package/THIRD_PARTY_NOTICES.md +882 -0
- package/bin/mlx-bun.convert.mjs +4 -0
- package/bin/mlx-bun.fuse.mjs +4 -0
- package/bin/mlx-bun.generate.mjs +4 -0
- package/bin/mlx-bun.lora.mjs +4 -0
- package/bin/mlx-bun.mjs +13 -21
- package/bin/mlx-bun.server.mjs +4 -0
- package/bin/mlx-bun.upload.mjs +4 -0
- package/dist/web/app.js +9584 -0
- package/package.json +50 -53
- package/src/cli/ane-install.ts +76 -0
- package/src/cli/args.ts +179 -0
- package/src/cli/engine-entry.ts +68 -0
- package/src/cli/inference.ts +214 -0
- package/src/cli/job-entry.ts +97 -0
- package/src/cli/library-host.ts +117 -0
- package/src/cli/main.ts +57 -0
- package/src/cli/memory-engine.ts +163 -0
- package/src/cli/memory.ts +36 -0
- package/src/cli/mlx-lm-aliases.ts +309 -0
- package/src/cli/model-choice.ts +33 -0
- package/src/cli/model-selection.ts +84 -0
- package/src/cli/module-host.ts +11 -0
- package/src/cli/module-verbs.ts +50 -0
- package/src/cli/numerical-policy.ts +12 -0
- package/src/cli/perplexity.ts +89 -0
- package/src/cli/serve-host.ts +423 -0
- package/src/cli/serve-isolated.ts +217 -0
- package/src/cli/serve-options.ts +106 -0
- package/src/cli/serve-state.ts +205 -0
- package/src/cli/serve.ts +500 -0
- package/src/cli/served-model-host.ts +102 -0
- package/src/cli/server-entry.ts +148 -0
- package/src/cli/serving-unit.ts +177 -0
- package/src/{tui.ts → cli/terminal.ts} +13 -8
- package/src/cli/worker-adapters.ts +34 -0
- package/src/cli/worker-entry.ts +272 -0
- package/src/cli/worker-unit.ts +179 -0
- package/src/engine/adapter-operation.ts +26 -0
- package/src/engine/cache-services.ts +114 -0
- package/src/engine/completion.ts +39 -0
- package/src/engine/generation-gateway.ts +358 -0
- package/src/engine/index.ts +59 -0
- package/src/engine/kv-budget.ts +120 -0
- package/src/engine/media-preparation.ts +232 -0
- package/src/engine/model-binding.ts +24 -0
- package/src/engine/model-host.ts +513 -0
- package/src/engine/model-serving.ts +89 -0
- package/src/{serve → engine}/preparation.ts +10 -8
- package/src/{serve → engine}/session-completion-engine.ts +6 -6
- package/src/engine/telemetry.ts +109 -0
- package/src/hub/downloads.ts +116 -0
- package/src/jobs/db.ts +16 -7
- package/src/jobs/events.ts +29 -0
- package/src/jobs/executable.ts +5 -0
- package/src/jobs/host.ts +51 -0
- package/src/jobs/in-process.ts +55 -0
- package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
- package/src/jobs/runner.ts +150 -231
- package/src/jobs/service.ts +75 -0
- package/src/jobs/sse.ts +59 -23
- package/src/jobs/worker-memory.ts +46 -0
- package/src/jobs/worker-process.ts +163 -0
- package/src/jobs/worker-supervisor.ts +258 -0
- package/src/modules.ts +58 -0
- package/src/publishing/catalog-hub.ts +18 -0
- package/src/publishing/credentials.ts +31 -0
- package/src/publishing/upload.ts +37 -0
- package/src/residency/leased-adapters.ts +21 -0
- package/src/residency/local-records.ts +16 -0
- package/src/residency/model-residency.ts +405 -0
- package/src/residency/record-index.ts +28 -0
- package/src/residency/resident-estimate.ts +50 -0
- package/src/{anthropic.ts → server/anthropic.ts} +3 -81
- package/src/server/cache-routes.ts +36 -0
- package/src/{serve → server}/chat-request.ts +12 -10
- package/src/{serve → server}/chat-stage.ts +27 -22
- package/src/{client.ts → server/client.ts} +28 -2
- package/src/{serve → server}/completion-executor.ts +42 -42
- package/src/server/decision-body.ts +55 -0
- package/src/server/decision-routes.ts +134 -0
- package/src/server/decision-wire.ts +228 -0
- package/src/{serve → server}/discovery-routes.ts +42 -83
- package/src/server/generated-token-history.ts +57 -0
- package/src/{serve → server}/http.ts +25 -13
- package/src/{serve → server}/inference-request.ts +5 -5
- package/src/server/job-routes.ts +20 -0
- package/src/server/media-prompt.ts +63 -0
- package/src/server/memory-completion-client.ts +46 -0
- package/src/server/model-routes.ts +192 -0
- package/src/{serve → server}/openai-wire.ts +3 -2
- package/src/{serve → server}/pipeline.ts +1 -1
- package/src/server/prompt-builder.ts +13 -0
- package/src/server/prompt-contracts.ts +17 -0
- package/src/server/proxy-routes.ts +187 -0
- package/src/server/publishing-routes.ts +41 -0
- package/src/{serve → server}/request-plan.ts +8 -8
- package/src/{serve → server}/request-prep.ts +82 -68
- package/src/{serve → server}/responses-client.ts +30 -13
- package/src/{responses.ts → server/responses.ts} +12 -93
- package/src/server/routes.ts +205 -0
- package/src/server/start.ts +72 -0
- package/src/server/status-routes.ts +108 -0
- package/src/{serve → server}/text-completion-stage.ts +25 -4
- package/src/server/text-prompt.ts +12 -0
- package/src/{serve → server}/token-streams.ts +36 -38
- package/src/server/worker-routes.ts +311 -0
- package/src/storage/paths.ts +61 -0
- package/src/web/assets.ts +37 -0
- package/src/web/{src → browser}/api.ts +5 -23
- package/src/web/browser/chat-host.ts +17 -0
- package/src/web/browser/installed-panels.ts +6 -0
- package/src/web/browser/main.ts +55 -0
- package/src/web/browser/markdown.ts +279 -0
- package/src/web/browser/memory-panel.ts +9 -0
- package/src/web/browser/palette.ts +64 -0
- package/src/web/browser/protocol.ts +13 -0
- package/src/web/browser/shell.ts +467 -0
- package/src/web/{src → browser}/status.ts +32 -36
- package/src/web/build.ts +51 -0
- package/src/web/public/app.html +1054 -0
- package/src/web/{sw.js → public/sw.js} +4 -3
- package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
- package/src/web/{vendor → public/vendor}/hljs.js +2 -2
- package/THIRD_PARTY_LICENSES.md +0 -51
- package/docs/README.md +0 -94
- package/docs/archive/hlg-sampling.md +0 -350
- package/docs/archive/investigations/audio-input-plan.md +0 -356
- package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
- package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
- package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
- package/docs/archive/investigations/curve-bisector-routing.md +0 -123
- package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
- package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
- package/docs/archive/investigations/dspark-handoff.md +0 -224
- package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
- package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
- package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
- package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
- package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
- package/docs/archive/investigations/lab-build-journal.md +0 -157
- package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
- package/docs/archive/investigations/openjarvis-fit.md +0 -198
- package/docs/archive/investigations/openwiki-evaluation.md +0 -249
- package/docs/archive/investigations/optimization_plan.md +0 -346
- package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
- package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
- package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
- package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
- package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
- package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
- package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
- package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
- package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
- package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
- package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
- package/docs/archive/planning/chat-ui-vision.md +0 -50
- package/docs/archive/planning/chunk-task-roadmap.md +0 -77
- package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
- package/docs/archive/planning/journal.md +0 -316
- package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
- package/docs/archive/planning/pi-integration-review.md +0 -178
- package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
- package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
- package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
- package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
- package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
- package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
- package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
- package/docs/archive/planning/web-ui-pass-plan.md +0 -213
- package/docs/archive/planning/website-readme-pass-plan.md +0 -224
- package/docs/design/batching.md +0 -1841
- package/docs/design/decode-speed-program.md +0 -2349
- package/docs/design/dreaming-nightly-pipeline.md +0 -684
- package/docs/design/generic-model-support.md +0 -705
- package/docs/design/kv-cache.md +0 -824
- package/docs/design/orpo-training.md +0 -680
- package/docs/design/speculative-decoding.md +0 -1990
- package/docs/design/turboquant.md +0 -2212
- package/docs/design/unified-engine-frontier-plan.md +0 -1880
- package/docs/design/web-chat-redesign.md +0 -695
- package/docs/planning/PRODUCT_ROADMAP.md +0 -594
- package/docs/planning/ResearchTopics.md +0 -119
- package/docs/planning/release-notes-v0.4.0.md +0 -45
- package/docs/planning/video-series.md +0 -421
- package/docs/reference/benchmarks.md +0 -3613
- package/docs/reference/cli.md +0 -585
- package/docs/reference/distribution.md +0 -355
- package/docs/reference/environment.md +0 -398
- package/docs/reference/glossary.md +0 -39
- package/docs/reference/library-api.md +0 -674
- package/docs/reference/memory.md +0 -260
- package/docs/reference/models.md +0 -334
- package/docs/reference/server-api.md +0 -1775
- package/docs/reference/server-config.md +0 -846
- package/docs/reference/training.md +0 -462
- package/docs/reference/troubleshooting.md +0 -158
- package/src/admission-plan.ts +0 -18
- package/src/audio/conformer.ts +0 -647
- package/src/audio/decode.ts +0 -133
- package/src/audio/features.ts +0 -202
- package/src/audio/transcode.ts +0 -67
- package/src/backends/mlx/assistant-target.ts +0 -46
- package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
- package/src/backends/mlx/autoregressive.ts +0 -108
- package/src/backends/mlx/batch-group.ts +0 -1790
- package/src/backends/mlx/cache-attachment.ts +0 -67
- package/src/backends/mlx/cache-layout.ts +0 -31
- package/src/backends/mlx/checkpoint-state.ts +0 -53
- package/src/backends/mlx/continuation-persistence.ts +0 -106
- package/src/backends/mlx/continuation-request.ts +0 -43
- package/src/backends/mlx/continuation.ts +0 -77
- package/src/backends/mlx/diffusion.ts +0 -39
- package/src/backends/mlx/draft-target.ts +0 -32
- package/src/backends/mlx/evaluation.ts +0 -105
- package/src/backends/mlx/fill-append.ts +0 -45
- package/src/backends/mlx/full-kv-row-append.ts +0 -32
- package/src/backends/mlx/full-kv-row-donor.ts +0 -54
- package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
- package/src/backends/mlx/gateway-binding.ts +0 -130
- package/src/backends/mlx/graph.ts +0 -38
- package/src/backends/mlx/kv-codec.ts +0 -9
- package/src/backends/mlx/kv-maintenance.ts +0 -133
- package/src/backends/mlx/kv-tensor-rows.ts +0 -212
- package/src/backends/mlx/memory-client.ts +0 -189
- package/src/backends/mlx/methods.ts +0 -90
- package/src/backends/mlx/model-prompt.ts +0 -164
- package/src/backends/mlx/model-serving.ts +0 -79
- package/src/backends/mlx/prefill-cohort.ts +0 -89
- package/src/backends/mlx/prefill-rows.ts +0 -200
- package/src/backends/mlx/prefill.ts +0 -33
- package/src/backends/mlx/projected-context-rows.ts +0 -72
- package/src/backends/mlx/request-policy.ts +0 -20
- package/src/backends/mlx/request-state-policy.ts +0 -41
- package/src/backends/mlx/rollback.ts +0 -89
- package/src/backends/mlx/row-sampling.ts +0 -49
- package/src/backends/mlx/serial-executor.ts +0 -242
- package/src/backends/mlx/speculative-group.ts +0 -359
- package/src/backends/mlx/speculative-round.ts +0 -161
- package/src/backends/mlx/speculative.ts +0 -114
- package/src/backends/mlx/state-rows.ts +0 -104
- package/src/backends/mlx/state-views.ts +0 -46
- package/src/backends/mlx/target-layout-capability.ts +0 -35
- package/src/backends/mlx/text-engine.ts +0 -73
- package/src/backends/mlx/transitioning-kv-rows.ts +0 -165
- package/src/chat-template.ts +0 -382
- package/src/cli.ts +0 -3168
- package/src/config.ts +0 -399
- package/src/contracts/completion.ts +0 -16
- package/src/contracts/execution.ts +0 -60
- package/src/contracts/generation.ts +0 -103
- package/src/contracts/host.ts +0 -7
- package/src/contracts/pi-provider.ts +0 -30
- package/src/contracts/pi.ts +0 -259
- package/src/contracts/prefix-cache.ts +0 -23
- package/src/contracts/resources.ts +0 -25
- package/src/contracts/sampling.ts +0 -23
- package/src/contracts/scheduling.ts +0 -45
- package/src/contracts/tsconfig.json +0 -14
- package/src/dataset/generators.ts +0 -1087
- package/src/dataset/index.ts +0 -12
- package/src/dataset/job.ts +0 -81
- package/src/dataset/llm.ts +0 -71
- package/src/dataset/registry.ts +0 -528
- package/src/diffusion/diffusion-generate.ts +0 -507
- package/src/download.ts +0 -505
- package/src/embed.ts +0 -77
- package/src/engine/admission.ts +0 -59
- package/src/engine/cancellation.ts +0 -38
- package/src/engine/engine.ts +0 -56
- package/src/engine/execution-coordinator.ts +0 -70
- package/src/engine/execution-plan.ts +0 -53
- package/src/engine/resources.ts +0 -53
- package/src/engine/scheduler.ts +0 -72
- package/src/engine/session.ts +0 -247
- package/src/eval/README.md +0 -130
- package/src/eval/ifeval.ts +0 -81
- package/src/eval/kl-prompts.ts +0 -29
- package/src/eval/kl.ts +0 -590
- package/src/eval/perplexity.ts +0 -164
- package/src/eval/quality-db.ts +0 -92
- package/src/eval/reference-logits.ts +0 -207
- package/src/eval/runner.ts +0 -124
- package/src/eval/score.ts +0 -49
- package/src/eval/tasks/bfcl.ts +0 -535
- package/src/eval/tasks/gsm8k.ts +0 -98
- package/src/eval/tasks/hashhop.ts +0 -139
- package/src/eval/tasks/humaneval.ts +0 -275
- package/src/eval/tasks/ifeval.ts +0 -525
- package/src/eval/tasks/mmlu.ts +0 -157
- package/src/eval/ultrafeedback.ts +0 -60
- package/src/evaldb.ts +0 -199
- package/src/expert-io.ts +0 -393
- package/src/expert-offload-build.ts +0 -77
- package/src/expert-offload.ts +0 -76
- package/src/expert-residency.ts +0 -1134
- package/src/expert-usage.ts +0 -415
- package/src/fill/echo-index.ts +0 -188
- package/src/fill/fill-session.ts +0 -452
- package/src/fill/proposal.ts +0 -68
- package/src/fill/schema-rows.ts +0 -506
- package/src/fill/tool-boundary.ts +0 -103
- package/src/fit.ts +0 -249
- package/src/generate.ts +0 -1346
- package/src/grammar.ts +0 -591
- package/src/harness-pi.ts +0 -184
- package/src/hf-push.ts +0 -628
- package/src/hub-rest.ts +0 -242
- package/src/index.ts +0 -63
- package/src/inference/denoising.ts +0 -17
- package/src/inference/draft-acceptance.ts +0 -41
- package/src/inference/draft-output.ts +0 -49
- package/src/inference/graph.ts +0 -33
- package/src/inference/prefill.ts +0 -39
- package/src/inference/rollback.ts +0 -7
- package/src/jobs/index.ts +0 -31
- package/src/jobs/job-entry.ts +0 -107
- package/src/jobs/task-client.ts +0 -19
- package/src/jobs/types.ts +0 -2
- package/src/kv-scheme.ts +0 -257
- package/src/kv-store.ts +0 -1099
- package/src/lab/README.md +0 -18
- package/src/lab/curve/curve-designer.html +0 -303
- package/src/lab/curve/curve-sampler.ts +0 -122
- package/src/lab/expert-trace/expert-trace.ts +0 -69
- package/src/lab/paged-kv/paged-kv-rows.ts +0 -83
- package/src/lab/paged-kv/paged-kv.ts +0 -270
- package/src/library.ts +0 -40
- package/src/lora.ts +0 -408
- package/src/md-modules.d.ts +0 -49
- package/src/media-fetch.ts +0 -442
- package/src/memory/article.ts +0 -558
- package/src/memory/chunk-validation.ts +0 -150
- package/src/memory/chunk.ts +0 -318
- package/src/memory/cluster.ts +0 -351
- package/src/memory/coverage.ts +0 -134
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +0 -253
- package/src/memory/entity.ts +0 -203
- package/src/memory/gate.ts +0 -187
- package/src/memory/history.ts +0 -152
- package/src/memory/ingest.ts +0 -335
- package/src/memory/model.ts +0 -183
- package/src/memory/normalize.ts +0 -671
- package/src/memory/parse.ts +0 -22
- package/src/memory/pipeline.ts +0 -275
- package/src/memory/prompts.ts +0 -65
- package/src/memory/query.ts +0 -334
- package/src/memory/reconcile.ts +0 -734
- package/src/memory/redirect.ts +0 -108
- package/src/memory/reindex.ts +0 -325
- package/src/memory/resolve.ts +0 -307
- package/src/memory/rest.ts +0 -245
- package/src/memory/route.ts +0 -272
- package/src/memory/schedule.ts +0 -165
- package/src/memory/stages.ts +0 -777
- package/src/memory/synthesize.ts +0 -1638
- package/src/memory/tools.ts +0 -749
- package/src/memory/vault.ts +0 -1194
- package/src/memory/wikify.ts +0 -1064
- package/src/mlx/array.ts +0 -388
- package/src/mlx/autograd.ts +0 -304
- package/src/mlx/checkpoint.ts +0 -149
- package/src/mlx/compile.ts +0 -168
- package/src/mlx/custom-vjp.ts +0 -188
- package/src/mlx/ffi.ts +0 -512
- package/src/mlx/materialize.ts +0 -23
- package/src/mlx/metal-kernel.ts +0 -159
- package/src/mlx/ops.ts +0 -1021
- package/src/mlx/quantized-rows.ts +0 -36
- package/src/mlx/token-bitmask.ts +0 -52
- package/src/mlx/turboquant-kv-decode.ts +0 -82
- package/src/mlx/turboquant-kv-inverse.ts +0 -122
- package/src/mlx/turboquant-ops.ts +0 -861
- package/src/mlx/turboquant-tables.ts +0 -237
- package/src/mmap.ts +0 -90
- package/src/model/artifact-identity.ts +0 -73
- package/src/model/batched-kv.ts +0 -235
- package/src/model/batched-mask.ts +0 -279
- package/src/model/batched-quant.ts +0 -212
- package/src/model/batched-quantized-kv.ts +0 -144
- package/src/model/batched-rotating-quant.ts +0 -353
- package/src/model/batched-rotating-state.ts +0 -149
- package/src/model/batched-rotating.ts +0 -338
- package/src/model/batched-row-storage.ts +0 -179
- package/src/model/batched-ssm.ts +0 -52
- package/src/model/batched-turboquant-kv.ts +0 -93
- package/src/model/compiled-decode.ts +0 -712
- package/src/model/decoded-kv-donor.ts +0 -12
- package/src/model/delayed-quantized-kv.ts +0 -72
- package/src/model/delayed-rotating-quantized-kv.ts +0 -93
- package/src/model/delayed-turboquant-kv.ts +0 -55
- package/src/model/diffusion-gemma.ts +0 -1026
- package/src/model/factory.ts +0 -189
- package/src/model/fingerprint.ts +0 -33
- package/src/model/flash-attention.ts +0 -694
- package/src/model/full-prefill-padding.ts +0 -56
- package/src/model/full-prefill-row.ts +0 -105
- package/src/model/gemma4-base.ts +0 -2351
- package/src/model/gemma4.ts +0 -1315
- package/src/model/generated/gemma4-12b.ts +0 -429
- package/src/model/generated/gemma4-26b.ts +0 -448
- package/src/model/generated/gemma4-e4b.ts +0 -717
- package/src/model/generated/index.ts +0 -18
- package/src/model/glm52-atlas.ts +0 -511
- package/src/model/glm52-cache.ts +0 -792
- package/src/model/glm52-config.ts +0 -262
- package/src/model/glm52-container.ts +0 -305
- package/src/model/glm52-coupling.ts +0 -388
- package/src/model/glm52-dsa.ts +0 -606
- package/src/model/glm52-expert-layout.ts +0 -253
- package/src/model/glm52-layout.ts +0 -152
- package/src/model/glm52-memory.ts +0 -500
- package/src/model/glm52-mla.ts +0 -792
- package/src/model/glm52-moe.ts +0 -223
- package/src/model/glm52-pilot.ts +0 -537
- package/src/model/glm52-reference.ts +0 -445
- package/src/model/glm52-residency.ts +0 -557
- package/src/model/glm52-resident-weights.ts +0 -135
- package/src/model/glm52-streamed-experts.ts +0 -453
- package/src/model/glm52-streamed-metal.ts +0 -610
- package/src/model/glm52-weights.ts +0 -503
- package/src/model/glm52.ts +0 -1132
- package/src/model/implementation.ts +0 -69
- package/src/model/kv-attention-view.ts +0 -67
- package/src/model/minicpm5.ts +0 -376
- package/src/model/profile.ts +0 -443
- package/src/model/qwen-append.ts +0 -12
- package/src/model/qwen-conv.ts +0 -52
- package/src/model/qwen3-delta-state.ts +0 -74
- package/src/model/qwen3-delta.ts +0 -451
- package/src/model/qwen3-moe.ts +0 -404
- package/src/model/qwen3-mrope.ts +0 -252
- package/src/model/qwen3.ts +0 -277
- package/src/model/qwen3_5-checkpoint.ts +0 -164
- package/src/model/qwen3_5.ts +0 -819
- package/src/model/rotating-kv-donor.ts +0 -35
- package/src/model/rotating-kv-layout.ts +0 -262
- package/src/model/rotating-row-transaction.ts +0 -35
- package/src/model/speculative-rotating-kv.ts +0 -101
- package/src/model/ssm-prefill-padding.ts +0 -39
- package/src/model/support.ts +0 -102
- package/src/model/trellis-balanced-scatter.ts +0 -51
- package/src/model/trellis-linear.ts +0 -672
- package/src/model/trellis-shared-m.ts +0 -151
- package/src/model/trellis-shared-scatter.ts +0 -144
- package/src/model/trellis-splitk-prefill.ts +0 -158
- package/src/model/trellis-tiled-prefill.ts +0 -136
- package/src/model/trellis-vector-expand.ts +0 -62
- package/src/model/trellis-wide-prefill.ts +0 -94
- package/src/model/turboquant-codec.ts +0 -83
- package/src/model/universal/archs.ts +0 -332
- package/src/model/universal/dense.ts +0 -612
- package/src/model/universal/modules.ts +0 -170
- package/src/model/universal/rope.ts +0 -381
- package/src/native/expert_io.c +0 -523
- package/src/native/frame_extract.swift +0 -140
- package/src/native-pack.ts +0 -162
- package/src/pi-provider.ts +0 -53
- package/src/pi-session.ts +0 -81
- package/src/pi-terminal.ts +0 -274
- package/src/pi-web.ts +0 -1932
- package/src/preflight.ts +0 -158
- package/src/prompt-cache.ts +0 -460
- package/src/quantize/allocator.ts +0 -627
- package/src/quantize/atomic-output.ts +0 -38
- package/src/quantize/calibration.ts +0 -181
- package/src/quantize/config-writer.ts +0 -145
- package/src/quantize/index.ts +0 -84
- package/src/quantize/job.ts +0 -162
- package/src/quantize/quantizer.ts +0 -666
- package/src/quantize/rotate.ts +0 -603
- package/src/quantize/safetensors-writer.ts +0 -249
- package/src/quantize/sensitivity.ts +0 -216
- package/src/quantize/trellis.ts +0 -586
- package/src/quantize/weight-transform.ts +0 -292
- package/src/registry.ts +0 -602
- package/src/runtime-config.ts +0 -81
- package/src/safetensors.ts +0 -158
- package/src/sampler.ts +0 -1011
- package/src/serve/admin-routes.ts +0 -184
- package/src/serve/assets/curve-terrain.html +0 -105
- package/src/serve/assets/training-inference-map.html +0 -575
- package/src/serve/aux-routes.ts +0 -132
- package/src/serve/batch-scheduler.ts +0 -8
- package/src/serve/checkpoint-identity.ts +0 -64
- package/src/serve/generation-gateway.ts +0 -496
- package/src/serve/isolate.ts +0 -609
- package/src/serve/kv-budget.ts +0 -14
- package/src/serve/lab-routes.ts +0 -341
- package/src/serve/lane-registry.ts +0 -45
- package/src/serve/model-admin-routes.ts +0 -155
- package/src/serve/model-binding.ts +0 -43
- package/src/serve/model-host.ts +0 -511
- package/src/serve/parent-application.ts +0 -56
- package/src/serve/prompt-response-trace.ts +0 -147
- package/src/serve/response-lease.ts +0 -24
- package/src/serve/session-search.ts +0 -326
- package/src/serve/static-routes.ts +0 -101
- package/src/server.ts +0 -1314
- package/src/spec/assistant-rows.ts +0 -92
- package/src/spec/assistant-source.ts +0 -126
- package/src/spec/cache-identity.ts +0 -22
- package/src/spec/deepspec-rows.ts +0 -29
- package/src/spec/deepspec-source.ts +0 -145
- package/src/spec/dflash-source.ts +0 -149
- package/src/spec/drafter.ts +0 -304
- package/src/spec/dspark/ab-stats.ts +0 -136
- package/src/spec/dspark/calibration.ts +0 -112
- package/src/spec/dspark/data-dflash.ts +0 -156
- package/src/spec/dspark/data.ts +0 -201
- package/src/spec/dspark/deepspec-module.ts +0 -776
- package/src/spec/dspark/generate-dflash.ts +0 -245
- package/src/spec/dspark/generate.ts +0 -290
- package/src/spec/dspark/loader.ts +0 -23
- package/src/spec/dspark/loss.ts +0 -213
- package/src/spec/dspark/module-dflash.ts +0 -560
- package/src/spec/dspark/module.ts +0 -540
- package/src/spec/dspark/quantize-drafter.ts +0 -91
- package/src/spec/dspark/sample.ts +0 -112
- package/src/spec/generate.ts +0 -276
- package/src/spec/glm52-mtp-graph.ts +0 -29
- package/src/spec/glm52-mtp-rows.ts +0 -132
- package/src/spec/glm52-mtp-source.ts +0 -317
- package/src/spec/glm52-mtp-state.ts +0 -33
- package/src/spec/ngram-source.ts +0 -204
- package/src/spec/projected-draft-rows.ts +0 -73
- package/src/spec/qwen-mtp-module.ts +0 -171
- package/src/spec/qwen-mtp-rows.ts +0 -219
- package/src/spec/qwen-mtp-source.ts +0 -383
- package/src/spec/qwen-mtp-state.ts +0 -30
- package/src/spec/serve-loop.ts +0 -585
- package/src/spec/source.ts +0 -215
- package/src/spec/two-model-rows.ts +0 -61
- package/src/spec/two-model.ts +0 -113
- package/src/ssd-cache.ts +0 -372
- package/src/ssd-durability.ts +0 -211
- package/src/tokenizer.ts +0 -335
- package/src/tool-approvals.ts +0 -131
- package/src/tool-call.ts +0 -695
- package/src/train/dataset.ts +0 -484
- package/src/train/diffusion-lora.ts +0 -216
- package/src/train/export.ts +0 -32
- package/src/train/flash-cce.ts +0 -1234
- package/src/train/forward.ts +0 -266
- package/src/train/fuse.ts +0 -243
- package/src/train/index.ts +0 -46
- package/src/train/job.ts +0 -205
- package/src/train/lora-params.ts +0 -254
- package/src/train/loss.ts +0 -1591
- package/src/train/merge.ts +0 -195
- package/src/train/optimizer.ts +0 -183
- package/src/train/prefix-shared.ts +0 -490
- package/src/train/rank.ts +0 -137
- package/src/train/segmented.ts +0 -1828
- package/src/train/steel-qmm-header.ts +0 -1461
- package/src/train/trainer.ts +0 -1221
- package/src/train/watch.ts +0 -542
- package/src/vision/diffusion-vision.ts +0 -620
- package/src/vision/embedder.ts +0 -168
- package/src/vision/preprocess.ts +0 -247
- package/src/vision/prompt.ts +0 -373
- package/src/vision/qwen3vl-preprocess.ts +0 -409
- package/src/vision/qwen3vl-prompt.ts +0 -196
- package/src/vision/qwen3vl-tower.ts +0 -555
- package/src/vision/siglip.ts +0 -599
- package/src/vision/video-frames.ts +0 -148
- package/src/web/app.html +0 -2143
- package/src/web/app.js +0 -5703
- package/src/web/skills/memory/SKILL.md +0 -120
- package/src/web/skills/web-research/SKILL.md +0 -46
- package/src/web/skills.ts +0 -97
- package/src/web/src/adapters-panel.ts +0 -288
- package/src/web/src/assistant.ts +0 -451
- package/src/web/src/chat.ts +0 -1057
- package/src/web/src/composer.ts +0 -959
- package/src/web/src/dataset.ts +0 -132
- package/src/web/src/finetune.ts +0 -267
- package/src/web/src/hub.ts +0 -428
- package/src/web/src/main.ts +0 -98
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +0 -572
- package/src/web/src/model-picker.ts +0 -159
- package/src/web/src/palette.ts +0 -346
- package/src/web/src/protocol.ts +0 -26
- package/src/web/src/quantize.ts +0 -167
- package/src/web/src/rag.ts +0 -274
- package/src/web/src/sessions.ts +0 -469
- package/src/web/src/shell.ts +0 -804
- package/src/web/src/ui-catalog.ts +0 -78
- package/src/web/vendor/README.md +0 -61
- package/src/web-assets.ts +0 -47
- package/src/web-tools.ts +0 -478
- package/src/weights.ts +0 -255
- /package/src/{serve → server}/completion-sink.ts +0 -0
- /package/src/web/{icon.svg → public/icon.svg} +0 -0
- /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
- /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
package/src/train/trainer.ts
DELETED
|
@@ -1,1221 +0,0 @@
|
|
|
1
|
-
// LoRA trainer: SFT (masked cross-entropy) and DPO loops over a single
|
|
2
|
-
// quantized base, differentiating only the LoRA A/B leaves.
|
|
3
|
-
//
|
|
4
|
-
// The value_and_grad closure differentiates [...flatA, ...flatB]. Inside the
|
|
5
|
-
// closure we swap the incoming primal handles into the LoraWeights so the
|
|
6
|
-
// model's forward builds its graph against the differentiated leaves; after
|
|
7
|
-
// AdamW.step replaces a leaf, the writeback callback pushes the new handle
|
|
8
|
-
// back into the LoraWeights so the next forward (and the next closure swap)
|
|
9
|
-
// sees it. Base quantized weights are never in argnums — they stay frozen.
|
|
10
|
-
|
|
11
|
-
import { MlxArray, pinnedBufferCount } from "../mlx/array";
|
|
12
|
-
import { clearCache, peakMemory, activeMemory, cacheMemory, resetPeakMemory } from "../mlx/ffi";
|
|
13
|
-
import { runtimeValue } from "../runtime-config";
|
|
14
|
-
|
|
15
|
-
const MEM_LOG = runtimeValue("MLX_BUN_MEM_LOG") === "1";
|
|
16
|
-
const gbStr = (b: number) => `${(b / 1e9).toFixed(2)}GB`;
|
|
17
|
-
import { ValueAndGrad } from "../mlx/autograd";
|
|
18
|
-
import * as ops from "../mlx/ops";
|
|
19
|
-
import { evalAll } from "../mlx/ops";
|
|
20
|
-
import type { RuntimeModel } from "../model/factory";
|
|
21
|
-
import { Gemma4Model, type GradCheckpointCtx } from "../model/gemma4";
|
|
22
|
-
import { MiniCPM5Model } from "../model/minicpm5";
|
|
23
|
-
import { setTrainingAttn } from "../model/flash-attention";
|
|
24
|
-
import { SegmentedBackward, SegmentedBackwardGemma4, SegmentedBackwardOrpo, SegmentedBackwardOrpoGemma4, SegmentedBackwardOrpoPrefix, SegmentedBackwardOrpoPrefixGemma4, planSegmentsBySize } from "./segmented";
|
|
25
|
-
import type { LoadedTokenizer } from "../tokenizer";
|
|
26
|
-
import type { ChatTemplate } from "../chat-template";
|
|
27
|
-
import type { Emit } from "../jobs/types";
|
|
28
|
-
import {
|
|
29
|
-
loadSftDataset, iterateSftBatches, loadDpoDataset, iterateDpoBatches,
|
|
30
|
-
rowLength, type SftBatch, type DpoBatch,
|
|
31
|
-
} from "./dataset";
|
|
32
|
-
import {
|
|
33
|
-
buildTrainableLora, attachForTraining, detachTraining, flatParams,
|
|
34
|
-
saveAdapter, disposeLora, warmStartFromAdapter, type TrainableLora, type SaveAdapterConfig,
|
|
35
|
-
} from "./lora-params";
|
|
36
|
-
import { resolveRanks, bitsMapFromModel, readPerLayerKl, DEFAULT_TARGET_MODULES, type RankScaling } from "./rank";
|
|
37
|
-
import { sftLoss, dpoLoss, dpoRefLogps, dpoMetrics, orpoLoss, orpoMetrics, type ChunkCtx, type SftScope } from "./loss";
|
|
38
|
-
import { orpoLossPrefixShared, orpoLossPrefixSharedGemma, splitPrefixBatch } from "./prefix-shared";
|
|
39
|
-
import { AdamW, warmupCosineSchedule } from "./optimizer";
|
|
40
|
-
import { writeFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
41
|
-
|
|
42
|
-
export interface TrainConfig {
|
|
43
|
-
method: "sft" | "dpo" | "orpo";
|
|
44
|
-
rank: number;
|
|
45
|
-
scale: number;
|
|
46
|
-
rankScaling: RankScaling;
|
|
47
|
-
targetModules: string[];
|
|
48
|
-
numLayers: number;
|
|
49
|
-
iters: number;
|
|
50
|
-
learningRate: number;
|
|
51
|
-
maxSeqLen: number;
|
|
52
|
-
batchSize: number;
|
|
53
|
-
gradAccumSteps: number;
|
|
54
|
-
seed: number;
|
|
55
|
-
stepsPerReport: number;
|
|
56
|
-
stepsPerEval: number;
|
|
57
|
-
betas: [number, number];
|
|
58
|
-
weightDecay: number;
|
|
59
|
-
/** LoRA-input dropout (PEFT-style), 0 = off. Training-only regularizer for
|
|
60
|
-
* small preference sets. Recompute-safe: the mask is keyed by (step, layer)
|
|
61
|
-
* so segmented/checkpointed recompute reproduces it. */
|
|
62
|
-
loraDropout: number;
|
|
63
|
-
/** rsLoRA: scale the LoRA update by α/√rank instead of α, so per-layer rank
|
|
64
|
-
* scaling (by_bits/by_kl) changes capacity, not step size. Saved in the
|
|
65
|
-
* adapter config so inference applies the same per-layer scale. */
|
|
66
|
-
rsLora: boolean;
|
|
67
|
-
/** LoRA+: LR multiplier for the B leaves (A stays at the base LR). >1 speeds
|
|
68
|
-
* the B-driven early learning (B is zero-init). 1 = off. */
|
|
69
|
-
loraPlusRatio: number;
|
|
70
|
-
/** Gradient checkpointing: recompute layer activations in the backward pass
|
|
71
|
-
* to bound memory at long context (mlx-lm grad_checkpoint; default in optiq).
|
|
72
|
-
* Numerically identical to off — pure memory↔compute trade. */
|
|
73
|
-
gradCheckpoint: boolean;
|
|
74
|
-
/** Intra-layer MLP split (lever 4): with gradCheckpoint on (Gemma4 only),
|
|
75
|
-
* checkpoint the attention and MLP sub-blocks separately with the post-attn
|
|
76
|
-
* residual as the boundary, so the backward recompute holds max(attn,MLP)+hMid
|
|
77
|
-
* instead of the whole layer. Numerically identical to gradCheckpoint alone —
|
|
78
|
-
* a finer memory↔compute split. No effect unless gradCheckpoint is on. */
|
|
79
|
-
mlpSplit: boolean;
|
|
80
|
-
/** Segmented backward: layers per segment (0 = off). Streams the SFT backward
|
|
81
|
-
* segment-by-segment so only one segment's activations are live at a time —
|
|
82
|
-
* beats per-layer checkpointing at long context, where naive checkpointing
|
|
83
|
-
* holds every layer's recompute activations at once and crashes. B=1 SFT
|
|
84
|
-
* only (the responseOnlyCe path); mutually exclusive with gradCheckpoint.
|
|
85
|
-
* Numerically identical to off. See docs/design/orpo-training.md. */
|
|
86
|
-
segmentSize: number;
|
|
87
|
-
/** Keep ALL eval-step checkpoints (full mountable adapters under
|
|
88
|
-
* checkpoints/step-<NNNNN>-val<loss>/) + write metrics.json. Off by default
|
|
89
|
-
* so server finetune jobs and tests stay clean; the chunk-finetune script
|
|
90
|
-
* turns it on so the best-ON-TASK checkpoint can be chosen by the real eval
|
|
91
|
-
* afterward (val is only a proxy — we keep them all and let the eval pick). */
|
|
92
|
-
saveCheckpoints: boolean;
|
|
93
|
-
/** Gradient clipping: scale grads so their global L2 norm ≤ this before the
|
|
94
|
-
* optimizer step (0 = off). Standard ORPO/DPO stability guard — without it a
|
|
95
|
-
* single high-norm batch makes AdamW take a huge step and the loss diverges. */
|
|
96
|
-
gradClipNorm: number;
|
|
97
|
-
/** Cap validation examples evaluated per eval step (0 = all). Val accuracy is
|
|
98
|
-
* correct/total over a FIXED subset — bounds the denominator AND the per-eval
|
|
99
|
-
* cost (full val over thousands of pairs is minutes each). */
|
|
100
|
-
valMaxExamples: number;
|
|
101
|
-
// DPO
|
|
102
|
-
dpoBeta: number;
|
|
103
|
-
dpoWarmupIters: number;
|
|
104
|
-
dpoLrSchedule: "constant" | "cosine";
|
|
105
|
-
// ORPO (reference-free; method-prefixed knobs, mirroring the dpo trio).
|
|
106
|
-
/** λ — weights ONLY the odds-ratio term (the SFT-NLL term stays unweighted). */
|
|
107
|
-
orpoLambda: number;
|
|
108
|
-
orpoWarmupIters: number;
|
|
109
|
-
orpoLrSchedule: "constant" | "cosine";
|
|
110
|
-
/** Token-chunk size for the ORPO head (0 = off). >0 computes the B=1
|
|
111
|
-
* response-only LM head in token-chunks, each rematerialized in the backward,
|
|
112
|
-
* bounding the dominant `[M, vocab]` logits term to `[chunkSize, vocab]`.
|
|
113
|
-
* Exact (numerically identical). Non-segmented path only (segmentSize takes
|
|
114
|
-
* precedence). See docs/design/orpo-training.md → chunked fused log-prob. */
|
|
115
|
-
orpoChunkSize: number;
|
|
116
|
-
/** Use the FUSED linear cross-entropy head (Liger/CCE-style): one CustomVjp with
|
|
117
|
-
* an analytic softmax−onehot backward — no autograd through the head, no
|
|
118
|
-
* retained `[M,vocab]` logits in either direction. Composes with
|
|
119
|
-
* `orpoChunkSize` as the token-chunk size (defaults to 512 when unset). B=1,
|
|
120
|
-
* non-segmented path only; Gemma + MiniCPM5 heads. Value bit-exact vs the
|
|
121
|
-
* full-logits head, grads in the bf16 class. See docs/design/orpo-training.md
|
|
122
|
-
* → fused linear-CE head. */
|
|
123
|
-
orpoFusedCe: boolean;
|
|
124
|
-
/** Route the fused linear-CE head through the flash-CCE Metal kernel: in-kernel
|
|
125
|
-
* quantized logits + online softmax (fwd) and dh accumulation (bwd) — neither
|
|
126
|
-
* `[M,vocab]` nor a dequantized head touches HBM. Lowest memory + fastest on
|
|
127
|
-
* large vocab (e4b 262k / CPM 130k). Implies the fused head; the Apple-CCE coeff
|
|
128
|
-
* filter is on by default in the backward. B=1, non-segmented path. */
|
|
129
|
-
orpoFlashCe: boolean;
|
|
130
|
-
/** Shared prompt-prefix ORPO (lever 7): one forward over [prompt; chosen; rejected]
|
|
131
|
-
* with a block-sparse mask + block-wise RoPE, so the shared prompt is encoded ONCE
|
|
132
|
-
* (token cost 2(P+R) → P+2R). B=1, non-segmented path; MiniCPM5 + Gemma4 (e4b).
|
|
133
|
-
* Falls back to the two-forward orpoLoss for rows whose chosen/rejected prompts
|
|
134
|
-
* differ. Composes with orpoFlashCe/orpoFusedCe (each branch routes through the
|
|
135
|
-
* [M,V]-free head). See src/train/prefix-shared.ts. */
|
|
136
|
-
orpoPrefixShared: boolean;
|
|
137
|
-
/** ORPO L_SFT scope (`sft_scope`). "full" (default; paper/TRL-faithful):
|
|
138
|
-
* the chosen-NLL term is the token-mean cross-entropy over the FULL
|
|
139
|
-
* prompt+response (only padding excluded), computed from the same chosen
|
|
140
|
-
* forward — matching TRL's ORPOTrainer chosen_nll_loss. "response":
|
|
141
|
-
* L_NLL = -ℓw (response-only), bit-exact to pre-sft_scope runs. The
|
|
142
|
-
* odds-ratio ℓw/ℓr terms are response-only length-normalized means in BOTH
|
|
143
|
-
* modes (that also matches TRL). Applies to every ORPO path (naive /
|
|
144
|
-
* chunked / fused / flash / prefix-shared / segmented). orpo only. */
|
|
145
|
-
sftScope: SftScope;
|
|
146
|
-
/** Warm-start: path to an existing adapter dir (with adapters.safetensors) whose
|
|
147
|
-
* LoRA weights initialize this run instead of the random/zero init. Continues
|
|
148
|
-
* training from a checkpoint's weights; the optimizer state and LR schedule restart.
|
|
149
|
-
* Rank/targets must match the checkpoint. "" = off (fresh init). */
|
|
150
|
-
warmStartAdapter: string;
|
|
151
|
-
// output
|
|
152
|
-
adapterPath: string;
|
|
153
|
-
baseModel: string;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export interface TrainResult {
|
|
157
|
-
adapterPath: string;
|
|
158
|
-
appliedRanks: Record<string, number>;
|
|
159
|
-
numIters: number;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export const DEFAULT_TRAIN_CONFIG: TrainConfig = {
|
|
163
|
-
method: "sft",
|
|
164
|
-
rank: 8,
|
|
165
|
-
scale: 1.0,
|
|
166
|
-
rankScaling: "by_bits",
|
|
167
|
-
targetModules: [...DEFAULT_TARGET_MODULES],
|
|
168
|
-
numLayers: -1,
|
|
169
|
-
iters: 100,
|
|
170
|
-
learningRate: 2e-4,
|
|
171
|
-
maxSeqLen: 512,
|
|
172
|
-
batchSize: 1,
|
|
173
|
-
gradAccumSteps: 1,
|
|
174
|
-
seed: 0,
|
|
175
|
-
stepsPerReport: 10,
|
|
176
|
-
stepsPerEval: 50,
|
|
177
|
-
betas: [0.9, 0.999],
|
|
178
|
-
weightDecay: 0.01,
|
|
179
|
-
loraDropout: 0.0, // off by default
|
|
180
|
-
rsLora: false, // off by default (opt-in; recommended when rank_scaling != constant)
|
|
181
|
-
loraPlusRatio: 1.0, // 1 = off
|
|
182
|
-
mlpSplit: false, // default off; only effective with gradCheckpoint (Gemma4)
|
|
183
|
-
gradCheckpoint: false, // default off until validated; flip to true (optiq default) after
|
|
184
|
-
segmentSize: 0, // default off; >0 enables segmented backward (MiniCPM5 SFT B=1)
|
|
185
|
-
saveCheckpoints: false, // off by default; scripts opt in (keep-all-checkpoints + metrics.json)
|
|
186
|
-
gradClipNorm: 1.0, // on by default (standard ORPO/DPO guard); 0 = off
|
|
187
|
-
valMaxExamples: 256, // fixed val subset → fast, meaningful correct/total
|
|
188
|
-
|
|
189
|
-
dpoBeta: 0.1,
|
|
190
|
-
dpoWarmupIters: 0,
|
|
191
|
-
dpoLrSchedule: "cosine",
|
|
192
|
-
|
|
193
|
-
orpoLambda: 0.1, // paper's primary value + TRL default; weights only L_OR
|
|
194
|
-
orpoWarmupIters: 0,
|
|
195
|
-
orpoLrSchedule: "cosine",
|
|
196
|
-
orpoChunkSize: 0, // default off; >0 = token-chunked head (bounds the [M,vocab] term)
|
|
197
|
-
orpoFusedCe: false, // default off; true = fused linear-CE head (analytic backward, no [M,vocab])
|
|
198
|
-
orpoFlashCe: false, // default off; true = flash-CCE Metal-kernel head (implies fused)
|
|
199
|
-
orpoPrefixShared: false, // default off; true = shared prompt-prefix single forward (B=1)
|
|
200
|
-
sftScope: "full", // paper/TRL-faithful chosen-NLL over prompt+response; "response" = pre-2026-07 behavior
|
|
201
|
-
warmStartAdapter: "", // default off; path to an adapter dir to warm-start LoRA weights from
|
|
202
|
-
adapterPath: "adapters",
|
|
203
|
-
baseModel: "",
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
/** Build the per-leaf LoRA+ LR multipliers for flatParams' stable
|
|
207
|
-
* `[...A, ...B]` order. `undefined` keeps AdamW's uniform-LR fast path. */
|
|
208
|
-
export function loraPlusLrScale(
|
|
209
|
-
paramCount: number,
|
|
210
|
-
targetCount: number,
|
|
211
|
-
ratio: number,
|
|
212
|
-
): number[] | undefined {
|
|
213
|
-
if (!Number.isFinite(ratio) || ratio < 1)
|
|
214
|
-
throw new RangeError(`loraPlusRatio must be a finite number >= 1 (got ${ratio})`);
|
|
215
|
-
if (paramCount !== targetCount * 2)
|
|
216
|
-
throw new Error(
|
|
217
|
-
`LoRA+ expected ${targetCount * 2} A/B leaves for ${targetCount} targets, got ${paramCount}`,
|
|
218
|
-
);
|
|
219
|
-
if (ratio === 1) return undefined;
|
|
220
|
-
return Array.from({ length: paramCount }, (_, i) => (i < targetCount ? 1 : ratio));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** One optimizer policy for SFT, DPO, and ORPO. Keeping LoRA+ here prevents a
|
|
224
|
-
* method-specific loop from silently accepting the knob without applying it. */
|
|
225
|
-
function createLoraOptimizer(
|
|
226
|
-
params: MlxArray[],
|
|
227
|
-
lora: TrainableLora,
|
|
228
|
-
cfg: TrainConfig,
|
|
229
|
-
emit: Emit,
|
|
230
|
-
): AdamW {
|
|
231
|
-
const lrScale = loraPlusLrScale(params.length, lora.targets.length, cfg.loraPlusRatio);
|
|
232
|
-
if (lrScale) emit({
|
|
233
|
-
type: "stage",
|
|
234
|
-
stage: "setup",
|
|
235
|
-
progress: 0.04,
|
|
236
|
-
message: `LoRA+ : B leaves at ${cfg.loraPlusRatio}× LR`,
|
|
237
|
-
});
|
|
238
|
-
return new AdamW(
|
|
239
|
-
params,
|
|
240
|
-
{
|
|
241
|
-
lr: cfg.learningRate,
|
|
242
|
-
betas: cfg.betas,
|
|
243
|
-
eps: 1e-8,
|
|
244
|
-
weightDecay: cfg.weightDecay,
|
|
245
|
-
lrScale,
|
|
246
|
-
},
|
|
247
|
-
(i, p) => writeParam(lora, i, p),
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** Run a LoRA fine-tune. Emits per-step train metrics, periodic val metrics,
|
|
252
|
-
* and a final stage:done with the adapter path + applied ranks. Saves the
|
|
253
|
-
* adapter (last + best-on-val) and returns where it landed. */
|
|
254
|
-
export async function trainLora(
|
|
255
|
-
model: RuntimeModel,
|
|
256
|
-
tok: LoadedTokenizer,
|
|
257
|
-
tmpl: ChatTemplate,
|
|
258
|
-
dataDir: string,
|
|
259
|
-
cfg: TrainConfig,
|
|
260
|
-
emit: Emit,
|
|
261
|
-
): Promise<TrainResult> {
|
|
262
|
-
emit({ type: "stage", stage: "setup", progress: 0.02, message: "resolving ranks" });
|
|
263
|
-
|
|
264
|
-
// Mixed-precision rank scaling: feed the per-layer bits (from the loaded
|
|
265
|
-
// model's quant specs) and, for by_kl, the recorded KL sensitivities
|
|
266
|
-
// (optiq_metadata.json). Without these maps, by_bits/by_kl silently fell back
|
|
267
|
-
// to uniform rank — so the "give the sensitive (8-bit) layers more adapter
|
|
268
|
-
// capacity" policy (optiq: one sensitivity signal, two optimizations — the
|
|
269
|
-
// bit assignment AND the LoRA rank) never actually fired.
|
|
270
|
-
const bitsMap = bitsMapFromModel(model);
|
|
271
|
-
const klMap = cfg.rankScaling === "by_kl" ? await readPerLayerKl(cfg.baseModel) : undefined;
|
|
272
|
-
const ranks = resolveRanks(model, {
|
|
273
|
-
rank: cfg.rank,
|
|
274
|
-
rankScaling: cfg.rankScaling,
|
|
275
|
-
targetModules: cfg.targetModules,
|
|
276
|
-
numLayers: cfg.numLayers,
|
|
277
|
-
bitsMap,
|
|
278
|
-
klMap,
|
|
279
|
-
});
|
|
280
|
-
const appliedRanks: Record<string, number> = Object.fromEntries(ranks);
|
|
281
|
-
|
|
282
|
-
// Surface the rank spread so mixed-precision scaling is observable (e.g.
|
|
283
|
-
// "r8×140 r16×28" means 28 sensitive layers got the wider adapter).
|
|
284
|
-
const rankCounts = new Map<number, number>();
|
|
285
|
-
for (const r of ranks.values()) rankCounts.set(r, (rankCounts.get(r) ?? 0) + 1);
|
|
286
|
-
const rankSpread = [...rankCounts.entries()]
|
|
287
|
-
.sort((a, b) => a[0] - b[0])
|
|
288
|
-
.map(([r, n]) => `r${r}×${n}`)
|
|
289
|
-
.join(" ");
|
|
290
|
-
emit({ type: "stage", stage: "setup", progress: 0.03,
|
|
291
|
-
message: `ranks (${cfg.rankScaling}): ${rankSpread}` });
|
|
292
|
-
|
|
293
|
-
const lora = buildTrainableLora(model, ranks, cfg.scale, cfg.seed, cfg.rsLora);
|
|
294
|
-
// Warm-start: continue from a saved adapter/checkpoint's weights (optimizer + LR
|
|
295
|
-
// schedule restart fresh). Must run before the optimizer is built (below) so it
|
|
296
|
-
// tracks the loaded leaves. Rank/targets must match the checkpoint.
|
|
297
|
-
if (cfg.warmStartAdapter) {
|
|
298
|
-
const n = warmStartFromAdapter(lora, cfg.warmStartAdapter);
|
|
299
|
-
emit({ type: "stage", stage: "setup", progress: 0.035,
|
|
300
|
-
message: `warm-start: loaded ${n} LoRA targets from ${cfg.warmStartAdapter} (optimizer + LR schedule restart)` });
|
|
301
|
-
}
|
|
302
|
-
attachForTraining(model, lora, "train");
|
|
303
|
-
model.loraState.dropoutRate = cfg.loraDropout; // per-step seed set inside the loop
|
|
304
|
-
// Training-mode fused GeGLU (Gemma e4b): kernel forward + hand-derived vjp, so
|
|
305
|
-
// autograd flows and the backward recomputes the gelu from the primal instead
|
|
306
|
-
// of retaining the spelled-out intermediates. No-op for non-Gemma models.
|
|
307
|
-
// Cleared in the finally below.
|
|
308
|
-
|
|
309
|
-
const saveCfg: SaveAdapterConfig = {
|
|
310
|
-
rank: cfg.rank,
|
|
311
|
-
scale: cfg.scale,
|
|
312
|
-
rankScaling: cfg.rankScaling,
|
|
313
|
-
targetModules: cfg.targetModules,
|
|
314
|
-
numLayers: cfg.numLayers,
|
|
315
|
-
method: cfg.method,
|
|
316
|
-
baseModel: cfg.baseModel,
|
|
317
|
-
rsLora: cfg.rsLora,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
// Keep-ALL-checkpoints + a durable metrics record. At each eval step we
|
|
321
|
-
// snapshot the adapter to checkpoints/step-<NNNNN>-val<loss>/ — a full,
|
|
322
|
-
// mountable adapter — so the best-ON-TASK checkpoint can be chosen by the
|
|
323
|
-
// real eval afterward (val is only a proxy for task quality, so we do NOT
|
|
324
|
-
// pick for the user). The loss/val trajectory is written to metrics.json so
|
|
325
|
-
// runs are comparable and survive (the tee'd stdout log is ephemeral). We
|
|
326
|
-
// wrap `emit` rather than touching the loops: it already streams train/val
|
|
327
|
-
// metrics, and the live LoRA leaves are the current step's weights at emit
|
|
328
|
-
// time, so saving here captures exactly that checkpoint.
|
|
329
|
-
const trainHistory: { step: number; loss: number }[] = [];
|
|
330
|
-
// For preference methods (dpo/orpo) the val MARGIN (mean chosen−rejected
|
|
331
|
-
// log-odds) and accuracy are the meaningful early-stop signals — val loss is a
|
|
332
|
-
// weak proxy. Capture them when the loop emits them so metrics.json lets you
|
|
333
|
-
// pick the best-MARGIN checkpoint (overfit shows as train↑ / val-margin flat).
|
|
334
|
-
const valHistory: { step: number; loss: number; margin?: number; accuracy?: number; checkpoint: string | null }[] = [];
|
|
335
|
-
const checkpointSaves: Promise<void>[] = [];
|
|
336
|
-
const startedAt = Date.now();
|
|
337
|
-
|
|
338
|
-
// Live, append-only metrics stream (the source `mlx-bun train-watch` tails).
|
|
339
|
-
// Always on — it's the durable per-step record, a few MB at most, single
|
|
340
|
-
// writer, append-only. The first line is a `meta` record (totals + config) so
|
|
341
|
-
// a viewer attaching mid-run has the denominators (iters) and run identity
|
|
342
|
-
// without replaying. Each subsequent line is a metric event + a wall-clock `t`
|
|
343
|
-
// (ms) so the viewer derives s/step and ETA itself. Best-effort: a logging
|
|
344
|
-
// failure must never kill a run.
|
|
345
|
-
mkdirSync(cfg.adapterPath, { recursive: true });
|
|
346
|
-
const metricsJsonl = `${cfg.adapterPath}/metrics.jsonl`;
|
|
347
|
-
const writeMetricLine = (o: Record<string, unknown>) => {
|
|
348
|
-
try { appendFileSync(metricsJsonl, JSON.stringify(o) + "\n"); } catch { /* never throw */ }
|
|
349
|
-
};
|
|
350
|
-
try { writeFileSync(metricsJsonl, ""); } catch { /* truncate prior run's stream */ }
|
|
351
|
-
writeMetricLine({
|
|
352
|
-
type: "meta", t: startedAt, method: cfg.method, model: cfg.baseModel,
|
|
353
|
-
iters: cfg.iters, learning_rate: cfg.learningRate, max_seq_length: cfg.maxSeqLen,
|
|
354
|
-
batch_size: cfg.batchSize, rank: cfg.rank, scale: cfg.scale,
|
|
355
|
-
orpo_lambda: cfg.method === "orpo" ? cfg.orpoLambda : undefined,
|
|
356
|
-
sft_scope: cfg.method === "orpo" ? cfg.sftScope : undefined,
|
|
357
|
-
adapter_path: cfg.adapterPath,
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
const collect: Emit = (e) => {
|
|
361
|
-
if (e.type === "metric") writeMetricLine({ ...e, t: Date.now() });
|
|
362
|
-
if (e.type === "metric" && e.kind === "train") {
|
|
363
|
-
trainHistory.push({ step: e.step, loss: e.loss });
|
|
364
|
-
} else if (e.type === "metric" && e.kind === "val") {
|
|
365
|
-
let checkpoint: string | null = null;
|
|
366
|
-
if (cfg.saveCheckpoints) {
|
|
367
|
-
const tag = `step-${String(e.step).padStart(5, "0")}-val${e.loss.toFixed(4)}`;
|
|
368
|
-
checkpoint = `${cfg.adapterPath}/checkpoints/${tag}`;
|
|
369
|
-
const save = saveAdapter(lora, checkpoint, saveCfg, appliedRanks);
|
|
370
|
-
checkpointSaves.push(save);
|
|
371
|
-
// Keep a handler attached while training continues. Promise.all below
|
|
372
|
-
// still surfaces the original rejection before the run can succeed.
|
|
373
|
-
void save.catch(() => {});
|
|
374
|
-
emit({ type: "stage", stage: "checkpoint", progress: e.step / cfg.iters,
|
|
375
|
-
message: `checkpoint ${tag}` });
|
|
376
|
-
}
|
|
377
|
-
const ev = e as { step: number; loss: number; margin?: number; accuracy?: number };
|
|
378
|
-
valHistory.push({ step: ev.step, loss: ev.loss, margin: ev.margin, accuracy: ev.accuracy, checkpoint });
|
|
379
|
-
}
|
|
380
|
-
emit(e);
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
try {
|
|
384
|
-
const result =
|
|
385
|
-
cfg.method === "dpo"
|
|
386
|
-
? await dpoLoop(model, tok, tmpl, dataDir, cfg, lora, collect)
|
|
387
|
-
: cfg.method === "orpo"
|
|
388
|
-
? await orpoLoop(model, tok, tmpl, dataDir, cfg, lora, collect)
|
|
389
|
-
: await sftLoop(model, tok, tmpl, dataDir, cfg, lora, collect);
|
|
390
|
-
|
|
391
|
-
await Promise.all(checkpointSaves);
|
|
392
|
-
|
|
393
|
-
// Final save (last adapter).
|
|
394
|
-
detachTraining(model, lora);
|
|
395
|
-
await saveAdapter(lora, cfg.adapterPath, saveCfg, appliedRanks);
|
|
396
|
-
|
|
397
|
-
// Durable, structured run record alongside the adapter (only when we kept
|
|
398
|
-
// checkpoints). Pick the best-val one for convenience, but ship ALL of them
|
|
399
|
-
// — the real eval decides; val is only a proxy.
|
|
400
|
-
if (cfg.saveCheckpoints) {
|
|
401
|
-
let bestVal: { step: number; loss: number; checkpoint: string | null } | null = null;
|
|
402
|
-
for (const v of valHistory) if (bestVal === null || v.loss < bestVal.loss) bestVal = v;
|
|
403
|
-
// Best by MARGIN too (preference methods) — the better checkpoint signal.
|
|
404
|
-
let bestMargin: typeof valHistory[number] | null = null;
|
|
405
|
-
for (const v of valHistory)
|
|
406
|
-
if (v.margin !== undefined && (bestMargin === null || v.margin > (bestMargin.margin ?? -Infinity))) bestMargin = v;
|
|
407
|
-
writeFileSync(`${cfg.adapterPath}/metrics.json`, JSON.stringify({
|
|
408
|
-
config: {
|
|
409
|
-
method: cfg.method, rank: cfg.rank, scale: cfg.scale,
|
|
410
|
-
learningRate: cfg.learningRate, maxSeqLen: cfg.maxSeqLen,
|
|
411
|
-
segmentSize: cfg.segmentSize, iters: cfg.iters,
|
|
412
|
-
numLayers: cfg.numLayers, baseModel: cfg.baseModel,
|
|
413
|
-
orpoLambda: cfg.method === "orpo" ? cfg.orpoLambda : undefined,
|
|
414
|
-
sftScope: cfg.method === "orpo" ? cfg.sftScope : undefined,
|
|
415
|
-
},
|
|
416
|
-
wallSeconds: (Date.now() - startedAt) / 1000,
|
|
417
|
-
peakGb: peakMemory() / 1e9,
|
|
418
|
-
finalTrainLoss: trainHistory.at(-1)?.loss ?? null,
|
|
419
|
-
finalValLoss: valHistory.at(-1)?.loss ?? null,
|
|
420
|
-
bestVal,
|
|
421
|
-
bestMargin, // null for SFT (no margin); the recommended pick for dpo/orpo
|
|
422
|
-
valTrajectory: valHistory,
|
|
423
|
-
trainLosses: trainHistory,
|
|
424
|
-
}, null, 2));
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
emit({
|
|
428
|
-
type: "stage",
|
|
429
|
-
stage: "done",
|
|
430
|
-
progress: 1,
|
|
431
|
-
message: `adapter saved to ${cfg.adapterPath}`,
|
|
432
|
-
adapter_path: cfg.adapterPath,
|
|
433
|
-
applied_ranks: appliedRanks,
|
|
434
|
-
});
|
|
435
|
-
return { adapterPath: cfg.adapterPath, appliedRanks, numIters: result.numIters };
|
|
436
|
-
} finally {
|
|
437
|
-
disposeLora(lora);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// ---------------------------------------------------------------------------
|
|
442
|
-
// Segment planning: uniform planSegmentsBySize only. A full-attention
|
|
443
|
-
// ISOLATION planner (kernel-review backlog #2 / docs/design/orpo-training.md
|
|
444
|
-
// §5) was built and A/B-measured 2026-07-02: ZERO peak win — mlx's sdpa
|
|
445
|
-
// backward is O(L²) for EVERY layer (~3.5 GB/layer @8K e4b), sliding included,
|
|
446
|
-
// so the worst segment is set by layer count alone and segment_size is the
|
|
447
|
-
// whole knob (seg1 = 14.59 GB @8K, +3% step time — fits the 24 GB M4 Pro).
|
|
448
|
-
// Evidence: seg-isolation-smoke.ts (deleted 2026-08-23; git history); note in segmented.ts.
|
|
449
|
-
// ---------------------------------------------------------------------------
|
|
450
|
-
|
|
451
|
-
// ---------------------------------------------------------------------------
|
|
452
|
-
// SFT loop
|
|
453
|
-
// ---------------------------------------------------------------------------
|
|
454
|
-
|
|
455
|
-
async function sftLoop(
|
|
456
|
-
model: RuntimeModel,
|
|
457
|
-
tok: LoadedTokenizer,
|
|
458
|
-
tmpl: ChatTemplate,
|
|
459
|
-
dataDir: string,
|
|
460
|
-
cfg: TrainConfig,
|
|
461
|
-
lora: TrainableLora,
|
|
462
|
-
emit: Emit,
|
|
463
|
-
): Promise<{ numIters: number }> {
|
|
464
|
-
emit({ type: "stage", stage: "data", progress: 0.05, message: "loading SFT dataset" });
|
|
465
|
-
const train = await loadSftDataset(`${dataDir}/train.jsonl`, tok, tmpl);
|
|
466
|
-
const valid = (await fileExists(`${dataDir}/valid.jsonl`))
|
|
467
|
-
? await loadSftDataset(`${dataDir}/valid.jsonl`, tok, tmpl)
|
|
468
|
-
: [];
|
|
469
|
-
|
|
470
|
-
const params = flatParams(lora);
|
|
471
|
-
const opt = createLoraOptimizer(params, lora, cfg, emit);
|
|
472
|
-
|
|
473
|
-
// Segmented backward: stream the SFT backward segment-by-segment (only one
|
|
474
|
-
// segment's activations live at a time). Phase A is MiniCPM5 SFT B=1 only;
|
|
475
|
-
// it replaces the single value_and_grad below and is mutually exclusive with
|
|
476
|
-
// gradient checkpointing (see docs/design/orpo-training.md).
|
|
477
|
-
const useSegmented = cfg.segmentSize > 0;
|
|
478
|
-
let segmented: SegmentedBackward | SegmentedBackwardGemma4 | null = null;
|
|
479
|
-
if (useSegmented) {
|
|
480
|
-
if (!(model instanceof MiniCPM5Model) && !(model instanceof Gemma4Model))
|
|
481
|
-
throw new Error("segmented backward (segmentSize > 0) is only wired for MiniCPM5 and Gemma4");
|
|
482
|
-
const ranges = planSegmentsBySize(model.layers.length, cfg.segmentSize);
|
|
483
|
-
segmented = model instanceof MiniCPM5Model
|
|
484
|
-
? new SegmentedBackward(model, lora, ranges)
|
|
485
|
-
: new SegmentedBackwardGemma4(model, lora, ranges);
|
|
486
|
-
emit({ type: "stage", stage: "setup", progress: 0.04,
|
|
487
|
-
message: `segmented backward: ${ranges.length} segments of <=${cfg.segmentSize} layers` });
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// The value_and_grad loss closure: temporarily swap the differentiated
|
|
491
|
-
// primals into the LoraWeights so the forward graph differentiates them,
|
|
492
|
-
// build the SFT loss against the active batch, then restore the original
|
|
493
|
-
// leaf wrappers (the primal wrappers get disposed by the autograd closure;
|
|
494
|
-
// the graph already captured their handles). Not built on the segmented path
|
|
495
|
-
// (segmentedSftGrads runs its own per-segment value_and_grads).
|
|
496
|
-
let currentBatch: SftBatch | null = null;
|
|
497
|
-
const vag = useSegmented
|
|
498
|
-
? null
|
|
499
|
-
: new ValueAndGrad((primals) => {
|
|
500
|
-
const saved = swapPrimals(lora, primals);
|
|
501
|
-
try {
|
|
502
|
-
return sftLoss(model, currentBatch!);
|
|
503
|
-
} finally {
|
|
504
|
-
restorePrimals(lora, saved);
|
|
505
|
-
}
|
|
506
|
-
}, params.map((_, i) => i));
|
|
507
|
-
|
|
508
|
-
// Gradient checkpointing: group each layer's LoRA weights so forwardLayers
|
|
509
|
-
// can thread them as explicit checkpoint inputs (see Gemma4Model.gradCkpt).
|
|
510
|
-
// Checkpoints are created per forward and disposed after each step's backward.
|
|
511
|
-
let ckptCtx: GradCheckpointCtx | null = null;
|
|
512
|
-
if (!useSegmented && cfg.gradCheckpoint && model instanceof Gemma4Model) {
|
|
513
|
-
// Partition each layer's LoRA into the attention sub-block (self_attn.*) and
|
|
514
|
-
// the MLP sub-block (everything else in the layer: gate/up/down_proj +
|
|
515
|
-
// per_layer_*) so the split-MLP checkpoint can wrap them independently.
|
|
516
|
-
const byLayer: GradCheckpointCtx["byLayer"] = new Map();
|
|
517
|
-
for (const t of lora.targets) {
|
|
518
|
-
const m = t.modulePath.match(/\.layers\.(\d+)\./);
|
|
519
|
-
if (!m) continue;
|
|
520
|
-
const li = Number(m[1]);
|
|
521
|
-
const ll = byLayer.get(li) ?? byLayer.set(li, { attn: [], mlp: [] }).get(li)!;
|
|
522
|
-
(t.modulePath.includes(".self_attn.") ? ll.attn : ll.mlp).push(t.lw);
|
|
523
|
-
}
|
|
524
|
-
ckptCtx = { byLayer, splitMlp: cfg.mlpSplit, keepAlive: [] };
|
|
525
|
-
model.gradCkpt = ckptCtx;
|
|
526
|
-
if (cfg.mlpSplit) emit({ type: "stage", stage: "setup", progress: 0.04,
|
|
527
|
-
message: "grad checkpoint: intra-layer attn/MLP split" });
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Training attention. Default = mlx fused SDPA (ops.sdpa): exact dQ/dK/dV
|
|
531
|
-
// (finite-difference verified) and fast, but O(L²) backward memory. The
|
|
532
|
-
// hand-rolled flash kernel is an opt-in (MLX_BUN_TRAIN_ATTN=flash) O(L)-memory
|
|
533
|
-
// path for memory-bound long context — now correct (its dK-transpose and
|
|
534
|
-
// dQ-causal-barrier bugs were fixed; FD-validated in
|
|
535
|
-
// flash-fd-check.ts (deleted 2026-08-23; git history)), though ~30× slower than ops.sdpa.
|
|
536
|
-
// Cleared in finally.
|
|
537
|
-
//
|
|
538
|
-
// GEMMA GUARD (same convention as the launcher's env sanitization in
|
|
539
|
-
// cli.ts / job.ts, but enforced at the trainer): e4b on this path SIGTRAPed
|
|
540
|
-
// (uncatchable native crash) at multi-K sequence lengths (>=2K, reproduced in
|
|
541
|
-
// segmented-grad-test-e4b.ts (deleted 2026-08-23; git history); docs/reference/training.md)
|
|
542
|
-
// and has NOT been re-validated at that scale since the two kernel fixes —
|
|
543
|
-
// the regression tests stop at T<=256. cli.ts even defaults e4b seq to 8192,
|
|
544
|
-
// so a stale `export MLX_BUN_TRAIN_ATTN=flash` from a MiniCPM5 experiment
|
|
545
|
-
// would otherwise ride silently into a crash mid-run. Refuse it for Gemma
|
|
546
|
-
// until the >=2K re-validation lands; MiniCPM5 stays allowed.
|
|
547
|
-
if (runtimeValue("MLX_BUN_TRAIN_ATTN") === "flash") {
|
|
548
|
-
if (model instanceof Gemma4Model)
|
|
549
|
-
throw new Error(
|
|
550
|
-
"MLX_BUN_TRAIN_ATTN=flash is disabled for Gemma models: e4b SIGTRAPs on this " +
|
|
551
|
-
"path at seq >= 2048 (docs/reference/training.md) and it has not been " +
|
|
552
|
-
"re-validated at that scale since the kernel fixes. Unset MLX_BUN_TRAIN_ATTN " +
|
|
553
|
-
"(ops.sdpa, the default, is exact and ~30x faster) or train MiniCPM5.",
|
|
554
|
-
);
|
|
555
|
-
emit({ type: "stage", stage: "setup", progress: 0.045,
|
|
556
|
-
message: "training attention: hand-rolled flash kernel (MLX_BUN_TRAIN_ATTN=flash; O(L) memory, ~30x slower than ops.sdpa)" });
|
|
557
|
-
setTrainingAttn("flash");
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Pad with the eos id when available (a real sentinel token); padded
|
|
561
|
-
// positions are excluded from both the loss and attention masks, so the
|
|
562
|
-
// exact pad value never affects the result — it just must be a valid id.
|
|
563
|
-
const padId = tok.eosTokenId ?? 0;
|
|
564
|
-
const batches = iterateSftBatches(train, cfg.batchSize, cfg.maxSeqLen, cfg.seed, true, padId);
|
|
565
|
-
// tokens/sec follows mlx-lm's tuner convention: accumulate the response-token
|
|
566
|
-
// count across the REPORT WINDOW and divide by the window's own wall time
|
|
567
|
-
// (validation time excluded) — not one step's count extrapolated over the
|
|
568
|
-
// whole run, which jumps ~10x step-to-step on variable-length data.
|
|
569
|
-
let windowTokens = 0;
|
|
570
|
-
let windowStart = Date.now();
|
|
571
|
-
|
|
572
|
-
try {
|
|
573
|
-
for (let step = 1; step <= cfg.iters; step++) {
|
|
574
|
-
if (MEM_LOG) resetPeakMemory();
|
|
575
|
-
|
|
576
|
-
// One optimizer step over cfg.gradAccumSteps micro-batches (pass-through
|
|
577
|
-
// when 1). afterMicroEval frees each micro-batch's gradient-checkpoint
|
|
578
|
-
// closures right after its backward so they don't pile up across
|
|
579
|
-
// accumulation.
|
|
580
|
-
const { loss: lossVal, grads } = accumulateStep(
|
|
581
|
-
cfg.gradAccumSteps,
|
|
582
|
-
() => {
|
|
583
|
-
currentBatch = batches.next().value as SftBatch;
|
|
584
|
-
windowTokens += countResponseTokens(currentBatch);
|
|
585
|
-
return segmented ? segmented.step(currentBatch!) : vag!.apply(flatParams(lora));
|
|
586
|
-
},
|
|
587
|
-
ckptCtx
|
|
588
|
-
? () => {
|
|
589
|
-
for (const ck of ckptCtx!.keepAlive) ck.dispose();
|
|
590
|
-
ckptCtx!.keepAlive.length = 0;
|
|
591
|
-
}
|
|
592
|
-
: undefined,
|
|
593
|
-
);
|
|
594
|
-
|
|
595
|
-
const gradNorm = globalNorm(grads); // grads already materialized above
|
|
596
|
-
clipGradsByNorm(grads, gradNorm, cfg.gradClipNorm);
|
|
597
|
-
opt.step(grads);
|
|
598
|
-
opt.evalState();
|
|
599
|
-
if (MEM_LOG) {
|
|
600
|
-
const pk = peakMemory(), act = activeMemory(), ch = cacheMemory();
|
|
601
|
-
clearCache();
|
|
602
|
-
console.log(` [mem] step ${step}: PEAK(live)=${gbStr(pk)} active=${gbStr(act)} cache=${gbStr(ch)} -> after clearCache active=${gbStr(activeMemory())}`);
|
|
603
|
-
} else {
|
|
604
|
-
clearCache();
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (step % cfg.stepsPerReport === 0 || step === 1) {
|
|
608
|
-
const elapsed = (Date.now() - windowStart) / 1000;
|
|
609
|
-
emit({
|
|
610
|
-
type: "metric",
|
|
611
|
-
kind: "train",
|
|
612
|
-
step,
|
|
613
|
-
loss: lossVal,
|
|
614
|
-
grad_norm: gradNorm,
|
|
615
|
-
learning_rate: opt.lr,
|
|
616
|
-
tokens_per_sec: elapsed > 0 ? windowTokens / elapsed : 0,
|
|
617
|
-
progress: step / cfg.iters,
|
|
618
|
-
message: `step ${step}/${cfg.iters} loss=${lossVal.toFixed(4)}`,
|
|
619
|
-
});
|
|
620
|
-
windowTokens = 0;
|
|
621
|
-
windowStart = Date.now();
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (valid.length > 0 && step % cfg.stepsPerEval === 0) {
|
|
625
|
-
// Eval is forward-only (no backward), so disable checkpointing for it —
|
|
626
|
-
// it adds no benefit and a forward-only pass fits without it.
|
|
627
|
-
if (ckptCtx) (model as Gemma4Model).gradCkpt = null;
|
|
628
|
-
const tVal = Date.now();
|
|
629
|
-
const { loss: vLoss, used, skipped } = evalSftLoss(model, valid, cfg);
|
|
630
|
-
if (ckptCtx) (model as Gemma4Model).gradCkpt = ckptCtx;
|
|
631
|
-
// A silently-shrinking val set must be visible: rows whose supervised
|
|
632
|
-
// span is empty after truncation are skipped BY DESIGN (mlx-lm drops
|
|
633
|
-
// them at dataset build time) — report how many, and over what n the
|
|
634
|
-
// mean was taken. Any other per-row failure now rethrows (see
|
|
635
|
-
// evalSftLoss).
|
|
636
|
-
emit({ type: "metric", kind: "val", step, loss: vLoss, progress: step / cfg.iters,
|
|
637
|
-
val_rows_used: used, val_rows_skipped: skipped,
|
|
638
|
-
...(skipped > 0 ? { message: `val loss over ${used} rows (${skipped} skipped: no response tokens to supervise)` } : {}) });
|
|
639
|
-
// validation wall time is not training throughput — keep it out of the window
|
|
640
|
-
windowStart += Date.now() - tVal;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
} finally {
|
|
644
|
-
setTrainingAttn(null);
|
|
645
|
-
vag?.dispose();
|
|
646
|
-
segmented?.dispose();
|
|
647
|
-
opt.dispose();
|
|
648
|
-
if (ckptCtx) {
|
|
649
|
-
(model as Gemma4Model).gradCkpt = null;
|
|
650
|
-
for (const ck of ckptCtx.keepAlive) ck.dispose();
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
return { numIters: cfg.iters };
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function evalSftLoss(
|
|
657
|
-
model: RuntimeModel, valid: ReturnType<typeof Array.prototype.slice>, cfg: TrainConfig,
|
|
658
|
-
): { loss: number; used: number; skipped: number } {
|
|
659
|
-
let total = 0;
|
|
660
|
-
let n = 0;
|
|
661
|
-
let skipped = 0;
|
|
662
|
-
for (const ex of valid as { ids: number[]; promptLen: number }[]) {
|
|
663
|
-
let ids = ex.ids;
|
|
664
|
-
let promptLen = ex.promptLen;
|
|
665
|
-
if (ids.length < 2) { skipped++; continue; }
|
|
666
|
-
if (ids.length > cfg.maxSeqLen) {
|
|
667
|
-
ids = ids.slice(0, cfg.maxSeqLen);
|
|
668
|
-
promptLen = Math.min(promptLen, Math.max(0, ids.length - 1));
|
|
669
|
-
}
|
|
670
|
-
const batch: SftBatch = { ids: [ids], promptLens: [promptLen] };
|
|
671
|
-
try {
|
|
672
|
-
const loss = sftLoss(model, batch);
|
|
673
|
-
total += loss.toFloat32()[0]!;
|
|
674
|
-
loss.dispose();
|
|
675
|
-
n++;
|
|
676
|
-
} catch (e) {
|
|
677
|
-
// ONLY the empty-supervision row is skippable (a truncated row whose
|
|
678
|
-
// response fell entirely past maxSeqLen — mlx-lm drops these at dataset
|
|
679
|
-
// build). Everything else (MLX OOM/shape/vjp errors surface as catchable
|
|
680
|
-
// JS Errors via the ffi handler) must rethrow: swallowing them silently
|
|
681
|
-
// reduces the val set to its easy rows and reports a plausible loss.
|
|
682
|
-
if (e instanceof Error && e.message === "sftLoss: no response tokens to supervise") skipped++;
|
|
683
|
-
else throw e;
|
|
684
|
-
}
|
|
685
|
-
clearCache();
|
|
686
|
-
}
|
|
687
|
-
return { loss: n > 0 ? total / n : 0, used: n, skipped };
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// ---------------------------------------------------------------------------
|
|
691
|
-
// DPO loop
|
|
692
|
-
// ---------------------------------------------------------------------------
|
|
693
|
-
|
|
694
|
-
async function dpoLoop(
|
|
695
|
-
model: RuntimeModel,
|
|
696
|
-
tok: LoadedTokenizer,
|
|
697
|
-
tmpl: ChatTemplate,
|
|
698
|
-
dataDir: string,
|
|
699
|
-
cfg: TrainConfig,
|
|
700
|
-
lora: TrainableLora,
|
|
701
|
-
emit: Emit,
|
|
702
|
-
): Promise<{ numIters: number }> {
|
|
703
|
-
emit({ type: "stage", stage: "data", progress: 0.05, message: "loading DPO dataset" });
|
|
704
|
-
const train = await loadDpoDataset(`${dataDir}/train.jsonl`, tok, tmpl, cfg.maxSeqLen);
|
|
705
|
-
const valid = (await fileExists(`${dataDir}/valid.jsonl`))
|
|
706
|
-
? await loadDpoDataset(`${dataDir}/valid.jsonl`, tok, tmpl, cfg.maxSeqLen)
|
|
707
|
-
: [];
|
|
708
|
-
|
|
709
|
-
const params = flatParams(lora);
|
|
710
|
-
const opt = createLoraOptimizer(params, lora, cfg, emit);
|
|
711
|
-
|
|
712
|
-
const schedule =
|
|
713
|
-
cfg.dpoLrSchedule === "cosine" || cfg.dpoWarmupIters > 0
|
|
714
|
-
? warmupCosineSchedule(cfg.learningRate, cfg.dpoWarmupIters, cfg.iters)
|
|
715
|
-
: null;
|
|
716
|
-
|
|
717
|
-
// Reference log-probs are computed each step OUTSIDE value_and_grad (so the
|
|
718
|
-
// reference activations free before the policy forwards), then read as
|
|
719
|
-
// detached constants inside the closure.
|
|
720
|
-
let currentBatch: DpoBatch | null = null;
|
|
721
|
-
let refChosen: MlxArray | null = null;
|
|
722
|
-
let refRejected: MlxArray | null = null;
|
|
723
|
-
|
|
724
|
-
const vag = new ValueAndGrad((primals) => {
|
|
725
|
-
const saved = swapPrimals(lora, primals);
|
|
726
|
-
try {
|
|
727
|
-
return dpoLoss(model, currentBatch!, cfg.dpoBeta, refChosen!, refRejected!);
|
|
728
|
-
} finally {
|
|
729
|
-
restorePrimals(lora, saved);
|
|
730
|
-
}
|
|
731
|
-
}, params.map((_, i) => i));
|
|
732
|
-
|
|
733
|
-
const padId = tok.eosTokenId ?? 0;
|
|
734
|
-
const batches = iterateDpoBatches(train, cfg.batchSize, cfg.seed, true, padId);
|
|
735
|
-
const t0 = Date.now();
|
|
736
|
-
|
|
737
|
-
try {
|
|
738
|
-
for (let step = 1; step <= cfg.iters; step++) {
|
|
739
|
-
if (schedule) opt.lr = schedule(step);
|
|
740
|
-
|
|
741
|
-
// One optimizer step over cfg.gradAccumSteps micro-batches (pass-through
|
|
742
|
-
// when 1). Reference log-probs are recomputed per micro-batch OUTSIDE the
|
|
743
|
-
// policy value_and_grad and read as detached constants inside it;
|
|
744
|
-
// afterMicroEval frees them once that micro-batch's grads are materialized.
|
|
745
|
-
const { loss: lossVal, grads } = accumulateStep(
|
|
746
|
-
cfg.gradAccumSteps,
|
|
747
|
-
() => {
|
|
748
|
-
currentBatch = batches.next().value as DpoBatch;
|
|
749
|
-
const refs = dpoRefLogps(model, lora, currentBatch);
|
|
750
|
-
refChosen = refs.refChosen;
|
|
751
|
-
refRejected = refs.refRejected;
|
|
752
|
-
return vag.apply(flatParams(lora));
|
|
753
|
-
},
|
|
754
|
-
() => {
|
|
755
|
-
refChosen?.dispose();
|
|
756
|
-
refRejected?.dispose();
|
|
757
|
-
refChosen = refRejected = null;
|
|
758
|
-
},
|
|
759
|
-
);
|
|
760
|
-
|
|
761
|
-
const gradNorm = globalNorm(grads);
|
|
762
|
-
clipGradsByNorm(grads, gradNorm, cfg.gradClipNorm);
|
|
763
|
-
opt.step(grads);
|
|
764
|
-
opt.evalState();
|
|
765
|
-
clearCache();
|
|
766
|
-
|
|
767
|
-
if (step % cfg.stepsPerReport === 0 || step === 1) {
|
|
768
|
-
const m = dpoMetrics(model, lora, currentBatch!, cfg.dpoBeta);
|
|
769
|
-
const elapsed = (Date.now() - t0) / 1000;
|
|
770
|
-
emit({
|
|
771
|
-
type: "metric",
|
|
772
|
-
kind: "train",
|
|
773
|
-
step,
|
|
774
|
-
loss: lossVal,
|
|
775
|
-
grad_norm: gradNorm,
|
|
776
|
-
learning_rate: opt.lr,
|
|
777
|
-
accuracy: m.accuracy,
|
|
778
|
-
margin: m.margin,
|
|
779
|
-
tokens_per_sec: 0,
|
|
780
|
-
progress: step / cfg.iters,
|
|
781
|
-
message: `step ${step}/${cfg.iters} loss=${lossVal.toFixed(4)} acc=${m.accuracy.toFixed(2)} margin=${m.margin.toFixed(3)}`,
|
|
782
|
-
});
|
|
783
|
-
clearCache();
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (valid.length > 0 && step % cfg.stepsPerEval === 0) {
|
|
787
|
-
let vl = 0, va = 0, vm = 0, vn = 0;
|
|
788
|
-
for (const ex of valid) {
|
|
789
|
-
const vb: DpoBatch = {
|
|
790
|
-
chosenIds: [ex.chosenIds], rejectedIds: [ex.rejectedIds],
|
|
791
|
-
chosenMask: [ex.chosenMask], rejectedMask: [ex.rejectedMask],
|
|
792
|
-
};
|
|
793
|
-
const m = dpoMetrics(model, lora, vb, cfg.dpoBeta);
|
|
794
|
-
vl += m.loss; va += m.accuracy; vm += m.margin; vn++;
|
|
795
|
-
clearCache();
|
|
796
|
-
}
|
|
797
|
-
emit({
|
|
798
|
-
type: "metric", kind: "val", step,
|
|
799
|
-
loss: vn ? vl / vn : 0,
|
|
800
|
-
accuracy: vn ? va / vn : 0,
|
|
801
|
-
margin: vn ? vm / vn : 0,
|
|
802
|
-
progress: step / cfg.iters,
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
} finally {
|
|
807
|
-
vag.dispose();
|
|
808
|
-
opt.dispose();
|
|
809
|
-
// Cast: refs are only ever assigned inside the accumulateStep closures, so
|
|
810
|
-
// TS's flow analysis narrows these to their `null` initializer here.
|
|
811
|
-
(refChosen as MlxArray | null)?.dispose();
|
|
812
|
-
(refRejected as MlxArray | null)?.dispose();
|
|
813
|
-
}
|
|
814
|
-
return { numIters: cfg.iters };
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// ---------------------------------------------------------------------------
|
|
818
|
-
// ORPO loop (reference-free: 2 forwards/step, no reference pass)
|
|
819
|
-
// ---------------------------------------------------------------------------
|
|
820
|
-
|
|
821
|
-
async function orpoLoop(
|
|
822
|
-
model: RuntimeModel,
|
|
823
|
-
tok: LoadedTokenizer,
|
|
824
|
-
tmpl: ChatTemplate,
|
|
825
|
-
dataDir: string,
|
|
826
|
-
cfg: TrainConfig,
|
|
827
|
-
lora: TrainableLora,
|
|
828
|
-
emit: Emit,
|
|
829
|
-
): Promise<{ numIters: number }> {
|
|
830
|
-
emit({ type: "stage", stage: "data", progress: 0.05, message: "loading preference dataset" });
|
|
831
|
-
const train = await loadDpoDataset(`${dataDir}/train.jsonl`, tok, tmpl, cfg.maxSeqLen);
|
|
832
|
-
const valid = (await fileExists(`${dataDir}/valid.jsonl`))
|
|
833
|
-
? await loadDpoDataset(`${dataDir}/valid.jsonl`, tok, tmpl, cfg.maxSeqLen)
|
|
834
|
-
: [];
|
|
835
|
-
|
|
836
|
-
const params = flatParams(lora);
|
|
837
|
-
const opt = createLoraOptimizer(params, lora, cfg, emit);
|
|
838
|
-
|
|
839
|
-
const schedule =
|
|
840
|
-
cfg.orpoLrSchedule === "cosine" || cfg.orpoWarmupIters > 0
|
|
841
|
-
? warmupCosineSchedule(cfg.learningRate, cfg.orpoWarmupIters, cfg.iters)
|
|
842
|
-
: null;
|
|
843
|
-
|
|
844
|
-
// Segmented backward for ORPO: stream chosen+rejected backwards segment-by-segment.
|
|
845
|
-
// MiniCPM5 + Gemma4 (e4b) — same model coverage as SFT segmented. B=1 only.
|
|
846
|
-
const useSegmented = cfg.segmentSize > 0;
|
|
847
|
-
// Prefix-sharing + segmented backward COMPOSE (M3-composition): the segmented
|
|
848
|
-
// backward streams the ONE prefix-shared concat forward [prompt; chosen; rejected]
|
|
849
|
-
// (block-sparse mask via PrefixSharedCache + block-wise RoPE via the prefix plan)
|
|
850
|
-
// segment-by-segment — so the prompt-encode-once saving holds AT long seq. Composed
|
|
851
|
-
// for BOTH MiniCPM5 and Gemma4/e4b (the e4b variant threads donor-KV + the
|
|
852
|
-
// logical-position sliding-window prefix mask through the segment boundaries).
|
|
853
|
-
const useSegmentedPrefix = cfg.orpoPrefixShared && useSegmented;
|
|
854
|
-
if (cfg.orpoPrefixShared && !(model instanceof MiniCPM5Model || model instanceof Gemma4Model))
|
|
855
|
-
throw new Error("orpoPrefixShared is only wired for MiniCPM5 and Gemma4");
|
|
856
|
-
// Fused linear-CE head INSIDE the segmented backward (orpo_fused_ce): bounds the
|
|
857
|
-
// head term to [chunk,V] alongside the per-segment layer savings. 0 = the
|
|
858
|
-
// full-[M,V] responseOnlyLogpMean head (unchanged). Reuses orpoChunkSize as the
|
|
859
|
-
// token-chunk (default 512), same as the non-segmented path.
|
|
860
|
-
// "flash implies fused": orpoFlashCe alone must still bound the segmented head to
|
|
861
|
-
// [chunk,V]. The non-prefix segmented class has no flash path, so flash routes
|
|
862
|
-
// through its fused/checkpoint chunked head (boundedHeadFromHidden) — never the
|
|
863
|
-
// full-[M,V] responseOnlyLogpMean, which would reintroduce [M,vocab] on e4b.
|
|
864
|
-
const segUsesFusedHead = cfg.orpoFusedCe || cfg.orpoFlashCe;
|
|
865
|
-
const segFusedChunk = segUsesFusedHead ? (cfg.orpoChunkSize > 0 ? cfg.orpoChunkSize : 512) : 0;
|
|
866
|
-
// The prefix segmented head gathers response positions then routes per-branch
|
|
867
|
-
// through the [M,V]-free flash/fused head (branchLogpMeanGathered, which takes a
|
|
868
|
-
// ChunkCtx). Its own sink is managed per-step inside the class.
|
|
869
|
-
const segPrefixChunk: ChunkCtx | undefined =
|
|
870
|
-
cfg.orpoFusedCe || cfg.orpoFlashCe
|
|
871
|
-
? { chunkSize: cfg.orpoChunkSize > 0 ? cfg.orpoChunkSize : 512, fused: true, flash: cfg.orpoFlashCe, sink: [] }
|
|
872
|
-
: undefined;
|
|
873
|
-
let segmentedOrpo: SegmentedBackwardOrpo | SegmentedBackwardOrpoGemma4 | null = null;
|
|
874
|
-
let segmentedOrpoPrefix: SegmentedBackwardOrpoPrefix | SegmentedBackwardOrpoPrefixGemma4 | null = null;
|
|
875
|
-
if (useSegmented) {
|
|
876
|
-
if (!(model instanceof MiniCPM5Model) && !(model instanceof Gemma4Model))
|
|
877
|
-
throw new Error("segmented backward (segmentSize > 0) for ORPO is only wired for MiniCPM5 and Gemma4");
|
|
878
|
-
const ranges = planSegmentsBySize(model.layers.length, cfg.segmentSize);
|
|
879
|
-
if (useSegmentedPrefix) {
|
|
880
|
-
// The composed prefix-shared segmented backward (MiniCPM5 or Gemma4/e4b), plus a
|
|
881
|
-
// plain segmented-ORPO fallback for rows whose chosen/rejected prompts differ
|
|
882
|
-
// (splitPrefixBatch returns null -> two-forward segmented step).
|
|
883
|
-
if (model instanceof MiniCPM5Model) {
|
|
884
|
-
segmentedOrpoPrefix = new SegmentedBackwardOrpoPrefix(model, lora, ranges, cfg.orpoLambda, segPrefixChunk, cfg.sftScope);
|
|
885
|
-
segmentedOrpo = new SegmentedBackwardOrpo(model, lora, ranges, cfg.orpoLambda, segFusedChunk, cfg.sftScope);
|
|
886
|
-
} else {
|
|
887
|
-
segmentedOrpoPrefix = new SegmentedBackwardOrpoPrefixGemma4(model as Gemma4Model, lora, ranges, cfg.orpoLambda, segPrefixChunk, cfg.sftScope);
|
|
888
|
-
segmentedOrpo = new SegmentedBackwardOrpoGemma4(model as Gemma4Model, lora, ranges, cfg.orpoLambda, segFusedChunk, cfg.sftScope);
|
|
889
|
-
}
|
|
890
|
-
emit({ type: "stage", stage: "setup", progress: 0.04,
|
|
891
|
-
message: `orpo segmented + prefix-share (${model instanceof Gemma4Model ? "e4b" : "MiniCPM5"}): single concat forward streamed over ${ranges.length} segments of <=${cfg.segmentSize} layers (two-forward fallback on prompt mismatch)` });
|
|
892
|
-
} else if (model instanceof MiniCPM5Model)
|
|
893
|
-
segmentedOrpo = new SegmentedBackwardOrpo(model, lora, ranges, cfg.orpoLambda, segFusedChunk, cfg.sftScope);
|
|
894
|
-
else
|
|
895
|
-
segmentedOrpo = new SegmentedBackwardOrpoGemma4(model as Gemma4Model, lora, ranges, cfg.orpoLambda, segFusedChunk, cfg.sftScope);
|
|
896
|
-
if (!useSegmentedPrefix) emit({ type: "stage", stage: "setup", progress: 0.04,
|
|
897
|
-
message: `orpo segmented backward: ${ranges.length} segments of <=${cfg.segmentSize} layers${segFusedChunk > 0 ? ` + fused linear-CE head (${segFusedChunk}/chunk)` : ""}` });
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// Token-chunked head (non-segmented path only): the per-chunk checkpoints are
|
|
901
|
-
// collected here and disposed after each micro-step's grads are eval'd (the
|
|
902
|
-
// afterMicroEval hook below) — that is when their backward recompute has run.
|
|
903
|
-
const chunkSink: Array<{ dispose(): void }> = [];
|
|
904
|
-
// Fused linear-CE head reuses orpoChunkSize as the token-chunk (default 512 when
|
|
905
|
-
// unset); otherwise the Checkpoint token-chunked head fires only when
|
|
906
|
-
// orpoChunkSize > 0. Both are non-segmented-path only.
|
|
907
|
-
const fusedChunkSize = cfg.orpoChunkSize > 0 ? cfg.orpoChunkSize : 512;
|
|
908
|
-
const chunk: ChunkCtx | undefined = useSegmented
|
|
909
|
-
? undefined
|
|
910
|
-
: cfg.orpoFusedCe || cfg.orpoFlashCe
|
|
911
|
-
? { chunkSize: fusedChunkSize, fused: true, flash: cfg.orpoFlashCe, sink: chunkSink }
|
|
912
|
-
: cfg.orpoChunkSize > 0
|
|
913
|
-
? { chunkSize: cfg.orpoChunkSize, sink: chunkSink }
|
|
914
|
-
: undefined;
|
|
915
|
-
if (chunk) emit({ type: "stage", stage: "setup", progress: 0.04,
|
|
916
|
-
message: chunk.flash
|
|
917
|
-
? `orpo flash-CCE (Metal kernel) head: ${chunk.chunkSize} tokens/chunk`
|
|
918
|
-
: chunk.fused
|
|
919
|
-
? `orpo fused linear-CE head: ${chunk.chunkSize} tokens/chunk`
|
|
920
|
-
: `orpo chunked head: ${cfg.orpoChunkSize} tokens/chunk` });
|
|
921
|
-
if (cfg.orpoPrefixShared && !useSegmentedPrefix) emit({ type: "stage", stage: "setup", progress: 0.04,
|
|
922
|
-
message: `orpo prefix-sharing: single forward over [prompt; chosen; rejected] (two-forward fallback on prompt mismatch)` });
|
|
923
|
-
const disposeChunkSink = () => { for (const c of chunkSink) c.dispose(); chunkSink.length = 0; };
|
|
924
|
-
|
|
925
|
-
// No reference forward (the whole point of ORPO) — the closure just builds the
|
|
926
|
-
// monolithic L_NLL + λ·L_OR over the active batch against the swapped primals.
|
|
927
|
-
let currentBatch: DpoBatch | null = null;
|
|
928
|
-
const vag = useSegmented
|
|
929
|
-
? null
|
|
930
|
-
: new ValueAndGrad((primals) => {
|
|
931
|
-
const saved = swapPrimals(lora, primals);
|
|
932
|
-
try {
|
|
933
|
-
// Prefix-sharing (B=1): one forward over [prompt; chosen; rejected] when the
|
|
934
|
-
// row's chosen/rejected share an identical prompt; else fall back two-forward.
|
|
935
|
-
if (cfg.orpoPrefixShared) {
|
|
936
|
-
const split = splitPrefixBatch(currentBatch!);
|
|
937
|
-
if (split) {
|
|
938
|
-
if (model instanceof MiniCPM5Model)
|
|
939
|
-
return orpoLossPrefixShared(model, split.promptIds, split.chosenResp, split.rejectedResp, cfg.orpoLambda, chunk, cfg.sftScope);
|
|
940
|
-
return orpoLossPrefixSharedGemma(model as Gemma4Model, split.promptIds, split.chosenResp, split.rejectedResp, cfg.orpoLambda, chunk, cfg.sftScope);
|
|
941
|
-
}
|
|
942
|
-
notePrefixFallback();
|
|
943
|
-
}
|
|
944
|
-
return orpoLoss(model, currentBatch!, cfg.orpoLambda, chunk, cfg.sftScope);
|
|
945
|
-
} finally {
|
|
946
|
-
restorePrimals(lora, saved);
|
|
947
|
-
}
|
|
948
|
-
}, params.map((_, i) => i));
|
|
949
|
-
|
|
950
|
-
const padId = tok.eosTokenId ?? 0;
|
|
951
|
-
const batches = iterateDpoBatches(train, cfg.batchSize, cfg.seed, true, padId);
|
|
952
|
-
const t0 = Date.now();
|
|
953
|
-
|
|
954
|
-
let dropoutCounter = 0;
|
|
955
|
-
// Prefix-share falls back to the two-forward path for any row whose chosen/rejected
|
|
956
|
-
// prompts aren't byte-identical (splitPrefixBatch → null). Log the FIRST fallback +
|
|
957
|
-
// count them, so a silent loss of the prompt-encode-once saving is visible.
|
|
958
|
-
let prefixFallbacks = 0;
|
|
959
|
-
const notePrefixFallback = () => {
|
|
960
|
-
prefixFallbacks++;
|
|
961
|
-
if (prefixFallbacks === 1) emit({ type: "stage", stage: "train", progress: 0,
|
|
962
|
-
message: `prefix-share: row prompt mismatch → two-forward fallback for this row (saving lost only on mismatched rows; counting…)` });
|
|
963
|
-
};
|
|
964
|
-
try {
|
|
965
|
-
for (let step = 1; step <= cfg.iters; step++) {
|
|
966
|
-
if (schedule) opt.lr = schedule(step);
|
|
967
|
-
|
|
968
|
-
// One optimizer step over cfg.gradAccumSteps micro-batches (pass-through
|
|
969
|
-
// when 1) — the long-context lever for a larger effective batch without
|
|
970
|
-
// the B>1 activation memory (see docs/design/orpo-training.md, Batching).
|
|
971
|
-
const { loss: lossVal, grads } = accumulateStep(cfg.gradAccumSteps, () => {
|
|
972
|
-
currentBatch = batches.next().value as DpoBatch;
|
|
973
|
-
// Per-micro-step dropout seed: constant across this micro-step's forward
|
|
974
|
-
// AND its backward recompute (segmented/chunked), so the mask reproduces.
|
|
975
|
-
if (cfg.loraDropout > 0) model.loraState.dropoutSeed = ++dropoutCounter;
|
|
976
|
-
// Segmented + prefix-share: stream the single concat forward when the row's
|
|
977
|
-
// chosen/rejected share an identical prompt; else fall back to the plain
|
|
978
|
-
// two-forward segmented step (segmentedOrpo, built alongside).
|
|
979
|
-
if (segmentedOrpoPrefix) {
|
|
980
|
-
const split = splitPrefixBatch(currentBatch!);
|
|
981
|
-
if (split) return segmentedOrpoPrefix.stepPrefix(split.promptIds, split.chosenResp, split.rejectedResp);
|
|
982
|
-
notePrefixFallback();
|
|
983
|
-
return segmentedOrpo!.step(currentBatch!);
|
|
984
|
-
}
|
|
985
|
-
return segmentedOrpo ? segmentedOrpo.step(currentBatch!) : vag!.apply(flatParams(lora));
|
|
986
|
-
}, disposeChunkSink); // free this micro-step's head checkpoints once its grads are eval'd
|
|
987
|
-
|
|
988
|
-
model.loraState.dropoutSeed = null; // metrics/val below run dropout-free
|
|
989
|
-
const gradNorm = globalNorm(grads);
|
|
990
|
-
clipGradsByNorm(grads, gradNorm, cfg.gradClipNorm);
|
|
991
|
-
opt.step(grads);
|
|
992
|
-
opt.evalState();
|
|
993
|
-
clearCache();
|
|
994
|
-
|
|
995
|
-
if (step % cfg.stepsPerReport === 0 || step === 1) {
|
|
996
|
-
// Diagnostics recompute the two forwards (like dpoMetrics) — cheap at
|
|
997
|
-
// B=1 (response-only head, no full logits) and gated by stepsPerReport.
|
|
998
|
-
// Reusing the value_and_grad forward is a shared-with-DPO follow-on.
|
|
999
|
-
const m = orpoMetrics(model, currentBatch!, cfg.orpoLambda, cfg.sftScope);
|
|
1000
|
-
const elapsed = (Date.now() - t0) / 1000;
|
|
1001
|
-
emit({
|
|
1002
|
-
type: "metric",
|
|
1003
|
-
kind: "train",
|
|
1004
|
-
step,
|
|
1005
|
-
loss: lossVal,
|
|
1006
|
-
grad_norm: gradNorm,
|
|
1007
|
-
learning_rate: opt.lr,
|
|
1008
|
-
accuracy: m.accuracy,
|
|
1009
|
-
margin: m.margin,
|
|
1010
|
-
tokens_per_sec: 0,
|
|
1011
|
-
peak_gb: peakMemory() / 1e9,
|
|
1012
|
-
active_gb: activeMemory() / 1e9,
|
|
1013
|
-
pinned: pinnedBufferCount(),
|
|
1014
|
-
progress: step / cfg.iters,
|
|
1015
|
-
message: `step ${step}/${cfg.iters} loss=${lossVal.toFixed(4)} nll=${m.nll.toFixed(4)} or=${m.or.toFixed(4)} acc=${m.accuracy.toFixed(2)} margin=${m.margin.toFixed(3)}`,
|
|
1016
|
-
});
|
|
1017
|
-
clearCache();
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
if (valid.length > 0 && step % cfg.stepsPerEval === 0) {
|
|
1021
|
-
// Val accuracy is correct/total over a FIXED subset (cap bounds both the
|
|
1022
|
-
// denominator and the per-eval cost). Each val example is a single B=1
|
|
1023
|
-
// pair, so m.accuracy ∈ {0,1} and va sums to the integer correct count.
|
|
1024
|
-
const valSet = cfg.valMaxExamples > 0 ? valid.slice(0, cfg.valMaxExamples) : valid;
|
|
1025
|
-
let vl = 0, va = 0, vm = 0, vn = 0;
|
|
1026
|
-
for (const ex of valSet) {
|
|
1027
|
-
const vb: DpoBatch = {
|
|
1028
|
-
chosenIds: [ex.chosenIds], rejectedIds: [ex.rejectedIds],
|
|
1029
|
-
chosenMask: [ex.chosenMask], rejectedMask: [ex.rejectedMask],
|
|
1030
|
-
};
|
|
1031
|
-
const m = orpoMetrics(model, vb, cfg.orpoLambda, cfg.sftScope);
|
|
1032
|
-
vl += m.loss; va += m.accuracy; vm += m.margin; vn++;
|
|
1033
|
-
clearCache();
|
|
1034
|
-
}
|
|
1035
|
-
emit({
|
|
1036
|
-
type: "metric", kind: "val", step,
|
|
1037
|
-
loss: vn ? vl / vn : 0,
|
|
1038
|
-
accuracy: vn ? va / vn : 0,
|
|
1039
|
-
n_correct: Math.round(va),
|
|
1040
|
-
n_total: vn,
|
|
1041
|
-
margin: vn ? vm / vn : 0,
|
|
1042
|
-
progress: step / cfg.iters,
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
} finally {
|
|
1047
|
-
disposeChunkSink(); // defensive: free any head checkpoints a throw left behind
|
|
1048
|
-
segmentedOrpoPrefix?.dispose();
|
|
1049
|
-
segmentedOrpo?.dispose();
|
|
1050
|
-
vag?.dispose();
|
|
1051
|
-
opt.dispose();
|
|
1052
|
-
}
|
|
1053
|
-
return { numIters: cfg.iters };
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
// ---------------------------------------------------------------------------
|
|
1057
|
-
// helpers
|
|
1058
|
-
// ---------------------------------------------------------------------------
|
|
1059
|
-
|
|
1060
|
-
/** Swap the incoming value_and_grad primals into the LoraWeights so the
|
|
1061
|
-
* forward graph differentiates them. Order: [...A, ...B] over sorted targets.
|
|
1062
|
-
* Returns the displaced original leaves so the caller can restore them (the
|
|
1063
|
-
* primal wrappers are disposed by the autograd closure after this returns). */
|
|
1064
|
-
function swapPrimals(lora: TrainableLora, primals: MlxArray[]): MlxArray[] {
|
|
1065
|
-
const n = lora.targets.length;
|
|
1066
|
-
const saved: MlxArray[] = [];
|
|
1067
|
-
for (let i = 0; i < n; i++) {
|
|
1068
|
-
saved.push(lora.targets[i]!.lw.a, lora.targets[i]!.lw.b);
|
|
1069
|
-
lora.targets[i]!.lw.a = primals[i]!;
|
|
1070
|
-
lora.targets[i]!.lw.b = primals[n + i]!;
|
|
1071
|
-
}
|
|
1072
|
-
return saved;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
/** Restore the original leaves displaced by swapPrimals (saved order is
|
|
1076
|
-
* [a0, b0, a1, b1, ...]). */
|
|
1077
|
-
function restorePrimals(lora: TrainableLora, saved: MlxArray[]): void {
|
|
1078
|
-
for (let i = 0; i < lora.targets.length; i++) {
|
|
1079
|
-
lora.targets[i]!.lw.a = saved[2 * i]!;
|
|
1080
|
-
lora.targets[i]!.lw.b = saved[2 * i + 1]!;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
/** Write a replaced param (AdamW output) back into its LoraWeights leaf. */
|
|
1085
|
-
function writeParam(lora: TrainableLora, i: number, p: MlxArray): void {
|
|
1086
|
-
const n = lora.targets.length;
|
|
1087
|
-
if (i < n) lora.targets[i]!.lw.a = p;
|
|
1088
|
-
else lora.targets[i - n]!.lw.b = p;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
/** Run one optimizer step's worth of gradient over `accumSteps` micro-batches:
|
|
1092
|
-
* backward each micro-batch, mean-accumulate the per-micro grads (each scaled
|
|
1093
|
-
* by 1/accumSteps), and return the mean loss + summed grads — a drop-in for the
|
|
1094
|
-
* single value_and_grad the loops used to call inline. The caller passes the
|
|
1095
|
-
* returned grads to opt.step (which takes ownership and disposes them).
|
|
1096
|
-
*
|
|
1097
|
-
* accumSteps <= 1 is a pass-through identical to the pre-accumulation path (one
|
|
1098
|
-
* micro-batch, no scaling) so the proven B=1 loops stay byte-for-byte unchanged.
|
|
1099
|
-
* For accumSteps > 1 the running accumulator (and that micro-batch's loss) is
|
|
1100
|
-
* eval'd before the next micro-batch's forward builds, so only ONE micro-batch's
|
|
1101
|
-
* activations are ever live — peak memory does NOT grow with accumSteps (the
|
|
1102
|
-
* whole point: a larger effective batch without the B>1 activation memory).
|
|
1103
|
-
*
|
|
1104
|
-
* The combined `evalAll([value, ...grads])` per micro-batch is deliberate: a
|
|
1105
|
-
* separate eval of `value` first would materialize then free the forward,
|
|
1106
|
-
* forcing the backward to recompute and hold every layer's activations — which
|
|
1107
|
-
* silently defeats gradient checkpointing (verified). Keep loss + grads in one
|
|
1108
|
-
* eval.
|
|
1109
|
-
*
|
|
1110
|
-
* `afterMicroEval` runs once per micro-batch right after its grads are
|
|
1111
|
-
* materialized (its backward is complete), in BOTH the pass-through and
|
|
1112
|
-
* accumulating paths — it frees per-micro-batch state the loss closure captured
|
|
1113
|
-
* (gradient-checkpoint closures for SFT, reference log-probs for DPO) so that
|
|
1114
|
-
* state does not pile up across accumulation.
|
|
1115
|
-
*
|
|
1116
|
-
* `micro` returns the micro-batch loss scalar + grads in flatParams order
|
|
1117
|
-
* ([...A, ...B] over targets); it transfers ownership of both to this helper,
|
|
1118
|
-
* which disposes the per-micro value and grads. */
|
|
1119
|
-
export function accumulateStep(
|
|
1120
|
-
accumSteps: number,
|
|
1121
|
-
micro: () => { value: MlxArray; grads: MlxArray[] },
|
|
1122
|
-
afterMicroEval?: () => void,
|
|
1123
|
-
): { loss: number; grads: MlxArray[] } {
|
|
1124
|
-
if (accumSteps <= 1) {
|
|
1125
|
-
const { value, grads } = micro();
|
|
1126
|
-
evalAll([value, ...grads]);
|
|
1127
|
-
const loss = value.toFloat32()[0]!;
|
|
1128
|
-
value.dispose();
|
|
1129
|
-
afterMicroEval?.();
|
|
1130
|
-
return { loss, grads };
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
const scale = 1 / accumSteps;
|
|
1134
|
-
let acc: MlxArray[] | null = null;
|
|
1135
|
-
let lossSum = 0;
|
|
1136
|
-
try {
|
|
1137
|
-
for (let i = 0; i < accumSteps; i++) {
|
|
1138
|
-
const { value, grads } = micro();
|
|
1139
|
-
if (acc === null) {
|
|
1140
|
-
acc = grads.map((g) => {
|
|
1141
|
-
const s = ops.mulScalar(g, scale);
|
|
1142
|
-
g.dispose();
|
|
1143
|
-
return s;
|
|
1144
|
-
});
|
|
1145
|
-
} else {
|
|
1146
|
-
const prev = acc;
|
|
1147
|
-
acc = grads.map((g, j) => {
|
|
1148
|
-
const s = ops.mulScalar(g, scale);
|
|
1149
|
-
const sum = ops.add(prev[j]!, s);
|
|
1150
|
-
s.dispose();
|
|
1151
|
-
prev[j]!.dispose();
|
|
1152
|
-
g.dispose();
|
|
1153
|
-
return sum;
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
// Eval the running accumulator (+ this micro-batch's loss) now so the
|
|
1157
|
-
// micro-batch's forward/backward graph frees before the next one builds —
|
|
1158
|
-
// this is what keeps peak flat across accumSteps.
|
|
1159
|
-
evalAll([value, ...acc]);
|
|
1160
|
-
lossSum += value.toFloat32()[0]!;
|
|
1161
|
-
value.dispose();
|
|
1162
|
-
afterMicroEval?.();
|
|
1163
|
-
clearCache();
|
|
1164
|
-
}
|
|
1165
|
-
const grads = acc!;
|
|
1166
|
-
acc = null; // ownership handed to the caller
|
|
1167
|
-
return { loss: lossSum / accumSteps, grads };
|
|
1168
|
-
} finally {
|
|
1169
|
-
// Non-null only if a micro-batch threw mid-accumulation (nulled on success).
|
|
1170
|
-
if (acc) for (const a of acc) a.dispose();
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
/** L2 norm over all grads (also forces eval). */
|
|
1175
|
-
function globalNorm(grads: MlxArray[]): number {
|
|
1176
|
-
const partial: MlxArray[] = grads.map((g) => {
|
|
1177
|
-
const sq = ops.square(g);
|
|
1178
|
-
const flat = ops.reshape(sq, [g.size]);
|
|
1179
|
-
const sum = ops.sumAxis(flat, 0, false);
|
|
1180
|
-
sq.dispose();
|
|
1181
|
-
flat.dispose();
|
|
1182
|
-
return sum;
|
|
1183
|
-
});
|
|
1184
|
-
evalAll(partial);
|
|
1185
|
-
let sumSq = 0;
|
|
1186
|
-
for (const p of partial) {
|
|
1187
|
-
sumSq += p.toFloat32()[0]!;
|
|
1188
|
-
p.dispose();
|
|
1189
|
-
}
|
|
1190
|
-
return Math.sqrt(sumSq);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
/** Clip grads in place to a global L2 norm of `maxNorm` (no-op if off or under
|
|
1194
|
-
* the cap). Scales every grad by maxNorm/normVal and disposes the originals so
|
|
1195
|
-
* the optimizer takes ownership of the clipped copies. `normVal` is the already-
|
|
1196
|
-
* computed global norm (avoids a second pass). */
|
|
1197
|
-
function clipGradsByNorm(grads: MlxArray[], normVal: number, maxNorm: number): void {
|
|
1198
|
-
if (maxNorm <= 0 || !(normVal > maxNorm)) return;
|
|
1199
|
-
const s = maxNorm / normVal;
|
|
1200
|
-
for (let i = 0; i < grads.length; i++) {
|
|
1201
|
-
const g = grads[i]!;
|
|
1202
|
-
grads[i] = ops.mulScalar(g, s);
|
|
1203
|
-
g.dispose();
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
/** Supervised (response) tokens across ALL rows of the batch — the count
|
|
1208
|
-
* tokens/sec is reported over (mlx-lm's n_tokens counts every row too). */
|
|
1209
|
-
export function countResponseTokens(batch: SftBatch): number {
|
|
1210
|
-
let total = 0;
|
|
1211
|
-
for (let r = 0; r < batch.ids.length; r++) {
|
|
1212
|
-
const promptLen = batch.promptLens[r]!;
|
|
1213
|
-
const len = rowLength(batch, r);
|
|
1214
|
-
total += Math.max(0, len - Math.max(promptLen, 1));
|
|
1215
|
-
}
|
|
1216
|
-
return total;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
async function fileExists(path: string): Promise<boolean> {
|
|
1220
|
-
return Bun.file(path).exists();
|
|
1221
|
-
}
|