mlx-bun 0.0.4
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/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- package/src/weights.ts +91 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
// LoRA trainer: SFT (masked cross-entropy) and DPO loops over a single
|
|
2
|
+
// quantized base, differentiating only the LoRA A/B leaves.
|
|
3
|
+
//
|
|
4
|
+
// The value_and_grad closure differentiates [...flatA, ...flatB]. Inside the
|
|
5
|
+
// closure we swap the incoming primal handles into the LoraWeights so the
|
|
6
|
+
// model's forward builds its graph against the differentiated leaves; after
|
|
7
|
+
// AdamW.step replaces a leaf, the writeback callback pushes the new handle
|
|
8
|
+
// back into the LoraWeights so the next forward (and the next closure swap)
|
|
9
|
+
// sees it. Base quantized weights are never in argnums — they stay frozen.
|
|
10
|
+
|
|
11
|
+
import { MlxArray } from "../mlx/array";
|
|
12
|
+
import { clearCache, peakMemory, activeMemory, cacheMemory, resetPeakMemory } from "../mlx/ffi";
|
|
13
|
+
|
|
14
|
+
const MEM_LOG = process.env.MLX_BUN_MEM_LOG === "1";
|
|
15
|
+
const gbStr = (b: number) => `${(b / 1e9).toFixed(2)}GB`;
|
|
16
|
+
import { ValueAndGrad } from "../mlx/autograd";
|
|
17
|
+
import * as ops from "../mlx/ops";
|
|
18
|
+
import { evalAll } from "../mlx/ops";
|
|
19
|
+
import type { RuntimeModel } from "../model/factory";
|
|
20
|
+
import { Gemma4Model, type GradCheckpointCtx } from "../model/gemma4";
|
|
21
|
+
import { MiniCPM5Model } from "../model/minicpm5";
|
|
22
|
+
import { setTrainingAttn } from "../model/flash-attention";
|
|
23
|
+
import { SegmentedBackward, SegmentedBackwardGemma4, planSegmentsBySize } from "./segmented";
|
|
24
|
+
import type { LoadedTokenizer } from "../tokenizer";
|
|
25
|
+
import type { ChatTemplate } from "../chat-template";
|
|
26
|
+
import type { Emit } from "../jobs/types";
|
|
27
|
+
import {
|
|
28
|
+
loadSftDataset, iterateSftBatches, loadDpoDataset, iterateDpoBatches,
|
|
29
|
+
rowLength, type SftBatch, type DpoBatch,
|
|
30
|
+
} from "./dataset";
|
|
31
|
+
import {
|
|
32
|
+
buildTrainableLora, attachForTraining, detachTraining, flatParams,
|
|
33
|
+
saveAdapter, disposeLora, type TrainableLora, type SaveAdapterConfig,
|
|
34
|
+
} from "./lora-params";
|
|
35
|
+
import { resolveRanks, DEFAULT_TARGET_MODULES, type RankScaling } from "./rank";
|
|
36
|
+
import { sftLoss, dpoLoss, dpoRefLogps, dpoMetrics } from "./loss";
|
|
37
|
+
import { AdamW, warmupCosineSchedule } from "./optimizer";
|
|
38
|
+
import { writeFileSync } from "node:fs";
|
|
39
|
+
|
|
40
|
+
export interface TrainConfig {
|
|
41
|
+
method: "sft" | "dpo";
|
|
42
|
+
rank: number;
|
|
43
|
+
scale: number;
|
|
44
|
+
rankScaling: RankScaling;
|
|
45
|
+
targetModules: string[];
|
|
46
|
+
numLayers: number;
|
|
47
|
+
iters: number;
|
|
48
|
+
learningRate: number;
|
|
49
|
+
maxSeqLen: number;
|
|
50
|
+
batchSize: number;
|
|
51
|
+
gradAccumSteps: number;
|
|
52
|
+
seed: number;
|
|
53
|
+
stepsPerReport: number;
|
|
54
|
+
stepsPerEval: number;
|
|
55
|
+
betas: [number, number];
|
|
56
|
+
weightDecay: number;
|
|
57
|
+
/** Gradient checkpointing: recompute layer activations in the backward pass
|
|
58
|
+
* to bound memory at long context (mlx-lm grad_checkpoint; default in optiq).
|
|
59
|
+
* Numerically identical to off — pure memory↔compute trade. */
|
|
60
|
+
gradCheckpoint: boolean;
|
|
61
|
+
/** Segmented backward: layers per segment (0 = off). Streams the SFT backward
|
|
62
|
+
* segment-by-segment so only one segment's activations are live at a time —
|
|
63
|
+
* beats per-layer checkpointing at long context, where naive checkpointing
|
|
64
|
+
* holds every layer's recompute activations at once and crashes. B=1 SFT
|
|
65
|
+
* only (the responseOnlyCe path); mutually exclusive with gradCheckpoint.
|
|
66
|
+
* Numerically identical to off. See docs/design/segmented-backward-training.md. */
|
|
67
|
+
segmentSize: number;
|
|
68
|
+
/** Keep ALL eval-step checkpoints (full mountable adapters under
|
|
69
|
+
* checkpoints/step-<NNNNN>-val<loss>/) + write metrics.json. Off by default
|
|
70
|
+
* so server finetune jobs and tests stay clean; the chunk-finetune script
|
|
71
|
+
* turns it on so the best-ON-TASK checkpoint can be chosen by the real eval
|
|
72
|
+
* afterward (val is only a proxy — we keep them all and let the eval pick). */
|
|
73
|
+
saveCheckpoints: boolean;
|
|
74
|
+
// DPO
|
|
75
|
+
dpoBeta: number;
|
|
76
|
+
dpoWarmupIters: number;
|
|
77
|
+
dpoLrSchedule: "constant" | "cosine";
|
|
78
|
+
// output
|
|
79
|
+
adapterPath: string;
|
|
80
|
+
baseModel: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface TrainResult {
|
|
84
|
+
adapterPath: string;
|
|
85
|
+
appliedRanks: Record<string, number>;
|
|
86
|
+
numIters: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const DEFAULT_TRAIN_CONFIG: TrainConfig = {
|
|
90
|
+
method: "sft",
|
|
91
|
+
rank: 8,
|
|
92
|
+
scale: 1.0,
|
|
93
|
+
rankScaling: "by_bits",
|
|
94
|
+
targetModules: [...DEFAULT_TARGET_MODULES],
|
|
95
|
+
numLayers: -1,
|
|
96
|
+
iters: 100,
|
|
97
|
+
learningRate: 2e-4,
|
|
98
|
+
maxSeqLen: 512,
|
|
99
|
+
batchSize: 1,
|
|
100
|
+
gradAccumSteps: 1,
|
|
101
|
+
seed: 0,
|
|
102
|
+
stepsPerReport: 10,
|
|
103
|
+
stepsPerEval: 50,
|
|
104
|
+
betas: [0.9, 0.999],
|
|
105
|
+
weightDecay: 0.01,
|
|
106
|
+
gradCheckpoint: false, // default off until validated; flip to true (optiq default) after
|
|
107
|
+
segmentSize: 0, // default off; >0 enables segmented backward (MiniCPM5 SFT B=1)
|
|
108
|
+
saveCheckpoints: false, // off by default; scripts opt in (keep-all-checkpoints + metrics.json)
|
|
109
|
+
|
|
110
|
+
dpoBeta: 0.1,
|
|
111
|
+
dpoWarmupIters: 0,
|
|
112
|
+
dpoLrSchedule: "cosine",
|
|
113
|
+
adapterPath: "adapters",
|
|
114
|
+
baseModel: "",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Run a LoRA fine-tune. Emits per-step train metrics, periodic val metrics,
|
|
118
|
+
* and a final stage:done with the adapter path + applied ranks. Saves the
|
|
119
|
+
* adapter (last + best-on-val) and returns where it landed. */
|
|
120
|
+
export async function trainLora(
|
|
121
|
+
model: RuntimeModel,
|
|
122
|
+
tok: LoadedTokenizer,
|
|
123
|
+
tmpl: ChatTemplate,
|
|
124
|
+
dataDir: string,
|
|
125
|
+
cfg: TrainConfig,
|
|
126
|
+
emit: Emit,
|
|
127
|
+
): Promise<TrainResult> {
|
|
128
|
+
emit({ type: "stage", stage: "setup", progress: 0.02, message: "resolving ranks" });
|
|
129
|
+
|
|
130
|
+
const ranks = resolveRanks(model, {
|
|
131
|
+
rank: cfg.rank,
|
|
132
|
+
rankScaling: cfg.rankScaling,
|
|
133
|
+
targetModules: cfg.targetModules,
|
|
134
|
+
numLayers: cfg.numLayers,
|
|
135
|
+
});
|
|
136
|
+
const appliedRanks: Record<string, number> = Object.fromEntries(ranks);
|
|
137
|
+
|
|
138
|
+
const lora = buildTrainableLora(model, ranks, cfg.scale, cfg.seed);
|
|
139
|
+
attachForTraining(model, lora, "train");
|
|
140
|
+
|
|
141
|
+
const saveCfg: SaveAdapterConfig = {
|
|
142
|
+
rank: cfg.rank,
|
|
143
|
+
scale: cfg.scale,
|
|
144
|
+
rankScaling: cfg.rankScaling,
|
|
145
|
+
targetModules: cfg.targetModules,
|
|
146
|
+
numLayers: cfg.numLayers,
|
|
147
|
+
method: cfg.method,
|
|
148
|
+
baseModel: cfg.baseModel,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Keep-ALL-checkpoints + a durable metrics record. At each eval step we
|
|
152
|
+
// snapshot the adapter to checkpoints/step-<NNNNN>-val<loss>/ — a full,
|
|
153
|
+
// mountable adapter — so the best-ON-TASK checkpoint can be chosen by the
|
|
154
|
+
// real eval afterward (val is only a proxy for task quality, so we do NOT
|
|
155
|
+
// pick for the user). The loss/val trajectory is written to metrics.json so
|
|
156
|
+
// runs are comparable and survive (the tee'd stdout log is ephemeral). We
|
|
157
|
+
// wrap `emit` rather than touching the loops: it already streams train/val
|
|
158
|
+
// metrics, and the live LoRA leaves are the current step's weights at emit
|
|
159
|
+
// time, so saving here captures exactly that checkpoint.
|
|
160
|
+
const trainHistory: { step: number; loss: number }[] = [];
|
|
161
|
+
const valHistory: { step: number; loss: number; checkpoint: string | null }[] = [];
|
|
162
|
+
const startedAt = Date.now();
|
|
163
|
+
const collect: Emit = (e) => {
|
|
164
|
+
if (e.type === "metric" && e.kind === "train") {
|
|
165
|
+
trainHistory.push({ step: e.step, loss: e.loss });
|
|
166
|
+
} else if (e.type === "metric" && e.kind === "val") {
|
|
167
|
+
let checkpoint: string | null = null;
|
|
168
|
+
if (cfg.saveCheckpoints) {
|
|
169
|
+
const tag = `step-${String(e.step).padStart(5, "0")}-val${e.loss.toFixed(4)}`;
|
|
170
|
+
checkpoint = `${cfg.adapterPath}/checkpoints/${tag}`;
|
|
171
|
+
saveAdapter(lora, checkpoint, saveCfg, appliedRanks);
|
|
172
|
+
emit({ type: "stage", stage: "checkpoint", progress: e.step / cfg.iters,
|
|
173
|
+
message: `checkpoint ${tag}` });
|
|
174
|
+
}
|
|
175
|
+
valHistory.push({ step: e.step, loss: e.loss, checkpoint });
|
|
176
|
+
}
|
|
177
|
+
emit(e);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const result =
|
|
182
|
+
cfg.method === "dpo"
|
|
183
|
+
? await dpoLoop(model, tok, tmpl, dataDir, cfg, lora, collect)
|
|
184
|
+
: await sftLoop(model, tok, tmpl, dataDir, cfg, lora, collect);
|
|
185
|
+
|
|
186
|
+
// Final save (last adapter).
|
|
187
|
+
detachTraining(model, lora);
|
|
188
|
+
saveAdapter(lora, cfg.adapterPath, saveCfg, appliedRanks);
|
|
189
|
+
|
|
190
|
+
// Durable, structured run record alongside the adapter (only when we kept
|
|
191
|
+
// checkpoints). Pick the best-val one for convenience, but ship ALL of them
|
|
192
|
+
// — the real eval decides; val is only a proxy.
|
|
193
|
+
if (cfg.saveCheckpoints) {
|
|
194
|
+
let bestVal: { step: number; loss: number; checkpoint: string | null } | null = null;
|
|
195
|
+
for (const v of valHistory) if (bestVal === null || v.loss < bestVal.loss) bestVal = v;
|
|
196
|
+
writeFileSync(`${cfg.adapterPath}/metrics.json`, JSON.stringify({
|
|
197
|
+
config: {
|
|
198
|
+
method: cfg.method, rank: cfg.rank, scale: cfg.scale,
|
|
199
|
+
learningRate: cfg.learningRate, maxSeqLen: cfg.maxSeqLen,
|
|
200
|
+
segmentSize: cfg.segmentSize, iters: cfg.iters,
|
|
201
|
+
numLayers: cfg.numLayers, baseModel: cfg.baseModel,
|
|
202
|
+
},
|
|
203
|
+
wallSeconds: (Date.now() - startedAt) / 1000,
|
|
204
|
+
peakGb: peakMemory() / 1e9,
|
|
205
|
+
finalTrainLoss: trainHistory.at(-1)?.loss ?? null,
|
|
206
|
+
finalValLoss: valHistory.at(-1)?.loss ?? null,
|
|
207
|
+
bestVal,
|
|
208
|
+
valTrajectory: valHistory,
|
|
209
|
+
trainLosses: trainHistory,
|
|
210
|
+
}, null, 2));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
emit({
|
|
214
|
+
type: "stage",
|
|
215
|
+
stage: "done",
|
|
216
|
+
progress: 1,
|
|
217
|
+
message: `adapter saved to ${cfg.adapterPath}`,
|
|
218
|
+
adapter_path: cfg.adapterPath,
|
|
219
|
+
applied_ranks: appliedRanks,
|
|
220
|
+
});
|
|
221
|
+
return { adapterPath: cfg.adapterPath, appliedRanks, numIters: result.numIters };
|
|
222
|
+
} finally {
|
|
223
|
+
disposeLora(lora);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// SFT loop
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
async function sftLoop(
|
|
232
|
+
model: RuntimeModel,
|
|
233
|
+
tok: LoadedTokenizer,
|
|
234
|
+
tmpl: ChatTemplate,
|
|
235
|
+
dataDir: string,
|
|
236
|
+
cfg: TrainConfig,
|
|
237
|
+
lora: TrainableLora,
|
|
238
|
+
emit: Emit,
|
|
239
|
+
): Promise<{ numIters: number }> {
|
|
240
|
+
emit({ type: "stage", stage: "data", progress: 0.05, message: "loading SFT dataset" });
|
|
241
|
+
const train = await loadSftDataset(`${dataDir}/train.jsonl`, tok, tmpl);
|
|
242
|
+
const valid = (await fileExists(`${dataDir}/valid.jsonl`))
|
|
243
|
+
? await loadSftDataset(`${dataDir}/valid.jsonl`, tok, tmpl)
|
|
244
|
+
: [];
|
|
245
|
+
|
|
246
|
+
const params = flatParams(lora);
|
|
247
|
+
const opt = new AdamW(
|
|
248
|
+
params,
|
|
249
|
+
{ lr: cfg.learningRate, betas: cfg.betas, eps: 1e-8, weightDecay: cfg.weightDecay },
|
|
250
|
+
(i, p) => writeParam(lora, i, p),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Segmented backward: stream the SFT backward segment-by-segment (only one
|
|
254
|
+
// segment's activations live at a time). Phase A is MiniCPM5 SFT B=1 only;
|
|
255
|
+
// it replaces the single value_and_grad below and is mutually exclusive with
|
|
256
|
+
// gradient checkpointing (see docs/design/segmented-backward-training.md).
|
|
257
|
+
const useSegmented = cfg.segmentSize > 0;
|
|
258
|
+
let segmented: SegmentedBackward | SegmentedBackwardGemma4 | null = null;
|
|
259
|
+
if (useSegmented) {
|
|
260
|
+
if (model instanceof MiniCPM5Model)
|
|
261
|
+
segmented = new SegmentedBackward(model, lora, planSegmentsBySize(model.layers.length, cfg.segmentSize));
|
|
262
|
+
else if (model instanceof Gemma4Model)
|
|
263
|
+
segmented = new SegmentedBackwardGemma4(model, lora, planSegmentsBySize(model.layers.length, cfg.segmentSize));
|
|
264
|
+
else
|
|
265
|
+
throw new Error("segmented backward (segmentSize > 0) is only wired for MiniCPM5 and Gemma4");
|
|
266
|
+
emit({ type: "stage", stage: "setup", progress: 0.04,
|
|
267
|
+
message: `segmented backward: ${Math.ceil(model.layers.length / cfg.segmentSize)} segments of <=${cfg.segmentSize} layers` });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// The value_and_grad loss closure: temporarily swap the differentiated
|
|
271
|
+
// primals into the LoraWeights so the forward graph differentiates them,
|
|
272
|
+
// build the SFT loss against the active batch, then restore the original
|
|
273
|
+
// leaf wrappers (the primal wrappers get disposed by the autograd closure;
|
|
274
|
+
// the graph already captured their handles). Not built on the segmented path
|
|
275
|
+
// (segmentedSftGrads runs its own per-segment value_and_grads).
|
|
276
|
+
let currentBatch: SftBatch | null = null;
|
|
277
|
+
const vag = useSegmented
|
|
278
|
+
? null
|
|
279
|
+
: new ValueAndGrad((primals) => {
|
|
280
|
+
const saved = swapPrimals(lora, primals);
|
|
281
|
+
try {
|
|
282
|
+
return sftLoss(model, currentBatch!);
|
|
283
|
+
} finally {
|
|
284
|
+
restorePrimals(lora, saved);
|
|
285
|
+
}
|
|
286
|
+
}, params.map((_, i) => i));
|
|
287
|
+
|
|
288
|
+
// Gradient checkpointing: group each layer's LoRA weights so forwardLayers
|
|
289
|
+
// can thread them as explicit checkpoint inputs (see Gemma4Model.gradCkpt).
|
|
290
|
+
// Checkpoints are created per forward and disposed after each step's backward.
|
|
291
|
+
let ckptCtx: GradCheckpointCtx | null = null;
|
|
292
|
+
if (!useSegmented && cfg.gradCheckpoint && model instanceof Gemma4Model) {
|
|
293
|
+
const byLayer: GradCheckpointCtx["byLayer"] = new Map();
|
|
294
|
+
for (const t of lora.targets) {
|
|
295
|
+
const m = t.modulePath.match(/\.layers\.(\d+)\./);
|
|
296
|
+
if (!m) continue;
|
|
297
|
+
const li = Number(m[1]);
|
|
298
|
+
(byLayer.get(li) ?? byLayer.set(li, []).get(li)!).push(t.lw);
|
|
299
|
+
}
|
|
300
|
+
ckptCtx = { byLayer, keepAlive: [] };
|
|
301
|
+
model.gradCkpt = ckptCtx;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Training attention. Default = mlx fused SDPA (ops.sdpa): verified correct
|
|
305
|
+
// dQ/dK/dV vs a hand-rolled reference (scripts/sdpa-vs-manual.ts: 0.00%) and
|
|
306
|
+
// fast (one fused kernel). flash is opt-in via MLX_BUN_TRAIN_ATTN=flash for an
|
|
307
|
+
// O(L) memory backward at very long context, BUT its dK kernel currently has
|
|
308
|
+
// a bug (~100% off) and is ~30x slower here — so it is NOT the default.
|
|
309
|
+
// Cleared in finally.
|
|
310
|
+
if (process.env.MLX_BUN_TRAIN_ATTN === "flash") setTrainingAttn("flash");
|
|
311
|
+
|
|
312
|
+
// Pad with the eos id when available (a real sentinel token); padded
|
|
313
|
+
// positions are excluded from both the loss and attention masks, so the
|
|
314
|
+
// exact pad value never affects the result — it just must be a valid id.
|
|
315
|
+
const padId = tok.eosTokenId ?? 0;
|
|
316
|
+
const batches = iterateSftBatches(train, cfg.batchSize, cfg.maxSeqLen, cfg.seed, true, padId);
|
|
317
|
+
const t0 = Date.now();
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
for (let step = 1; step <= cfg.iters; step++) {
|
|
321
|
+
currentBatch = batches.next().value as SftBatch;
|
|
322
|
+
const tokensThisStep = countResponseTokens(currentBatch);
|
|
323
|
+
|
|
324
|
+
if (MEM_LOG) resetPeakMemory();
|
|
325
|
+
const { value, grads } = segmented
|
|
326
|
+
? segmented.step(currentBatch!)
|
|
327
|
+
: vag!.apply(flatParams(lora));
|
|
328
|
+
// Evaluate the loss AND grads in ONE mlx_eval so the backward shares the
|
|
329
|
+
// forward pass. Evaluating `value` on its own first (e.g. via toFloat32)
|
|
330
|
+
// materializes then frees the forward, forcing the backward to recompute
|
|
331
|
+
// and hold ALL layer activations — which silently defeats gradient
|
|
332
|
+
// checkpointing (verified: separate-eval ~0% memory savings, combined-eval
|
|
333
|
+
// restores it). Keep this single combined eval before reading either.
|
|
334
|
+
evalAll([value, ...grads]);
|
|
335
|
+
const lossVal = value.toFloat32()[0]!;
|
|
336
|
+
value.dispose();
|
|
337
|
+
|
|
338
|
+
const gradNorm = globalNorm(grads); // grads already materialized above
|
|
339
|
+
opt.step(grads);
|
|
340
|
+
opt.evalState();
|
|
341
|
+
if (MEM_LOG) {
|
|
342
|
+
const pk = peakMemory(), act = activeMemory(), ch = cacheMemory();
|
|
343
|
+
clearCache();
|
|
344
|
+
console.log(` [mem] step ${step}: PEAK(live)=${gbStr(pk)} active=${gbStr(act)} cache=${gbStr(ch)} -> after clearCache active=${gbStr(activeMemory())}`);
|
|
345
|
+
} else {
|
|
346
|
+
clearCache();
|
|
347
|
+
}
|
|
348
|
+
if (ckptCtx) {
|
|
349
|
+
// Grads are materialized (globalNorm/evalState), so the recompute is
|
|
350
|
+
// done — free this step's checkpoint closures before the next forward.
|
|
351
|
+
for (const ck of ckptCtx.keepAlive) ck.dispose();
|
|
352
|
+
ckptCtx.keepAlive.length = 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (step % cfg.stepsPerReport === 0 || step === 1) {
|
|
356
|
+
const elapsed = (Date.now() - t0) / 1000;
|
|
357
|
+
emit({
|
|
358
|
+
type: "metric",
|
|
359
|
+
kind: "train",
|
|
360
|
+
step,
|
|
361
|
+
loss: lossVal,
|
|
362
|
+
grad_norm: gradNorm,
|
|
363
|
+
learning_rate: opt.lr,
|
|
364
|
+
tokens_per_sec: elapsed > 0 ? (step * tokensThisStep) / elapsed : 0,
|
|
365
|
+
progress: step / cfg.iters,
|
|
366
|
+
message: `step ${step}/${cfg.iters} loss=${lossVal.toFixed(4)}`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (valid.length > 0 && step % cfg.stepsPerEval === 0) {
|
|
371
|
+
// Eval is forward-only (no backward), so disable checkpointing for it —
|
|
372
|
+
// it adds no benefit and a forward-only pass fits without it.
|
|
373
|
+
if (ckptCtx) (model as Gemma4Model).gradCkpt = null;
|
|
374
|
+
const vLoss = evalSftLoss(model, valid, cfg);
|
|
375
|
+
if (ckptCtx) (model as Gemma4Model).gradCkpt = ckptCtx;
|
|
376
|
+
emit({ type: "metric", kind: "val", step, loss: vLoss, progress: step / cfg.iters });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} finally {
|
|
380
|
+
setTrainingAttn(null);
|
|
381
|
+
vag?.dispose();
|
|
382
|
+
segmented?.dispose();
|
|
383
|
+
opt.dispose();
|
|
384
|
+
if (ckptCtx) {
|
|
385
|
+
(model as Gemma4Model).gradCkpt = null;
|
|
386
|
+
for (const ck of ckptCtx.keepAlive) ck.dispose();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { numIters: cfg.iters };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function evalSftLoss(model: RuntimeModel, valid: ReturnType<typeof Array.prototype.slice>, cfg: TrainConfig): number {
|
|
393
|
+
let total = 0;
|
|
394
|
+
let n = 0;
|
|
395
|
+
for (const ex of valid as { ids: number[]; promptLen: number }[]) {
|
|
396
|
+
let ids = ex.ids;
|
|
397
|
+
let promptLen = ex.promptLen;
|
|
398
|
+
if (ids.length < 2) continue;
|
|
399
|
+
if (ids.length > cfg.maxSeqLen) {
|
|
400
|
+
ids = ids.slice(0, cfg.maxSeqLen);
|
|
401
|
+
promptLen = Math.min(promptLen, Math.max(0, ids.length - 1));
|
|
402
|
+
}
|
|
403
|
+
const batch: SftBatch = { ids: [ids], promptLens: [promptLen] };
|
|
404
|
+
try {
|
|
405
|
+
const loss = sftLoss(model, batch);
|
|
406
|
+
total += loss.toFloat32()[0]!;
|
|
407
|
+
loss.dispose();
|
|
408
|
+
n++;
|
|
409
|
+
} catch {
|
|
410
|
+
// skip examples with no response tokens
|
|
411
|
+
}
|
|
412
|
+
clearCache();
|
|
413
|
+
}
|
|
414
|
+
return n > 0 ? total / n : 0;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// DPO loop
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
async function dpoLoop(
|
|
422
|
+
model: RuntimeModel,
|
|
423
|
+
tok: LoadedTokenizer,
|
|
424
|
+
tmpl: ChatTemplate,
|
|
425
|
+
dataDir: string,
|
|
426
|
+
cfg: TrainConfig,
|
|
427
|
+
lora: TrainableLora,
|
|
428
|
+
emit: Emit,
|
|
429
|
+
): Promise<{ numIters: number }> {
|
|
430
|
+
emit({ type: "stage", stage: "data", progress: 0.05, message: "loading DPO dataset" });
|
|
431
|
+
const train = await loadDpoDataset(`${dataDir}/train.jsonl`, tok, tmpl, cfg.maxSeqLen);
|
|
432
|
+
const valid = (await fileExists(`${dataDir}/valid.jsonl`))
|
|
433
|
+
? await loadDpoDataset(`${dataDir}/valid.jsonl`, tok, tmpl, cfg.maxSeqLen)
|
|
434
|
+
: [];
|
|
435
|
+
|
|
436
|
+
const params = flatParams(lora);
|
|
437
|
+
const opt = new AdamW(
|
|
438
|
+
params,
|
|
439
|
+
{ lr: cfg.learningRate, betas: cfg.betas, eps: 1e-8, weightDecay: cfg.weightDecay },
|
|
440
|
+
(i, p) => writeParam(lora, i, p),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const schedule =
|
|
444
|
+
cfg.dpoLrSchedule === "cosine" || cfg.dpoWarmupIters > 0
|
|
445
|
+
? warmupCosineSchedule(cfg.learningRate, cfg.dpoWarmupIters, cfg.iters)
|
|
446
|
+
: null;
|
|
447
|
+
|
|
448
|
+
// Reference log-probs are computed each step OUTSIDE value_and_grad (so the
|
|
449
|
+
// reference activations free before the policy forwards), then read as
|
|
450
|
+
// detached constants inside the closure.
|
|
451
|
+
let currentBatch: DpoBatch | null = null;
|
|
452
|
+
let refChosen: MlxArray | null = null;
|
|
453
|
+
let refRejected: MlxArray | null = null;
|
|
454
|
+
|
|
455
|
+
const vag = new ValueAndGrad((primals) => {
|
|
456
|
+
const saved = swapPrimals(lora, primals);
|
|
457
|
+
try {
|
|
458
|
+
return dpoLoss(model, currentBatch!, cfg.dpoBeta, refChosen!, refRejected!);
|
|
459
|
+
} finally {
|
|
460
|
+
restorePrimals(lora, saved);
|
|
461
|
+
}
|
|
462
|
+
}, params.map((_, i) => i));
|
|
463
|
+
|
|
464
|
+
const padId = tok.eosTokenId ?? 0;
|
|
465
|
+
const batches = iterateDpoBatches(train, cfg.batchSize, cfg.seed, true, padId);
|
|
466
|
+
const t0 = Date.now();
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
for (let step = 1; step <= cfg.iters; step++) {
|
|
470
|
+
currentBatch = batches.next().value as DpoBatch;
|
|
471
|
+
if (schedule) opt.lr = schedule(step);
|
|
472
|
+
|
|
473
|
+
const refs = dpoRefLogps(model, lora, currentBatch);
|
|
474
|
+
refChosen = refs.refChosen;
|
|
475
|
+
refRejected = refs.refRejected;
|
|
476
|
+
|
|
477
|
+
const { value, grads } = vag.apply(flatParams(lora));
|
|
478
|
+
const lossVal = value.toFloat32()[0]!;
|
|
479
|
+
value.dispose();
|
|
480
|
+
|
|
481
|
+
const gradNorm = globalNorm(grads);
|
|
482
|
+
opt.step(grads);
|
|
483
|
+
opt.evalState();
|
|
484
|
+
|
|
485
|
+
refChosen.dispose();
|
|
486
|
+
refRejected.dispose();
|
|
487
|
+
refChosen = refRejected = null;
|
|
488
|
+
clearCache();
|
|
489
|
+
|
|
490
|
+
if (step % cfg.stepsPerReport === 0 || step === 1) {
|
|
491
|
+
const m = dpoMetrics(model, lora, currentBatch, cfg.dpoBeta);
|
|
492
|
+
const elapsed = (Date.now() - t0) / 1000;
|
|
493
|
+
emit({
|
|
494
|
+
type: "metric",
|
|
495
|
+
kind: "train",
|
|
496
|
+
step,
|
|
497
|
+
loss: lossVal,
|
|
498
|
+
grad_norm: gradNorm,
|
|
499
|
+
learning_rate: opt.lr,
|
|
500
|
+
accuracy: m.accuracy,
|
|
501
|
+
margin: m.margin,
|
|
502
|
+
tokens_per_sec: 0,
|
|
503
|
+
progress: step / cfg.iters,
|
|
504
|
+
message: `step ${step}/${cfg.iters} loss=${lossVal.toFixed(4)} acc=${m.accuracy.toFixed(2)} margin=${m.margin.toFixed(3)}`,
|
|
505
|
+
});
|
|
506
|
+
clearCache();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (valid.length > 0 && step % cfg.stepsPerEval === 0) {
|
|
510
|
+
let vl = 0, va = 0, vm = 0, vn = 0;
|
|
511
|
+
for (const ex of valid) {
|
|
512
|
+
const vb: DpoBatch = {
|
|
513
|
+
chosenIds: [ex.chosenIds], rejectedIds: [ex.rejectedIds],
|
|
514
|
+
chosenMask: [ex.chosenMask], rejectedMask: [ex.rejectedMask],
|
|
515
|
+
};
|
|
516
|
+
const m = dpoMetrics(model, lora, vb, cfg.dpoBeta);
|
|
517
|
+
vl += m.loss; va += m.accuracy; vm += m.margin; vn++;
|
|
518
|
+
clearCache();
|
|
519
|
+
}
|
|
520
|
+
emit({
|
|
521
|
+
type: "metric", kind: "val", step,
|
|
522
|
+
loss: vn ? vl / vn : 0,
|
|
523
|
+
accuracy: vn ? va / vn : 0,
|
|
524
|
+
margin: vn ? vm / vn : 0,
|
|
525
|
+
progress: step / cfg.iters,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} finally {
|
|
530
|
+
vag.dispose();
|
|
531
|
+
opt.dispose();
|
|
532
|
+
refChosen?.dispose();
|
|
533
|
+
refRejected?.dispose();
|
|
534
|
+
}
|
|
535
|
+
return { numIters: cfg.iters };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ---------------------------------------------------------------------------
|
|
539
|
+
// helpers
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
/** Swap the incoming value_and_grad primals into the LoraWeights so the
|
|
543
|
+
* forward graph differentiates them. Order: [...A, ...B] over sorted targets.
|
|
544
|
+
* Returns the displaced original leaves so the caller can restore them (the
|
|
545
|
+
* primal wrappers are disposed by the autograd closure after this returns). */
|
|
546
|
+
function swapPrimals(lora: TrainableLora, primals: MlxArray[]): MlxArray[] {
|
|
547
|
+
const n = lora.targets.length;
|
|
548
|
+
const saved: MlxArray[] = [];
|
|
549
|
+
for (let i = 0; i < n; i++) {
|
|
550
|
+
saved.push(lora.targets[i]!.lw.a, lora.targets[i]!.lw.b);
|
|
551
|
+
lora.targets[i]!.lw.a = primals[i]!;
|
|
552
|
+
lora.targets[i]!.lw.b = primals[n + i]!;
|
|
553
|
+
}
|
|
554
|
+
return saved;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Restore the original leaves displaced by swapPrimals (saved order is
|
|
558
|
+
* [a0, b0, a1, b1, ...]). */
|
|
559
|
+
function restorePrimals(lora: TrainableLora, saved: MlxArray[]): void {
|
|
560
|
+
for (let i = 0; i < lora.targets.length; i++) {
|
|
561
|
+
lora.targets[i]!.lw.a = saved[2 * i]!;
|
|
562
|
+
lora.targets[i]!.lw.b = saved[2 * i + 1]!;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Write a replaced param (AdamW output) back into its LoraWeights leaf. */
|
|
567
|
+
function writeParam(lora: TrainableLora, i: number, p: MlxArray): void {
|
|
568
|
+
const n = lora.targets.length;
|
|
569
|
+
if (i < n) lora.targets[i]!.lw.a = p;
|
|
570
|
+
else lora.targets[i - n]!.lw.b = p;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** L2 norm over all grads (also forces eval). */
|
|
574
|
+
function globalNorm(grads: MlxArray[]): number {
|
|
575
|
+
const partial: MlxArray[] = grads.map((g) => {
|
|
576
|
+
const sq = ops.square(g);
|
|
577
|
+
const flat = ops.reshape(sq, [g.size]);
|
|
578
|
+
const sum = ops.sumAxis(flat, 0, false);
|
|
579
|
+
sq.dispose();
|
|
580
|
+
flat.dispose();
|
|
581
|
+
return sum;
|
|
582
|
+
});
|
|
583
|
+
evalAll(partial);
|
|
584
|
+
let sumSq = 0;
|
|
585
|
+
for (const p of partial) {
|
|
586
|
+
sumSq += p.toFloat32()[0]!;
|
|
587
|
+
p.dispose();
|
|
588
|
+
}
|
|
589
|
+
return Math.sqrt(sumSq);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function countResponseTokens(batch: SftBatch): number {
|
|
593
|
+
const promptLen = batch.promptLens[0]!;
|
|
594
|
+
const len = batch.ids[0]!.length;
|
|
595
|
+
return Math.max(0, len - Math.max(promptLen, 1));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
599
|
+
return Bun.file(path).exists();
|
|
600
|
+
}
|