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.
Files changed (50) hide show
  1. package/README.md +166 -93
  2. package/docs/design/adapters-end-to-end.md +50 -46
  3. package/docs/design/compat-cli-surface-design.md +10 -8
  4. package/docs/design/docs-reorg-plan.md +14 -1
  5. package/docs/design/hlg-sampling.md +12 -2
  6. package/docs/design/memory-system.md +1 -1
  7. package/docs/design/optimization_plan.md +4 -2
  8. package/docs/design/orpo-future-enhancements.md +298 -0
  9. package/docs/design/orpo-training.md +1199 -0
  10. package/docs/design/parallel-slots.md +34 -26
  11. package/docs/design/runtime-isolation.md +1 -1
  12. package/docs/design/segmented-backward-training.md +58 -39
  13. package/docs/design/tauri-desktop-app.md +127 -0
  14. package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
  15. package/docs/investigations/starter-model-port-handoff.md +32 -13
  16. package/docs/investigations/steel-flash-cce-handoff.md +431 -0
  17. package/docs/planning/IDEAS.md +29 -0
  18. package/docs/planning/PRODUCT_ROADMAP.md +147 -27
  19. package/docs/planning/chunk-task-roadmap.md +1 -1
  20. package/docs/planning/journal.md +161 -0
  21. package/docs/reference/distribution.md +61 -5
  22. package/docs/reference/library-api.md +29 -14
  23. package/docs/reference/orpo-quickstart.md +91 -0
  24. package/docs/reference/server-api.md +105 -7
  25. package/docs/reference/server-config.md +12 -5
  26. package/docs/reference/training.md +292 -0
  27. package/package.json +1 -1
  28. package/src/eval/ifeval.ts +138 -0
  29. package/src/eval/ultrafeedback.ts +60 -0
  30. package/src/lora.ts +12 -7
  31. package/src/mlx/metal-kernel.ts +18 -4
  32. package/src/model/flash-attention.ts +32 -16
  33. package/src/model/fused-geglu-kernel.ts +111 -0
  34. package/src/model/gemma4-base.ts +39 -1
  35. package/src/model/gemma4.ts +196 -21
  36. package/src/model/minicpm5.ts +44 -3
  37. package/src/server.ts +3 -3
  38. package/src/train/dataset.ts +18 -3
  39. package/src/train/flash-cce.ts +1162 -0
  40. package/src/train/job.ts +26 -2
  41. package/src/train/lora-params.ts +53 -4
  42. package/src/train/loss.ts +941 -0
  43. package/src/train/optimizer.ts +10 -3
  44. package/src/train/prefix-shared.ts +435 -0
  45. package/src/train/rank.ts +36 -0
  46. package/src/train/segmented.ts +1148 -3
  47. package/src/train/steel-qmm-header.ts +1461 -0
  48. package/src/train/trainer.ts +509 -52
  49. package/src/web/app.html +21 -5
  50. /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
  };
@@ -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
- targets.push({ modulePath, linear, lw: { a, b, scale, rank } });
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
- for (const t of lora.targets) {
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
  };