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.
Files changed (90) hide show
  1. package/README.md +175 -93
  2. package/docs/dag/training-inference-map.html +575 -0
  3. package/docs/design/adapters-end-to-end.md +50 -46
  4. package/docs/design/compat-cli-surface-design.md +10 -8
  5. package/docs/design/docs-reorg-plan.md +14 -1
  6. package/docs/design/hlg-sampling.md +12 -2
  7. package/docs/design/memory-system.md +193 -295
  8. package/docs/design/optimization_plan.md +4 -2
  9. package/docs/design/orpo-dynamic-lambda.md +179 -0
  10. package/docs/design/orpo-future-enhancements.md +340 -0
  11. package/docs/design/orpo-training.md +1212 -0
  12. package/docs/design/parallel-slots.md +34 -26
  13. package/docs/design/parity-tier-dag.md +143 -0
  14. package/docs/design/runtime-isolation.md +1 -1
  15. package/docs/design/segmented-backward-training.md +58 -39
  16. package/docs/design/tauri-desktop-app.md +127 -0
  17. package/docs/design/w4a16-compute-precision-spike.md +232 -0
  18. package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
  19. package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
  20. package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
  21. package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
  22. package/docs/investigations/starter-model-port-handoff.md +32 -13
  23. package/docs/investigations/steel-flash-cce-handoff.md +489 -0
  24. package/docs/investigations/trainer-validation-experiment.md +173 -0
  25. package/docs/planning/IDEAS.md +29 -0
  26. package/docs/planning/PRODUCT_ROADMAP.md +147 -27
  27. package/docs/planning/chunk-task-roadmap.md +1 -1
  28. package/docs/planning/journal.md +161 -0
  29. package/docs/reference/distribution.md +61 -5
  30. package/docs/reference/library-api.md +29 -14
  31. package/docs/reference/memory.md +77 -0
  32. package/docs/reference/orpo-quickstart.md +130 -0
  33. package/docs/reference/server-api.md +105 -7
  34. package/docs/reference/server-config.md +12 -5
  35. package/docs/reference/training.md +296 -0
  36. package/package.json +1 -1
  37. package/src/cli.ts +651 -18
  38. package/src/eval/ifeval.ts +138 -0
  39. package/src/eval/runner.ts +76 -6
  40. package/src/eval/tasks/bfcl.ts +49 -15
  41. package/src/eval/tasks/gsm8k.ts +21 -9
  42. package/src/eval/tasks/hashhop.ts +33 -4
  43. package/src/eval/tasks/humaneval.ts +30 -8
  44. package/src/eval/tasks/ifeval.ts +52 -18
  45. package/src/eval/tasks/mmlu.ts +85 -74
  46. package/src/eval/ultrafeedback.ts +60 -0
  47. package/src/jobs/runner.ts +6 -0
  48. package/src/lora.ts +12 -7
  49. package/src/memory/chunk.ts +25 -0
  50. package/src/memory/cluster.ts +24 -0
  51. package/src/memory/db.ts +114 -0
  52. package/src/memory/ingest.ts +22 -0
  53. package/src/memory/pipeline.ts +63 -0
  54. package/src/memory/prompts.ts +23 -0
  55. package/src/memory/schedule.ts +163 -0
  56. package/src/memory/synthesize.ts +29 -0
  57. package/src/memory/tools.ts +399 -0
  58. package/src/memory/vault.ts +694 -0
  59. package/src/memory/wikify.ts +23 -0
  60. package/src/mlx/metal-kernel.ts +31 -4
  61. package/src/model/flash-attention.ts +37 -19
  62. package/src/model/fused-geglu-kernel.ts +111 -0
  63. package/src/model/gemma4-base.ts +39 -1
  64. package/src/model/gemma4.ts +196 -21
  65. package/src/model/minicpm5.ts +44 -3
  66. package/src/pi-provider.ts +29 -13
  67. package/src/pi-session.ts +81 -0
  68. package/src/pi-terminal.ts +13 -24
  69. package/src/pi-web.ts +246 -166
  70. package/src/quantize/job.ts +17 -2
  71. package/src/quantize/quantizer.ts +26 -2
  72. package/src/server.ts +216 -47
  73. package/src/tokenizer.ts +6 -2
  74. package/src/train/dataset.ts +29 -6
  75. package/src/train/flash-cce.ts +1175 -0
  76. package/src/train/job.ts +64 -3
  77. package/src/train/lora-params.ts +53 -4
  78. package/src/train/loss.ts +947 -0
  79. package/src/train/optimizer.ts +10 -3
  80. package/src/train/prefix-shared.ts +435 -0
  81. package/src/train/rank.ts +36 -0
  82. package/src/train/segmented.ts +1148 -3
  83. package/src/train/steel-qmm-header.ts +1461 -0
  84. package/src/train/trainer.ts +570 -54
  85. package/src/train/watch.ts +542 -0
  86. package/src/web/app.html +285 -19
  87. package/src/web/skills/memory/SKILL.md +97 -0
  88. package/src/web/skills/web-research/SKILL.md +3 -1
  89. package/src/web/skills.ts +46 -8
  90. /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
- segmentSize: c.segment_size ?? DEFAULT_TRAIN_CONFIG.segmentSize,
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);
@@ -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
  };