mlx-bun 0.0.4

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 (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/THIRD_PARTY_LICENSES.md +41 -0
  4. package/bin/mlx-bun.mjs +32 -0
  5. package/docs/curve-designer.html +303 -0
  6. package/docs/design/adapters-end-to-end.md +201 -0
  7. package/docs/design/compat-cli-surface-design.md +227 -0
  8. package/docs/design/docs-reorg-plan.md +152 -0
  9. package/docs/design/hlg-sampling.md +340 -0
  10. package/docs/design/memory-system.md +339 -0
  11. package/docs/design/optimization_plan.md +344 -0
  12. package/docs/design/parallel-slots.md +515 -0
  13. package/docs/design/runtime-isolation.md +247 -0
  14. package/docs/design/segmented-backward-training.md +508 -0
  15. package/docs/investigations/chunk-finetune-experiment.md +84 -0
  16. package/docs/investigations/curve-bisector-routing.md +123 -0
  17. package/docs/investigations/curve-runs/bisector-route.json +98 -0
  18. package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
  19. package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
  20. package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
  21. package/docs/investigations/curve-terrain.html +105 -0
  22. package/docs/investigations/expert-offload-single-user-moe.md +292 -0
  23. package/docs/investigations/hlg-ab.html +34 -0
  24. package/docs/investigations/hlg-explorer.html +238 -0
  25. package/docs/investigations/hlg-figs/cliff.svg +1 -0
  26. package/docs/investigations/hlg-figs/cross.svg +1 -0
  27. package/docs/investigations/hlg-figs/edges.svg +1 -0
  28. package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
  29. package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
  30. package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
  31. package/docs/investigations/hlg-figs/terrain.svg +1 -0
  32. package/docs/investigations/hlg-report.html +262 -0
  33. package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
  34. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
  35. package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
  36. package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
  37. package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
  38. package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
  39. package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
  40. package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
  41. package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
  42. package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
  43. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
  44. package/docs/investigations/hlg-runs/hlg-map.json +383 -0
  45. package/docs/investigations/hlg-runs/run0-original.log +159 -0
  46. package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
  47. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
  48. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
  49. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
  50. package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
  51. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
  52. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
  53. package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
  54. package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
  55. package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
  56. package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
  57. package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
  58. package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
  59. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
  60. package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
  61. package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
  62. package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
  63. package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
  64. package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
  65. package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
  66. package/docs/investigations/hlg-runs/runN-verify.log +116 -0
  67. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
  68. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
  69. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
  70. package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
  71. package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
  72. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
  73. package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
  74. package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
  75. package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
  76. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
  77. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
  78. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
  79. package/docs/investigations/hlg-sampling-investigation.md +970 -0
  80. package/docs/investigations/lab-build-journal.md +157 -0
  81. package/docs/investigations/pi-builtin-investigation.md +308 -0
  82. package/docs/investigations/starter-model-port-handoff.md +323 -0
  83. package/docs/planning/IDEAS.md +26 -0
  84. package/docs/planning/PRODUCT_ROADMAP.md +426 -0
  85. package/docs/planning/ResearchTopics.md +119 -0
  86. package/docs/planning/chunk-task-roadmap.md +77 -0
  87. package/docs/planning/journal.md +155 -0
  88. package/docs/reference/distribution.md +121 -0
  89. package/docs/reference/embedding.md +168 -0
  90. package/docs/reference/library-api.md +153 -0
  91. package/docs/reference/server-api.md +224 -0
  92. package/docs/reference/server-config.md +243 -0
  93. package/docs/segmented-backward-handoff.md +148 -0
  94. package/package.json +68 -0
  95. package/src/anthropic.ts +491 -0
  96. package/src/chat-template.ts +186 -0
  97. package/src/cli.ts +1075 -0
  98. package/src/config.ts +229 -0
  99. package/src/curve-sampler.ts +122 -0
  100. package/src/dataset/generators.ts +1087 -0
  101. package/src/dataset/index.ts +12 -0
  102. package/src/dataset/job.ts +81 -0
  103. package/src/dataset/llm.ts +71 -0
  104. package/src/dataset/registry.ts +528 -0
  105. package/src/download.ts +283 -0
  106. package/src/eval/README.md +86 -0
  107. package/src/eval/kl-prompts.ts +29 -0
  108. package/src/eval/kl.ts +294 -0
  109. package/src/eval/quality-db.ts +92 -0
  110. package/src/eval/runner.ts +102 -0
  111. package/src/eval/score.ts +49 -0
  112. package/src/eval/tasks/bfcl.ts +500 -0
  113. package/src/eval/tasks/gsm8k.ts +85 -0
  114. package/src/eval/tasks/hashhop.ts +109 -0
  115. package/src/eval/tasks/humaneval.ts +252 -0
  116. package/src/eval/tasks/ifeval.ts +354 -0
  117. package/src/eval/tasks/mmlu.ts +145 -0
  118. package/src/evaldb.ts +199 -0
  119. package/src/expert-offload-build.ts +77 -0
  120. package/src/expert-offload.ts +61 -0
  121. package/src/expert-trace.ts +67 -0
  122. package/src/fit.ts +254 -0
  123. package/src/generate.ts +451 -0
  124. package/src/harness-pi.ts +177 -0
  125. package/src/hf-push.ts +628 -0
  126. package/src/index.ts +22 -0
  127. package/src/jobs/db.ts +139 -0
  128. package/src/jobs/index.ts +29 -0
  129. package/src/jobs/job-entry.ts +106 -0
  130. package/src/jobs/runner.ts +304 -0
  131. package/src/jobs/sse.ts +114 -0
  132. package/src/jobs/types.ts +58 -0
  133. package/src/kv-store.ts +185 -0
  134. package/src/lora.ts +314 -0
  135. package/src/md-modules.d.ts +7 -0
  136. package/src/mlx/array.ts +237 -0
  137. package/src/mlx/autograd.ts +267 -0
  138. package/src/mlx/checkpoint.ts +116 -0
  139. package/src/mlx/compile.ts +158 -0
  140. package/src/mlx/custom-vjp.ts +140 -0
  141. package/src/mlx/ffi.ts +433 -0
  142. package/src/mlx/metal-kernel.ts +130 -0
  143. package/src/mlx/ops.ts +707 -0
  144. package/src/mmap.ts +90 -0
  145. package/src/model/batched-mask.ts +204 -0
  146. package/src/model/batched-rotating.ts +300 -0
  147. package/src/model/compiled-decode.ts +681 -0
  148. package/src/model/factory.ts +27 -0
  149. package/src/model/fingerprint.ts +33 -0
  150. package/src/model/flash-attention.ts +674 -0
  151. package/src/model/fused-decode-kernel.ts +305 -0
  152. package/src/model/fused-geglu-kernel.ts +87 -0
  153. package/src/model/gemma4-base.ts +1616 -0
  154. package/src/model/gemma4.ts +1012 -0
  155. package/src/model/generated/gemma4-12b.ts +526 -0
  156. package/src/model/generated/gemma4-26b.ts +500 -0
  157. package/src/model/generated/gemma4-e4b.ts +757 -0
  158. package/src/model/generated/index.ts +18 -0
  159. package/src/model/minicpm5.ts +278 -0
  160. package/src/model/qwen3-delta.ts +224 -0
  161. package/src/model/qwen3_5.ts +429 -0
  162. package/src/model/support.ts +46 -0
  163. package/src/native-pack.ts +152 -0
  164. package/src/pi-provider.ts +95 -0
  165. package/src/pi-terminal.ts +264 -0
  166. package/src/pi-web.ts +924 -0
  167. package/src/preflight.ts +123 -0
  168. package/src/prompt-cache.ts +113 -0
  169. package/src/quantize/allocator.ts +627 -0
  170. package/src/quantize/calibration.ts +180 -0
  171. package/src/quantize/config-writer.ts +132 -0
  172. package/src/quantize/index.ts +57 -0
  173. package/src/quantize/job.ts +136 -0
  174. package/src/quantize/quantizer.ts +462 -0
  175. package/src/quantize/safetensors-writer.ts +171 -0
  176. package/src/quantize/sensitivity.ts +234 -0
  177. package/src/registry.ts +336 -0
  178. package/src/responses.ts +774 -0
  179. package/src/safetensors.ts +158 -0
  180. package/src/sampler.ts +517 -0
  181. package/src/serve/batch-scheduler.ts +362 -0
  182. package/src/serve/generation-gateway.ts +160 -0
  183. package/src/server.ts +1891 -0
  184. package/src/spec/drafter.ts +261 -0
  185. package/src/spec/generate.ts +221 -0
  186. package/src/status-page.html +497 -0
  187. package/src/tokenizer.ts +38 -0
  188. package/src/tool-call.ts +242 -0
  189. package/src/train/dataset.ts +461 -0
  190. package/src/train/export.ts +32 -0
  191. package/src/train/forward.ts +229 -0
  192. package/src/train/index.ts +43 -0
  193. package/src/train/job.ts +148 -0
  194. package/src/train/lora-params.ts +194 -0
  195. package/src/train/loss.ts +342 -0
  196. package/src/train/merge.ts +195 -0
  197. package/src/train/optimizer.ts +176 -0
  198. package/src/train/rank.ts +101 -0
  199. package/src/train/segmented.ts +520 -0
  200. package/src/train/trainer.ts +600 -0
  201. package/src/tui.ts +161 -0
  202. package/src/vision/embedder.ts +168 -0
  203. package/src/vision/preprocess.ts +247 -0
  204. package/src/vision/prompt.ts +158 -0
  205. package/src/vision/siglip.ts +599 -0
  206. package/src/web/app.html +2471 -0
  207. package/src/web/skills/web-research/SKILL.md +44 -0
  208. package/src/web/skills.ts +59 -0
  209. package/src/web-tools.ts +426 -0
  210. package/src/weights.ts +91 -0
@@ -0,0 +1,145 @@
1
+ // MMLU 5-shot — multi-domain multiple-choice knowledge. Port of
2
+ // optiq/eval/mmlu.py: per-subject dev exemplars, A/B/C/D format, greedy
3
+ // decode, extract the predicted answer letter, compare to the gold index.
4
+ //
5
+ // The Python reference scores via first-letter logit argmax (no generation).
6
+ // The mlx-bun harness routes everything through generateText() (decoded
7
+ // completion). For an instruct model the faithful argmax-on-raw-text path
8
+ // underscores on a small noisy sample, so we drive the chat template: the
9
+ // 5-shot exemplars become the priming context and an explicit instruction
10
+ // asks for ONLY the single letter A/B/C/D. Extraction is robust to chatty
11
+ // formatting (</think> wrappers, "Answer: X", "(X)", "**X**", bare letter).
12
+ // Prompt format, header, exemplar selection, and gold mapping are verbatim.
13
+
14
+ import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
15
+
16
+ interface MmluRow {
17
+ question: string;
18
+ subject: string;
19
+ choices: string[];
20
+ answer: number; // 0..3 → A/B/C/D
21
+ }
22
+
23
+ export interface MmluResult {
24
+ nCorrect: number;
25
+ nTotal: number;
26
+ accuracy: number; // 0..1
27
+ }
28
+
29
+ const LETTERS = ["A", "B", "C", "D"] as const;
30
+
31
+ /** 'high_school_us_history' → 'high school us history'. */
32
+ function formatSubject(subject: string): string {
33
+ return subject.replace(/_/g, " ");
34
+ }
35
+
36
+ /** Format one example; append the letter when `answerIdx` is given (demos). */
37
+ function formatExample(question: string, choices: string[], answerIdx?: number): string {
38
+ let s = question.trim() + "\n";
39
+ for (let i = 0; i < choices.length; i++) s += `${LETTERS[i]}. ${choices[i]}\n`;
40
+ s += "Answer:";
41
+ if (answerIdx !== undefined) s += ` ${LETTERS[answerIdx]}\n`;
42
+ return s;
43
+ }
44
+
45
+ /** 5-shot prompt: subject header → n demos → open test question.
46
+ *
47
+ * Built for the chat template: the header + worked exemplars prime the model,
48
+ * and a trailing instruction pins the output to a single bare letter. The
49
+ * exemplars still close their "Answer: X" line so the model sees the expected
50
+ * shape, and the final question is left open ("Answer:") for the model. */
51
+ function buildPrompt(row: MmluRow, devExamples: MmluRow[], nShots: number): string {
52
+ let prompt = `The following are multiple choice questions (with answers) about ${formatSubject(row.subject)}.\n\n`;
53
+ for (const ex of devExamples.slice(0, nShots)) {
54
+ prompt += formatExample(ex.question, ex.choices, ex.answer);
55
+ prompt += "\n";
56
+ }
57
+ prompt += formatExample(row.question, row.choices);
58
+ prompt += "\n\nRespond with ONLY the single letter (A, B, C, or D) of the correct answer. Do not explain.";
59
+ return prompt;
60
+ }
61
+
62
+ /** Model output → predicted letter index 0..3, else null. Robust to chatty
63
+ * instruct formatting: strips a leading <think>…</think> block, then prefers
64
+ * an explicit "Answer: X" / "(X)" / "**X**" marker, falling back to the first
65
+ * standalone A/B/C/D token (one not glued inside a longer word). */
66
+ export function extractAnswer(textIn: string): number | null {
67
+ let text = textIn;
68
+ if (text.includes("</think>")) text = text.split("</think>").pop() ?? text;
69
+
70
+ const toIdx = (ch: string): number => LETTERS.indexOf(ch.toUpperCase() as (typeof LETTERS)[number]);
71
+
72
+ // Explicit answer markers, in priority order.
73
+ const markers = [
74
+ /(?:answer|correct answer|the answer is)\s*(?:is|:)?\s*\(?\*{0,2}([A-Da-d])\*{0,2}\)?/i,
75
+ /\*\*\s*([A-Da-d])\s*\*\*/, // **X**
76
+ /\(\s*([A-Da-d])\s*\)/, // (X)
77
+ ];
78
+ for (const re of markers) {
79
+ const m = text.match(re);
80
+ if (m) return toIdx(m[1]!);
81
+ }
82
+
83
+ // Fallback: first standalone letter (word-boundary, not embedded in a word).
84
+ const standalone = text.match(/\b([A-D])\b/);
85
+ if (standalone) return toIdx(standalone[1]!);
86
+
87
+ return null;
88
+ }
89
+
90
+ export async function evaluateMmlu(
91
+ tm: TaskModel,
92
+ opts: { nSamples?: number; nShots?: number; maxTokens?: number; seed?: number } = {},
93
+ ): Promise<MmluResult> {
94
+ const nShots = opts.nShots ?? 5;
95
+ const maxTokens = opts.maxTokens ?? 16;
96
+ const seed = opts.seed ?? 42;
97
+
98
+ const testRows = loadJsonl<MmluRow>("mmlu_test");
99
+ const devRows = loadJsonl<MmluRow>("mmlu_dev");
100
+
101
+ // Per-subject dev exemplars for prompt construction.
102
+ const devBySubject = new Map<string, MmluRow[]>();
103
+ for (const ex of devRows) {
104
+ const list = devBySubject.get(ex.subject) ?? [];
105
+ list.push(ex);
106
+ devBySubject.set(ex.subject, list);
107
+ }
108
+
109
+ // Stratified sample: roughly equal questions per subject (matches mmlu.py),
110
+ // deterministic via the shared seeded subsampler.
111
+ const testBySubject = new Map<string, number[]>();
112
+ for (let i = 0; i < testRows.length; i++) {
113
+ const subj = testRows[i]!.subject;
114
+ const list = testBySubject.get(subj) ?? [];
115
+ list.push(i);
116
+ testBySubject.set(subj, list);
117
+ }
118
+ const subjects = [...testBySubject.keys()].sort();
119
+ const nSamples = opts.nSamples ?? 1000;
120
+ const perSubject = Math.max(1, Math.floor(nSamples / subjects.length));
121
+
122
+ let sampled: number[] = [];
123
+ for (const subj of subjects) {
124
+ const pool = testBySubject.get(subj)!;
125
+ const take = Math.min(perSubject, pool.length);
126
+ for (const j of sampleIndices(pool.length, take, seed)) sampled.push(pool[j]!);
127
+ }
128
+ if (sampled.length > nSamples) {
129
+ const keep = sampleIndices(sampled.length, nSamples, seed);
130
+ sampled = keep.map((k) => sampled[k]!);
131
+ }
132
+
133
+ let nCorrect = 0;
134
+ for (let k = 0; k < sampled.length; k++) {
135
+ const item = testRows[sampled[k]!]!;
136
+ const devEx = devBySubject.get(item.subject) ?? [];
137
+ const out = await generateText(tm, buildPrompt(item, devEx, nShots), { maxTokens, useChat: true });
138
+ const pred = extractAnswer(out);
139
+ if (pred !== null && pred === item.answer) nCorrect++;
140
+ if ((k + 1) % 10 === 0 || k + 1 === sampled.length)
141
+ process.stderr.write(`\r mmlu ${k + 1}/${sampled.length} acc=${((nCorrect / (k + 1)) * 100).toFixed(1)}%`);
142
+ }
143
+ process.stderr.write("\n");
144
+ return { nCorrect, nTotal: sampled.length, accuracy: sampled.length ? nCorrect / sampled.length : 0 };
145
+ }
package/src/evaldb.ts ADDED
@@ -0,0 +1,199 @@
1
+ // Eval DB: every benchmark run recorded with model + config + commit
2
+ // (bun:sqlite). The fit calculator's predictions are validated against
3
+ // the measured peaks recorded here.
4
+
5
+ import { Database } from "bun:sqlite";
6
+ import { mkdirSync } from "node:fs";
7
+
8
+ export const DEFAULT_EVAL_DB = `${process.env.HOME}/.cache/mlx-bun/evals.sqlite`;
9
+
10
+ const SCHEMA = `
11
+ CREATE TABLE IF NOT EXISTS runs (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ ts INTEGER NOT NULL,
14
+ model_path TEXT NOT NULL,
15
+ commit_sha TEXT,
16
+ prompt_tokens INTEGER NOT NULL,
17
+ cached_tokens INTEGER NOT NULL DEFAULT 0,
18
+ generated_tokens INTEGER NOT NULL,
19
+ prefill_tps REAL NOT NULL,
20
+ decode_tps REAL NOT NULL,
21
+ peak_bytes INTEGER NOT NULL,
22
+ predicted_peak_bytes INTEGER,
23
+ predicted_decode_tps REAL,
24
+ notes TEXT
25
+ );
26
+
27
+ -- Phase 18 P1 — parallel-load harness. ONE row per configuration point
28
+ -- (a concurrency level or an arrival-rate target), not per request: the
29
+ -- single-stream runs table can't hold p50/p95 distributions or a
30
+ -- concurrency axis. Same additive-migration discipline as runs (these
31
+ -- are real measurements, never dropped). stack + machine_state mirror the
32
+ -- runs columns so cross-stack load rows read the same way.
33
+ CREATE TABLE IF NOT EXISTS load_runs (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ ts INTEGER NOT NULL,
36
+ model_id TEXT NOT NULL,
37
+ base_url TEXT NOT NULL,
38
+ stack TEXT NOT NULL DEFAULT 'mlx-bun',
39
+ commit_sha TEXT,
40
+ mode TEXT NOT NULL, -- 'closed-loop' | 'open-loop'
41
+ concurrency INTEGER NOT NULL, -- in-flight cap (closed) or requester count (open)
42
+ target_rpm REAL, -- open-loop arrival target per requester (null = closed)
43
+ max_tokens INTEGER NOT NULL,
44
+ requests_ok INTEGER NOT NULL,
45
+ requests_err INTEGER NOT NULL,
46
+ duration_s REAL NOT NULL, -- wall time of the measured window
47
+ ttft_p50_ms REAL NOT NULL,
48
+ ttft_p95_ms REAL NOT NULL,
49
+ e2e_p50_ms REAL NOT NULL,
50
+ e2e_p95_ms REAL NOT NULL,
51
+ e2e_p99_ms REAL NOT NULL,
52
+ agg_tps REAL NOT NULL, -- generated tokens/s summed across streams
53
+ per_req_tps REAL NOT NULL, -- median per-stream decode tok/s
54
+ achieved_rpm REAL NOT NULL, -- completed requests/min over the window
55
+ peak_bytes INTEGER, -- from /stats if exposed, else null
56
+ notes TEXT,
57
+ machine_state TEXT
58
+ );
59
+ `;
60
+
61
+ export interface EvalRun {
62
+ modelPath: string;
63
+ commitSha?: string | null;
64
+ promptTokens: number;
65
+ cachedTokens?: number;
66
+ generatedTokens: number;
67
+ prefillTps: number;
68
+ decodeTps: number;
69
+ peakBytes: number;
70
+ predictedPeakBytes?: number | null;
71
+ predictedDecodeTps?: number | null;
72
+ notes?: string | null;
73
+ /** Which engine produced the number: mlx-bun (default) | mlx-lm | optiq. */
74
+ stack?: string;
75
+ /** JSON snapshot of preflight machine state (swap, free %, thermal). */
76
+ machineState?: string | null;
77
+ }
78
+
79
+ /** One configuration point of a parallel-load sweep (Phase 18 P1). */
80
+ export interface LoadRun {
81
+ modelId: string;
82
+ baseUrl: string;
83
+ stack?: string;
84
+ commitSha?: string | null;
85
+ /** 'closed-loop' (fixed in-flight) or 'open-loop' (target arrival rate). */
86
+ mode: "closed-loop" | "open-loop";
87
+ /** In-flight cap (closed-loop) or requester count (open-loop). */
88
+ concurrency: number;
89
+ /** Per-requester target rpm (open-loop only; null for closed-loop). */
90
+ targetRpm?: number | null;
91
+ maxTokens: number;
92
+ requestsOk: number;
93
+ requestsErr: number;
94
+ durationS: number;
95
+ ttftP50Ms: number;
96
+ ttftP95Ms: number;
97
+ e2eP50Ms: number;
98
+ e2eP95Ms: number;
99
+ e2eP99Ms: number;
100
+ /** Generated tokens/s summed across all streams in the window. */
101
+ aggTps: number;
102
+ /** Median per-stream decode tok/s. */
103
+ perReqTps: number;
104
+ /** Completed requests per minute over the measured window. */
105
+ achievedRpm: number;
106
+ /** Peak resident bytes sampled from /stats, if the server exposes it. */
107
+ peakBytes?: number | null;
108
+ notes?: string | null;
109
+ machineState?: string | null;
110
+ }
111
+
112
+ export class EvalDB {
113
+ readonly db: Database;
114
+
115
+ constructor(dbPath: string = DEFAULT_EVAL_DB) {
116
+ if (dbPath !== ":memory:") {
117
+ try { mkdirSync(dbPath.slice(0, dbPath.lastIndexOf("/")), { recursive: true }); } catch {}
118
+ }
119
+ this.db = new Database(dbPath);
120
+ this.db.exec(SCHEMA);
121
+ // Additive migrations — eval rows are real data, never dropped
122
+ // (unlike the registry, which is a rebuildable cache).
123
+ const cols = (this.db.query("PRAGMA table_info(runs)").all() as { name: string }[])
124
+ .map((c) => c.name);
125
+ if (!cols.includes("stack"))
126
+ this.db.exec("ALTER TABLE runs ADD COLUMN stack TEXT NOT NULL DEFAULT 'mlx-bun'");
127
+ if (!cols.includes("machine_state"))
128
+ this.db.exec("ALTER TABLE runs ADD COLUMN machine_state TEXT");
129
+ }
130
+
131
+ record(run: EvalRun): void {
132
+ this.db.prepare(`
133
+ INSERT INTO runs (ts, model_path, commit_sha, prompt_tokens, cached_tokens,
134
+ generated_tokens, prefill_tps, decode_tps, peak_bytes,
135
+ predicted_peak_bytes, predicted_decode_tps, notes, stack, machine_state)
136
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
137
+ `).run(
138
+ Date.now(), run.modelPath, run.commitSha ?? null,
139
+ run.promptTokens, run.cachedTokens ?? 0, run.generatedTokens,
140
+ run.prefillTps, run.decodeTps, run.peakBytes,
141
+ run.predictedPeakBytes ?? null, run.predictedDecodeTps ?? null,
142
+ run.notes ?? null, run.stack ?? "mlx-bun", run.machineState ?? null,
143
+ );
144
+ }
145
+
146
+ /** Record one parallel-load configuration point (Phase 18 P1). */
147
+ recordLoad(run: LoadRun): void {
148
+ this.db.prepare(`
149
+ INSERT INTO load_runs (ts, model_id, base_url, stack, commit_sha, mode,
150
+ concurrency, target_rpm, max_tokens, requests_ok, requests_err,
151
+ duration_s, ttft_p50_ms, ttft_p95_ms, e2e_p50_ms, e2e_p95_ms, e2e_p99_ms,
152
+ agg_tps, per_req_tps, achieved_rpm, peak_bytes, notes, machine_state)
153
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
154
+ `).run(
155
+ Date.now(), run.modelId, run.baseUrl, run.stack ?? "mlx-bun",
156
+ run.commitSha ?? null, run.mode, run.concurrency, run.targetRpm ?? null,
157
+ run.maxTokens, run.requestsOk, run.requestsErr, run.durationS,
158
+ run.ttftP50Ms, run.ttftP95Ms, run.e2eP50Ms, run.e2eP95Ms, run.e2eP99Ms,
159
+ run.aggTps, run.perReqTps, run.achievedRpm, run.peakBytes ?? null,
160
+ run.notes ?? null, run.machineState ?? null,
161
+ );
162
+ }
163
+
164
+ recentLoad(limit = 20): Record<string, unknown>[] {
165
+ return this.db
166
+ .query("SELECT * FROM load_runs ORDER BY ts DESC LIMIT ?")
167
+ .all(limit) as Record<string, unknown>[];
168
+ }
169
+
170
+ /** Latest mlx-bun measurement for a model snapshot (status page:
171
+ * measured numbers beat predictions when available). */
172
+ latestFor(modelPath: string): { decodeTps: number; ts: number; notes: string | null } | null {
173
+ const r = this.db
174
+ .query("SELECT decode_tps, ts, notes FROM runs WHERE model_path = ? AND stack = 'mlx-bun' ORDER BY ts DESC LIMIT 1")
175
+ .get(modelPath) as { decode_tps: number; ts: number; notes: string | null } | null;
176
+ return r ? { decodeTps: r.decode_tps, ts: r.ts, notes: r.notes } : null;
177
+ }
178
+
179
+ recent(limit = 20): Record<string, unknown>[] {
180
+ return this.db
181
+ .query("SELECT * FROM runs ORDER BY ts DESC LIMIT ?")
182
+ .all(limit) as Record<string, unknown>[];
183
+ }
184
+
185
+ close(): void {
186
+ this.db.close();
187
+ }
188
+ }
189
+
190
+ export function gitCommit(): string | null {
191
+ try {
192
+ const r = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
193
+ cwd: import.meta.dir,
194
+ });
195
+ return r.exitCode === 0 ? r.stdout.toString().trim() : null;
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
@@ -0,0 +1,77 @@
1
+ // E1 expert offload — BUILD (PLAN Phase 19).
2
+ //
3
+ // Produces the page-aligned offload file (experts.bin + manifest.json): each
4
+ // expert tensor's bytes are copied to a 16 KB-aligned offset so the GPU can
5
+ // gather it directly from a read-only file mmap (safetensors packs tensors at
6
+ // arbitrary offsets; the Phase-1 GPU-alignment rule needs page alignment).
7
+ // Kept separate from the runtime (src/expert-offload.ts) so the hot model
8
+ // path doesn't import the converter / safetensors-writer code.
9
+
10
+ import {
11
+ openSync, writeSync, closeSync, writeFileSync, mkdirSync, existsSync, statSync, readFileSync,
12
+ } from "node:fs";
13
+ import { ShardedSafetensors } from "./safetensors";
14
+ import type { OffloadEntry, OffloadManifest } from "./expert-offload";
15
+
16
+ const PAGE = 16384;
17
+ const alignUp = (n: number, a: number) => Math.ceil(n / a) * a;
18
+ const isExpertTensor = (n: string) => n.includes("switch_glu") || n.includes(".experts.");
19
+ const layerOf = (n: string) => Number(n.match(/layers\.(\d+)/)?.[1] ?? -1);
20
+
21
+ /** Reuse `<modelPath>/.mlx-bun-offload` if present and valid for this model,
22
+ * else build it. Returns the offload dir (pass to activateExpertOffload). */
23
+ export async function ensureOffloadFile(
24
+ modelPath: string,
25
+ onProgress?: (msg: string) => void,
26
+ outDir = `${modelPath}/.mlx-bun-offload`,
27
+ ): Promise<string> {
28
+ const bin = `${outDir}/experts.bin`;
29
+ const man = `${outDir}/manifest.json`;
30
+ if (existsSync(bin) && existsSync(man)) {
31
+ try {
32
+ const m = JSON.parse(readFileSync(man, "utf8")) as OffloadManifest;
33
+ if (m.model === modelPath && statSync(bin).size === m.totalBytes) {
34
+ onProgress?.(`reusing ${bin}`);
35
+ return outDir;
36
+ }
37
+ } catch { /* fall through to rebuild */ }
38
+ }
39
+ await buildOffloadFile(modelPath, outDir, onProgress);
40
+ return outDir;
41
+ }
42
+
43
+ export async function buildOffloadFile(
44
+ modelPath: string,
45
+ outDir: string,
46
+ onProgress?: (msg: string) => void,
47
+ layerCap = Infinity,
48
+ ): Promise<OffloadManifest> {
49
+ const shards = await ShardedSafetensors.open(modelPath);
50
+ const names = shards.tensorNames
51
+ .filter(isExpertTensor)
52
+ .filter((n) => layerOf(n) < layerCap)
53
+ .sort((a, b) => layerOf(a) - layerOf(b) || a.localeCompare(b));
54
+ if (!names.length) throw new Error(`${modelPath}: no expert tensors (not an MoE model?)`);
55
+
56
+ mkdirSync(outDir, { recursive: true });
57
+ const fd = openSync(`${outDir}/experts.bin`, "w");
58
+ const pad = Buffer.alloc(PAGE);
59
+ let offset = 0;
60
+ let i = 0;
61
+ const tensors: OffloadEntry[] = [];
62
+ for (const name of names) {
63
+ const aligned = alignUp(offset, PAGE);
64
+ if (aligned > offset) { writeSync(fd, pad, 0, aligned - offset); offset = aligned; }
65
+ const info = shards.info(name);
66
+ const bytes = shards.view(name); // zero-copy view into the source mmap
67
+ writeSync(fd, bytes);
68
+ tensors.push({ name, offset, length: bytes.length, dtype: info.dtype, shape: info.shape });
69
+ offset += bytes.length;
70
+ if (++i % 30 === 0) onProgress?.(`packing experts ${(offset / 1e9).toFixed(1)} GB (${i}/${names.length})`);
71
+ }
72
+ closeSync(fd);
73
+ const manifest: OffloadManifest = { page: PAGE, model: modelPath, totalBytes: offset, tensors };
74
+ writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest));
75
+ onProgress?.(`wrote ${(offset / 1e9).toFixed(2)} GB, ${tensors.length} tensors, ${new Set(names.map(layerOf)).size} layers`);
76
+ return manifest;
77
+ }
@@ -0,0 +1,61 @@
1
+ // E1 expert offload — RUNTIME (PLAN Phase 19).
2
+ //
3
+ // Routes expert WEIGHT tensors through a read-only file mmap (clean,
4
+ // page-aligned pages that cost ~0 phys_footprint and that the GPU gathers
5
+ // directly — probes confirmed bit-exact + non-wiring) instead of the
6
+ // anonymous mlx_load_safetensors copy that counts against memory pressure.
7
+ //
8
+ // Activated two ways, both before model construction:
9
+ // - env MLX_BUN_EXPERT_OFFLOAD=<dir> (scripts / direct runs)
10
+ // - activateExpertOffload(dir) (CLI `--expert-offload`)
11
+ // The file is produced by src/expert-offload-build.ts. Tensors not in the
12
+ // manifest (or when never activated) fall back to the resident path, so this
13
+ // is inert by default and safe on a partially-converted file.
14
+
15
+ import { readFileSync } from "node:fs";
16
+ import { MmapFile } from "./mmap";
17
+ import { MlxArray, SAFETENSORS_TO_MLX } from "./mlx/array";
18
+ import type { SafetensorsDtype } from "./safetensors";
19
+
20
+ export interface OffloadEntry {
21
+ name: string;
22
+ offset: number;
23
+ length: number;
24
+ dtype: SafetensorsDtype;
25
+ shape: number[];
26
+ }
27
+ export interface OffloadManifest {
28
+ page: number;
29
+ model: string;
30
+ totalBytes: number;
31
+ tensors: OffloadEntry[];
32
+ }
33
+
34
+ let mm: MmapFile | null = null;
35
+ let manifest: Map<string, OffloadEntry> | null = null;
36
+
37
+ /** Open the offload file at `dir` (experts.bin + manifest.json) and route
38
+ * expert weights through it. Call BEFORE the model is constructed. */
39
+ export function activateExpertOffload(dir: string): void {
40
+ const parsed = JSON.parse(readFileSync(`${dir}/manifest.json`, "utf8")) as OffloadManifest;
41
+ manifest = new Map(parsed.tensors.map((t) => [t.name, t]));
42
+ mm = MmapFile.open(`${dir}/experts.bin`, "ro");
43
+ process.stderr.write(`[expert-offload] mmap ${dir}/experts.bin (${parsed.tensors.length} tensors)\n`);
44
+ }
45
+
46
+ export function isExpertOffload(): boolean {
47
+ return mm !== null;
48
+ }
49
+
50
+ /** Zero-copy mmap-backed array for `name` if offload is active and the tensor
51
+ * is in the manifest; else null. The converter aligns every tensor to 16 KB
52
+ * and the mmap base is page-aligned, so the pointer is page-aligned —
53
+ * required for the GPU to gather from it. */
54
+ export function expertOffloadArray(name: string): MlxArray | null {
55
+ if (!mm || !manifest) return null;
56
+ const e = manifest.get(name);
57
+ if (!e) return null;
58
+ return MlxArray.fromView(mm.view(e.offset, e.length), e.shape, SAFETENSORS_TO_MLX[e.dtype]);
59
+ }
60
+
61
+ if (process.env.MLX_BUN_EXPERT_OFFLOAD) activateExpertOffload(process.env.MLX_BUN_EXPERT_OFFLOAD);
@@ -0,0 +1,67 @@
1
+ // E0 expert-routing tracer (PLAN Phase 19 — expert offload).
2
+ //
3
+ // Records every MoE router decision as JSONL — one record per router call:
4
+ // { c: callSeq, l: layerIdx, s: [B, L, k], i: [flattened uint32 expert ids] }
5
+ // Analyse with scripts/analyze-expert-trace.ts (coverage curve, working-set
6
+ // size, within-task stability, cross-task specialisation).
7
+ //
8
+ // Two ways to drive it:
9
+ // - env: `MLX_BUN_EXPERT_TRACE=<path> mlx-bun serve ...` (auto-starts here)
10
+ // - programmatic: beginExpertTrace(path) / endExpertTrace() per domain
11
+ // (scripts/run-expert-trace.ts uses this to trace several domains from
12
+ // one model load).
13
+ //
14
+ // Recording adds a per-call GPU->host sync (it reads the routed indices back),
15
+ // so a traced run decodes slower — it's a MEASUREMENT tool, not a serving
16
+ // path. When no trace is active, isExpertTracing() is false and the hook in
17
+ // Router.forward is a single branch (zero overhead) — inert by default, same
18
+ // discipline as the slots flag.
19
+
20
+ import { Dtype } from "./mlx/ffi";
21
+ import type { MlxArray } from "./mlx/array";
22
+
23
+ let sink: Bun.FileSink | null = null;
24
+ let seq = 0;
25
+ let written = 0;
26
+
27
+ export function beginExpertTrace(path: string): void {
28
+ if (sink) endExpertTrace();
29
+ sink = Bun.file(path).writer();
30
+ seq = 0;
31
+ written = 0;
32
+ sink.write(JSON.stringify({ meta: "mlx-bun-expert-trace-v1", note: "one record per router call" }) + "\n");
33
+ process.stderr.write(`[expert-trace] recording -> ${path}\n`);
34
+ }
35
+
36
+ export function endExpertTrace(): void {
37
+ if (!sink) return;
38
+ sink.flush();
39
+ sink.end();
40
+ process.stderr.write(`[expert-trace] wrote ${written} records (${seq} calls)\n`);
41
+ sink = null;
42
+ }
43
+
44
+ /** Alias kept for the process-exit handler / env callers. */
45
+ export const finishExpertTrace = endExpertTrace;
46
+
47
+ export function isExpertTracing(): boolean {
48
+ return sink !== null;
49
+ }
50
+
51
+ /** Record one router decision. `indices` is [B, L, k] uint32 (expert ids). */
52
+ export function recordRouting(layer: number, indices: MlxArray): void {
53
+ if (!sink) return;
54
+ const shape = indices.shape; // [B, L, k]
55
+ const f = indices.astype(Dtype.float32); // ids <=127 are exact in f32
56
+ const data = f.toFloat32();
57
+ f.dispose();
58
+ const ids = new Array<number>(data.length);
59
+ for (let i = 0; i < data.length; i++) ids[i] = Math.round(data[i]!);
60
+ sink.write(JSON.stringify({ c: seq++, l: layer, s: shape, i: ids }) + "\n");
61
+ if (++written % 2000 === 0) sink.flush();
62
+ }
63
+
64
+ if (process.env.MLX_BUN_EXPERT_TRACE) beginExpertTrace(process.env.MLX_BUN_EXPERT_TRACE);
65
+
66
+ process.on("exit", () => { try { endExpertTrace(); } catch { /* best-effort */ } });
67
+ process.on("SIGINT", () => { endExpertTrace(); process.exit(0); });