mlx-bun 0.0.8 → 0.0.10

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 (144) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-perf-path.md +232 -0
  3. package/docs/design/batching-v2-plan.md +515 -0
  4. package/docs/design/bucketing-stage.md +196 -0
  5. package/docs/design/compat-cli-surface-design.md +11 -11
  6. package/docs/design/docs-reorg-plan.md +1 -1
  7. package/docs/design/dreaming-nightly-pipeline.md +291 -0
  8. package/docs/design/dspark-speculative-decoding.md +106 -0
  9. package/docs/design/generic-model-support.md +381 -0
  10. package/docs/design/memory-inference-path.md +422 -0
  11. package/docs/design/memory-synthesis.md +287 -0
  12. package/docs/design/minicpm5-decode-megakernel.md +484 -0
  13. package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
  14. package/docs/design/omlx-adoption-map.md +90 -0
  15. package/docs/design/orpo-dynamic-lambda.md +41 -0
  16. package/docs/design/orpo-training.md +35 -5
  17. package/docs/design/parallel-slots.md +12 -1
  18. package/docs/design/parity-tier-dag.md +13 -5
  19. package/docs/design/segmented-backward-training.md +15 -0
  20. package/docs/design/spec-decode-larger-targets.md +229 -0
  21. package/docs/design/ssd-kv-cold-tier.md +158 -0
  22. package/docs/design/tauri-desktop-app.md +2 -2
  23. package/docs/design/the-dreaming-handoff.md +141 -0
  24. package/docs/design/the-dreaming-master-plan.md +446 -0
  25. package/docs/design/write-pipeline-entity.md +468 -0
  26. package/docs/investigations/curve-bisector-routing.md +1 -1
  27. package/docs/investigations/decode-roofline-lookagain.md +348 -0
  28. package/docs/investigations/dspark-handoff.md +58 -0
  29. package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
  30. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  31. package/docs/planning/curve-sampler-research-plan.md +331 -0
  32. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  33. package/docs/planning/pi-integration-review.md +178 -0
  34. package/docs/planning/release-notes-v0.0.10.md +57 -0
  35. package/docs/planning/release-notes-v0.0.9.md +122 -0
  36. package/docs/planning/web-ui-pass-plan.md +213 -0
  37. package/docs/planning/website-readme-pass-plan.md +224 -0
  38. package/docs/reference/cli.md +255 -0
  39. package/docs/reference/embedding.md +7 -2
  40. package/docs/reference/library-api.md +22 -3
  41. package/docs/reference/memory.md +5 -5
  42. package/docs/reference/models.md +137 -0
  43. package/docs/reference/orpo-quickstart.md +6 -0
  44. package/docs/reference/server-api.md +187 -9
  45. package/docs/reference/server-config.md +43 -9
  46. package/docs/reference/training.md +31 -12
  47. package/package.json +1 -1
  48. package/src/anthropic.ts +22 -0
  49. package/src/assets/curve-designer.html +1 -1
  50. package/src/chat-template.ts +25 -7
  51. package/src/cli.ts +1077 -90
  52. package/src/config.ts +4 -1
  53. package/src/download.ts +99 -8
  54. package/src/embed.ts +77 -0
  55. package/src/eval/perplexity.ts +164 -0
  56. package/src/eval/runner.ts +15 -6
  57. package/src/eval/tasks/bfcl.ts +1 -1
  58. package/src/generate.ts +149 -18
  59. package/src/harness-pi.ts +43 -17
  60. package/src/index.ts +2 -0
  61. package/src/kv-store.ts +240 -98
  62. package/src/lora.ts +2 -2
  63. package/src/memory/article.ts +558 -0
  64. package/src/memory/chunk-validation.ts +150 -0
  65. package/src/memory/chunk.ts +305 -12
  66. package/src/memory/cluster.ts +344 -17
  67. package/src/memory/coverage.ts +134 -0
  68. package/src/memory/crosslink.ts +0 -0
  69. package/src/memory/db.ts +184 -45
  70. package/src/memory/entity.ts +203 -0
  71. package/src/memory/gate.ts +187 -0
  72. package/src/memory/history.ts +152 -0
  73. package/src/memory/ingest.ts +324 -11
  74. package/src/memory/model.ts +321 -0
  75. package/src/memory/normalize.ts +671 -0
  76. package/src/memory/parse.ts +22 -0
  77. package/src/memory/pipeline.ts +242 -30
  78. package/src/memory/prompts.ts +42 -0
  79. package/src/memory/query.ts +334 -0
  80. package/src/memory/reconcile.ts +734 -0
  81. package/src/memory/redirect.ts +108 -0
  82. package/src/memory/reindex.ts +325 -0
  83. package/src/memory/resolve.ts +307 -0
  84. package/src/memory/route.ts +272 -0
  85. package/src/memory/schedule.ts +6 -4
  86. package/src/memory/stages.ts +774 -0
  87. package/src/memory/synthesize.ts +1629 -20
  88. package/src/memory/tools.ts +446 -96
  89. package/src/memory/vault.ts +462 -35
  90. package/src/memory/wikify.ts +1057 -16
  91. package/src/mlx/array.ts +21 -0
  92. package/src/mlx/ffi.ts +3 -0
  93. package/src/mlx/ops.ts +72 -0
  94. package/src/model/compiled-decode.ts +43 -8
  95. package/src/model/factory.ts +29 -8
  96. package/src/model/flash-attention.ts +4 -2
  97. package/src/model/fused-decode-kernel.ts +14 -7
  98. package/src/model/fused-mlp-kernel.ts +169 -0
  99. package/src/model/fused-swiglu-kernel.ts +240 -0
  100. package/src/model/gemma4-base.ts +96 -7
  101. package/src/model/gemma4.ts +33 -2
  102. package/src/model/minicpm5.ts +106 -8
  103. package/src/model/qwen3-delta.ts +48 -1
  104. package/src/model/qwen3.ts +276 -0
  105. package/src/model/qwen3_5.ts +11 -2
  106. package/src/model/steel-linear-kernel.ts +96 -0
  107. package/src/model/support.ts +38 -8
  108. package/src/model/universal/archs.ts +332 -0
  109. package/src/model/universal/dense.ts +576 -0
  110. package/src/model/universal/modules.ts +163 -0
  111. package/src/model/universal/rope.ts +366 -0
  112. package/src/pi-provider.ts +1 -1
  113. package/src/pi-terminal.ts +29 -8
  114. package/src/pi-web.ts +19 -2
  115. package/src/prompt-cache.ts +55 -9
  116. package/src/quantize/config-writer.ts +1 -1
  117. package/src/registry.ts +219 -8
  118. package/src/responses.ts +23 -0
  119. package/src/sampler.ts +220 -9
  120. package/src/serve/batch-scheduler.ts +307 -83
  121. package/src/serve/generation-gateway.ts +156 -29
  122. package/src/server.ts +746 -73
  123. package/src/spec/drafter.ts +81 -49
  124. package/src/spec/dspark/data-dflash.ts +156 -0
  125. package/src/spec/dspark/data.ts +201 -0
  126. package/src/spec/dspark/generate-dflash.ts +193 -0
  127. package/src/spec/dspark/generate.ts +290 -0
  128. package/src/spec/dspark/loss.ts +213 -0
  129. package/src/spec/dspark/module-dflash.ts +361 -0
  130. package/src/spec/dspark/module.ts +540 -0
  131. package/src/spec/dspark/sample.ts +112 -0
  132. package/src/spec/generate.ts +40 -19
  133. package/src/ssd-cache.ts +213 -0
  134. package/src/status-page.html +1 -1
  135. package/src/tokenizer.ts +7 -0
  136. package/src/train/flash-cce.ts +81 -23
  137. package/src/train/fuse.ts +243 -0
  138. package/src/train/index.ts +3 -0
  139. package/src/train/job.ts +8 -0
  140. package/src/train/loss.ts +341 -40
  141. package/src/train/prefix-shared.ts +62 -11
  142. package/src/train/segmented.ts +215 -55
  143. package/src/train/trainer.ts +112 -38
  144. package/src/web/skills/memory/SKILL.md +40 -17
@@ -0,0 +1,243 @@
1
+ // Fuse a LoRA adapter into base model weights → a standalone model snapshot
2
+ // (mlx_lm.fuse counterpart, minus GGUF/de-quantize export).
3
+ //
4
+ // Math (matches mlx-lm's LoRALinear.fuse and our QuantizedLinear.forward):
5
+ // forward: y = x @ W_dequantᵀ + scale · ((x @ A) @ B)
6
+ // fused: W'[out,in] = W + (scale · B)ᵀ.astype(bf16) @ Aᵀ.astype(bf16)
7
+ // mlx-lm casts the (scaled) LoRA factors to the base dtype BEFORE the matmul,
8
+ // then re-quantizes with the module's OWN source spec (bits/group unchanged) —
9
+ // we do the same, so the output config.json is the source config verbatim.
10
+ //
11
+ // Everything runs on the CPU stream (same discipline as src/quantize/quantizer:
12
+ // the safetensors loader lives there, and one module is materialized, fused,
13
+ // and disposed at a time). Modules the adapter does not touch pass through
14
+ // bit-identical (no dequant→requant round trip).
15
+
16
+ import { existsSync, mkdirSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { clearCache, Dtype } from "../mlx/ffi";
19
+ import { cpuStream, type MlxArray } from "../mlx/array";
20
+ import * as ops from "../mlx/ops";
21
+ import { Weights } from "../weights";
22
+ import { loadModelConfig, quantFor } from "../config";
23
+ import { loadAdapterTensors, readAdapterScale, adapterWeightsFile } from "../lora";
24
+ import { writeShardedSafetensors, type NamedTensor } from "../quantize/safetensors-writer";
25
+ import { copyAuxFiles } from "../quantize/config-writer";
26
+
27
+ /** Quantize the allocator cache this often (in modules processed). */
28
+ const CACHE_CLEAR_EVERY = 16;
29
+
30
+ export interface FuseStats {
31
+ outDir: string;
32
+ /** Modules whose weights had an adapter delta folded in. */
33
+ fusedModules: number;
34
+ /** Adapter tensors that matched no base weight (skipped). */
35
+ skippedAdapterTensors: number;
36
+ totalTensors: number;
37
+ }
38
+
39
+ export interface FuseProgress {
40
+ stage: string;
41
+ message: string;
42
+ /** 0..1 fraction of modules processed. */
43
+ progress: number;
44
+ }
45
+
46
+ /** Resolve the adapter safetensors file: `<dir>/adapters.safetensors`,
47
+ * `<dir>/best/adapters.safetensors`, `adapter_model.safetensors`, or an
48
+ * explicit file path. */
49
+ function resolveAdapterFile(dir: string): string {
50
+ const best = join(dir, "best");
51
+ if (existsSync(join(best, "adapters.safetensors"))) return join(best, "adapters.safetensors");
52
+ if (dir.endsWith(".safetensors") && existsSync(dir)) return dir;
53
+ return adapterWeightsFile(dir);
54
+ }
55
+
56
+ /** Group adapter tensors into (modulePath → {a, b}); disposes non-LoRA
57
+ * tensors and returns how many were skipped. */
58
+ function groupPairs(tensors: Map<string, MlxArray>): {
59
+ pairs: Map<string, { a?: MlxArray; b?: MlxArray }>;
60
+ skipped: number;
61
+ } {
62
+ const pairs = new Map<string, { a?: MlxArray; b?: MlxArray }>();
63
+ let skipped = 0;
64
+ for (const [name, arr] of tensors) {
65
+ const m = name.match(/^(.*)\.(lora_a|lora_A|lora_b|lora_B)(\.weight)?$/);
66
+ if (!m) { skipped++; arr.dispose(); continue; }
67
+ const slot = pairs.get(m[1]!) ?? {};
68
+ if (m[2]!.toLowerCase() === "lora_a") slot.a = arr; else slot.b = arr;
69
+ pairs.set(m[1]!, slot);
70
+ }
71
+ return { pairs, skipped };
72
+ }
73
+
74
+ /**
75
+ * Fuse the adapter at `adapterDir` into the base model at `modelDir` and
76
+ * write a complete, loadable snapshot to `outDir` (weights + config.json +
77
+ * tokenizer aux files). Quantization layout is preserved verbatim: fused
78
+ * modules are dequantized, updated, and re-quantized with their own source
79
+ * spec; untouched modules pass through bit-identical.
80
+ */
81
+ export async function fuseAdapter(
82
+ modelDir: string,
83
+ adapterDir: string,
84
+ outDir: string,
85
+ onProgress?: (e: FuseProgress) => void,
86
+ ): Promise<FuseStats> {
87
+ const progress = (stage: string, message: string, frac: number) =>
88
+ onProgress?.({ stage, message, progress: frac });
89
+
90
+ progress("loading", `Reading ${modelDir}`, 0);
91
+ const config = await loadModelConfig(modelDir);
92
+ const weights = await Weights.open(modelDir);
93
+
94
+ const adapterFile = resolveAdapterFile(adapterDir);
95
+ const { scale, rsLora } = await readAdapterScale(adapterDir);
96
+ const { pairs, skipped } = groupPairs(loadAdapterTensors(adapterFile));
97
+
98
+ // Map each adapter module path to the base weight-file module path,
99
+ // probing the VLM-wrapped/unwrapped `language_model.` prefixes both ways
100
+ // (same normalization as AdapterManager.mount).
101
+ const weightNames = new Set(weights.tensorNames);
102
+ const byModule = new Map<string, { a: MlxArray; b: MlxArray }>();
103
+ let unmatched = 0;
104
+ const disposeAll = () => {
105
+ for (const { a, b } of pairs.values()) { a?.dispose(); b?.dispose(); }
106
+ };
107
+ try {
108
+ for (const [path, { a, b }] of pairs) {
109
+ if (!a || !b) throw new Error(`${path}: adapter has only one of lora_a/lora_b`);
110
+ const candidates = [
111
+ path,
112
+ `language_model.${path}`,
113
+ path.startsWith("language_model.") ? path.slice("language_model.".length) : null,
114
+ ].filter((c): c is string => c !== null);
115
+ const hit = candidates.find((c) => weightNames.has(`${c}.weight`));
116
+ if (!hit) { unmatched++; continue; }
117
+ byModule.set(hit, { a, b });
118
+ }
119
+ if (byModule.size === 0)
120
+ throw new Error(
121
+ `no adapter tensors match the base model's weights — was ` +
122
+ `${adapterFile} trained for a different base?`,
123
+ );
124
+ } catch (e) {
125
+ disposeAll();
126
+ weights.dispose();
127
+ throw e;
128
+ }
129
+
130
+ mkdirSync(outDir, { recursive: true });
131
+
132
+ // Walk tensors in source order (diff-friendly like the quantizer): group
133
+ // names into module bases so a fused module's weight/scales/biases are
134
+ // regenerated together.
135
+ const names = weights.tensorNames;
136
+ const moduleSuffixes = new Map<string, Set<string>>();
137
+ const passthroughNames: string[] = [];
138
+ for (const name of names) {
139
+ const m = /^(.*)\.(weight|scales|biases)$/.exec(name);
140
+ if (!m) { passthroughNames.push(name); continue; }
141
+ let set = moduleSuffixes.get(m[1]!);
142
+ if (!set) { set = new Set(); moduleSuffixes.set(m[1]!, set); }
143
+ set.add(m[2]!);
144
+ }
145
+
146
+ const out: NamedTensor[] = [];
147
+ let fusedModules = 0;
148
+ let processed = 0;
149
+ const totalModules = moduleSuffixes.size;
150
+
151
+ try {
152
+ for (const [base, suffixes] of moduleSuffixes) {
153
+ processed++;
154
+ if (processed % CACHE_CLEAR_EVERY === 0) clearCache();
155
+ const pair = suffixes.has("weight") ? byModule.get(base) : undefined;
156
+ if (!pair) {
157
+ for (const suf of suffixes) {
158
+ const n = `${base}.${suf}`;
159
+ out.push({ name: n, array: weights.tensor(n) });
160
+ }
161
+ continue;
162
+ }
163
+ progress("fusing", `Module ${processed}/${totalModules}: ${base}`, processed / totalModules);
164
+
165
+ // Materialize the full-precision weight [out, in].
166
+ const srcSpec = suffixes.has("scales") ? quantFor(config.quantization, base) : null;
167
+ let w: MlxArray;
168
+ if (srcSpec) {
169
+ const packed = weights.tensor(`${base}.weight`);
170
+ const scales = weights.tensor(`${base}.scales`);
171
+ const biases = suffixes.has("biases") ? weights.tensor(`${base}.biases`) : null;
172
+ w = ops.dequantize(packed, scales, biases, srcSpec, cpuStream);
173
+ } else {
174
+ w = weights.tensor(`${base}.weight`);
175
+ }
176
+ const [outF, inF] = w.shape as [number, number];
177
+ const { a, b } = pair;
178
+ const [aIn, rank] = a.shape as [number, number];
179
+ const [bRank, bOut] = b.shape as [number, number];
180
+ if (aIn !== inF || bRank !== rank || bOut !== outF)
181
+ throw new Error(
182
+ `${base}: shape mismatch — lora_a [${a.shape}] / lora_b [${b.shape}] ` +
183
+ `vs base [out ${outF}, in ${inF}]`,
184
+ );
185
+
186
+ // delta[out,in] = (scale·B)ᵀ.astype(bf16) @ Aᵀ.astype(bf16)
187
+ // (rsLoRA: effective per-layer scale is α/√rank, matching mount/training.)
188
+ const effScale = rsLora ? scale / Math.sqrt(rank) : scale;
189
+ const bScaled = ops.mulScalar(b, effScale, cpuStream);
190
+ const bT = ops.transposeAxes(bScaled, [1, 0], cpuStream);
191
+ bScaled.dispose();
192
+ const bT16 = bT.astype(Dtype.bfloat16, cpuStream);
193
+ bT.dispose();
194
+ const aT = ops.transposeAxes(a, [1, 0], cpuStream);
195
+ const aT16 = aT.astype(Dtype.bfloat16, cpuStream);
196
+ aT.dispose();
197
+ const delta = ops.matmul(bT16, aT16, cpuStream);
198
+ bT16.dispose();
199
+ aT16.dispose();
200
+
201
+ const w16 = w.dtype === Dtype.bfloat16 ? w : w.astype(Dtype.bfloat16, cpuStream);
202
+ if (w16 !== w) w.dispose();
203
+ const fusedW = ops.add(w16, delta, cpuStream);
204
+ w16.dispose();
205
+ delta.dispose();
206
+
207
+ if (srcSpec) {
208
+ const q = ops.quantize(fusedW, srcSpec.groupSize, srcSpec.bits, srcSpec.mode, cpuStream);
209
+ fusedW.dispose();
210
+ out.push({ name: `${base}.weight`, array: q.packed });
211
+ out.push({ name: `${base}.scales`, array: q.scales });
212
+ out.push({ name: `${base}.biases`, array: q.biases });
213
+ } else {
214
+ out.push({ name: `${base}.weight`, array: fusedW });
215
+ }
216
+ fusedModules++;
217
+ }
218
+ for (const name of passthroughNames) out.push({ name, array: weights.tensor(name) });
219
+
220
+ progress("writing", `Writing ${out.length} tensors to ${outDir}`, 1);
221
+ writeShardedSafetensors(outDir, out);
222
+
223
+ // Config is unchanged (same quantization layout) — copy it and the aux
224
+ // files (tokenizer, chat template, …) verbatim for a loadable snapshot.
225
+ await Bun.write(join(outDir, "config.json"), Bun.file(join(modelDir, "config.json")));
226
+ await copyAuxFiles(modelDir, outDir);
227
+
228
+ progress("done", `Fused ${fusedModules} module(s) → ${outDir}`, 1);
229
+ return {
230
+ outDir,
231
+ fusedModules,
232
+ skippedAdapterTensors: skipped + unmatched * 2,
233
+ totalTensors: out.length,
234
+ };
235
+ } finally {
236
+ for (const t of out) {
237
+ try { t.array.dispose(); } catch {}
238
+ }
239
+ disposeAll();
240
+ weights.dispose();
241
+ clearCache();
242
+ }
243
+ }
@@ -14,6 +14,7 @@ export { trainForward, buildBatchedPadMask } from "./forward";
14
14
 
15
15
  export {
16
16
  sftLoss, dpoLoss, dpoRefLogps, dpoMetrics,
17
+ orpoLoss, orpoMetrics, type SftScope,
17
18
  } from "./loss";
18
19
 
19
20
  export {
@@ -38,6 +39,8 @@ export {
38
39
 
39
40
  export { mergeAdapters, type MergeStats } from "./merge";
40
41
 
42
+ export { fuseAdapter, type FuseStats, type FuseProgress } from "./fuse";
43
+
41
44
  export { exportAdapter, type ExportManifest } from "./export";
42
45
 
43
46
  export { finetuneRunner, inspectDataset } from "./job";
package/src/train/job.ts CHANGED
@@ -51,6 +51,7 @@ interface FinetuneSubmit {
51
51
  orpo_fused_ce?: boolean;
52
52
  orpo_flash_ce?: boolean;
53
53
  orpo_prefix_shared?: boolean;
54
+ sft_scope?: "full" | "response";
54
55
  warm_start_adapter?: string;
55
56
  }
56
57
 
@@ -68,6 +69,12 @@ function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir:
68
69
  // quantized-head pieces if the base turns out to be unquantized.
69
70
  const isOrpo = (c.method ?? "sft") === "orpo";
70
71
 
72
+ // ORPO L_SFT scope: "full" (default; paper/TRL-faithful) | "response"
73
+ // (pre-2026-07 behavior). Validate here so a typo fails at submit, not mid-run.
74
+ const sftScope = c.sft_scope ?? DEFAULT_TRAIN_CONFIG.sftScope;
75
+ if (sftScope !== "full" && sftScope !== "response")
76
+ throw new Error(`finetune job: sft_scope must be "full" or "response" (got ${JSON.stringify(c.sft_scope)})`);
77
+
71
78
  const cfg: TrainConfig = {
72
79
  ...DEFAULT_TRAIN_CONFIG,
73
80
  method: c.method ?? "sft",
@@ -105,6 +112,7 @@ function parseConfig(raw: Record<string, unknown>): { modelDir: string; dataDir:
105
112
  orpoFusedCe: c.orpo_fused_ce ?? DEFAULT_TRAIN_CONFIG.orpoFusedCe,
106
113
  orpoFlashCe: c.orpo_flash_ce ?? (isOrpo ? true : DEFAULT_TRAIN_CONFIG.orpoFlashCe),
107
114
  orpoPrefixShared: c.orpo_prefix_shared ?? (isOrpo ? true : DEFAULT_TRAIN_CONFIG.orpoPrefixShared),
115
+ sftScope,
108
116
  warmStartAdapter: c.warm_start_adapter ?? DEFAULT_TRAIN_CONFIG.warmStartAdapter,
109
117
  adapterPath: c.adapter_path,
110
118
  baseModel: c.model_dir,