mlx-bun 0.0.4 → 0.0.6
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 +175 -93
- package/docs/dag/training-inference-map.html +575 -0
- 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 +193 -295
- package/docs/design/optimization_plan.md +4 -2
- package/docs/design/orpo-dynamic-lambda.md +179 -0
- package/docs/design/orpo-future-enhancements.md +340 -0
- package/docs/design/orpo-training.md +1212 -0
- package/docs/design/parallel-slots.md +34 -26
- package/docs/design/parity-tier-dag.md +143 -0
- 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/design/w4a16-compute-precision-spike.md +232 -0
- package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
- package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
- package/docs/investigations/orpo-uf-testing-handoff.md +126 -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 +489 -0
- package/docs/investigations/trainer-validation-experiment.md +173 -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/memory.md +77 -0
- package/docs/reference/orpo-quickstart.md +130 -0
- package/docs/reference/server-api.md +105 -7
- package/docs/reference/server-config.md +12 -5
- package/docs/reference/training.md +296 -0
- package/package.json +1 -1
- package/src/cli.ts +651 -18
- package/src/eval/ifeval.ts +138 -0
- package/src/eval/runner.ts +76 -6
- package/src/eval/tasks/bfcl.ts +49 -15
- package/src/eval/tasks/gsm8k.ts +21 -9
- package/src/eval/tasks/hashhop.ts +33 -4
- package/src/eval/tasks/humaneval.ts +30 -8
- package/src/eval/tasks/ifeval.ts +52 -18
- package/src/eval/tasks/mmlu.ts +85 -74
- package/src/eval/ultrafeedback.ts +60 -0
- package/src/jobs/runner.ts +6 -0
- package/src/lora.ts +12 -7
- package/src/memory/chunk.ts +25 -0
- package/src/memory/cluster.ts +24 -0
- package/src/memory/db.ts +114 -0
- package/src/memory/ingest.ts +22 -0
- package/src/memory/pipeline.ts +63 -0
- package/src/memory/prompts.ts +23 -0
- package/src/memory/schedule.ts +163 -0
- package/src/memory/synthesize.ts +29 -0
- package/src/memory/tools.ts +399 -0
- package/src/memory/vault.ts +694 -0
- package/src/memory/wikify.ts +23 -0
- package/src/mlx/metal-kernel.ts +31 -4
- package/src/model/flash-attention.ts +37 -19
- 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/pi-provider.ts +29 -13
- package/src/pi-session.ts +81 -0
- package/src/pi-terminal.ts +13 -24
- package/src/pi-web.ts +246 -166
- package/src/quantize/job.ts +17 -2
- package/src/quantize/quantizer.ts +26 -2
- package/src/server.ts +216 -47
- package/src/tokenizer.ts +6 -2
- package/src/train/dataset.ts +29 -6
- package/src/train/flash-cce.ts +1175 -0
- package/src/train/job.ts +64 -3
- package/src/train/lora-params.ts +53 -4
- package/src/train/loss.ts +947 -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 +570 -54
- package/src/train/watch.ts +542 -0
- package/src/web/app.html +285 -19
- package/src/web/skills/memory/SKILL.md +97 -0
- package/src/web/skills/web-research/SKILL.md +3 -1
- package/src/web/skills.ts +46 -8
- /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,26 @@ 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;
|
|
42
|
+
grad_clip_norm?: number;
|
|
43
|
+
val_max_examples?: number;
|
|
38
44
|
dpo_beta?: number;
|
|
39
45
|
dpo_warmup_iters?: number;
|
|
40
46
|
dpo_lr_schedule?: "constant" | "cosine";
|
|
47
|
+
orpo_lambda?: number;
|
|
48
|
+
orpo_warmup_iters?: number;
|
|
49
|
+
orpo_lr_schedule?: "constant" | "cosine";
|
|
50
|
+
orpo_chunk_size?: number;
|
|
51
|
+
orpo_fused_ce?: boolean;
|
|
52
|
+
orpo_flash_ce?: boolean;
|
|
53
|
+
orpo_prefix_shared?: boolean;
|
|
54
|
+
warm_start_adapter?: string;
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir: string; cfg: TrainConfig } {
|
|
@@ -46,6 +60,14 @@ function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir:
|
|
|
46
60
|
if (!c.data_dir) throw new Error("finetune job: missing data_dir");
|
|
47
61
|
if (!c.adapter_path) throw new Error("finetune job: missing adapter_path");
|
|
48
62
|
|
|
63
|
+
// For ORPO the full L3 stack is the validated production recipe (what the
|
|
64
|
+
// `mlx-bun train` CLI turns on): flash-CCE head + prefix-sharing + segmented
|
|
65
|
+
// backward + token-chunked head. DEFAULT_TRAIN_CONFIG keeps them OFF (they're
|
|
66
|
+
// opt-in for SFT), so default them ON here when method=orpo — an explicit
|
|
67
|
+
// value from the caller (CLI/API) still wins via ??. finetuneRunner drops the
|
|
68
|
+
// quantized-head pieces if the base turns out to be unquantized.
|
|
69
|
+
const isOrpo = (c.method ?? "sft") === "orpo";
|
|
70
|
+
|
|
49
71
|
const cfg: TrainConfig = {
|
|
50
72
|
...DEFAULT_TRAIN_CONFIG,
|
|
51
73
|
method: c.method ?? "sft",
|
|
@@ -55,7 +77,7 @@ function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir:
|
|
|
55
77
|
targetModules: c.target_modules ?? DEFAULT_TRAIN_CONFIG.targetModules,
|
|
56
78
|
numLayers: c.num_layers ?? DEFAULT_TRAIN_CONFIG.numLayers,
|
|
57
79
|
iters: c.iters ?? DEFAULT_TRAIN_CONFIG.iters,
|
|
58
|
-
learningRate: c.learning_rate ?? (c.method === "dpo" ? 5e-5 : DEFAULT_TRAIN_CONFIG.learningRate),
|
|
80
|
+
learningRate: c.learning_rate ?? (c.method === "dpo" ? 5e-5 : c.method === "orpo" ? 1e-5 : DEFAULT_TRAIN_CONFIG.learningRate),
|
|
59
81
|
maxSeqLen: c.max_seq_length ?? DEFAULT_TRAIN_CONFIG.maxSeqLen,
|
|
60
82
|
batchSize: c.batch_size ?? DEFAULT_TRAIN_CONFIG.batchSize,
|
|
61
83
|
gradAccumSteps: c.grad_accumulation_steps ?? DEFAULT_TRAIN_CONFIG.gradAccumSteps,
|
|
@@ -64,12 +86,26 @@ function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir:
|
|
|
64
86
|
stepsPerEval: c.steps_per_eval ?? DEFAULT_TRAIN_CONFIG.stepsPerEval,
|
|
65
87
|
betas: DEFAULT_TRAIN_CONFIG.betas,
|
|
66
88
|
weightDecay: c.weight_decay ?? DEFAULT_TRAIN_CONFIG.weightDecay,
|
|
89
|
+
loraDropout: c.lora_dropout ?? DEFAULT_TRAIN_CONFIG.loraDropout,
|
|
90
|
+
rsLora: c.rs_lora ?? DEFAULT_TRAIN_CONFIG.rsLora,
|
|
91
|
+
loraPlusRatio: c.lora_plus_ratio ?? DEFAULT_TRAIN_CONFIG.loraPlusRatio,
|
|
67
92
|
gradCheckpoint: c.grad_checkpoint ?? DEFAULT_TRAIN_CONFIG.gradCheckpoint,
|
|
68
|
-
|
|
93
|
+
mlpSplit: c.mlp_split ?? DEFAULT_TRAIN_CONFIG.mlpSplit,
|
|
94
|
+
segmentSize: c.segment_size ?? (isOrpo ? 2 : DEFAULT_TRAIN_CONFIG.segmentSize),
|
|
69
95
|
saveCheckpoints: c.save_checkpoints ?? DEFAULT_TRAIN_CONFIG.saveCheckpoints,
|
|
96
|
+
gradClipNorm: c.grad_clip_norm ?? DEFAULT_TRAIN_CONFIG.gradClipNorm,
|
|
97
|
+
valMaxExamples: c.val_max_examples ?? DEFAULT_TRAIN_CONFIG.valMaxExamples,
|
|
70
98
|
dpoBeta: c.dpo_beta ?? DEFAULT_TRAIN_CONFIG.dpoBeta,
|
|
71
99
|
dpoWarmupIters: c.dpo_warmup_iters ?? DEFAULT_TRAIN_CONFIG.dpoWarmupIters,
|
|
72
100
|
dpoLrSchedule: c.dpo_lr_schedule ?? DEFAULT_TRAIN_CONFIG.dpoLrSchedule,
|
|
101
|
+
orpoLambda: c.orpo_lambda ?? DEFAULT_TRAIN_CONFIG.orpoLambda,
|
|
102
|
+
orpoWarmupIters: c.orpo_warmup_iters ?? DEFAULT_TRAIN_CONFIG.orpoWarmupIters,
|
|
103
|
+
orpoLrSchedule: c.orpo_lr_schedule ?? DEFAULT_TRAIN_CONFIG.orpoLrSchedule,
|
|
104
|
+
orpoChunkSize: c.orpo_chunk_size ?? (isOrpo ? 512 : DEFAULT_TRAIN_CONFIG.orpoChunkSize),
|
|
105
|
+
orpoFusedCe: c.orpo_fused_ce ?? DEFAULT_TRAIN_CONFIG.orpoFusedCe,
|
|
106
|
+
orpoFlashCe: c.orpo_flash_ce ?? (isOrpo ? true : DEFAULT_TRAIN_CONFIG.orpoFlashCe),
|
|
107
|
+
orpoPrefixShared: c.orpo_prefix_shared ?? (isOrpo ? true : DEFAULT_TRAIN_CONFIG.orpoPrefixShared),
|
|
108
|
+
warmStartAdapter: c.warm_start_adapter ?? DEFAULT_TRAIN_CONFIG.warmStartAdapter,
|
|
73
109
|
adapterPath: c.adapter_path,
|
|
74
110
|
baseModel: c.model_dir,
|
|
75
111
|
};
|
|
@@ -79,6 +115,18 @@ function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir:
|
|
|
79
115
|
export const finetuneRunner: JobRunner = async (emit, config) => {
|
|
80
116
|
const { modelDir, dataDir, cfg } = parseConfig(config);
|
|
81
117
|
|
|
118
|
+
// Gemma/e4b needs these training env flags set BEFORE the model loads — same
|
|
119
|
+
// as the `mlx-bun train` CLI. The subprocess job path doesn't inherit them
|
|
120
|
+
// from the server, so without this a web-submitted ORPO/SFT run on Gemma uses
|
|
121
|
+
// the wrong kernels. ??= so an explicit env (or the CLI) still wins.
|
|
122
|
+
try {
|
|
123
|
+
const isGemma = (await Bun.file(`${modelDir}/config.json`).text()).toLowerCase().includes("gemma");
|
|
124
|
+
if (isGemma) {
|
|
125
|
+
process.env.MLX_BUN_PERF_KERNEL ??= "0";
|
|
126
|
+
process.env.MLX_BUN_FUSED_GELU ??= "0";
|
|
127
|
+
}
|
|
128
|
+
} catch {}
|
|
129
|
+
|
|
82
130
|
emit({ type: "stage", stage: "load", progress: 0.01, message: `loading model ${modelDir}` });
|
|
83
131
|
|
|
84
132
|
const { loadModelConfig } = await import("../config");
|
|
@@ -88,6 +136,19 @@ export const finetuneRunner: JobRunner = async (emit, config) => {
|
|
|
88
136
|
const { ChatTemplate } = await import("../chat-template");
|
|
89
137
|
|
|
90
138
|
const modelConfig = await loadModelConfig(modelDir);
|
|
139
|
+
|
|
140
|
+
// The flash-CCE / fused-CE heads dequantize a QUANTIZED head in-kernel; on an
|
|
141
|
+
// unquantized (bf16) base they can't run, so fall back to the full-logits head.
|
|
142
|
+
// (Quantize the base first to get the large-vocab memory win — see the
|
|
143
|
+
// Quantize tab.)
|
|
144
|
+
if (cfg.method === "orpo" && !modelConfig.quantization && (cfg.orpoFlashCe || cfg.orpoFusedCe)) {
|
|
145
|
+
emit({
|
|
146
|
+
type: "stage", stage: "load",
|
|
147
|
+
message: "unquantized base — flash/fused-CE head needs a quantized head; using full-logits head",
|
|
148
|
+
});
|
|
149
|
+
cfg.orpoFlashCe = false;
|
|
150
|
+
cfg.orpoFusedCe = false;
|
|
151
|
+
}
|
|
91
152
|
const weights = await Weights.open(modelDir);
|
|
92
153
|
const model = createModel(weights, modelConfig);
|
|
93
154
|
const tok = await loadTokenizer(modelDir);
|
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
|
};
|