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