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,229 @@
|
|
|
1
|
+
// Training forward pass: full-sequence logits with no KV reuse.
|
|
2
|
+
//
|
|
3
|
+
// B=1 (no padding): trainForward runs the model's stock forwardHidden over a
|
|
4
|
+
// fresh cache exactly as before — bit-identical to the single-example trainer.
|
|
5
|
+
//
|
|
6
|
+
// B>1 (padded batch): every row in the batch is padded to a common length L,
|
|
7
|
+
// but the rows have different *true* lengths. A plain causal mask would let a
|
|
8
|
+
// real query attend to padded keys (and let padded queries pollute the cache
|
|
9
|
+
// view), so for batched forwards we build an explicit [B, 1, L, L] boolean
|
|
10
|
+
// mask that is the causal mask AND-ed with a per-row "key is within this row's
|
|
11
|
+
// valid length" mask, and route it through the model's attention via a cache
|
|
12
|
+
// wrapper whose makeMask returns it. The wrapper delegates every KV operation
|
|
13
|
+
// to a real cache (KVCache/RotatingKVCache are already shape-generic over B),
|
|
14
|
+
// so only the mask differs from the stock path. Window/offset handling is
|
|
15
|
+
// preserved: training always runs offset-0 full-sequence forwards, so the
|
|
16
|
+
// causal-with-window mask is built at offset 0, exactly matching what the
|
|
17
|
+
// underlying cache's makeMask would have produced for the unpadded case.
|
|
18
|
+
|
|
19
|
+
import { MlxArray } from "../mlx/array";
|
|
20
|
+
import { Dtype } from "../mlx/ffi";
|
|
21
|
+
import * as ops from "../mlx/ops";
|
|
22
|
+
import { createCausalMask, type Cache, type Mask } from "../model/gemma4-base";
|
|
23
|
+
import type { RuntimeModel } from "../model/factory";
|
|
24
|
+
|
|
25
|
+
/** Stateless cache for the training forward. Training is always a single
|
|
26
|
+
* offset-0 full-sequence pass, so `updateAndFetch` is a pure pass-through (no
|
|
27
|
+
* buffer, no state mutation) and `makeMask` is the offset-0 causal/windowed
|
|
28
|
+
* mask — identical to KVCache at offset 0. The statelessness is REQUIRED for
|
|
29
|
+
* gradient checkpointing: mlx_checkpoint re-runs the layer closure during the
|
|
30
|
+
* backward recompute, and a stateful (appending) cache would corrupt on the
|
|
31
|
+
* second run. */
|
|
32
|
+
export class TrainingCache implements Cache {
|
|
33
|
+
offset = 0;
|
|
34
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
35
|
+
// Return fresh views: the caller disposes its input k/v right after (its
|
|
36
|
+
// contract with the real KVCache, which returns buffer slices). A
|
|
37
|
+
// full-range slice is a cheap view node; mlx keeps the source alive
|
|
38
|
+
// through it, so disposing the inputs is safe.
|
|
39
|
+
return [k.slice([0, 0, 0, 0], k.shape), v.slice([0, 0, 0, 0], v.shape)];
|
|
40
|
+
}
|
|
41
|
+
makeMask(N: number, windowSize: number | null): Mask {
|
|
42
|
+
if (N === 1) return { mode: "", arr: null };
|
|
43
|
+
if (windowSize === null || N <= windowSize) return { mode: "causal", arr: null };
|
|
44
|
+
return { mode: "array", arr: createCausalMask(N, 0, windowSize) };
|
|
45
|
+
}
|
|
46
|
+
state(): MlxArray[] {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
isTrimmable(): boolean {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
trim(_n: number): void {
|
|
53
|
+
/* offset is pinned at 0 */
|
|
54
|
+
}
|
|
55
|
+
dispose(): void {
|
|
56
|
+
/* owns no arrays */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Run a full-sequence forward for training.
|
|
61
|
+
* @param ids int32 array [B, L].
|
|
62
|
+
* @returns logits [B, L, V] (caller owns; dispose when done).
|
|
63
|
+
*
|
|
64
|
+
* When `validLengths` is omitted (or every row is full-length L) the batch
|
|
65
|
+
* has no padding and the model's stock mask path is used unchanged — the
|
|
66
|
+
* B=1 result is bit-identical to the original trainer. When some row is
|
|
67
|
+
* shorter than L, a padding-aware batched mask is injected. */
|
|
68
|
+
export function trainForwardHidden(
|
|
69
|
+
model: RuntimeModel,
|
|
70
|
+
ids: MlxArray,
|
|
71
|
+
validLengths?: number[],
|
|
72
|
+
): MlxArray {
|
|
73
|
+
const [B, L] = ids.shape as [number, number];
|
|
74
|
+
const needsPadMask =
|
|
75
|
+
validLengths !== undefined && validLengths.some((v) => v < L);
|
|
76
|
+
|
|
77
|
+
if (needsPadMask) {
|
|
78
|
+
// B>1 padded path: dense cache wrapped with the per-row padding mask
|
|
79
|
+
// (unchanged; not yet checkpointed — B=1 is the checkpointing target).
|
|
80
|
+
const realCache = model.makeCache();
|
|
81
|
+
const cache = wrapWithBatchedMask(realCache, B, L, validLengths!);
|
|
82
|
+
try {
|
|
83
|
+
return model.forwardHidden(ids, cache);
|
|
84
|
+
} finally {
|
|
85
|
+
for (const c of realCache) c.dispose();
|
|
86
|
+
for (const c of cache) (c as BatchedMaskCache).disposeOwnMask();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// B=1 / no padding: stateless offset-0 caches (one per donor layer), so the
|
|
91
|
+
// forward is pure and safe to recompute under gradient checkpointing.
|
|
92
|
+
const probe = model.makeCache();
|
|
93
|
+
const cache: Cache[] = probe.map(() => new TrainingCache());
|
|
94
|
+
for (const c of probe) c.dispose();
|
|
95
|
+
try {
|
|
96
|
+
return model.forwardHidden(ids, cache); // [B, L, hidden]
|
|
97
|
+
} finally {
|
|
98
|
+
for (const c of cache) c.dispose();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Full-sequence forward returning logits [B, L, V]. Convenience over
|
|
103
|
+
* trainForwardHidden + logitsFromHidden. The SFT B=1 path deliberately skips
|
|
104
|
+
* this and applies the LM head only at response positions (responseOnlyCe)
|
|
105
|
+
* to avoid materializing the [B, L, vocab] logits — the dominant training
|
|
106
|
+
* memory term at long context with a 262k vocab. */
|
|
107
|
+
export function trainForward(
|
|
108
|
+
model: RuntimeModel,
|
|
109
|
+
ids: MlxArray,
|
|
110
|
+
validLengths?: number[],
|
|
111
|
+
): MlxArray {
|
|
112
|
+
const h = trainForwardHidden(model, ids, validLengths);
|
|
113
|
+
const logits = model.logitsFromHidden(h);
|
|
114
|
+
h.dispose();
|
|
115
|
+
return logits;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Build the padding-aware additive-style boolean mask for one window type.
|
|
119
|
+
* Shape `[B, 1, L, L]`, true where query i may attend to key j:
|
|
120
|
+
* allow[b,0,i,j] = (j <= i) (causal, offset 0)
|
|
121
|
+
* AND (j < validLen[b]) (key within row b's true length)
|
|
122
|
+
* [AND (i < j + window) if a sliding window applies]
|
|
123
|
+
* Built from createCausalMask(L, 0, window) (the model's own [L, L] bool
|
|
124
|
+
* matrix) broadcast over B, masked by a per-row [B,1,1,L] key-validity mask.
|
|
125
|
+
* Caller owns the returned array. */
|
|
126
|
+
export function buildBatchedPadMask(
|
|
127
|
+
B: number,
|
|
128
|
+
L: number,
|
|
129
|
+
validLengths: number[],
|
|
130
|
+
windowSize: number | null,
|
|
131
|
+
): MlxArray {
|
|
132
|
+
// [L, L] bool causal (+window) matrix at offset 0 — exactly the model's.
|
|
133
|
+
const causal2d = createCausalMask(L, 0, windowSize); // [L, L] bool
|
|
134
|
+
const causal4d = ops.reshape(causal2d, [1, 1, L, L]);
|
|
135
|
+
causal2d.dispose();
|
|
136
|
+
|
|
137
|
+
// Per-row key validity: keyValid[b, 0, 0, j] = j < validLengths[b].
|
|
138
|
+
// Build as int32 {0,1} then compare to get bool, broadcasting over q (i).
|
|
139
|
+
const keyValidData = new Int32Array(B * L);
|
|
140
|
+
for (let b = 0; b < B; b++) {
|
|
141
|
+
const vlen = validLengths[b]!;
|
|
142
|
+
for (let j = 0; j < L; j++) keyValidData[b * L + j] = j < vlen ? 1 : 0;
|
|
143
|
+
}
|
|
144
|
+
const keyValidI = MlxArray.fromInt32(keyValidData, [B, 1, 1, L]);
|
|
145
|
+
const zero = MlxArray.fromInt32(new Int32Array([0]), []);
|
|
146
|
+
const keyValid = ops.less(zero, keyValidI); // 0 < x → bool true where x==1
|
|
147
|
+
zero.dispose();
|
|
148
|
+
keyValidI.dispose();
|
|
149
|
+
|
|
150
|
+
const allow = ops.logicalAnd(causal4d, keyValid); // broadcasts [1,1,L,L] & [B,1,1,L]
|
|
151
|
+
causal4d.dispose();
|
|
152
|
+
keyValid.dispose();
|
|
153
|
+
return allow; // [B, 1, L, L] bool
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Cache wrapper that forwards every KV op to a real cache but overrides
|
|
157
|
+
* makeMask to return a precomputed padding-aware batched mask. One mask is
|
|
158
|
+
* built per distinct window size (null = full attention, else the sliding
|
|
159
|
+
* window) and shared across all caches of that type; ownership of the mask
|
|
160
|
+
* arrays stays with the wrapper (disposeOwnMask), NOT the model — the model's
|
|
161
|
+
* forwardLayers only disposes the mask it pulled from makeMask, so makeMask
|
|
162
|
+
* hands back a fresh view each call and keeps the canonical copy itself. */
|
|
163
|
+
class BatchedMaskCache implements Cache {
|
|
164
|
+
constructor(
|
|
165
|
+
private readonly inner: Cache,
|
|
166
|
+
private readonly window: number | null,
|
|
167
|
+
private readonly maskStore: Map<number | null, MlxArray>,
|
|
168
|
+
) {}
|
|
169
|
+
|
|
170
|
+
get offset(): number {
|
|
171
|
+
return this.inner.offset;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
175
|
+
return this.inner.updateAndFetch(k, v);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
makeMask(N: number, _windowSize: number | null): Mask {
|
|
179
|
+
const canonical = this.maskStore.get(this.window)!;
|
|
180
|
+
// Hand back a fresh full-range view so the model can dispose it without
|
|
181
|
+
// touching our canonical copy. slice over all 4 axes = cheap view node.
|
|
182
|
+
const [B, , L] = canonical.shape as [number, number, number, number];
|
|
183
|
+
const view = canonical.slice([0, 0, 0, 0], [B, 1, L, L]);
|
|
184
|
+
return { mode: "array", arr: view };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
state(): MlxArray[] {
|
|
188
|
+
return this.inner.state();
|
|
189
|
+
}
|
|
190
|
+
isTrimmable(): boolean {
|
|
191
|
+
return this.inner.isTrimmable();
|
|
192
|
+
}
|
|
193
|
+
trim(n: number): void {
|
|
194
|
+
this.inner.trim(n);
|
|
195
|
+
}
|
|
196
|
+
dispose(): void {
|
|
197
|
+
this.inner.dispose();
|
|
198
|
+
}
|
|
199
|
+
/** Dispose the canonical mask copies (shared store; idempotent via clear). */
|
|
200
|
+
disposeOwnMask(): void {
|
|
201
|
+
for (const m of this.maskStore.values()) m.dispose();
|
|
202
|
+
this.maskStore.clear();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Wrap each real cache in a BatchedMaskCache, precomputing one padding-aware
|
|
207
|
+
* mask per distinct window. Caches without a sliding window get the
|
|
208
|
+
* full-attention (null) mask. */
|
|
209
|
+
function wrapWithBatchedMask(
|
|
210
|
+
realCache: Cache[],
|
|
211
|
+
B: number,
|
|
212
|
+
L: number,
|
|
213
|
+
validLengths: number[],
|
|
214
|
+
): Cache[] {
|
|
215
|
+
const windows = realCache.map((c) => cacheWindow(c));
|
|
216
|
+
const distinct = new Set(windows);
|
|
217
|
+
const store = new Map<number | null, MlxArray>();
|
|
218
|
+
for (const w of distinct) store.set(w, buildBatchedPadMask(B, L, validLengths, w));
|
|
219
|
+
return realCache.map((c, i) => new BatchedMaskCache(c, windows[i]!, store));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The sliding-window size a cache enforces, or null for full attention.
|
|
223
|
+
* RotatingKVCache stores its window as `maxSize`; KVCache has none. Mirrors
|
|
224
|
+
* gemma4 forwardLayers, where sliding layers use windowSize == the rotating
|
|
225
|
+
* cache's maxSize. (Quantized KV caches are not used on the training path.) */
|
|
226
|
+
function cacheWindow(c: Cache): number | null {
|
|
227
|
+
const maxSize = (c as { maxSize?: number }).maxSize;
|
|
228
|
+
return typeof maxSize === "number" ? maxSize : null;
|
|
229
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Public surface for native LoRA + DPO fine-tuning.
|
|
2
|
+
|
|
3
|
+
export { ValueAndGrad } from "../mlx/autograd";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
loadSftDataset, iterateSftBatches, encodeSftRow, sftRowFormat,
|
|
7
|
+
loadDpoDataset, iterateDpoBatches, encodeDpoRow, tokenizePair,
|
|
8
|
+
probeFormat, rowLength, dpoChosenLength, dpoRejectedLength, PAD_TO,
|
|
9
|
+
type SftExample, type SftBatch, type SftRowFormat,
|
|
10
|
+
type DpoExample, type DpoBatch, type DatasetFormat,
|
|
11
|
+
} from "./dataset";
|
|
12
|
+
|
|
13
|
+
export { trainForward, buildBatchedPadMask } from "./forward";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
sftLoss, dpoLoss, dpoRefLogps, dpoMetrics,
|
|
17
|
+
} from "./loss";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
AdamW, warmupCosineSchedule, type AdamWOptions,
|
|
21
|
+
} from "./optimizer";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
buildTrainableLora, attachForTraining, detachTraining, setLoraScale,
|
|
25
|
+
flatA, flatB, flatParams, saveAdapter, disposeLora,
|
|
26
|
+
type TrainableLora, type TrainableTarget, type SaveAdapterConfig,
|
|
27
|
+
} from "./lora-params";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
resolveRanks, DEFAULT_TARGET_MODULES,
|
|
31
|
+
type RankScaling, type ResolveRanksOptions,
|
|
32
|
+
} from "./rank";
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
trainLora, DEFAULT_TRAIN_CONFIG,
|
|
36
|
+
type TrainConfig, type TrainResult,
|
|
37
|
+
} from "./trainer";
|
|
38
|
+
|
|
39
|
+
export { mergeAdapters, type MergeStats } from "./merge";
|
|
40
|
+
|
|
41
|
+
export { exportAdapter, type ExportManifest } from "./export";
|
|
42
|
+
|
|
43
|
+
export { finetuneRunner, inspectDataset } from "./job";
|
package/src/train/job.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// The finetune JobRunner: wires a submitted finetune job to trainLora.
|
|
2
|
+
// Speaks the shared src/jobs/types.ts contract. Registered as kind "finetune"
|
|
3
|
+
// so both submitInProcess and the subprocess job-entry resolver find it
|
|
4
|
+
// (job-entry.ts KIND_MODULES maps finetune → ../train/job.ts).
|
|
5
|
+
|
|
6
|
+
import type { JobRunner } from "../jobs/types";
|
|
7
|
+
import { registerRunner } from "../jobs/runner";
|
|
8
|
+
import {
|
|
9
|
+
setWiredLimit, synchronize, maxRecommendedWorkingSetSize,
|
|
10
|
+
} from "../mlx/ffi";
|
|
11
|
+
import { gpuStream } from "../mlx/array";
|
|
12
|
+
import { trainLora, DEFAULT_TRAIN_CONFIG, type TrainConfig } from "./trainer";
|
|
13
|
+
import { probeFormat } from "./dataset";
|
|
14
|
+
|
|
15
|
+
/** Parse the submit config into a TrainConfig (with model/data paths). */
|
|
16
|
+
interface FinetuneSubmit {
|
|
17
|
+
model_dir: string;
|
|
18
|
+
data_dir: string;
|
|
19
|
+
adapter_path: string;
|
|
20
|
+
method?: "sft" | "dpo";
|
|
21
|
+
rank?: number;
|
|
22
|
+
scale?: number;
|
|
23
|
+
rank_scaling?: TrainConfig["rankScaling"];
|
|
24
|
+
target_modules?: string[];
|
|
25
|
+
num_layers?: number;
|
|
26
|
+
iters?: number;
|
|
27
|
+
learning_rate?: number;
|
|
28
|
+
max_seq_length?: number;
|
|
29
|
+
batch_size?: number;
|
|
30
|
+
grad_accumulation_steps?: number;
|
|
31
|
+
seed?: number;
|
|
32
|
+
steps_per_report?: number;
|
|
33
|
+
steps_per_eval?: number;
|
|
34
|
+
weight_decay?: number;
|
|
35
|
+
grad_checkpoint?: boolean;
|
|
36
|
+
segment_size?: number;
|
|
37
|
+
save_checkpoints?: boolean;
|
|
38
|
+
dpo_beta?: number;
|
|
39
|
+
dpo_warmup_iters?: number;
|
|
40
|
+
dpo_lr_schedule?: "constant" | "cosine";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir: string; cfg: TrainConfig } {
|
|
44
|
+
const c = raw as unknown as FinetuneSubmit;
|
|
45
|
+
if (!c.model_dir) throw new Error("finetune job: missing model_dir");
|
|
46
|
+
if (!c.data_dir) throw new Error("finetune job: missing data_dir");
|
|
47
|
+
if (!c.adapter_path) throw new Error("finetune job: missing adapter_path");
|
|
48
|
+
|
|
49
|
+
const cfg: TrainConfig = {
|
|
50
|
+
...DEFAULT_TRAIN_CONFIG,
|
|
51
|
+
method: c.method ?? "sft",
|
|
52
|
+
rank: c.rank ?? DEFAULT_TRAIN_CONFIG.rank,
|
|
53
|
+
scale: c.scale ?? DEFAULT_TRAIN_CONFIG.scale,
|
|
54
|
+
rankScaling: c.rank_scaling ?? DEFAULT_TRAIN_CONFIG.rankScaling,
|
|
55
|
+
targetModules: c.target_modules ?? DEFAULT_TRAIN_CONFIG.targetModules,
|
|
56
|
+
numLayers: c.num_layers ?? DEFAULT_TRAIN_CONFIG.numLayers,
|
|
57
|
+
iters: c.iters ?? DEFAULT_TRAIN_CONFIG.iters,
|
|
58
|
+
learningRate: c.learning_rate ?? (c.method === "dpo" ? 5e-5 : DEFAULT_TRAIN_CONFIG.learningRate),
|
|
59
|
+
maxSeqLen: c.max_seq_length ?? DEFAULT_TRAIN_CONFIG.maxSeqLen,
|
|
60
|
+
batchSize: c.batch_size ?? DEFAULT_TRAIN_CONFIG.batchSize,
|
|
61
|
+
gradAccumSteps: c.grad_accumulation_steps ?? DEFAULT_TRAIN_CONFIG.gradAccumSteps,
|
|
62
|
+
seed: c.seed ?? DEFAULT_TRAIN_CONFIG.seed,
|
|
63
|
+
stepsPerReport: c.steps_per_report ?? DEFAULT_TRAIN_CONFIG.stepsPerReport,
|
|
64
|
+
stepsPerEval: c.steps_per_eval ?? DEFAULT_TRAIN_CONFIG.stepsPerEval,
|
|
65
|
+
betas: DEFAULT_TRAIN_CONFIG.betas,
|
|
66
|
+
weightDecay: c.weight_decay ?? DEFAULT_TRAIN_CONFIG.weightDecay,
|
|
67
|
+
gradCheckpoint: c.grad_checkpoint ?? DEFAULT_TRAIN_CONFIG.gradCheckpoint,
|
|
68
|
+
segmentSize: c.segment_size ?? DEFAULT_TRAIN_CONFIG.segmentSize,
|
|
69
|
+
saveCheckpoints: c.save_checkpoints ?? DEFAULT_TRAIN_CONFIG.saveCheckpoints,
|
|
70
|
+
dpoBeta: c.dpo_beta ?? DEFAULT_TRAIN_CONFIG.dpoBeta,
|
|
71
|
+
dpoWarmupIters: c.dpo_warmup_iters ?? DEFAULT_TRAIN_CONFIG.dpoWarmupIters,
|
|
72
|
+
dpoLrSchedule: c.dpo_lr_schedule ?? DEFAULT_TRAIN_CONFIG.dpoLrSchedule,
|
|
73
|
+
adapterPath: c.adapter_path,
|
|
74
|
+
baseModel: c.model_dir,
|
|
75
|
+
};
|
|
76
|
+
return { modelDir: c.model_dir, dataDir: c.data_dir, cfg };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const finetuneRunner: JobRunner = async (emit, config) => {
|
|
80
|
+
const { modelDir, dataDir, cfg } = parseConfig(config);
|
|
81
|
+
|
|
82
|
+
emit({ type: "stage", stage: "load", progress: 0.01, message: `loading model ${modelDir}` });
|
|
83
|
+
|
|
84
|
+
const { loadModelConfig } = await import("../config");
|
|
85
|
+
const { Weights } = await import("../weights");
|
|
86
|
+
const { createModel } = await import("../model/factory");
|
|
87
|
+
const { loadTokenizer } = await import("../tokenizer");
|
|
88
|
+
const { ChatTemplate } = await import("../chat-template");
|
|
89
|
+
|
|
90
|
+
const modelConfig = await loadModelConfig(modelDir);
|
|
91
|
+
const weights = await Weights.open(modelDir);
|
|
92
|
+
const model = createModel(weights, modelConfig);
|
|
93
|
+
const tok = await loadTokenizer(modelDir);
|
|
94
|
+
const tmpl = await ChatTemplate.load(modelDir);
|
|
95
|
+
|
|
96
|
+
// Scoped wired limit around the whole run (set → train → synchronize →
|
|
97
|
+
// restore). Subprocess-isolated so pinning is safe (PLAN Phase 6 rule:
|
|
98
|
+
// never leave the limit raised process-permanently).
|
|
99
|
+
const oldLimit = setWiredLimit(maxRecommendedWorkingSetSize());
|
|
100
|
+
try {
|
|
101
|
+
const result = await trainLora(model, tok, tmpl, dataDir, cfg, emit);
|
|
102
|
+
return { outputPath: result.adapterPath };
|
|
103
|
+
} finally {
|
|
104
|
+
synchronize(gpuStream);
|
|
105
|
+
setWiredLimit(oldLimit);
|
|
106
|
+
weights.dispose();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Inspect a dataset directory before submit: counts + detected format. */
|
|
111
|
+
export async function inspectDataset(dataDir: string): Promise<{
|
|
112
|
+
ok: boolean;
|
|
113
|
+
n_train: number;
|
|
114
|
+
n_valid: number;
|
|
115
|
+
format: string;
|
|
116
|
+
error?: string;
|
|
117
|
+
}> {
|
|
118
|
+
try {
|
|
119
|
+
const trainPath = `${dataDir}/train.jsonl`;
|
|
120
|
+
if (!(await Bun.file(trainPath).exists()))
|
|
121
|
+
return { ok: false, n_train: 0, n_valid: 0, format: "unknown", error: `${trainPath} not found` };
|
|
122
|
+
const nTrain = await countAndProbe(trainPath);
|
|
123
|
+
const validPath = `${dataDir}/valid.jsonl`;
|
|
124
|
+
const nValid = (await Bun.file(validPath).exists()) ? await countAndProbe(validPath) : { n: 0, fmt: "" };
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
n_train: nTrain.n,
|
|
128
|
+
n_valid: nValid.n,
|
|
129
|
+
format: nTrain.fmt,
|
|
130
|
+
};
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false, n_train: 0, n_valid: 0, format: "unknown",
|
|
134
|
+
error: e instanceof Error ? e.message : String(e),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function countAndProbe(path: string): Promise<{ n: number; fmt: string }> {
|
|
140
|
+
const text = await Bun.file(path).text();
|
|
141
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
142
|
+
let fmt = "unknown";
|
|
143
|
+
if (lines.length > 0) fmt = probeFormat(JSON.parse(lines[0]!) as Record<string, unknown>);
|
|
144
|
+
return { n: lines.length, fmt };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Self-register on import (mirrors the dataset/quantize convention).
|
|
148
|
+
registerRunner("finetune", finetuneRunner);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Trainable LoRA parameter management: build A/B leaves, attach them to the
|
|
2
|
+
// model's quantized linears for the forward pass, expose a stable flat order
|
|
3
|
+
// for value_and_grad argnums, and save the adapter in the AdapterManager.mount
|
|
4
|
+
// format (lora_a / lora_b tensor names + optiq_lora_config.json + PEFT
|
|
5
|
+
// adapter_config.json).
|
|
6
|
+
//
|
|
7
|
+
// Init follows mlx-lm LoRALinear.from_base: A ~ uniform(-1/sqrt(in), 1/sqrt(in)),
|
|
8
|
+
// B = zeros, so the LoRA residual starts at exactly zero (the adapted model ==
|
|
9
|
+
// base model at step 0). Trained in f32. (mlx-lm uses uniform, not normal —
|
|
10
|
+
// matching it keeps L1 training faithful to the parent.)
|
|
11
|
+
|
|
12
|
+
import { mkdirSync } from "node:fs";
|
|
13
|
+
import { ptr } from "bun:ffi";
|
|
14
|
+
import { C, Dtype } from "../mlx/ffi";
|
|
15
|
+
import { MlxArray } from "../mlx/array";
|
|
16
|
+
import * as ops from "../mlx/ops";
|
|
17
|
+
import type { RuntimeModel } from "../model/factory";
|
|
18
|
+
import type { LoraWeights, QuantizedLinear } from "../model/gemma4-base";
|
|
19
|
+
|
|
20
|
+
const cstr = (s: string) => Buffer.from(s + "\0", "utf8");
|
|
21
|
+
|
|
22
|
+
/** One trainable LoRA target: the owning linear + its A/B leaves. */
|
|
23
|
+
export interface TrainableTarget {
|
|
24
|
+
modulePath: string;
|
|
25
|
+
linear: QuantizedLinear;
|
|
26
|
+
lw: LoraWeights;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The full set of trainable LoRA leaves for one adapter. */
|
|
30
|
+
export interface TrainableLora {
|
|
31
|
+
adapterId: string;
|
|
32
|
+
scale: number;
|
|
33
|
+
targets: TrainableTarget[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Build trainable A/B leaves for the given ranks (one entry per module path).
|
|
37
|
+
* Modules absent from `ranks` are skipped. A is normal-init, B is zeros. */
|
|
38
|
+
export function buildTrainableLora(
|
|
39
|
+
model: RuntimeModel,
|
|
40
|
+
ranks: Map<string, number>,
|
|
41
|
+
scale: number,
|
|
42
|
+
seed: number,
|
|
43
|
+
): TrainableLora {
|
|
44
|
+
const linears = model.loraTargets();
|
|
45
|
+
const key = ops.randomKey(BigInt(seed >>> 0));
|
|
46
|
+
// One subkey per target for reproducible, independent A inits.
|
|
47
|
+
const paths = [...ranks.keys()].filter((p) => linears.has(p)).sort();
|
|
48
|
+
const subkeys = ops.randomSplitNum(key, Math.max(1, paths.length));
|
|
49
|
+
key.dispose();
|
|
50
|
+
|
|
51
|
+
const targets: TrainableTarget[] = [];
|
|
52
|
+
paths.forEach((modulePath, i) => {
|
|
53
|
+
const linear = linears.get(modulePath)!;
|
|
54
|
+
const rank = ranks.get(modulePath)!;
|
|
55
|
+
const inF = linear.inFeatures;
|
|
56
|
+
const outF = linear.outFeatures;
|
|
57
|
+
const subkeyRow = subkeys.slice([i, 0], [i + 1, 2]); // [1,2]
|
|
58
|
+
const subkey = ops.reshape(subkeyRow, [2]); // mlx wants a [2] key
|
|
59
|
+
subkeyRow.dispose();
|
|
60
|
+
const aScale = 1 / Math.sqrt(inF);
|
|
61
|
+
const a = ops.randomUniform([inF, rank], Dtype.float32, -aScale, aScale, subkey);
|
|
62
|
+
subkey.dispose();
|
|
63
|
+
const b = ops.zeros([rank, outF], Dtype.float32);
|
|
64
|
+
targets.push({ modulePath, linear, lw: { a, b, scale, rank } });
|
|
65
|
+
});
|
|
66
|
+
subkeys.dispose();
|
|
67
|
+
|
|
68
|
+
return { adapterId: "train", scale, targets };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Attach the trainable leaves to the model so the forward pass picks them up
|
|
72
|
+
* via loraState.active. */
|
|
73
|
+
export function attachForTraining(model: RuntimeModel, lora: TrainableLora, adapterId: string): void {
|
|
74
|
+
lora.adapterId = adapterId;
|
|
75
|
+
for (const t of lora.targets) {
|
|
76
|
+
(t.linear.adapters ??= new Map()).set(adapterId, t.lw);
|
|
77
|
+
t.linear.loraState = model.loraState;
|
|
78
|
+
}
|
|
79
|
+
model.loraState.active = [adapterId];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Detach the trainable leaves (after training / before save). */
|
|
83
|
+
export function detachTraining(model: RuntimeModel, lora: TrainableLora): void {
|
|
84
|
+
for (const t of lora.targets) t.linear.adapters?.delete(lora.adapterId);
|
|
85
|
+
model.loraState.active = model.loraState.active.filter((x) => x !== lora.adapterId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Set the LoRA scale on every trainable leaf (DPO reference forward uses 0). */
|
|
89
|
+
export function setLoraScale(lora: TrainableLora, s: number): void {
|
|
90
|
+
for (const t of lora.targets) t.lw.scale = s;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A leaves in stable (sorted-by-modulePath) order — the order used for
|
|
94
|
+
* value_and_grad argnums and AdamW state. */
|
|
95
|
+
export function flatA(lora: TrainableLora): MlxArray[] {
|
|
96
|
+
return lora.targets.map((t) => t.lw.a);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** B leaves in the same stable order, appended after all A leaves. */
|
|
100
|
+
export function flatB(lora: TrainableLora): MlxArray[] {
|
|
101
|
+
return lora.targets.map((t) => t.lw.b);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** [...A, ...B] — the full differentiable parameter vector. */
|
|
105
|
+
export function flatParams(lora: TrainableLora): MlxArray[] {
|
|
106
|
+
return [...flatA(lora), ...flatB(lora)];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Adapter config written alongside the weights. */
|
|
110
|
+
export interface SaveAdapterConfig {
|
|
111
|
+
rank: number;
|
|
112
|
+
scale: number;
|
|
113
|
+
rankScaling: string;
|
|
114
|
+
targetModules: string[];
|
|
115
|
+
numLayers: number;
|
|
116
|
+
method: string;
|
|
117
|
+
baseModel: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Save the trained adapter into `dir` in the AdapterManager.mount format:
|
|
121
|
+
* adapters.safetensors (lora_a / lora_b tensor names), optiq_lora_config.json,
|
|
122
|
+
* and a PEFT-compatible adapter_config.json. */
|
|
123
|
+
export function saveAdapter(
|
|
124
|
+
lora: TrainableLora,
|
|
125
|
+
dir: string,
|
|
126
|
+
cfg: SaveAdapterConfig,
|
|
127
|
+
appliedRanks: Record<string, number>,
|
|
128
|
+
): void {
|
|
129
|
+
mkdirSync(dir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
// Build the native string→array map and insert lora_a / lora_b per target.
|
|
132
|
+
const map = C.mlx_map_string_to_array_new();
|
|
133
|
+
const meta = C.mlx_map_string_to_string_new();
|
|
134
|
+
try {
|
|
135
|
+
for (const t of lora.targets) {
|
|
136
|
+
// Materialize before save (the leaves are live graph nodes).
|
|
137
|
+
t.lw.a.eval();
|
|
138
|
+
t.lw.b.eval();
|
|
139
|
+
if (C.mlx_map_string_to_array_insert(map, ptr(cstr(`${t.modulePath}.lora_a`)), t.lw.a.handle) !== 0)
|
|
140
|
+
throw new Error(`map insert ${t.modulePath}.lora_a failed`);
|
|
141
|
+
if (C.mlx_map_string_to_array_insert(map, ptr(cstr(`${t.modulePath}.lora_b`)), t.lw.b.handle) !== 0)
|
|
142
|
+
throw new Error(`map insert ${t.modulePath}.lora_b failed`);
|
|
143
|
+
}
|
|
144
|
+
const file = `${dir}/adapters.safetensors`;
|
|
145
|
+
if (C.mlx_save_safetensors(ptr(cstr(file)), map, meta) !== 0)
|
|
146
|
+
throw new Error(`mlx_save_safetensors(${file}) failed`);
|
|
147
|
+
} finally {
|
|
148
|
+
C.mlx_map_string_to_array_free(map);
|
|
149
|
+
C.mlx_map_string_to_string_free(meta);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// optiq_lora_config.json — AdapterManager.readAdapterScale reads
|
|
153
|
+
// lora_parameters.scale + rank from here first.
|
|
154
|
+
const optiqCfg = {
|
|
155
|
+
rank: cfg.rank,
|
|
156
|
+
scale: cfg.scale,
|
|
157
|
+
rank_scaling: cfg.rankScaling,
|
|
158
|
+
method: cfg.method,
|
|
159
|
+
target_modules: cfg.targetModules,
|
|
160
|
+
num_layers: cfg.numLayers,
|
|
161
|
+
lora_parameters: { scale: cfg.scale, rank: cfg.rank },
|
|
162
|
+
applied_ranks: appliedRanks,
|
|
163
|
+
source_model: cfg.baseModel,
|
|
164
|
+
};
|
|
165
|
+
Bun.write(`${dir}/optiq_lora_config.json`, JSON.stringify(optiqCfg, null, 2) + "\n");
|
|
166
|
+
|
|
167
|
+
// PEFT-compatible adapter_config.json (loadable by mlx-lm / PEFT).
|
|
168
|
+
const peftCfg = {
|
|
169
|
+
fine_tune_type: "lora",
|
|
170
|
+
num_layers: cfg.numLayers,
|
|
171
|
+
lora_parameters: { rank: cfg.rank, scale: cfg.scale, dropout: 0.0, keys: null },
|
|
172
|
+
base_model_name_or_path: cfg.baseModel,
|
|
173
|
+
bias: "none",
|
|
174
|
+
fan_in_fan_out: false,
|
|
175
|
+
inference_mode: false,
|
|
176
|
+
init_lora_weights: true,
|
|
177
|
+
lora_alpha: Math.round(cfg.rank * cfg.scale),
|
|
178
|
+
lora_dropout: 0.0,
|
|
179
|
+
peft_type: "LORA",
|
|
180
|
+
r: cfg.rank,
|
|
181
|
+
target_modules: cfg.targetModules,
|
|
182
|
+
task_type: "CAUSAL_LM",
|
|
183
|
+
optiq: { rank_scaling: cfg.rankScaling, applied_ranks: appliedRanks },
|
|
184
|
+
};
|
|
185
|
+
Bun.write(`${dir}/adapter_config.json`, JSON.stringify(peftCfg, null, 2) + "\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Dispose all trainable leaves (frees A/B). */
|
|
189
|
+
export function disposeLora(lora: TrainableLora): void {
|
|
190
|
+
for (const t of lora.targets) {
|
|
191
|
+
t.lw.a.dispose();
|
|
192
|
+
t.lw.b.dispose();
|
|
193
|
+
}
|
|
194
|
+
}
|