mlx-bun 0.0.4 → 0.0.5
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 +166 -93
- package/docs/design/adapters-end-to-end.md +50 -46
- package/docs/design/compat-cli-surface-design.md +10 -8
- package/docs/design/docs-reorg-plan.md +14 -1
- package/docs/design/hlg-sampling.md +12 -2
- package/docs/design/memory-system.md +1 -1
- package/docs/design/optimization_plan.md +4 -2
- package/docs/design/orpo-future-enhancements.md +298 -0
- package/docs/design/orpo-training.md +1199 -0
- package/docs/design/parallel-slots.md +34 -26
- package/docs/design/runtime-isolation.md +1 -1
- package/docs/design/segmented-backward-training.md +58 -39
- package/docs/design/tauri-desktop-app.md +127 -0
- package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
- package/docs/investigations/starter-model-port-handoff.md +32 -13
- package/docs/investigations/steel-flash-cce-handoff.md +431 -0
- package/docs/planning/IDEAS.md +29 -0
- package/docs/planning/PRODUCT_ROADMAP.md +147 -27
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/journal.md +161 -0
- package/docs/reference/distribution.md +61 -5
- package/docs/reference/library-api.md +29 -14
- package/docs/reference/orpo-quickstart.md +91 -0
- package/docs/reference/server-api.md +105 -7
- package/docs/reference/server-config.md +12 -5
- package/docs/reference/training.md +292 -0
- package/package.json +1 -1
- package/src/eval/ifeval.ts +138 -0
- package/src/eval/ultrafeedback.ts +60 -0
- package/src/lora.ts +12 -7
- package/src/mlx/metal-kernel.ts +18 -4
- package/src/model/flash-attention.ts +32 -16
- package/src/model/fused-geglu-kernel.ts +111 -0
- package/src/model/gemma4-base.ts +39 -1
- package/src/model/gemma4.ts +196 -21
- package/src/model/minicpm5.ts +44 -3
- package/src/server.ts +3 -3
- package/src/train/dataset.ts +18 -3
- package/src/train/flash-cce.ts +1162 -0
- package/src/train/job.ts +26 -2
- package/src/train/lora-params.ts +53 -4
- package/src/train/loss.ts +941 -0
- package/src/train/optimizer.ts +10 -3
- package/src/train/prefix-shared.ts +435 -0
- package/src/train/rank.ts +36 -0
- package/src/train/segmented.ts +1148 -3
- package/src/train/steel-qmm-header.ts +1461 -0
- package/src/train/trainer.ts +509 -52
- package/src/web/app.html +21 -5
- /package/{docs → src/assets}/curve-designer.html +0 -0
package/src/train/job.ts
CHANGED
|
@@ -17,7 +17,7 @@ interface FinetuneSubmit {
|
|
|
17
17
|
model_dir: string;
|
|
18
18
|
data_dir: string;
|
|
19
19
|
adapter_path: string;
|
|
20
|
-
method?: "sft" | "dpo";
|
|
20
|
+
method?: "sft" | "dpo" | "orpo";
|
|
21
21
|
rank?: number;
|
|
22
22
|
scale?: number;
|
|
23
23
|
rank_scaling?: TrainConfig["rankScaling"];
|
|
@@ -32,12 +32,24 @@ interface FinetuneSubmit {
|
|
|
32
32
|
steps_per_report?: number;
|
|
33
33
|
steps_per_eval?: number;
|
|
34
34
|
weight_decay?: number;
|
|
35
|
+
lora_dropout?: number;
|
|
36
|
+
rs_lora?: boolean;
|
|
37
|
+
lora_plus_ratio?: number;
|
|
35
38
|
grad_checkpoint?: boolean;
|
|
39
|
+
mlp_split?: boolean;
|
|
36
40
|
segment_size?: number;
|
|
37
41
|
save_checkpoints?: boolean;
|
|
38
42
|
dpo_beta?: number;
|
|
39
43
|
dpo_warmup_iters?: number;
|
|
40
44
|
dpo_lr_schedule?: "constant" | "cosine";
|
|
45
|
+
orpo_lambda?: number;
|
|
46
|
+
orpo_warmup_iters?: number;
|
|
47
|
+
orpo_lr_schedule?: "constant" | "cosine";
|
|
48
|
+
orpo_chunk_size?: number;
|
|
49
|
+
orpo_fused_ce?: boolean;
|
|
50
|
+
orpo_flash_ce?: boolean;
|
|
51
|
+
orpo_prefix_shared?: boolean;
|
|
52
|
+
warm_start_adapter?: string;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir: string; cfg: TrainConfig } {
|
|
@@ -55,7 +67,7 @@ function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir:
|
|
|
55
67
|
targetModules: c.target_modules ?? DEFAULT_TRAIN_CONFIG.targetModules,
|
|
56
68
|
numLayers: c.num_layers ?? DEFAULT_TRAIN_CONFIG.numLayers,
|
|
57
69
|
iters: c.iters ?? DEFAULT_TRAIN_CONFIG.iters,
|
|
58
|
-
learningRate: c.learning_rate ?? (c.method === "dpo" ? 5e-5 : DEFAULT_TRAIN_CONFIG.learningRate),
|
|
70
|
+
learningRate: c.learning_rate ?? (c.method === "dpo" ? 5e-5 : c.method === "orpo" ? 1e-5 : DEFAULT_TRAIN_CONFIG.learningRate),
|
|
59
71
|
maxSeqLen: c.max_seq_length ?? DEFAULT_TRAIN_CONFIG.maxSeqLen,
|
|
60
72
|
batchSize: c.batch_size ?? DEFAULT_TRAIN_CONFIG.batchSize,
|
|
61
73
|
gradAccumSteps: c.grad_accumulation_steps ?? DEFAULT_TRAIN_CONFIG.gradAccumSteps,
|
|
@@ -64,12 +76,24 @@ function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir:
|
|
|
64
76
|
stepsPerEval: c.steps_per_eval ?? DEFAULT_TRAIN_CONFIG.stepsPerEval,
|
|
65
77
|
betas: DEFAULT_TRAIN_CONFIG.betas,
|
|
66
78
|
weightDecay: c.weight_decay ?? DEFAULT_TRAIN_CONFIG.weightDecay,
|
|
79
|
+
loraDropout: c.lora_dropout ?? DEFAULT_TRAIN_CONFIG.loraDropout,
|
|
80
|
+
rsLora: c.rs_lora ?? DEFAULT_TRAIN_CONFIG.rsLora,
|
|
81
|
+
loraPlusRatio: c.lora_plus_ratio ?? DEFAULT_TRAIN_CONFIG.loraPlusRatio,
|
|
67
82
|
gradCheckpoint: c.grad_checkpoint ?? DEFAULT_TRAIN_CONFIG.gradCheckpoint,
|
|
83
|
+
mlpSplit: c.mlp_split ?? DEFAULT_TRAIN_CONFIG.mlpSplit,
|
|
68
84
|
segmentSize: c.segment_size ?? DEFAULT_TRAIN_CONFIG.segmentSize,
|
|
69
85
|
saveCheckpoints: c.save_checkpoints ?? DEFAULT_TRAIN_CONFIG.saveCheckpoints,
|
|
70
86
|
dpoBeta: c.dpo_beta ?? DEFAULT_TRAIN_CONFIG.dpoBeta,
|
|
71
87
|
dpoWarmupIters: c.dpo_warmup_iters ?? DEFAULT_TRAIN_CONFIG.dpoWarmupIters,
|
|
72
88
|
dpoLrSchedule: c.dpo_lr_schedule ?? DEFAULT_TRAIN_CONFIG.dpoLrSchedule,
|
|
89
|
+
orpoLambda: c.orpo_lambda ?? DEFAULT_TRAIN_CONFIG.orpoLambda,
|
|
90
|
+
orpoWarmupIters: c.orpo_warmup_iters ?? DEFAULT_TRAIN_CONFIG.orpoWarmupIters,
|
|
91
|
+
orpoLrSchedule: c.orpo_lr_schedule ?? DEFAULT_TRAIN_CONFIG.orpoLrSchedule,
|
|
92
|
+
orpoChunkSize: c.orpo_chunk_size ?? DEFAULT_TRAIN_CONFIG.orpoChunkSize,
|
|
93
|
+
orpoFusedCe: c.orpo_fused_ce ?? DEFAULT_TRAIN_CONFIG.orpoFusedCe,
|
|
94
|
+
orpoFlashCe: c.orpo_flash_ce ?? DEFAULT_TRAIN_CONFIG.orpoFlashCe,
|
|
95
|
+
orpoPrefixShared: c.orpo_prefix_shared ?? DEFAULT_TRAIN_CONFIG.orpoPrefixShared,
|
|
96
|
+
warmStartAdapter: c.warm_start_adapter ?? DEFAULT_TRAIN_CONFIG.warmStartAdapter,
|
|
73
97
|
adapterPath: c.adapter_path,
|
|
74
98
|
baseModel: c.model_dir,
|
|
75
99
|
};
|
package/src/train/lora-params.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { MlxArray } from "../mlx/array";
|
|
|
16
16
|
import * as ops from "../mlx/ops";
|
|
17
17
|
import type { RuntimeModel } from "../model/factory";
|
|
18
18
|
import type { LoraWeights, QuantizedLinear } from "../model/gemma4-base";
|
|
19
|
+
import { loadAdapterTensors } from "../lora";
|
|
19
20
|
|
|
20
21
|
const cstr = (s: string) => Buffer.from(s + "\0", "utf8");
|
|
21
22
|
|
|
@@ -40,6 +41,7 @@ export function buildTrainableLora(
|
|
|
40
41
|
ranks: Map<string, number>,
|
|
41
42
|
scale: number,
|
|
42
43
|
seed: number,
|
|
44
|
+
rsLora = false,
|
|
43
45
|
): TrainableLora {
|
|
44
46
|
const linears = model.loraTargets();
|
|
45
47
|
const key = ops.randomKey(BigInt(seed >>> 0));
|
|
@@ -61,21 +63,62 @@ export function buildTrainableLora(
|
|
|
61
63
|
const a = ops.randomUniform([inF, rank], Dtype.float32, -aScale, aScale, subkey);
|
|
62
64
|
subkey.dispose();
|
|
63
65
|
const b = ops.zeros([rank, outF], Dtype.float32);
|
|
64
|
-
|
|
66
|
+
// rsLoRA: α/√rank so per-layer rank scaling changes capacity, not step size.
|
|
67
|
+
const lwScale = rsLora ? scale / Math.sqrt(rank) : scale;
|
|
68
|
+
targets.push({ modulePath, linear, lw: { a, b, scale: lwScale, rank } });
|
|
65
69
|
});
|
|
66
70
|
subkeys.dispose();
|
|
67
71
|
|
|
68
72
|
return { adapterId: "train", scale, targets };
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
/** Warm-start: overwrite a freshly-built LoRA's A/B leaves with the weights saved in
|
|
76
|
+
* `dir/adapters.safetensors` (saveAdapter's `${modulePath}.lora_a/b` names), so a run
|
|
77
|
+
* continues from a checkpoint's WEIGHTS. The optimizer state and LR schedule restart —
|
|
78
|
+
* Adam re-warms its moments in a few steps — so this is a weights warm-start, not a
|
|
79
|
+
* bit-perfect resume. The checkpoint's rank/targets must match this LoRA (shapes are
|
|
80
|
+
* checked). MUST be called BEFORE the optimizer is built so it tracks the loaded
|
|
81
|
+
* handles. Returns the number of targets loaded. */
|
|
82
|
+
export function warmStartFromAdapter(lora: TrainableLora, dir: string): number {
|
|
83
|
+
const file = `${dir}/adapters.safetensors`;
|
|
84
|
+
const tensors = loadAdapterTensors(file); // caller owns; disposed below
|
|
85
|
+
const adopted = new Set<MlxArray>();
|
|
86
|
+
try {
|
|
87
|
+
// Two phases so the swap is atomic: validate EVERY target first (throws before
|
|
88
|
+
// anything is disposed), then dispose+adopt. A mid-loop failure must not leave
|
|
89
|
+
// lora.targets half-replaced (some checkpoint leaves, some fresh init).
|
|
90
|
+
const pending: Array<{ t: (typeof lora.targets)[number]; a: MlxArray; b: MlxArray }> = [];
|
|
91
|
+
for (const t of lora.targets) {
|
|
92
|
+
const a = tensors.get(`${t.modulePath}.lora_a`);
|
|
93
|
+
const b = tensors.get(`${t.modulePath}.lora_b`);
|
|
94
|
+
if (!a || !b) throw new Error(`warm-start: ${t.modulePath} missing lora_a/lora_b in ${file}`);
|
|
95
|
+
if (a.dtype !== Dtype.float32 || b.dtype !== Dtype.float32)
|
|
96
|
+
throw new Error(`warm-start: ${t.modulePath} tensors must be f32`);
|
|
97
|
+
if (a.shape[0] !== t.lw.a.shape[0] || a.shape[1] !== t.lw.a.shape[1] ||
|
|
98
|
+
b.shape[0] !== t.lw.b.shape[0] || b.shape[1] !== t.lw.b.shape[1])
|
|
99
|
+
throw new Error(`warm-start: shape mismatch at ${t.modulePath} — the checkpoint's rank/targets must match this run`);
|
|
100
|
+
pending.push({ t, a, b });
|
|
101
|
+
}
|
|
102
|
+
for (const { t, a, b } of pending) {
|
|
103
|
+
t.lw.a.dispose(); t.lw.b.dispose(); // free the fresh random/zero init
|
|
104
|
+
t.lw.a = a; t.lw.b = b; // adopt the checkpoint weights as the live leaves
|
|
105
|
+
adopted.add(a); adopted.add(b);
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
for (const [, arr] of tensors) if (!adopted.has(arr)) arr.dispose();
|
|
109
|
+
}
|
|
110
|
+
return lora.targets.length;
|
|
111
|
+
}
|
|
112
|
+
|
|
71
113
|
/** Attach the trainable leaves to the model so the forward pass picks them up
|
|
72
114
|
* via loraState.active. */
|
|
73
115
|
export function attachForTraining(model: RuntimeModel, lora: TrainableLora, adapterId: string): void {
|
|
74
116
|
lora.adapterId = adapterId;
|
|
75
|
-
|
|
117
|
+
lora.targets.forEach((t, i) => {
|
|
76
118
|
(t.linear.adapters ??= new Map()).set(adapterId, t.lw);
|
|
77
119
|
t.linear.loraState = model.loraState;
|
|
78
|
-
|
|
120
|
+
t.linear.dropoutId = i; // stable per-target key for recompute-safe dropout
|
|
121
|
+
});
|
|
79
122
|
model.loraState.active = [adapterId];
|
|
80
123
|
}
|
|
81
124
|
|
|
@@ -83,6 +126,8 @@ export function attachForTraining(model: RuntimeModel, lora: TrainableLora, adap
|
|
|
83
126
|
export function detachTraining(model: RuntimeModel, lora: TrainableLora): void {
|
|
84
127
|
for (const t of lora.targets) t.linear.adapters?.delete(lora.adapterId);
|
|
85
128
|
model.loraState.active = model.loraState.active.filter((x) => x !== lora.adapterId);
|
|
129
|
+
model.loraState.dropoutRate = 0; // disable training dropout for inference
|
|
130
|
+
model.loraState.dropoutSeed = null;
|
|
86
131
|
}
|
|
87
132
|
|
|
88
133
|
/** Set the LoRA scale on every trainable leaf (DPO reference forward uses 0). */
|
|
@@ -115,6 +160,9 @@ export interface SaveAdapterConfig {
|
|
|
115
160
|
numLayers: number;
|
|
116
161
|
method: string;
|
|
117
162
|
baseModel: string;
|
|
163
|
+
/** rsLoRA: if true the effective per-layer scale is α/√rank (the loader must
|
|
164
|
+
* apply the same, so it's recorded in optiq_lora_config.json). */
|
|
165
|
+
rsLora?: boolean;
|
|
118
166
|
}
|
|
119
167
|
|
|
120
168
|
/** Save the trained adapter into `dir` in the AdapterManager.mount format:
|
|
@@ -156,9 +204,10 @@ export function saveAdapter(
|
|
|
156
204
|
scale: cfg.scale,
|
|
157
205
|
rank_scaling: cfg.rankScaling,
|
|
158
206
|
method: cfg.method,
|
|
207
|
+
rs_lora: cfg.rsLora ?? false,
|
|
159
208
|
target_modules: cfg.targetModules,
|
|
160
209
|
num_layers: cfg.numLayers,
|
|
161
|
-
lora_parameters: { scale: cfg.scale, rank: cfg.rank },
|
|
210
|
+
lora_parameters: { scale: cfg.scale, rank: cfg.rank, rs_lora: cfg.rsLora ?? false },
|
|
162
211
|
applied_ranks: appliedRanks,
|
|
163
212
|
source_model: cfg.baseModel,
|
|
164
213
|
};
|