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.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- 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
|
+
}
|