mlx-bun 0.0.9 → 0.0.11
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 +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +255 -0
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +98 -0
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +18 -2
- package/docs/design/ssd-kv-cold-tier.md +288 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/decode-roofline-lookagain.md +92 -6
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/kernel-perf-review-2026-07.md +105 -26
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.10.md +57 -0
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +251 -119
- package/docs/reference/training.md +20 -16
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +402 -107
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -16
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +618 -122
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +127 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +179 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +391 -34
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +123 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +99 -25
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +338 -32
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +61 -3
- package/src/serve/batch-scheduler.ts +1001 -76
- package/src/serve/generation-gateway.ts +292 -30
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1460 -105
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +285 -0
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +29 -26
- package/src/train/job.ts +0 -2
- package/src/train/loss.ts +53 -11
- package/src/train/prefix-shared.ts +30 -7
- package/src/train/segmented.ts +150 -46
- package/src/train/trainer.ts +22 -15
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
package/src/train/loss.ts
CHANGED
|
@@ -138,6 +138,36 @@ export function sftLoss(model: RuntimeModel, batch: SftBatch): MlxArray {
|
|
|
138
138
|
return scalar;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/** model.logitsFromHidden for TRAINING heads, with the upstream mlx
|
|
142
|
+
* quantized_matmul bug worked around: transpose=false returns WRONG values at
|
|
143
|
+
* row count 2 or 3 (reproduced on stock mlx 0.31.2; M=1 and M>=4 correct,
|
|
144
|
+
* transpose=true correct at every M). mlx's autograd computes dh for a
|
|
145
|
+
* transpose=true head qmm via an INTERNAL transpose=false call at the
|
|
146
|
+
* cotangent's row count — so any differentiated [M,V] logits with M in {2,3}
|
|
147
|
+
* (2-3-token responses, 2-3-token tail chunks, tiny prompt spans) got a
|
|
148
|
+
* silently wrong dh (measured relnorm ~0.35-0.99 vs the f32 ground truth).
|
|
149
|
+
* Fix: pad the span to 4 rows with zeros and slice the logits back — the pad
|
|
150
|
+
* rows' cotangents are exactly zero (slice adjoint), and the internal vjp
|
|
151
|
+
* then runs at M=4 where the kernel is correct. */
|
|
152
|
+
export function logitsFromHiddenPadM(
|
|
153
|
+
model: { logitsFromHidden(h: MlxArray): MlxArray },
|
|
154
|
+
hSpan: MlxArray, // [1, M, hidden]
|
|
155
|
+
): MlxArray {
|
|
156
|
+
const M = hSpan.shape[1]!;
|
|
157
|
+
if (M !== 2 && M !== 3) return model.logitsFromHidden(hSpan);
|
|
158
|
+
const [B, , H] = hSpan.shape as [number, number, number];
|
|
159
|
+
const pad = ops.zeros([B, 4 - M, H], hSpan.dtype);
|
|
160
|
+
const hPad = ops.concatAxis([hSpan, pad], 1);
|
|
161
|
+
pad.dispose();
|
|
162
|
+
const lgPad = model.logitsFromHidden(hPad); // [1, 4, V]
|
|
163
|
+
hPad.dispose();
|
|
164
|
+
const start = MlxArray.fromInt32(new Int32Array([0]), [1]);
|
|
165
|
+
const lg = ops.sliceDynamic(lgPad, start, [1], [B, M, lgPad.shape[2]!]);
|
|
166
|
+
start.dispose();
|
|
167
|
+
lgPad.dispose();
|
|
168
|
+
return lg;
|
|
169
|
+
}
|
|
170
|
+
|
|
141
171
|
/** B=1 masked CE that applies the LM head only at the supervised (response)
|
|
142
172
|
* span. The supervised input positions are the contiguous range
|
|
143
173
|
* [promptLen-1, len-1) (each predicts the next, response, token). The LM head
|
|
@@ -158,7 +188,7 @@ export function responseOnlyCe(model: RuntimeModel, h: MlxArray, batch: SftBatch
|
|
|
158
188
|
const hidden = h.shape[2]!;
|
|
159
189
|
const start = MlxArray.fromInt32(new Int32Array([startT]), [1]);
|
|
160
190
|
const hResp = ops.sliceDynamic(h, start, [1], [1, M, hidden]);
|
|
161
|
-
const logits = model
|
|
191
|
+
const logits = logitsFromHiddenPadM(model, hResp); // [1, M, V] (M in {2,3} padded — mlx qmm bug)
|
|
162
192
|
const V = logits.shape[2]!;
|
|
163
193
|
const logits2d = ops.reshape(logits, [M, V]);
|
|
164
194
|
|
|
@@ -198,7 +228,7 @@ export function responseOnlyLogpMean(
|
|
|
198
228
|
const hidden = h.shape[2]!;
|
|
199
229
|
const start = MlxArray.fromInt32(new Int32Array([startT]), [1]);
|
|
200
230
|
const hResp = ops.sliceDynamic(h, start, [1], [1, M, hidden]); // [1, M, hidden]
|
|
201
|
-
const logits = model
|
|
231
|
+
const logits = logitsFromHiddenPadM(model, hResp); // [1, M, V] (M in {2,3} padded — mlx qmm bug)
|
|
202
232
|
const V = logits.shape[2]!;
|
|
203
233
|
const logits2d = ops.reshape(logits, [M, V]);
|
|
204
234
|
|
|
@@ -504,7 +534,7 @@ export function spanLogpMeanFromHidden(
|
|
|
504
534
|
const hidden = h.shape[2]!;
|
|
505
535
|
const start = MlxArray.fromInt32(new Int32Array([s0]), [1]);
|
|
506
536
|
const hSpan = ops.sliceDynamic(h, start, [1], [1, n, hidden]); // [1, n, hidden]
|
|
507
|
-
const logits = model
|
|
537
|
+
const logits = logitsFromHiddenPadM(model, hSpan); // [1, n, V] (n in {2,3} padded — mlx qmm bug)
|
|
508
538
|
const V = logits.shape[2]!;
|
|
509
539
|
const logits2d = ops.reshape(logits, [n, V]);
|
|
510
540
|
|
|
@@ -624,7 +654,7 @@ export function branchLogpMeanB1(
|
|
|
624
654
|
const hidden = h.shape[2]!;
|
|
625
655
|
const start = MlxArray.fromInt32(new Int32Array([startT]), [1]);
|
|
626
656
|
const hResp = ops.sliceDynamic(h, start, [1], [1, M, hidden]); // [1, M, hidden]
|
|
627
|
-
const logits = model
|
|
657
|
+
const logits = logitsFromHiddenPadM(model, hResp); // [1, M, V] — head only on the response span (M in {2,3} padded — mlx qmm bug)
|
|
628
658
|
const V = logits.shape[2]!;
|
|
629
659
|
const logits2d = ops.reshape(logits, [M, V]);
|
|
630
660
|
|
|
@@ -698,7 +728,7 @@ function chunkedLogpMeanB1(
|
|
|
698
728
|
|
|
699
729
|
const ckpt = new Checkpoint((inp) => {
|
|
700
730
|
const hc = inp[0]!;
|
|
701
|
-
const logits = model
|
|
731
|
+
const logits = logitsFromHiddenPadM(model, hc); // [1, Cc, V] — recomputed in backward (Cc in {2,3} padded — mlx qmm bug)
|
|
702
732
|
const V = logits.shape[2]!;
|
|
703
733
|
const l2 = ops.reshape(logits, [Cc, V]);
|
|
704
734
|
const targets = MlxArray.fromInt32(tHost, [Cc, 1]);
|
|
@@ -765,7 +795,7 @@ export function chunkedLogpMeanFromHidden(
|
|
|
765
795
|
for (let i = 0; i < Cc; i++) tHost[i] = ids[c0 + 1 + i]!;
|
|
766
796
|
const ckpt = new Checkpoint((inp) => {
|
|
767
797
|
const hc = inp[0]!;
|
|
768
|
-
const logits = model
|
|
798
|
+
const logits = logitsFromHiddenPadM(model, hc); // [1, Cc, V] — recomputed in backward (Cc in {2,3} padded — mlx qmm bug)
|
|
769
799
|
const V = logits.shape[2]!;
|
|
770
800
|
const l2 = ops.reshape(logits, [Cc, V]);
|
|
771
801
|
const targets = MlxArray.fromInt32(tHost, [Cc, 1]);
|
|
@@ -1093,8 +1123,9 @@ function makeVocabBlockedHeadVjp(
|
|
|
1093
1123
|
/** flash-CCE head as a CustomVjp: forward runs the Metal kernel → Σ logp (stashing
|
|
1094
1124
|
* `lse` for the backward, avoiding a forward recompute); backward runs the kernel's
|
|
1095
1125
|
* `dh` for unit cotangent then scales by the scalar cotangent `c` (loss = Σ logp →
|
|
1096
|
-
* ∂loss/∂logp_t = c for all t). The coeff filter + vocab-block skip
|
|
1097
|
-
*
|
|
1126
|
+
* ∂loss/∂logp_t = c for all t). The coeff filter + vocab-block skip default ON at
|
|
1127
|
+
* 1e-5 (2026-07-02 flip; real-data gates in flash-cce.ts) — set
|
|
1128
|
+
* MLX_BUN_CCE_BWD_{FILTER,BLOCK}_EPS=0 for the EXACT dh. */
|
|
1098
1129
|
function makeFlashCceHeadVjp(
|
|
1099
1130
|
head: HeadQuant, targetsHost: Int32Array, M: number,
|
|
1100
1131
|
sink: Array<{ dispose(): void }>,
|
|
@@ -1125,8 +1156,8 @@ function makeFlashCceHeadVjp(
|
|
|
1125
1156
|
const c = cots[0]!; // [1] = ∂loss/∂(Σ logp); uniform over tokens
|
|
1126
1157
|
// dh for unit cotangent, then × c (avoids a host read of the cotangent).
|
|
1127
1158
|
const ones = new Array(M).fill(1) as number[];
|
|
1128
|
-
// filter + block-skip
|
|
1129
|
-
//
|
|
1159
|
+
// filter + block-skip default ON at 1e-5 (env-overridable; =0 → exact dh).
|
|
1160
|
+
// savedBlockMax feeds the ~lossless vocab-block skip.
|
|
1130
1161
|
const dhUnit = flashCceBackward(hR, fch, targets, savedLse!, ones, undefined, savedBlockMax!); // [M, hidden] f32
|
|
1131
1162
|
const cM = c.dtype === dhUnit.dtype ? c : c.astype(dhUnit.dtype);
|
|
1132
1163
|
const dh = ops.mul(dhUnit, cM); // broadcast [M,hidden]·[1]
|
|
@@ -1157,8 +1188,19 @@ export function fusedRespLogpMean(
|
|
|
1157
1188
|
// Vocab-blocked (full CCE) path: even [chunk,V] never materializes — only
|
|
1158
1189
|
// [chunk,vocabBlock]. Otherwise the whole-vocab analytic head below.
|
|
1159
1190
|
const vb = vocabBlock > 0 && vocabBlock < V ? vocabBlock : 0;
|
|
1191
|
+
// Auto-dispatch by M (kernel backlog #3; measured 2026-07-02 on the M1 Max,
|
|
1192
|
+
// scripts/experiments/head-dispatch-sweep.ts): the fused QM head is FASTER
|
|
1193
|
+
// at short M (e4b M=64 1.6×, M=512 1.13×; CPM5 faster at EVERY M) — the
|
|
1194
|
+
// flash head's win is residency, plus time at long M on e4b (M≥2048 after
|
|
1195
|
+
// the 2026-07-02 filter/blockMax flip; 8K: 10.7 vs 13.2 s). So a
|
|
1196
|
+
// requested-flash batch with M below MLX_BUN_FLASH_MIN_M (default 1024, the
|
|
1197
|
+
// e4b time-tie point where flash's memory win takes over) takes the EXACT
|
|
1198
|
+
// fused head instead — short rows get the faster exact head for free.
|
|
1199
|
+
// MLX_BUN_FLASH_MIN_M=0 always honors flash (the pre-dispatch behavior).
|
|
1200
|
+
const flashMinM = Number(process.env.MLX_BUN_FLASH_MIN_M ?? "1024");
|
|
1201
|
+
const useFlash = flash && (flashMinM <= 0 || M >= flashMinM);
|
|
1160
1202
|
let op: CustomVjp;
|
|
1161
|
-
if (
|
|
1203
|
+
if (useFlash) {
|
|
1162
1204
|
op = makeFlashCceHeadVjp(head, targetsHost, M, sink);
|
|
1163
1205
|
} else if (vb > 0) {
|
|
1164
1206
|
op = makeVocabBlockedHeadVjp(head, targetsHost, M, hidden, chunk, vb);
|
|
@@ -28,7 +28,7 @@ import * as ops from "../mlx/ops";
|
|
|
28
28
|
import { createCausalMask, type Cache, type Mask } from "../model/gemma4-base";
|
|
29
29
|
import { MiniCPM5Model, setMiniCpmPrefixPlan } from "../model/minicpm5";
|
|
30
30
|
import { Gemma4Model, setGemmaPrefixPlan } from "../model/gemma4";
|
|
31
|
-
import { orpoLossFromLogps, fusedRespLogpMean, combineFullNll, type ChunkCtx, type SftScope } from "./loss";
|
|
31
|
+
import { orpoLossFromLogps, fusedRespLogpMean, combineFullNll, logitsFromHiddenPadM, type ChunkCtx, type SftScope } from "./loss";
|
|
32
32
|
import type { DpoBatch } from "./dataset";
|
|
33
33
|
|
|
34
34
|
/** A model that can project final-norm hidden states to vocab logits — the only
|
|
@@ -63,17 +63,40 @@ export function blockSparseMask(P: number, Rc: number, Rr: number): MlxArray {
|
|
|
63
63
|
* MiniCPM5's runLayerRange calls makeMask once and disposes the returned arr. */
|
|
64
64
|
export class PrefixSharedCache implements Cache {
|
|
65
65
|
offset = 0;
|
|
66
|
-
constructor(
|
|
66
|
+
constructor(
|
|
67
|
+
private readonly P: number, private readonly Rc: number, private readonly Rr: number,
|
|
68
|
+
// Step-shared mask memo (kernel-review backlog #9): the [T,T] block-sparse
|
|
69
|
+
// mask is IDENTICAL for every layer, segment forward, and backward
|
|
70
|
+
// recompute of a step, but each runLayerRange call rebuilt it from its
|
|
71
|
+
// elementwise ops (2·nSeg+ rebuilds per step in the segmented prefix
|
|
72
|
+
// class). With a memo it is built once; makeMask hands out cheap slice
|
|
73
|
+
// VIEWS (consumers dispose their view — runLayerRange does), and the
|
|
74
|
+
// first cache disposed frees the memo. Use prefixSharedCaches() to build
|
|
75
|
+
// a memo-sharing batch.
|
|
76
|
+
private readonly memo?: { mask: MlxArray | null },
|
|
77
|
+
) {}
|
|
67
78
|
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
68
79
|
return [k.slice([0, 0, 0, 0], k.shape), v.slice([0, 0, 0, 0], v.shape)];
|
|
69
80
|
}
|
|
70
81
|
makeMask(_N: number, _windowSize: number | null): Mask {
|
|
71
|
-
return { mode: "array", arr: blockSparseMask(this.P, this.Rc, this.Rr) };
|
|
82
|
+
if (!this.memo) return { mode: "array", arr: blockSparseMask(this.P, this.Rc, this.Rr) };
|
|
83
|
+
if (!this.memo.mask) this.memo.mask = blockSparseMask(this.P, this.Rc, this.Rr);
|
|
84
|
+
const m = this.memo.mask;
|
|
85
|
+
return { mode: "array", arr: m.slice([0, 0], m.shape) };
|
|
72
86
|
}
|
|
73
87
|
state(): MlxArray[] { return []; }
|
|
74
88
|
isTrimmable(): boolean { return true; }
|
|
75
89
|
trim(_n: number): void { /* offset pinned at 0 */ }
|
|
76
|
-
dispose(): void {
|
|
90
|
+
dispose(): void {
|
|
91
|
+
if (this.memo?.mask) { this.memo.mask.dispose(); this.memo.mask = null; }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One PrefixSharedCache per layer, all sharing a single block-sparse-mask
|
|
96
|
+
* memo for the step (see the constructor note). */
|
|
97
|
+
export function prefixSharedCaches(nLayers: number, P: number, Rc: number, Rr: number): Cache[] {
|
|
98
|
+
const memo: { mask: MlxArray | null } = { mask: null };
|
|
99
|
+
return Array.from({ length: nLayers }, () => new PrefixSharedCache(P, Rc, Rr, memo));
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
/** Length-normalized mean log-prob over the predictions at hidden positions
|
|
@@ -87,7 +110,7 @@ function gatheredLogpMean(model: LogitProjector, h: MlxArray, gatherIdx: number[
|
|
|
87
110
|
const hSel = ops.takeAxis(h, idxArr, 1); // [1, M, hidden]
|
|
88
111
|
idxArr.dispose();
|
|
89
112
|
if (hSel.shape[2] !== hidden) throw new Error("gatheredLogpMean: hidden mismatch");
|
|
90
|
-
const logits = model
|
|
113
|
+
const logits = logitsFromHiddenPadM(model, hSel); // [1, M, V] (M in {2,3} padded — mlx qmm bug)
|
|
91
114
|
hSel.dispose();
|
|
92
115
|
const V = logits.shape[2]!;
|
|
93
116
|
const logits2d = ops.reshape(logits, [M, V]);
|
|
@@ -185,7 +208,7 @@ export function orpoLossPrefixShared(
|
|
|
185
208
|
concat.set(chosenResp, P);
|
|
186
209
|
concat.set(rejectedResp, P + Rc);
|
|
187
210
|
const ids = MlxArray.fromInt32(concat, [1, T]);
|
|
188
|
-
const caches: Cache[] = model.layers.
|
|
211
|
+
const caches: Cache[] = prefixSharedCaches(model.layers.length, P, Rc, Rr);
|
|
189
212
|
|
|
190
213
|
let h: MlxArray;
|
|
191
214
|
setMiniCpmPrefixPlan({ P, Rc, Rr });
|
|
@@ -230,7 +253,7 @@ export function prefixSharedLogps(
|
|
|
230
253
|
const concat = new Int32Array(T);
|
|
231
254
|
concat.set(promptIds, 0); concat.set(chosenResp, P); concat.set(rejectedResp, P + Rc);
|
|
232
255
|
const ids = MlxArray.fromInt32(concat, [1, T]);
|
|
233
|
-
const caches: Cache[] = model.layers.
|
|
256
|
+
const caches: Cache[] = prefixSharedCaches(model.layers.length, P, Rc, Rr);
|
|
234
257
|
setMiniCpmPrefixPlan({ P, Rc, Rr });
|
|
235
258
|
let h: MlxArray;
|
|
236
259
|
try { h = model.forwardHidden(ids, caches); }
|
package/src/train/segmented.ts
CHANGED
|
@@ -36,18 +36,31 @@
|
|
|
36
36
|
|
|
37
37
|
import { MlxArray } from "../mlx/array";
|
|
38
38
|
import * as ops from "../mlx/ops";
|
|
39
|
+
import { peakMemory } from "../mlx/ffi";
|
|
39
40
|
import { Vjp } from "../mlx/autograd";
|
|
41
|
+
|
|
42
|
+
// MLX_BUN_SEG_MEM_LOG=1: log the within-step peak after each phase of the
|
|
43
|
+
// segmented step (forward+boundaries / head vjp / each segment backward).
|
|
44
|
+
// The trainer resets the peak at step start (MLX_BUN_MEM_LOG=1), so the phase
|
|
45
|
+
// where the monotone peak JUMPS is where the step's memory actually lives.
|
|
46
|
+
// (This is how the backlog-#2 refutation below was measured. An inter-segment
|
|
47
|
+
// clearCache was also tried and had NO effect — the watermark is live memory,
|
|
48
|
+
// not allocator cache retention.)
|
|
49
|
+
const SEG_MEM = process.env.MLX_BUN_SEG_MEM_LOG === "1";
|
|
50
|
+
const segMem = (label: string): void => {
|
|
51
|
+
if (SEG_MEM) console.log(` [seg-mem] ${label}: peak=${(peakMemory() / 1e9).toFixed(2)} GB`);
|
|
52
|
+
};
|
|
40
53
|
import { MiniCPM5Model, setMiniCpmPrefixPlan } from "../model/minicpm5";
|
|
41
54
|
import type { Gemma4Model } from "../model/gemma4";
|
|
42
55
|
import { setGemmaPrefixPlan } from "../model/gemma4";
|
|
43
56
|
import type { Cache, Mask, SharedKv } from "../model/gemma4-base";
|
|
44
57
|
import { TrainingCache } from "./forward";
|
|
45
58
|
import {
|
|
46
|
-
|
|
59
|
+
responseOnlyLogpMean, fusedLogpMeanFromHidden, chunkedLogpMeanFromHidden,
|
|
47
60
|
orpoLossFromLogps, spanLogpMeanFromHidden, respSpanFromMask, combineFullNll,
|
|
48
61
|
type ChunkCtx, type SftScope, type HeadSpan,
|
|
49
62
|
} from "./loss";
|
|
50
|
-
import {
|
|
63
|
+
import { prefixSharedCaches, prefixGatherIdx, branchLogpMeanGathered, blockSparsePrefixMaskGemma, prefixFullNll } from "./prefix-shared";
|
|
51
64
|
|
|
52
65
|
// Experiment toggle: which bounded head to use inside the segmented mlx_vjp.
|
|
53
66
|
// "checkpoint" = per-chunk Checkpoint recompute (bounds memory under nesting);
|
|
@@ -65,7 +78,36 @@ function boundedHeadFromHidden(
|
|
|
65
78
|
: chunkedLogpMeanFromHidden(model, h, ids, mask, chunkSize, sink, span);
|
|
66
79
|
}
|
|
67
80
|
import type { TrainableLora } from "./lora-params";
|
|
68
|
-
import { type SftBatch, type DpoBatch } from "./dataset";
|
|
81
|
+
import { rowLength, type SftBatch, type DpoBatch } from "./dataset";
|
|
82
|
+
|
|
83
|
+
// Token-chunk size for the bounded SFT head (the ORPO paths take theirs from
|
|
84
|
+
// cfg.orpoChunkSize; SFT has no per-run knob, 512 matches the ORPO default).
|
|
85
|
+
const SFT_HEAD_CHUNK = Number(process.env.MLX_BUN_SEG_HEAD_CHUNK ?? "512");
|
|
86
|
+
|
|
87
|
+
/** [M,V]-bounded response-only CE from post-finalNorm hidden — the SFT
|
|
88
|
+
* segmented head (kernel-review backlog #8). Same span arithmetic and
|
|
89
|
+
* token-mean reduction as loss.ts responseOnlyCe, but the logits only ever
|
|
90
|
+
* exist per token-chunk ([chunk,V] Checkpoint recompute, or the fused
|
|
91
|
+
* CustomVjp head via MLX_BUN_SEG_HEAD) instead of the full [1,M,V] + its
|
|
92
|
+
* cotangent inside the head vjp. CE = −(mean response logp); per-position
|
|
93
|
+
* grads are bit-exact vs the whole-vocab head (chunking is over TOKEN
|
|
94
|
+
* positions; each position's full-V lse is computed unchanged — only the
|
|
95
|
+
* scalar loss VALUE can differ in fp association across chunks). */
|
|
96
|
+
export function boundedSftCe(
|
|
97
|
+
model: MiniCPM5Model | Gemma4Model, hn: MlxArray, batch: SftBatch,
|
|
98
|
+
sink: Array<{ dispose(): void }>, chunkSize = SFT_HEAD_CHUNK,
|
|
99
|
+
): MlxArray {
|
|
100
|
+
const L = batch.ids[0]!.length;
|
|
101
|
+
const startT = Math.max(0, batch.promptLens[0]! - 1); // first supervised input position
|
|
102
|
+
const M = Math.min(rowLength(batch, 0), L) - 1 - startT;
|
|
103
|
+
if (M <= 0) throw new Error("sftLoss: no response tokens to supervise");
|
|
104
|
+
const mean = boundedHeadFromHidden(model, hn, batch.ids[0]!, [], chunkSize, sink, { startT, M });
|
|
105
|
+
const neg = ops.mulScalar(mean, -1); // CE = −(mean logp); exact sign flip
|
|
106
|
+
mean.dispose();
|
|
107
|
+
const loss = ops.sumAxis(neg, 0, false); // [1] -> scalar, matches the vjp cotangent `one`
|
|
108
|
+
neg.dispose();
|
|
109
|
+
return loss;
|
|
110
|
+
}
|
|
69
111
|
|
|
70
112
|
/** Split `nLayers` into contiguous `[lo, hi)` ranges of at most `segSize`
|
|
71
113
|
* layers each (the last range may be shorter). `segSize` is the memory knob:
|
|
@@ -77,24 +119,50 @@ export function planSegmentsBySize(nLayers: number, segSize: number): [number, n
|
|
|
77
119
|
return ranges;
|
|
78
120
|
}
|
|
79
121
|
|
|
122
|
+
// NOTE (2026-07-02, kernel-review backlog #2 REFUTED BY MEASUREMENT): a
|
|
123
|
+
// "full-attention isolation" planner (singleton segments for e4b's 7 full
|
|
124
|
+
// layers) was built and A/B'd @8K — ZERO peak win (18.09 vs 18.02 GB). The
|
|
125
|
+
// design §5 premise is false: mlx's sdpa BACKWARD materializes the O(L²)
|
|
126
|
+
// scores for EVERY layer (~3.5 GB/layer @8K on e4b — sliding's window is just
|
|
127
|
+
// an additive mask; measured: sliding pair +7.1 GB ≈ sliding+full +7.15). The
|
|
128
|
+
// worst segment is set by LAYER COUNT alone, so segSize IS the whole knob:
|
|
129
|
+
// segment_size 1 measures 14.59 GB @8K (+3% step time) vs 17.5 @seg2. Below
|
|
130
|
+
// that needs the [M,V]-bounded SFT head (backlog #8, ~3 GB head-vjp spike)
|
|
131
|
+
// and/or an O(L)-memory attention backward. Evidence:
|
|
132
|
+
// scripts/experiments/seg-isolation-smoke.ts + MLX_BUN_SEG_MEM_LOG=1 probes.
|
|
133
|
+
|
|
80
134
|
/** Detach `a` into a graph-free LEAF holding its exact (evaluated) bytes, then
|
|
81
135
|
* dispose `a`. mlx retains the upstream graph after `eval`, so a segment
|
|
82
136
|
* boundary (or a value_and_grad output) would otherwise keep its whole layer
|
|
83
137
|
* stack's activations alive. Copying the bytes into a fresh leaf frees that
|
|
84
138
|
* graph. The leaf is a valid differentiable input for the per-segment vag. */
|
|
85
139
|
function detachLeaf(a: MlxArray): MlxArray {
|
|
86
|
-
// Force a row-major copy first:
|
|
87
|
-
//
|
|
88
|
-
//
|
|
140
|
+
// Force a row-major copy first: the buffer is read linearly, so a
|
|
141
|
+
// NON-contiguous input (e.g. a transposed donor K/V view) would be
|
|
142
|
+
// byte-scrambled. `ops.contiguous` is a no-op for already-contiguous arrays
|
|
89
143
|
// (hidden boundaries) and materializes row-major for strided views.
|
|
144
|
+
// detachCopy = ONE mlx→mlx copy (backlog #9: the old
|
|
145
|
+
// fromBytesCopy(rawBytes()) route was two copies via the JS heap).
|
|
90
146
|
const c = ops.contiguous(a);
|
|
91
|
-
c.
|
|
92
|
-
const leaf = MlxArray.fromBytesCopy(c.rawBytes(), c.shape, c.dtype);
|
|
147
|
+
const leaf = c.detachCopy();
|
|
93
148
|
c.dispose();
|
|
94
149
|
a.dispose();
|
|
95
150
|
return leaf;
|
|
96
151
|
}
|
|
97
152
|
|
|
153
|
+
/** Batch detach: ONE evalAll barrier for the whole set (backlog #9 — each
|
|
154
|
+
* per-array eval was a separate blocking GPU drain; a segment's vjp returns
|
|
155
|
+
* dh + 2·consumed-donors + 2·targets arrays, so the per-segment sync count
|
|
156
|
+
* collapses from ~2+2n to 1), then one mlx→mlx copy each. Disposes inputs. */
|
|
157
|
+
function detachLeaves(arrs: MlxArray[]): MlxArray[] {
|
|
158
|
+
const cs = arrs.map((a) => ops.contiguous(a));
|
|
159
|
+
ops.evalAll(cs);
|
|
160
|
+
const leaves = cs.map((c) => c.detachCopy());
|
|
161
|
+
for (const c of cs) c.dispose();
|
|
162
|
+
for (const a of arrs) a.dispose();
|
|
163
|
+
return leaves;
|
|
164
|
+
}
|
|
165
|
+
|
|
98
166
|
/** Layer index a target's modulePath belongs to, or -1 (e.g. embedding/head). */
|
|
99
167
|
function targetLayer(modulePath: string): number {
|
|
100
168
|
const m = modulePath.match(/\.layers\.(\d+)\./);
|
|
@@ -120,6 +188,10 @@ export class SegmentedBackward {
|
|
|
120
188
|
private readonly one: MlxArray; // scalar cotangent 1.0 for the (scalar) head loss
|
|
121
189
|
// Per-step context the (reused) closures read.
|
|
122
190
|
private curBatch: SftBatch | null = null;
|
|
191
|
+
// Bounded-head per-step transients (chunk slices + Checkpoint/CustomVjp
|
|
192
|
+
// objects): the head backward recomputes through them, so they are disposed
|
|
193
|
+
// only AFTER the head vjp's outputs are detached (which forces the eval).
|
|
194
|
+
private headSink: Array<{ dispose(): void }> = [];
|
|
123
195
|
private disposed = false;
|
|
124
196
|
|
|
125
197
|
constructor(
|
|
@@ -140,11 +212,17 @@ export class SegmentedBackward {
|
|
|
140
212
|
this.caches = Array.from({ length: this.nLayers }, () => new TrainingCache());
|
|
141
213
|
this.one = MlxArray.fromFloat32(new Float32Array([1]), []); // d(loss)/d(loss) = 1
|
|
142
214
|
|
|
143
|
-
// Loss head: vjp of finalNorm +
|
|
215
|
+
// Loss head: vjp of finalNorm + the [M,V]-BOUNDED response CE over the last
|
|
216
|
+
// boundary leaf (kernel-review backlog #8: the old responseOnlyCe here
|
|
217
|
+
// materialized [1,M,V] logits + cotangents inside the head vjp — measured
|
|
218
|
+
// ~3 GB @8K on e4b, the exact transient the segmentation exists to kill).
|
|
219
|
+
// CE = −(mean response logp); span and reduction identical to
|
|
220
|
+
// responseOnlyCe, per-position grads bit-exact (chunking is over token
|
|
221
|
+
// positions — each position's full-V lse/softmax is computed unchanged).
|
|
144
222
|
// outputs[0] = loss scalar; vjps[0] = dLoss/d(last residual stream) = dh.
|
|
145
223
|
this.headVjp = new Vjp((p) => {
|
|
146
224
|
const hn = model.finalNorm.forward(p[0]!);
|
|
147
|
-
const loss =
|
|
225
|
+
const loss = boundedSftCe(model, hn, this.curBatch!, this.headSink);
|
|
148
226
|
hn.dispose();
|
|
149
227
|
return [loss];
|
|
150
228
|
}, 1);
|
|
@@ -204,9 +282,17 @@ export class SegmentedBackward {
|
|
|
204
282
|
}
|
|
205
283
|
|
|
206
284
|
// --- 2. Loss head over the last boundary (vjp with cotangent 1.0). ---
|
|
285
|
+
// The bounded head pushes per-step chunk slices + Checkpoints into
|
|
286
|
+
// headSink; the head BACKWARD recomputes through them, so materialize
|
|
287
|
+
// the full head-vjp graph BEFORE freeing the sink (lazy-eval UAF
|
|
288
|
+
// discipline, same as the ORPO classes).
|
|
289
|
+
this.headSink = [];
|
|
207
290
|
const head = this.headVjp.apply([boundaries[nSeg]!], [this.one]);
|
|
291
|
+
ops.evalAll([head.outputs[0]!, head.vjps[0]!]);
|
|
208
292
|
value = detachLeaf(head.outputs[0]!); // scalar loss, detached from the head graph (frees logits)
|
|
209
293
|
dhOut = detachLeaf(head.vjps[0]!); // dLoss / d(last residual stream), graph-free
|
|
294
|
+
for (const d of this.headSink) d.dispose();
|
|
295
|
+
this.headSink = [];
|
|
210
296
|
boundaries[nSeg]!.dispose();
|
|
211
297
|
boundaries[nSeg] = null;
|
|
212
298
|
|
|
@@ -220,13 +306,15 @@ export class SegmentedBackward {
|
|
|
220
306
|
res.outputs[0]!.dispose(); // segment output — unused (only its vjp matters)
|
|
221
307
|
// vjps[0] = dh_in (grad w.r.t. this segment's input boundary) -> dh_out
|
|
222
308
|
// for segment k-1; the rest scatter into the global flat vector. Detach
|
|
223
|
-
//
|
|
309
|
+
// to graph-free leaves (eval doesn't detach; keeps peak bounded) — ONE
|
|
310
|
+
// barrier for the segment's whole vjp set.
|
|
311
|
+
const leaves = detachLeaves(res.vjps);
|
|
224
312
|
idxs.forEach((j, s) => {
|
|
225
|
-
grads[j] =
|
|
226
|
-
grads[n + j] =
|
|
313
|
+
grads[j] = leaves[1 + 2 * s]!; // dA
|
|
314
|
+
grads[n + j] = leaves[1 + 2 * s + 1]!; // dB
|
|
227
315
|
});
|
|
228
316
|
dhOut!.dispose(); // consumed as this segment's cotangent
|
|
229
|
-
dhOut =
|
|
317
|
+
dhOut = leaves[0]!;
|
|
230
318
|
boundaries[k]!.dispose();
|
|
231
319
|
boundaries[k] = null;
|
|
232
320
|
}
|
|
@@ -238,6 +326,8 @@ export class SegmentedBackward {
|
|
|
238
326
|
} finally {
|
|
239
327
|
inputIds.dispose();
|
|
240
328
|
this.curBatch = null;
|
|
329
|
+
for (const d of this.headSink) d.dispose(); // no-op unless the head threw mid-step
|
|
330
|
+
this.headSink = [];
|
|
241
331
|
dhOut?.dispose();
|
|
242
332
|
for (const b of boundaries) b?.dispose();
|
|
243
333
|
if (!transferred) {
|
|
@@ -295,6 +385,8 @@ export class SegmentedBackwardGemma4 {
|
|
|
295
385
|
private curBatch: SftBatch | null = null;
|
|
296
386
|
private curMasks: Map<string, Mask> | null = null;
|
|
297
387
|
private curPerLayer: MlxArray | null = null;
|
|
388
|
+
// Bounded-head per-step transients — see SegmentedBackward.headSink.
|
|
389
|
+
private headSink: Array<{ dispose(): void }> = [];
|
|
298
390
|
private disposed = false;
|
|
299
391
|
|
|
300
392
|
constructor(
|
|
@@ -337,11 +429,11 @@ export class SegmentedBackwardGemma4 {
|
|
|
337
429
|
this.caches = Array.from({ length: model.numDonors }, () => new TrainingCache());
|
|
338
430
|
this.one = MlxArray.fromFloat32(new Float32Array([1]), []);
|
|
339
431
|
|
|
340
|
-
// Loss head (identical to MiniCPM5): finalNorm +
|
|
432
|
+
// Loss head (identical to MiniCPM5): finalNorm + bounded response CE over the last
|
|
341
433
|
// boundary, vjp with cotangent 1.0 -> [loss], [dh].
|
|
342
434
|
this.headVjp = new Vjp((p) => {
|
|
343
435
|
const hn = model.finalNorm.forward(p[0]!);
|
|
344
|
-
const loss =
|
|
436
|
+
const loss = boundedSftCe(model, hn, this.curBatch!, this.headSink);
|
|
345
437
|
hn.dispose();
|
|
346
438
|
return [loss];
|
|
347
439
|
}, 1);
|
|
@@ -437,14 +529,22 @@ export class SegmentedBackwardGemma4 {
|
|
|
437
529
|
for (const [d, kv] of donorKvOut)
|
|
438
530
|
if (!produces[k]!.includes(d) && kv.kind === "plain") { kv.keys.dispose(); kv.values.dispose(); }
|
|
439
531
|
boundaries.push(detachLeaf(h));
|
|
532
|
+
segMem(`fwd seg ${k} [${lo},${hi})`);
|
|
440
533
|
}
|
|
441
534
|
|
|
442
|
-
// --- 2. Loss head (vjp, cotangent 1.0).
|
|
535
|
+
// --- 2. Loss head (vjp, cotangent 1.0). Bounded-head sink discipline:
|
|
536
|
+
// materialize the head-vjp graph BEFORE freeing the sink (see the
|
|
537
|
+
// MiniCPM5 class / ORPO classes — lazy-eval UAF). ---
|
|
538
|
+
this.headSink = [];
|
|
443
539
|
const head = this.headVjp.apply([boundaries[nSeg]!], [this.one]);
|
|
540
|
+
ops.evalAll([head.outputs[0]!, head.vjps[0]!]);
|
|
444
541
|
value = detachLeaf(head.outputs[0]!);
|
|
445
542
|
dhOut = detachLeaf(head.vjps[0]!);
|
|
543
|
+
for (const d of this.headSink) d.dispose();
|
|
544
|
+
this.headSink = [];
|
|
446
545
|
boundaries[nSeg]!.dispose();
|
|
447
546
|
boundaries[nSeg] = null;
|
|
547
|
+
segMem("head vjp");
|
|
448
548
|
|
|
449
549
|
// --- 3. Backward, reverse over segments (vjp, cotangents dh + dKV). ---
|
|
450
550
|
for (let k = nSeg - 1; k >= 0; k--) {
|
|
@@ -466,17 +566,17 @@ export class SegmentedBackwardGemma4 {
|
|
|
466
566
|
const res = this.segVjps[k]!.apply(primals, cots);
|
|
467
567
|
for (const o of res.outputs) o.dispose();
|
|
468
568
|
|
|
469
|
-
// vjps[0] = dh_in; then (dk,dv) per consumed donor; then (da,db) per
|
|
470
|
-
|
|
569
|
+
// vjps[0] = dh_in; then (dk,dv) per consumed donor; then (da,db) per
|
|
570
|
+
// LoRA — detached in ONE barrier (backlog #9).
|
|
571
|
+
const leaves = detachLeaves(res.vjps);
|
|
572
|
+
const newDh = leaves[0]!;
|
|
471
573
|
cons.forEach((d, ci) => {
|
|
472
|
-
|
|
473
|
-
const dv = detachLeaf(res.vjps[1 + 2 * ci + 1]!);
|
|
474
|
-
this.accumulateDKV(dKV, d, dk, dv);
|
|
574
|
+
this.accumulateDKV(dKV, d, leaves[1 + 2 * ci]!, leaves[1 + 2 * ci + 1]!);
|
|
475
575
|
});
|
|
476
576
|
const base = 1 + 2 * cons.length;
|
|
477
577
|
idxs.forEach((j, s) => {
|
|
478
|
-
grads[j] =
|
|
479
|
-
grads[n + j] =
|
|
578
|
+
grads[j] = leaves[base + 2 * s]!;
|
|
579
|
+
grads[n + j] = leaves[base + 2 * s + 1]!;
|
|
480
580
|
});
|
|
481
581
|
|
|
482
582
|
dh.dispose();
|
|
@@ -488,6 +588,7 @@ export class SegmentedBackwardGemma4 {
|
|
|
488
588
|
}
|
|
489
589
|
boundaries[k]!.dispose();
|
|
490
590
|
boundaries[k] = null;
|
|
591
|
+
segMem(`bwd seg ${k} [${ranges[k]![0]},${ranges[k]![1]})`);
|
|
491
592
|
}
|
|
492
593
|
|
|
493
594
|
transferred = true;
|
|
@@ -498,6 +599,8 @@ export class SegmentedBackwardGemma4 {
|
|
|
498
599
|
this.curPerLayer?.dispose();
|
|
499
600
|
this.curPerLayer = null;
|
|
500
601
|
if (this.curMasks) { for (const m of this.curMasks.values()) m.arr?.dispose(); this.curMasks = null; }
|
|
602
|
+
for (const d of this.headSink) d.dispose(); // no-op unless the head threw mid-step
|
|
603
|
+
this.headSink = [];
|
|
501
604
|
dhOut?.dispose();
|
|
502
605
|
for (const b of boundaries) b?.dispose();
|
|
503
606
|
for (const kv of donorBnd.values()) { kv.keys.dispose(); kv.values.dispose(); }
|
|
@@ -725,12 +828,13 @@ export class SegmentedBackwardOrpo {
|
|
|
725
828
|
for (const j of idxs) params.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
|
|
726
829
|
const res = this.segVjps[k]!.apply(params, [dhC!]);
|
|
727
830
|
res.outputs[0]!.dispose();
|
|
831
|
+
const leaves = detachLeaves(res.vjps); // one barrier (backlog #9)
|
|
728
832
|
idxs.forEach((j, s) => {
|
|
729
|
-
gradsC[j] =
|
|
730
|
-
gradsC[n + j] =
|
|
833
|
+
gradsC[j] = leaves[1 + 2 * s]!; // dA
|
|
834
|
+
gradsC[n + j] = leaves[1 + 2 * s + 1]!; // dB
|
|
731
835
|
});
|
|
732
836
|
dhC!.dispose();
|
|
733
|
-
dhC =
|
|
837
|
+
dhC = leaves[0]!;
|
|
734
838
|
bndC[k]!.dispose(); bndC[k] = null;
|
|
735
839
|
}
|
|
736
840
|
dhC!.dispose(); dhC = null; // embedding grad, discarded (no LoRA on embed)
|
|
@@ -742,12 +846,13 @@ export class SegmentedBackwardOrpo {
|
|
|
742
846
|
for (const j of idxs) params.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
|
|
743
847
|
const res = this.segVjps[k]!.apply(params, [dhR!]);
|
|
744
848
|
res.outputs[0]!.dispose();
|
|
849
|
+
const leaves = detachLeaves(res.vjps); // one barrier (backlog #9)
|
|
745
850
|
idxs.forEach((j, s) => {
|
|
746
|
-
gradsR[j] =
|
|
747
|
-
gradsR[n + j] =
|
|
851
|
+
gradsR[j] = leaves[1 + 2 * s]!; // dA
|
|
852
|
+
gradsR[n + j] = leaves[1 + 2 * s + 1]!; // dB
|
|
748
853
|
});
|
|
749
854
|
dhR!.dispose();
|
|
750
|
-
dhR =
|
|
855
|
+
dhR = leaves[0]!;
|
|
751
856
|
bndR[k]!.dispose(); bndR[k] = null;
|
|
752
857
|
}
|
|
753
858
|
dhR!.dispose(); dhR = null; // embedding grad, discarded
|
|
@@ -912,7 +1017,7 @@ export class SegmentedBackwardOrpoPrefix {
|
|
|
912
1017
|
this.headSink = [];
|
|
913
1018
|
this.curChunk = this.chunkCtx ? { ...this.chunkCtx, sink: this.headSink } : undefined;
|
|
914
1019
|
// One prefix cache per layer (captures P/Rc/Rr; makeMask -> block-sparse mask).
|
|
915
|
-
this.curCaches = model.layers.
|
|
1020
|
+
this.curCaches = prefixSharedCaches(model.layers.length, P, Rc, Rr);
|
|
916
1021
|
|
|
917
1022
|
const nSeg = ranges.length;
|
|
918
1023
|
const concat = new Int32Array(T);
|
|
@@ -957,12 +1062,13 @@ export class SegmentedBackwardOrpoPrefix {
|
|
|
957
1062
|
for (const j of idxs) params.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
|
|
958
1063
|
const res = this.segVjps[k]!.apply(params, [dhOut!]);
|
|
959
1064
|
res.outputs[0]!.dispose();
|
|
1065
|
+
const leaves = detachLeaves(res.vjps); // one barrier (backlog #9)
|
|
960
1066
|
idxs.forEach((j, s) => {
|
|
961
|
-
grads[j] =
|
|
962
|
-
grads[n + j] =
|
|
1067
|
+
grads[j] = leaves[1 + 2 * s]!; // dA
|
|
1068
|
+
grads[n + j] = leaves[1 + 2 * s + 1]!; // dB
|
|
963
1069
|
});
|
|
964
1070
|
dhOut!.dispose();
|
|
965
|
-
dhOut =
|
|
1071
|
+
dhOut = leaves[0]!;
|
|
966
1072
|
boundaries[k]!.dispose(); boundaries[k] = null;
|
|
967
1073
|
}
|
|
968
1074
|
// Segment-0 dh_in = grad w.r.t. the embedding output (no LoRA on embed) — drop.
|
|
@@ -1232,16 +1338,15 @@ export class SegmentedBackwardOrpoGemma4 {
|
|
|
1232
1338
|
const res = this.segVjps[k]!.apply(primals, cots);
|
|
1233
1339
|
for (const o of res.outputs) o.dispose();
|
|
1234
1340
|
|
|
1235
|
-
const
|
|
1341
|
+
const leaves = detachLeaves(res.vjps); // one barrier (backlog #9)
|
|
1342
|
+
const newDh = leaves[0]!;
|
|
1236
1343
|
cons.forEach((d, ci) => {
|
|
1237
|
-
|
|
1238
|
-
const dv = detachLeaf(res.vjps[1 + 2 * ci + 1]!);
|
|
1239
|
-
this.accumulateDKV(dKV, d, dk, dv);
|
|
1344
|
+
this.accumulateDKV(dKV, d, leaves[1 + 2 * ci]!, leaves[1 + 2 * ci + 1]!);
|
|
1240
1345
|
});
|
|
1241
1346
|
const base = 1 + 2 * cons.length;
|
|
1242
1347
|
idxs.forEach((j, s) => {
|
|
1243
|
-
grads[j] =
|
|
1244
|
-
grads[n + j] =
|
|
1348
|
+
grads[j] = leaves[base + 2 * s]!;
|
|
1349
|
+
grads[n + j] = leaves[base + 2 * s + 1]!;
|
|
1245
1350
|
});
|
|
1246
1351
|
|
|
1247
1352
|
dh.dispose();
|
|
@@ -1663,16 +1768,15 @@ export class SegmentedBackwardOrpoPrefixGemma4 {
|
|
|
1663
1768
|
const res = this.segVjps[k]!.apply(primals, cots);
|
|
1664
1769
|
for (const o of res.outputs) o.dispose();
|
|
1665
1770
|
|
|
1666
|
-
const
|
|
1771
|
+
const leaves = detachLeaves(res.vjps); // one barrier (backlog #9)
|
|
1772
|
+
const newDh = leaves[0]!;
|
|
1667
1773
|
cons.forEach((d, ci) => {
|
|
1668
|
-
|
|
1669
|
-
const dv = detachLeaf(res.vjps[1 + 2 * ci + 1]!);
|
|
1670
|
-
this.accumulateDKV(dKV, d, dk, dv);
|
|
1774
|
+
this.accumulateDKV(dKV, d, leaves[1 + 2 * ci]!, leaves[1 + 2 * ci + 1]!);
|
|
1671
1775
|
});
|
|
1672
1776
|
const base = 1 + 2 * cons.length;
|
|
1673
1777
|
idxs.forEach((j, s) => {
|
|
1674
|
-
grads[j] =
|
|
1675
|
-
grads[n + j] =
|
|
1778
|
+
grads[j] = leaves[base + 2 * s]!;
|
|
1779
|
+
grads[n + j] = leaves[base + 2 * s + 1]!;
|
|
1676
1780
|
});
|
|
1677
1781
|
|
|
1678
1782
|
dh.dispose();
|