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
package/src/eval/kl.ts ADDED
@@ -0,0 +1,294 @@
1
+ // KL-divergence drift eval — port of optiq/eval/kl.py, adapted for the
2
+ // fused-kernel head-to-head.
3
+ //
4
+ // KL(ref ‖ cand) per token = Σ_v softmax(ref)·(logsoftmax(ref) − logsoftmax(cand)),
5
+ // reported as mean (primary), median, and p95 over N prompts × seq tokens.
6
+ // Computation is TEACHER-FORCED: the same fixed prompt is fed to both
7
+ // arms and the per-position distributions are compared — the repo's rule
8
+ // (free-running greedy "measures chaos"; see teacher-forced-gating).
9
+ //
10
+ // Two reference modes:
11
+ // * self-flag (DEFAULT, the kernel-drift gate): one model, one weight
12
+ // load; forward each prompt twice with an env lever set to its
13
+ // reference value vs its candidate value. Directly measures the drift
14
+ // a perf lever introduces. Fits in RAM trivially (no second model).
15
+ // * two-model (optiq-style absolute quality): a separate reference
16
+ // model (bf16 if it fits, else uniform-4bit). Both resident.
17
+ //
18
+ // NOTE (M0 scope): this uses the compat forward() path (plain caches), so
19
+ // it gates the shared-ops fusions (gelu / norm+add) and the tiled-vs-
20
+ // unfused SDPA lever. A serving-path variant (quantized KV + the generated
21
+ // class, teacher-forced prefill) is the next step — see eval README.
22
+
23
+ import { existsSync } from "node:fs";
24
+ import { MlxArray } from "../mlx/array";
25
+ import { Dtype } from "../mlx/ffi";
26
+ import * as ops from "../mlx/ops";
27
+ import { Registry } from "../registry";
28
+ import { Weights } from "../weights";
29
+ import { loadModelConfig, type ModelConfig } from "../config";
30
+ import { createModel, type RuntimeModel } from "../model/factory";
31
+ import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
32
+ import { maybeQuantizeKv } from "../generate";
33
+
34
+ export interface KLResult {
35
+ nPrompts: number;
36
+ seqLen: number;
37
+ meanKl: number; // mean of per-prompt mean KL — the primary scalar
38
+ medianKl: number; // median over all pooled per-token KLs
39
+ p95Kl: number; // tail (long-distribution drift)
40
+ elapsedSec: number;
41
+ refLabel: string;
42
+ }
43
+
44
+ /** KL(p ‖ q) per token for logits [1, T, V]; returns a length-T array. */
45
+ export function klPerToken(pLogits: MlxArray, qLogits: MlxArray): Float32Array {
46
+ const f32 = (a: MlxArray): { arr: MlxArray; owned: boolean } =>
47
+ a.dtype === Dtype.float32 ? { arr: a, owned: false } : { arr: a.astype(Dtype.float32), owned: true };
48
+ const pp = f32(pLogits);
49
+ const qq = f32(qLogits);
50
+ const p = pp.arr;
51
+ const q = qq.arr;
52
+
53
+ const lseP = ops.logsumexpAxis(p, -1, true); // [1,T,1]
54
+ const lseQ = ops.logsumexpAxis(q, -1, true);
55
+ const logP = ops.sub(p, lseP); // [1,T,V]
56
+ const logQ = ops.sub(q, lseQ);
57
+ const pProbs = ops.softmaxAxis(p, -1, true); // precise softmax
58
+ const diff = ops.sub(logP, logQ);
59
+ const prod = ops.mul(pProbs, diff);
60
+ const kl = ops.sumAxis(prod, -1, false); // [1,T]
61
+ const out = kl.toFloat32();
62
+
63
+ for (const a of [lseP, lseQ, logP, logQ, pProbs, diff, prod, kl]) a.dispose();
64
+ if (pp.owned) p.dispose();
65
+ if (qq.owned) q.dispose();
66
+ return out;
67
+ }
68
+
69
+ const mean = (xs: number[]): number => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0);
70
+ function percentile(xs: number[], pct: number): number {
71
+ if (!xs.length) return 0;
72
+ const s = [...xs].sort((a, b) => a - b);
73
+ const i = Math.min(s.length - 1, Math.max(0, Math.floor((pct / 100) * s.length)));
74
+ return s[i]!;
75
+ }
76
+
77
+ function aggregate(perPrompt: Float32Array[]): { mean: number; median: number; p95: number } {
78
+ const promptMeans = perPrompt.map((a) => mean(Array.from(a)));
79
+ const pooled: number[] = [];
80
+ for (const a of perPrompt) for (const v of a) pooled.push(v);
81
+ return { mean: mean(promptMeans), median: percentile(pooled, 50), p95: percentile(pooled, 95) };
82
+ }
83
+
84
+ /** A local model dir if `query` is a path, else resolved via the registry. */
85
+ export function resolveModelDir(query: string): string {
86
+ if (existsSync(query) && existsSync(`${query}/config.json`)) return query;
87
+ return new Registry().resolve(query).path;
88
+ }
89
+
90
+ export interface Runnable {
91
+ model: RuntimeModel;
92
+ tokenizer: LoadedTokenizer;
93
+ config: ModelConfig;
94
+ dir: string;
95
+ }
96
+
97
+ export async function loadRunnable(query: string): Promise<Runnable> {
98
+ const dir = resolveModelDir(query);
99
+ const config = await loadModelConfig(dir);
100
+ const weights = await Weights.open(dir);
101
+ const model = createModel(weights, config);
102
+ const tokenizer = await loadTokenizer(dir);
103
+ return { model, tokenizer, config, dir };
104
+ }
105
+
106
+ /** Forward a fixed token sequence, return logits [1, L, V] (caller disposes). */
107
+ function forwardLogits(model: RuntimeModel, tokens: number[]): MlxArray {
108
+ const cache = model.makeCache();
109
+ try {
110
+ return model.forward(tokens, cache);
111
+ } finally {
112
+ for (const c of cache) c.dispose();
113
+ }
114
+ }
115
+
116
+ function prepPrompts(prompts: string[], tok: LoadedTokenizer, seqLen: number, n: number): number[][] {
117
+ const tiled: string[] = [];
118
+ while (tiled.length < n) tiled.push(...prompts);
119
+ return tiled.slice(0, n).map((p) => tok.encode(p).slice(0, seqLen));
120
+ }
121
+
122
+ /** self-flag drift gate: ref = env[flag]=refValue, cand = env[flag]=candValue. */
123
+ export async function evaluateKlSelfFlag(opts: {
124
+ candidate: string;
125
+ flag: string;
126
+ refValue: string;
127
+ candValue: string;
128
+ prompts: string[];
129
+ nPrompts?: number;
130
+ seqLen?: number;
131
+ }): Promise<KLResult> {
132
+ const t0 = Date.now();
133
+ const n = opts.nPrompts ?? 64;
134
+ const seqLen = opts.seqLen ?? 256;
135
+ const { model, tokenizer } = await loadRunnable(opts.candidate);
136
+ const tokenized = prepPrompts(opts.prompts, tokenizer, seqLen, n);
137
+
138
+ const prev = process.env[opts.flag];
139
+ const perPrompt: Float32Array[] = [];
140
+ try {
141
+ for (const ids of tokenized) {
142
+ process.env[opts.flag] = opts.refValue;
143
+ const p = forwardLogits(model, ids);
144
+ process.env[opts.flag] = opts.candValue;
145
+ const q = forwardLogits(model, ids);
146
+ const T = Math.min(p.shape[1]!, q.shape[1]!);
147
+ const pT = p.slice([0, 0, 0], [1, T, p.shape[2]!]);
148
+ const qT = q.slice([0, 0, 0], [1, T, q.shape[2]!]);
149
+ perPrompt.push(klPerToken(pT, qT));
150
+ for (const a of [p, q, pT, qT]) a.dispose();
151
+ }
152
+ } finally {
153
+ if (prev === undefined) delete process.env[opts.flag];
154
+ else process.env[opts.flag] = prev;
155
+ }
156
+
157
+ const agg = aggregate(perPrompt);
158
+ return {
159
+ nPrompts: n, seqLen, meanKl: agg.mean, medianKl: agg.median, p95Kl: agg.p95,
160
+ elapsedSec: (Date.now() - t0) / 1000,
161
+ refLabel: `self:${opts.flag}=${opts.refValue}→${opts.candValue}`,
162
+ };
163
+ }
164
+
165
+ /** two-model absolute drift: KL(reference ‖ candidate) on shared prompts. */
166
+ export async function evaluateKlTwoModel(opts: {
167
+ candidate: string;
168
+ reference: string;
169
+ prompts: string[];
170
+ nPrompts?: number;
171
+ seqLen?: number;
172
+ }): Promise<KLResult> {
173
+ const t0 = Date.now();
174
+ const n = opts.nPrompts ?? 64;
175
+ const seqLen = opts.seqLen ?? 256;
176
+ const ref = await loadRunnable(opts.reference);
177
+ const cand = await loadRunnable(opts.candidate);
178
+ const tokenized = prepPrompts(opts.prompts, ref.tokenizer, seqLen, n);
179
+
180
+ const perPrompt: Float32Array[] = [];
181
+ for (const ids of tokenized) {
182
+ const p = forwardLogits(ref.model, ids);
183
+ const q = forwardLogits(cand.model, ids);
184
+ const T = Math.min(p.shape[1]!, q.shape[1]!);
185
+ const pT = p.slice([0, 0, 0], [1, T, p.shape[2]!]);
186
+ const qT = q.slice([0, 0, 0], [1, T, q.shape[2]!]);
187
+ perPrompt.push(klPerToken(pT, qT));
188
+ for (const a of [p, q, pT, qT]) a.dispose();
189
+ }
190
+
191
+ const agg = aggregate(perPrompt);
192
+ return {
193
+ nPrompts: n, seqLen, meanKl: agg.mean, medianKl: agg.median, p95Kl: agg.p95,
194
+ elapsedSec: (Date.now() - t0) / 1000,
195
+ refLabel: opts.reference,
196
+ };
197
+ }
198
+
199
+ // --- serving-path (decode) KL — the M0b gate -----------------------------
200
+ // The compat forward() KL above runs plain bf16 caches → it can't see the
201
+ // quantized-KV serving path. This variant reproduces serving exactly: bf16
202
+ // prefill, then maybeQuantizeKv (the generate.ts hook) converts the populated
203
+ // mixed-4/8-bit caches, then a TEACHER-FORCED decode over the prompt tail —
204
+ // where the generated quantized SDPA (and the fused kernels) actually run.
205
+
206
+ /** CPU KL(p ‖ q) for two logit vectors (same math as klPerToken, per step). */
207
+ function klScalar(p: Float32Array, q: Float32Array): number {
208
+ const V = p.length;
209
+ let mp = -Infinity;
210
+ let mq = -Infinity;
211
+ for (let i = 0; i < V; i++) { if (p[i]! > mp) mp = p[i]!; if (q[i]! > mq) mq = q[i]!; }
212
+ let sp = 0;
213
+ let sq = 0;
214
+ for (let i = 0; i < V; i++) { sp += Math.exp(p[i]! - mp); sq += Math.exp(q[i]! - mq); }
215
+ const lsep = mp + Math.log(sp);
216
+ const lseq = mq + Math.log(sq);
217
+ let kl = 0;
218
+ for (let i = 0; i < V; i++) {
219
+ const pp = Math.exp(p[i]! - lsep);
220
+ if (pp > 0) kl += pp * ((p[i]! - lsep) - (q[i]! - lseq));
221
+ }
222
+ return kl;
223
+ }
224
+
225
+ /** Prefill (bf16) → quantize → teacher-forced decode; return per-step logit
226
+ * distributions for the last `tokens.length - prefillLen` positions. */
227
+ function decodeArm(
228
+ model: RuntimeModel, config: ModelConfig, tokens: number[], prefillLen: number,
229
+ flag: string, value: string,
230
+ ): Float32Array[] {
231
+ const prev = process.env[flag];
232
+ process.env[flag] = value;
233
+ try {
234
+ const cache = model.makeCache();
235
+ try {
236
+ const lp = model.forward(tokens.slice(0, prefillLen), cache); // bf16 prefill
237
+ lp.dispose();
238
+ // quantize the populated caches (mixed 4/8 per kv_config) — exactly
239
+ // what generate() does between prefill and decode.
240
+ maybeQuantizeKv(cache, { kvConfig: config.kvQuant ?? undefined, quantizedKvStart: 0 });
241
+ const out: Float32Array[] = [];
242
+ for (let i = prefillLen; i < tokens.length; i++) {
243
+ const logits = model.forward([tokens[i]!], cache); // [1,1,V] quantized decode
244
+ out.push(logits.toFloat32());
245
+ logits.dispose();
246
+ }
247
+ return out;
248
+ } finally {
249
+ for (const c of cache) c.dispose();
250
+ }
251
+ } finally {
252
+ if (prev === undefined) delete process.env[flag];
253
+ else process.env[flag] = prev;
254
+ }
255
+ }
256
+
257
+ /** Serving-path drift gate: KL between two arms of a perf lever, measured on
258
+ * the teacher-forced quantized-decode path (the real e4b serving path). */
259
+ export async function evaluateKlServingDecode(opts: {
260
+ candidate: string;
261
+ flag: string;
262
+ refValue: string;
263
+ candValue: string;
264
+ prompts: string[];
265
+ nPrompts?: number;
266
+ seqLen?: number;
267
+ decodeSteps?: number;
268
+ }): Promise<KLResult> {
269
+ const t0 = Date.now();
270
+ const n = opts.nPrompts ?? 64;
271
+ const seqLen = opts.seqLen ?? 256;
272
+ const decodeSteps = opts.decodeSteps ?? 32;
273
+ const { model, tokenizer, config } = await loadRunnable(opts.candidate);
274
+ const tokenized = prepPrompts(opts.prompts, tokenizer, seqLen, n);
275
+
276
+ const perPrompt: Float32Array[] = [];
277
+ for (const tokens of tokenized) {
278
+ if (tokens.length < 2) continue;
279
+ const prefillLen = Math.max(1, tokens.length - decodeSteps);
280
+ const refSteps = decodeArm(model, config, tokens, prefillLen, opts.flag, opts.refValue);
281
+ const candSteps = decodeArm(model, config, tokens, prefillLen, opts.flag, opts.candValue);
282
+ const m = Math.min(refSteps.length, candSteps.length);
283
+ const kls = new Float32Array(m);
284
+ for (let s = 0; s < m; s++) kls[s] = klScalar(refSteps[s]!, candSteps[s]!);
285
+ perPrompt.push(kls);
286
+ }
287
+
288
+ const agg = aggregate(perPrompt);
289
+ return {
290
+ nPrompts: perPrompt.length, seqLen, meanKl: agg.mean, medianKl: agg.median, p95Kl: agg.p95,
291
+ elapsedSec: (Date.now() - t0) / 1000,
292
+ refLabel: `serving-decode self:${opts.flag}=${opts.refValue}→${opts.candValue} (${decodeSteps} steps)`,
293
+ };
294
+ }
@@ -0,0 +1,92 @@
1
+ // Quality-eval DB — the capability/KL counterpart to evaldb.ts (which is
2
+ // throughput-only: prefill_tps/decode_tps/peak_bytes). Lives in the SAME
3
+ // sqlite file (~/.cache/mlx-bun/evals.sqlite) as a separate `quality_runs`
4
+ // table so the throughput head-to-head and the quality gate sit together.
5
+ //
6
+ // One row = one (model, config) measured on one task (or the aggregate
7
+ // "capability" / "kl" rows). `config_json` records the perf levers that
8
+ // were active (compile / fused-decode / perf-kernel) so a row is
9
+ // self-describing: which arm of the head-to-head produced this number.
10
+
11
+ import { Database } from "bun:sqlite";
12
+ import { mkdirSync } from "node:fs";
13
+ import { DEFAULT_EVAL_DB } from "../evaldb";
14
+
15
+ const SCHEMA = `
16
+ CREATE TABLE IF NOT EXISTS quality_runs (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ ts INTEGER NOT NULL,
19
+ model_path TEXT NOT NULL,
20
+ commit_sha TEXT,
21
+ task TEXT NOT NULL, -- 'kl' | 'gsm8k' | 'mmlu' | ... | 'capability'
22
+ config_json TEXT NOT NULL, -- {compile,fusedDecode,perfKernel,...} active levers
23
+ n_samples INTEGER NOT NULL,
24
+ -- task accuracy (percent 0-100); null for kl-only rows
25
+ pct REAL,
26
+ -- KL drift block (null for task rows)
27
+ kl_mean REAL,
28
+ kl_median REAL,
29
+ kl_p95 REAL,
30
+ kl_ref TEXT, -- 'self:<FLAG>' | model path/id of the reference
31
+ -- aggregate capability score (null except on 'capability' rows)
32
+ capability_score REAL,
33
+ disk_gb REAL,
34
+ notes TEXT,
35
+ machine_state TEXT
36
+ );
37
+ `;
38
+
39
+ export interface QualityRun {
40
+ modelPath: string;
41
+ commitSha?: string | null;
42
+ task: string;
43
+ /** Active perf levers when this number was produced. */
44
+ config: Record<string, string | boolean | number | null>;
45
+ nSamples: number;
46
+ pct?: number | null;
47
+ klMean?: number | null;
48
+ klMedian?: number | null;
49
+ klP95?: number | null;
50
+ klRef?: string | null;
51
+ capabilityScore?: number | null;
52
+ diskGb?: number | null;
53
+ notes?: string | null;
54
+ machineState?: string | null;
55
+ }
56
+
57
+ export class QualityDB {
58
+ readonly db: Database;
59
+
60
+ constructor(dbPath: string = DEFAULT_EVAL_DB) {
61
+ if (dbPath !== ":memory:") {
62
+ try { mkdirSync(dbPath.slice(0, dbPath.lastIndexOf("/")), { recursive: true }); } catch {}
63
+ }
64
+ this.db = new Database(dbPath);
65
+ this.db.exec(SCHEMA);
66
+ }
67
+
68
+ record(run: QualityRun): void {
69
+ this.db.prepare(`
70
+ INSERT INTO quality_runs (ts, model_path, commit_sha, task, config_json,
71
+ n_samples, pct, kl_mean, kl_median, kl_p95, kl_ref,
72
+ capability_score, disk_gb, notes, machine_state)
73
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
74
+ `).run(
75
+ Date.now(), run.modelPath, run.commitSha ?? null, run.task,
76
+ JSON.stringify(run.config), run.nSamples,
77
+ run.pct ?? null, run.klMean ?? null, run.klMedian ?? null, run.klP95 ?? null,
78
+ run.klRef ?? null, run.capabilityScore ?? null, run.diskGb ?? null,
79
+ run.notes ?? null, run.machineState ?? null,
80
+ );
81
+ }
82
+
83
+ recent(limit = 20): Record<string, unknown>[] {
84
+ return this.db
85
+ .query("SELECT * FROM quality_runs ORDER BY ts DESC LIMIT ?")
86
+ .all(limit) as Record<string, unknown>[];
87
+ }
88
+
89
+ close(): void {
90
+ this.db.close();
91
+ }
92
+ }
@@ -0,0 +1,102 @@
1
+ // Task-eval runner: load a model and greedily complete prompts through the
2
+ // REAL serving path. Unlike the M0 KL (compat forward() + plain caches),
3
+ // this drives generate() with the model's kv_config, so generation runs the
4
+ // mixed-4/8-bit quantized-KV path the generated class actually serves.
5
+
6
+ import { homedir } from "node:os";
7
+ import { readFileSync } from "node:fs";
8
+ import { generate, type GenerateOptions } from "../generate";
9
+ import { ChatTemplate } from "../chat-template";
10
+ import { loadModelConfig, type ModelConfig } from "../config";
11
+ import { Weights } from "../weights";
12
+ import { createModel, type RuntimeModel } from "../model/factory";
13
+ import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
14
+ import { resolveModelDir } from "./kl";
15
+
16
+ export const EVAL_DATA_DIR = process.env.MLX_BUN_EVAL_DATA ?? `${homedir()}/.cache/mlx-bun/eval-data`;
17
+
18
+ /** Read a jsonl dataset exported by scripts/eval/export-datasets.py. */
19
+ export function loadJsonl<T = Record<string, unknown>>(name: string): T[] {
20
+ const path = `${EVAL_DATA_DIR}/${name}.jsonl`;
21
+ const out: T[] = [];
22
+ for (const line of readFileSync(path, "utf8").split("\n")) {
23
+ const t = line.trim();
24
+ if (t) out.push(JSON.parse(t) as T);
25
+ }
26
+ return out;
27
+ }
28
+
29
+ /** Deterministic seeded subsample of [0, total): mulberry32 shuffle, sorted. */
30
+ export function sampleIndices(total: number, n: number, seed = 42): number[] {
31
+ let s = seed >>> 0;
32
+ const rand = (): number => {
33
+ s = (s + 0x6d2b79f5) | 0;
34
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
35
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
36
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
37
+ };
38
+ const idx = Array.from({ length: total }, (_, i) => i);
39
+ for (let i = total - 1; i > 0; i--) {
40
+ const j = Math.floor(rand() * (i + 1));
41
+ [idx[i], idx[j]] = [idx[j]!, idx[i]!];
42
+ }
43
+ return idx.slice(0, Math.min(n, total)).sort((a, b) => a - b);
44
+ }
45
+
46
+ /** Sampler keys an eval arm may force across every task (greedy/HLG/curve). */
47
+ type SamplerArm = Partial<Pick<GenerateOptions, "temperature" | "topP" | "topK" | "seed" | "hlg" | "curve">>;
48
+
49
+ export interface TaskModel {
50
+ model: RuntimeModel;
51
+ tokenizer: LoadedTokenizer;
52
+ template: ChatTemplate | null;
53
+ config: ModelConfig;
54
+ dir: string;
55
+ /** When set, FORCES this sampler on every generateText call, overriding the
56
+ * task's own (greedy) default — used to run the whole suite under one sampler
57
+ * arm (e.g. the v2 curve vs the default chat recipe) for the degradation gate. */
58
+ samplerOverride?: SamplerArm;
59
+ }
60
+
61
+ export async function loadTaskModel(query: string): Promise<TaskModel> {
62
+ const dir = resolveModelDir(query);
63
+ const config = await loadModelConfig(dir);
64
+ const weights = await Weights.open(dir);
65
+ const model = createModel(weights, config);
66
+ const tokenizer = await loadTokenizer(dir);
67
+ let template: ChatTemplate | null = null;
68
+ try { template = await ChatTemplate.load(dir); } catch { template = null; }
69
+ return { model, tokenizer, template, config, dir };
70
+ }
71
+
72
+ export interface GenOpts {
73
+ maxTokens?: number;
74
+ /** Wrap `body` as a single user turn in the chat template (default true). */
75
+ useChat?: boolean;
76
+ /** Sampling overrides (default greedy). Lets the HLG/diversity evals vary the
77
+ * sampler — temperature, top-p/k, seed, hlg — while sharing this prompt path. */
78
+ sampler?: Partial<Pick<GenerateOptions, "temperature" | "topP" | "topK" | "seed" | "hlg" | "curve">>;
79
+ }
80
+
81
+ /** Complete `body` via the model's real quantized-KV path. Greedy by default;
82
+ * pass `opts.sampler` to drive temperature/HLG/etc. */
83
+ export async function generateText(tm: TaskModel, body: string, opts: GenOpts = {}): Promise<string> {
84
+ const maxTokens = opts.maxTokens ?? 256;
85
+ const text = opts.useChat !== false && tm.template
86
+ ? tm.template.render([{ role: "user", content: body }], { addGenerationPrompt: true })
87
+ : body;
88
+ const ids = tm.tokenizer.encode(text);
89
+
90
+ const kv = tm.config.kvQuant?.length ? tm.config.kvQuant : undefined;
91
+ const gen = generate(tm.model, ids, {
92
+ maxTokens,
93
+ temperature: 0, // greedy default — deterministic head-to-head arms
94
+ ...(kv ? { kvConfig: kv, quantizedKvStart: 0 } : {}),
95
+ ...(opts.sampler ?? {}), // overrides temperature when provided
96
+ ...(tm.samplerOverride ?? {}), // arm override wins over the task's own sampler
97
+ });
98
+
99
+ const out: number[] = [];
100
+ for await (const { token } of gen) out.push(token);
101
+ return tm.tokenizer.decode(out, true);
102
+ }
@@ -0,0 +1,49 @@
1
+ // Capability score — port of optiq/eval/score.py.
2
+ //
3
+ // A single scalar: the simple UNWEIGHTED mean of whichever of the six
4
+ // benchmarks (MMLU, GSM8K, IFEval, BFCL, HumanEval, HashHop) were run.
5
+ // Disk size is reported alongside but NEVER enters the score — no hidden
6
+ // quality/disk tradeoff baked into the number (optiq's explicit design
7
+ // choice). Missing components are skipped, not zero-filled.
8
+
9
+ export interface CapabilityScore {
10
+ /** 0–100, simple mean of the provided benchmark percents. */
11
+ score: number;
12
+ /** Per-benchmark contribution to the mean (only the ones that ran). */
13
+ components: Record<string, number>;
14
+ /** Reported separately; does NOT affect `score`. */
15
+ diskGb: number;
16
+ }
17
+
18
+ export type TaskName = "MMLU" | "GSM8K" | "IFEval" | "BFCL" | "HumanEval" | "HashHop";
19
+
20
+ /** Unweighted mean of the provided task percents (each 0–100). */
21
+ export function computeCapabilityScore(
22
+ percents: Partial<Record<TaskName, number>>,
23
+ diskGb: number,
24
+ ): CapabilityScore {
25
+ const components: Record<string, number> = {};
26
+ for (const [name, val] of Object.entries(percents))
27
+ if (val !== undefined && val !== null && Number.isFinite(val))
28
+ components[name] = val;
29
+
30
+ const keys = Object.keys(components);
31
+ const score = keys.length === 0
32
+ ? 0
33
+ : keys.reduce((a, k) => a + components[k]!, 0) / keys.length;
34
+
35
+ return { score, components, diskGb };
36
+ }
37
+
38
+ /** Sum of `*.safetensors` shard sizes for a local model dir, in GB. */
39
+ export function diskGbForDir(modelDir: string): number {
40
+ try {
41
+ const { readdirSync, statSync } = require("node:fs") as typeof import("node:fs");
42
+ let total = 0;
43
+ for (const f of readdirSync(modelDir))
44
+ if (f.endsWith(".safetensors")) total += statSync(`${modelDir}/${f}`).size;
45
+ return total / 1024 ** 3;
46
+ } catch {
47
+ return 0;
48
+ }
49
+ }