mlx-bun 0.0.10 → 0.0.11
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/README.md +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +250 -121
- package/docs/reference/training.md +14 -15
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +356 -105
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -24
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +442 -88
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +106 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +363 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/serve/batch-scheduler.ts +965 -72
- package/src/serve/generation-gateway.ts +214 -11
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1380 -135
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +107 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -2
- package/src/train/trainer.ts +1 -4
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
package/src/config.ts
CHANGED
|
@@ -38,6 +38,13 @@ export interface TextConfig {
|
|
|
38
38
|
numExperts: number;
|
|
39
39
|
topKExperts: number;
|
|
40
40
|
moeIntermediateSize: number;
|
|
41
|
+
/** Qwen3-MoE sparse-block gating (qwen3_moe): a layer is MoE unless it is
|
|
42
|
+
* in `mlpOnlyLayers` and `(idx+1) % decoderSparseStep == 0` selects it.
|
|
43
|
+
* `normTopkProb` renormalizes the top-k gate weights. Defaults are inert
|
|
44
|
+
* for non-qwen3_moe families. */
|
|
45
|
+
decoderSparseStep: number;
|
|
46
|
+
mlpOnlyLayers: number[];
|
|
47
|
+
normTopkProb: boolean;
|
|
41
48
|
/** Qwen3.5 hybrid (gated-DeltaNet) geometry; 0/false for other families. */
|
|
42
49
|
linearNumValueHeads: number;
|
|
43
50
|
linearNumKeyHeads: number;
|
|
@@ -77,6 +84,37 @@ export interface KvQuantSpec {
|
|
|
77
84
|
groupSize: number;
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
/** TurboQuant scheme (docs/design/turboquant-kv.md): rotation-based KV
|
|
88
|
+
* quantization, a distinct axis from the uniform/per-layer affine kvQuant
|
|
89
|
+
* above (composes with it in principle; v1 ships as a standalone CLI-only
|
|
90
|
+
* runtime lever, same class as uniform kvBits — see
|
|
91
|
+
* src/generate.ts maybeQuantizeKv). Valid kBits: {2,4,5,8}; vBits:
|
|
92
|
+
* {2,3,4,5,8} (docs/design/turboquant-kv.md's supported Lloyd-Max tables). */
|
|
93
|
+
export interface TurboQuantScheme {
|
|
94
|
+
kBits: number;
|
|
95
|
+
vBits: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const TURBOQUANT_VALID_KBITS = [2, 4, 5, 8] as const;
|
|
99
|
+
export const TURBOQUANT_VALID_VBITS = [2, 3, 4, 5, 8] as const;
|
|
100
|
+
|
|
101
|
+
/** Parse `turbo` / `turbo:k<bits>v<bits>` (default k8v3). Returns null for
|
|
102
|
+
* non-turbo strings (caller falls through to the uniform/config parser);
|
|
103
|
+
* throws Error with a user-facing message for a malformed turbo spec. */
|
|
104
|
+
export function parseTurboQuantScheme(raw: string): TurboQuantScheme | null {
|
|
105
|
+
if (raw !== "turbo" && !raw.startsWith("turbo:")) return null;
|
|
106
|
+
if (raw === "turbo") return { kBits: 8, vBits: 3 };
|
|
107
|
+
const m = /^turbo:k(\d+)v(\d+)$/.exec(raw);
|
|
108
|
+
if (!m) throw new Error(`--kv-quant turbo spec must look like "turbo:k<bits>v<bits>" (got "${raw}")`);
|
|
109
|
+
const kBits = Number(m[1]);
|
|
110
|
+
const vBits = Number(m[2]);
|
|
111
|
+
if (!(TURBOQUANT_VALID_KBITS as readonly number[]).includes(kBits))
|
|
112
|
+
throw new Error(`--kv-quant turbo: kBits must be one of ${TURBOQUANT_VALID_KBITS.join(",")} (got ${kBits})`);
|
|
113
|
+
if (!(TURBOQUANT_VALID_VBITS as readonly number[]).includes(vBits))
|
|
114
|
+
throw new Error(`--kv-quant turbo: vBits must be one of ${TURBOQUANT_VALID_VBITS.join(",")} (got ${vBits})`);
|
|
115
|
+
return { kBits, vBits };
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
export interface ModelConfig {
|
|
81
119
|
modelDir: string;
|
|
82
120
|
modelType: string;
|
|
@@ -93,6 +131,11 @@ export interface ModelConfig {
|
|
|
93
131
|
function parseRope(raw: Record<string, any>): Record<string, RopeParams> {
|
|
94
132
|
const out: Record<string, RopeParams> = {};
|
|
95
133
|
for (const [kind, p] of Object.entries(raw ?? {})) {
|
|
134
|
+
// transformers ≥5.10 configs mix flat scalars into the per-attention-type
|
|
135
|
+
// map (`rope_parameters: { full_attention: {...}, rope_theta: null,
|
|
136
|
+
// rope_type: "default", ... }` — the DeepSpec drafter ships this shape);
|
|
137
|
+
// only object entries are attention-type params.
|
|
138
|
+
if (!p || typeof p !== "object") continue;
|
|
96
139
|
out[kind] = {
|
|
97
140
|
ropeTheta: p.rope_theta,
|
|
98
141
|
ropeType: p.rope_type ?? "default",
|
|
@@ -103,7 +146,7 @@ function parseRope(raw: Record<string, any>): Record<string, RopeParams> {
|
|
|
103
146
|
return out;
|
|
104
147
|
}
|
|
105
148
|
|
|
106
|
-
function parseQuantization(raw: Record<string, any> | undefined): QuantizationConfig | null {
|
|
149
|
+
export function parseQuantization(raw: Record<string, any> | undefined): QuantizationConfig | null {
|
|
107
150
|
if (!raw) return null;
|
|
108
151
|
const def: QuantSpec = {
|
|
109
152
|
bits: raw.bits,
|
|
@@ -215,8 +258,12 @@ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
|
|
|
215
258
|
numKvSharedLayers: t.num_kv_shared_layers ?? 0,
|
|
216
259
|
enableMoeBlock: t.enable_moe_block ?? false,
|
|
217
260
|
numExperts: t.num_experts ?? 0,
|
|
218
|
-
|
|
261
|
+
// gemma4 uses `top_k_experts`; qwen3_moe uses `num_experts_per_tok`.
|
|
262
|
+
topKExperts: t.top_k_experts ?? t.num_experts_per_tok ?? 0,
|
|
219
263
|
moeIntermediateSize: t.moe_intermediate_size ?? 0,
|
|
264
|
+
decoderSparseStep: t.decoder_sparse_step ?? 1,
|
|
265
|
+
mlpOnlyLayers: t.mlp_only_layers ?? [],
|
|
266
|
+
normTopkProb: t.norm_topk_prob ?? false,
|
|
220
267
|
linearNumValueHeads: t.linear_num_value_heads ?? 0,
|
|
221
268
|
linearNumKeyHeads: t.linear_num_key_heads ?? 0,
|
|
222
269
|
linearKeyHeadDim: t.linear_key_head_dim ?? 0,
|
package/src/download.ts
CHANGED
|
@@ -114,6 +114,28 @@ function acquireLock(blobPath: string, name: string): () => void {
|
|
|
114
114
|
throw new Error(`could not acquire ${lockPath}`);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** Reject a repo file path that could escape the snapshot directory once
|
|
118
|
+
* joined onto it. `rfilename` comes verbatim from the remote HF API's
|
|
119
|
+
* `siblings[].rfilename` (listRepoFiles) — for a repo the caller doesn't
|
|
120
|
+
* control (attacker-owned/attacker-referenced), that string is untrusted
|
|
121
|
+
* input reaching a path-join + symlink write (downloadModel's
|
|
122
|
+
* `join(snapDir, f.rfilename)` + `symlinkSync`). node:path's `join()`
|
|
123
|
+
* normalizes `..` segments, so an rfilename like
|
|
124
|
+
* `../../../../home/x/.ssh/authorized_keys` would otherwise resolve
|
|
125
|
+
* OUTSIDE the intended snapshots dir and write a symlink at an arbitrary
|
|
126
|
+
* filesystem path. Reachable over HTTP via POST /api/hub/download
|
|
127
|
+
* (src/hub-rest.ts), which passes any caller-supplied `repo` straight
|
|
128
|
+
* through with no allowlist — this check is the actual gate, not that
|
|
129
|
+
* route. A leading `/` (absolute path) is equally rejected: join() would
|
|
130
|
+
* otherwise treat it as relative to snapDir and still normalize safely,
|
|
131
|
+
* but there's no legitimate reason a repo file entry needs one, and
|
|
132
|
+
* rejecting it removes any doubt. */
|
|
133
|
+
export function isSafeRepoFilename(rfilename: string): boolean {
|
|
134
|
+
if (!rfilename || rfilename.startsWith("/") || rfilename.includes("\0")) return false;
|
|
135
|
+
const parts = rfilename.split("/");
|
|
136
|
+
return parts.every((p) => p !== "" && p !== "." && p !== "..");
|
|
137
|
+
}
|
|
138
|
+
|
|
117
139
|
/** git blob identity: sha1("blob <size>\0" + content) — what blobId is. */
|
|
118
140
|
export function gitBlobSha1(bytes: Uint8Array): string {
|
|
119
141
|
const hasher = new Bun.CryptoHasher("sha1");
|
|
@@ -294,6 +316,14 @@ export function downloadsSnapshot(): DownloadStatus[] {
|
|
|
294
316
|
return downloadLog.slice(-5);
|
|
295
317
|
}
|
|
296
318
|
|
|
319
|
+
/** True when a download for this exact repoId is currently "active" in this
|
|
320
|
+
* process's tracker (used by the Hub's POST /api/hub/download to refuse a
|
|
321
|
+
* duplicate kick-off — same repoId, still running — rather than starting a
|
|
322
|
+
* second writer that would race the first on the same blob lockfile). */
|
|
323
|
+
export function isDownloadActive(repoId: string): boolean {
|
|
324
|
+
return downloadLog.some((d) => d.repoId === repoId && d.state === "active");
|
|
325
|
+
}
|
|
326
|
+
|
|
297
327
|
export async function downloadModel(
|
|
298
328
|
repoId: string, opts: DownloadOptions = {},
|
|
299
329
|
): Promise<string> {
|
|
@@ -303,6 +333,16 @@ export async function downloadModel(
|
|
|
303
333
|
const hub = opts.cacheDir ?? DEFAULT_HUB;
|
|
304
334
|
|
|
305
335
|
const listing = await listRepoFiles(repoId, { ...opts, token });
|
|
336
|
+
// Fail the whole download closed (not skip-and-continue) if the repo's
|
|
337
|
+
// file listing contains a path-traversal rfilename — see
|
|
338
|
+
// isSafeRepoFilename's doc comment. This gates BOTH the CLI and the web
|
|
339
|
+
// Hub's POST /api/hub/download (src/hub-rest.ts), which accepts any
|
|
340
|
+
// caller-supplied repo string with no allowlist of its own.
|
|
341
|
+
for (const f of listing.files) {
|
|
342
|
+
if (!isSafeRepoFilename(f.rfilename)) {
|
|
343
|
+
throw new Error(`${repoId}: refusing unsafe file path in repo listing: ${JSON.stringify(f.rfilename)}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
306
346
|
const repoDir = join(hub, `models--${repoId.replaceAll("/", "--")}`);
|
|
307
347
|
const blobsDir = join(repoDir, "blobs");
|
|
308
348
|
const snapDir = join(repoDir, "snapshots", listing.sha);
|
package/src/eval/README.md
CHANGED
|
@@ -36,8 +36,8 @@ repo rule: free-running greedy "measures chaos").
|
|
|
36
36
|
# drift gate on an existing non-bit-exact lever (validates the harness today):
|
|
37
37
|
bun scripts/eval.ts kl --candidate e4b
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
bun scripts/eval.ts kl --candidate e4b --self
|
|
39
|
+
# self-arm A/B on an output-affecting lever (fused-sdpa on vs off):
|
|
40
|
+
bun scripts/eval.ts kl --candidate e4b --self MLX_BUN_NO_FUSED_SDPA --ref-value 1 --cand-value 0
|
|
41
41
|
|
|
42
42
|
# optiq-style absolute drift vs a reference model:
|
|
43
43
|
bun scripts/eval.ts kl --candidate e4b --reference <bf16-or-4bit-id>
|
|
@@ -55,9 +55,10 @@ the throughput `runs` table). Each row records the active perf levers
|
|
|
55
55
|
|
|
56
56
|
- **M0 (done):** KL drift gate + capability score + `quality_runs` DB + CLI.
|
|
57
57
|
- **M0b (done):** *serving-path* KL (`evaluateKlServingDecode` in `kl.ts`,
|
|
58
|
-
`bun scripts/eval.ts kl --decode`). Validated: identical arms →
|
|
59
|
-
`MLX_BUN_FUSED_DECODE` tiled-decode → KL 0.27 (real
|
|
60
|
-
|
|
58
|
+
`bun scripts/eval.ts kl --decode`). Validated at the time: identical arms →
|
|
59
|
+
KL 0; the since-deleted `MLX_BUN_FUSED_DECODE` tiled-decode → KL 0.27 (real
|
|
60
|
+
drift on the quantized path — the lever was removed 2026-07-05, but the
|
|
61
|
+
validation stands). Motivation below — the compat KL couldn't see it:
|
|
61
62
|
The compat `forward()` path with `makeCache()` uses **plain bf16 caches**. That is wrong
|
|
62
63
|
twice over: (1) e4b's KV is **per-layer mixed 4-bit/8-bit** (group 64, from
|
|
63
64
|
`kv_config.json` → `config.kvQuant`; 8-bit layers 3,4,9,10,11,14, 4-bit
|
|
@@ -67,7 +68,7 @@ the throughput `runs` table). Each row records the active perf levers
|
|
|
67
68
|
caches, then `cache[i] = c.toQuantized(e.groupSize, e.bits)` per
|
|
68
69
|
`config.kvQuant` (the logic already at `generate.ts:68-86`), so the
|
|
69
70
|
teacher-forced prefill runs the real mixed-4/8-bit quantized path and the
|
|
70
|
-
perf levers (NO_FUSED_SDPA
|
|
71
|
+
perf levers (NO_FUSED_SDPA) actually bite.
|
|
71
72
|
- **M1 (done):** all six capability tasks ported + runtime-validated, plus
|
|
72
73
|
`capability` aggregation and the `smoketest` (KL + GSM8K-50) gate. Datasets
|
|
73
74
|
exported by `scripts/eval/export-datasets.py` → `~/.cache/mlx-bun/eval-data/`.
|
package/src/eval/kl.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { Dtype } from "../mlx/ffi";
|
|
|
26
26
|
import * as ops from "../mlx/ops";
|
|
27
27
|
import { Registry } from "../registry";
|
|
28
28
|
import { Weights } from "../weights";
|
|
29
|
-
import { loadModelConfig, type ModelConfig } from "../config";
|
|
29
|
+
import { loadModelConfig, type ModelConfig, type TurboQuantScheme } from "../config";
|
|
30
30
|
import { createModel, type RuntimeModel } from "../model/factory";
|
|
31
31
|
import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
|
|
32
32
|
import { maybeQuantizeKv } from "../generate";
|
|
@@ -223,10 +223,14 @@ function klScalar(p: Float32Array, q: Float32Array): number {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/** Prefill (bf16) → quantize → teacher-forced decode; return per-step logit
|
|
226
|
-
* distributions for the last `tokens.length - prefillLen` positions.
|
|
226
|
+
* distributions for the last `tokens.length - prefillLen` positions.
|
|
227
|
+
* `kvOverride` replaces the default kvConfig-from-model-config scheme when
|
|
228
|
+
* given — the seam evaluateKlKvArm uses to run a TurboQuant (or bf16-null)
|
|
229
|
+
* arm instead of the env-flag self-flip. */
|
|
227
230
|
function decodeArm(
|
|
228
231
|
model: RuntimeModel, config: ModelConfig, tokens: number[], prefillLen: number,
|
|
229
232
|
flag: string, value: string,
|
|
233
|
+
kvOverride?: Parameters<typeof maybeQuantizeKv>[1],
|
|
230
234
|
): Float32Array[] {
|
|
231
235
|
const prev = process.env[flag];
|
|
232
236
|
process.env[flag] = value;
|
|
@@ -237,7 +241,10 @@ function decodeArm(
|
|
|
237
241
|
lp.dispose();
|
|
238
242
|
// quantize the populated caches (mixed 4/8 per kv_config) — exactly
|
|
239
243
|
// what generate() does between prefill and decode.
|
|
240
|
-
maybeQuantizeKv(
|
|
244
|
+
maybeQuantizeKv(
|
|
245
|
+
cache,
|
|
246
|
+
kvOverride ?? { kvConfig: config.kvQuant ?? undefined, quantizedKvStart: 0 },
|
|
247
|
+
);
|
|
241
248
|
const out: Float32Array[] = [];
|
|
242
249
|
for (let i = prefillLen; i < tokens.length; i++) {
|
|
243
250
|
const logits = model.forward([tokens[i]!], cache); // [1,1,V] quantized decode
|
|
@@ -292,3 +299,52 @@ export async function evaluateKlServingDecode(opts: {
|
|
|
292
299
|
refLabel: `serving-decode self:${opts.flag}=${opts.refValue}→${opts.candValue} (${decodeSteps} steps)`,
|
|
293
300
|
};
|
|
294
301
|
}
|
|
302
|
+
|
|
303
|
+
/** KV-scheme A/B on the teacher-forced serving-decode path: bf16 baseline
|
|
304
|
+
* (no kvOverride) vs an explicit KV scheme (e.g. TurboQuant), same model /
|
|
305
|
+
* same prompts / same weight load — the quality-vs-bpw curve gate
|
|
306
|
+
* (scripts/eval-turboquant-curve.ts). Unlike evaluateKlServingDecode (env-flag
|
|
307
|
+
* self-flip), the two arms differ by an actual GenerateOptions kv scheme
|
|
308
|
+
* object, so this is the seam for schemes that aren't env-lever-shaped. */
|
|
309
|
+
export async function evaluateKlKvArm(opts: {
|
|
310
|
+
candidate: string;
|
|
311
|
+
/** Undefined ⇒ bf16 (no quantization). */
|
|
312
|
+
candidateScheme?: { turboQuant?: TurboQuantScheme; kvBits?: number; kvConfig?: ModelConfig["kvQuant"] };
|
|
313
|
+
prompts: string[];
|
|
314
|
+
nPrompts?: number;
|
|
315
|
+
seqLen?: number;
|
|
316
|
+
decodeSteps?: number;
|
|
317
|
+
}): Promise<KLResult> {
|
|
318
|
+
const t0 = Date.now();
|
|
319
|
+
const n = opts.nPrompts ?? 64;
|
|
320
|
+
const seqLen = opts.seqLen ?? 256;
|
|
321
|
+
const decodeSteps = opts.decodeSteps ?? 32;
|
|
322
|
+
const { model, tokenizer, config } = await loadRunnable(opts.candidate);
|
|
323
|
+
const tokenized = prepPrompts(opts.prompts, tokenizer, seqLen, n);
|
|
324
|
+
const noopFlag = "MLX_BUN_EVAL_KV_ARM_NOOP"; // decodeArm always flips a flag; this one is inert
|
|
325
|
+
const refKv = {}; // bf16: maybeQuantizeKv no-ops with no kvBits/kvConfig/turboQuant
|
|
326
|
+
const candKv = opts.candidateScheme
|
|
327
|
+
? { ...opts.candidateScheme, quantizedKvStart: 0, kvConfig: opts.candidateScheme.kvConfig ?? undefined }
|
|
328
|
+
: {};
|
|
329
|
+
|
|
330
|
+
const perPrompt: Float32Array[] = [];
|
|
331
|
+
for (const tokens of tokenized) {
|
|
332
|
+
if (tokens.length < 2) continue;
|
|
333
|
+
const prefillLen = Math.max(1, tokens.length - decodeSteps);
|
|
334
|
+
const refSteps = decodeArm(model, config, tokens, prefillLen, noopFlag, "0", refKv);
|
|
335
|
+
const candSteps = decodeArm(model, config, tokens, prefillLen, noopFlag, "0", candKv);
|
|
336
|
+
const m = Math.min(refSteps.length, candSteps.length);
|
|
337
|
+
const kls = new Float32Array(m);
|
|
338
|
+
for (let s = 0; s < m; s++) kls[s] = klScalar(refSteps[s]!, candSteps[s]!);
|
|
339
|
+
perPrompt.push(kls);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const agg = aggregate(perPrompt);
|
|
343
|
+
return {
|
|
344
|
+
nPrompts: perPrompt.length, seqLen, meanKl: agg.mean, medianKl: agg.median, p95Kl: agg.p95,
|
|
345
|
+
elapsedSec: (Date.now() - t0) / 1000,
|
|
346
|
+
refLabel: opts.candidateScheme?.turboQuant
|
|
347
|
+
? `bf16 vs turbo k${opts.candidateScheme.turboQuant.kBits}v${opts.candidateScheme.turboQuant.vBits} (${decodeSteps} steps)`
|
|
348
|
+
: `bf16 vs kv-scheme (${decodeSteps} steps)`,
|
|
349
|
+
};
|
|
350
|
+
}
|
package/src/eval/quality-db.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// One row = one (model, config) measured on one task (or the aggregate
|
|
7
7
|
// "capability" / "kl" rows). `config_json` records the perf levers that
|
|
8
|
-
// were active (compile / fused-
|
|
8
|
+
// were active (compile / fused-sdpa / kv-quant) so a row is
|
|
9
9
|
// self-describing: which arm of the head-to-head produced this number.
|
|
10
10
|
|
|
11
11
|
import { Database } from "bun:sqlite";
|
|
@@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS quality_runs (
|
|
|
19
19
|
model_path TEXT NOT NULL,
|
|
20
20
|
commit_sha TEXT,
|
|
21
21
|
task TEXT NOT NULL, -- 'kl' | 'gsm8k' | 'mmlu' | ... | 'capability'
|
|
22
|
-
config_json TEXT NOT NULL, -- {compile,
|
|
22
|
+
config_json TEXT NOT NULL, -- {compile,fusedSdpa,kvQuant,...} active levers
|
|
23
23
|
n_samples INTEGER NOT NULL,
|
|
24
24
|
-- task accuracy (percent 0-100); null for kl-only rows
|
|
25
25
|
pct REAL,
|
package/src/eval/runner.ts
CHANGED
|
@@ -99,11 +99,11 @@ export interface GenOpts {
|
|
|
99
99
|
* (its capability numbers are non-thinking; e.g. MiniCPM5 IFEval 64.7 is
|
|
100
100
|
* non-thinking). Set MLX_BUN_EVAL_THINK=1 or pass true to evaluate thinking mode. */
|
|
101
101
|
enableThinking?: boolean;
|
|
102
|
-
/** Explicit KV-quant scheme (cli `generate --l2/--
|
|
102
|
+
/** Explicit KV-quant scheme (cli `generate --l2/--kv-quant`, resolved with
|
|
103
103
|
* serve's semantics). Overrides the MLX_BUN_EVAL_KV_QUANT env default; an empty
|
|
104
104
|
* object forces bf16. When it names quantized KV, generation runs the product
|
|
105
105
|
* generate() path (same tokens as `serve` with the same scheme). */
|
|
106
|
-
kvScheme?: Pick<GenerateOptions, "kvBits" | "kvConfig" | "quantizedKvStart">;
|
|
106
|
+
kvScheme?: Pick<GenerateOptions, "kvBits" | "kvConfig" | "quantizedKvStart" | "turboQuant">;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/** Bit-exact greedy decode: raw `model.forward` + argmax loop, which matches mlx-lm
|
|
@@ -159,7 +159,7 @@ export async function generateText(tm: TaskModel, body: string, opts: GenOpts =
|
|
|
159
159
|
(process.env.MLX_BUN_EVAL_KV_QUANT === "1" && tm.config.kvQuant?.length)
|
|
160
160
|
? { kvConfig: tm.config.kvQuant, quantizedKvStart: 0 } : undefined;
|
|
161
161
|
const kvScheme = opts.kvScheme ?? envKv;
|
|
162
|
-
const kvActive = !!(kvScheme && (kvScheme.kvBits || kvScheme.kvConfig?.length));
|
|
162
|
+
const kvActive = !!(kvScheme && (kvScheme.kvBits || kvScheme.kvConfig?.length || kvScheme.turboQuant));
|
|
163
163
|
|
|
164
164
|
// Parity default — greedy + full-precision KV + no sampler arm: decode bit-exactly
|
|
165
165
|
// via the raw forward (matches mlx-lm token-for-token). Sampler arms / kv-quant fall
|
package/src/expert-offload.ts
CHANGED
|
@@ -50,12 +50,25 @@ export function isExpertOffload(): boolean {
|
|
|
50
50
|
/** Zero-copy mmap-backed array for `name` if offload is active and the tensor
|
|
51
51
|
* is in the manifest; else null. The converter aligns every tensor to 16 KB
|
|
52
52
|
* and the mmap base is page-aligned, so the pointer is page-aligned —
|
|
53
|
-
* required for the GPU to gather from it.
|
|
53
|
+
* required for the GPU to gather from it.
|
|
54
|
+
*
|
|
55
|
+
* Lifetime: these arrays are created ONCE at model load
|
|
56
|
+
* (QuantizedSwitchLinear.load, gemma4-base.ts) and live as model weights;
|
|
57
|
+
* every decode's gather_qmm references them, so GPU command buffers retain
|
|
58
|
+
* the mlx buffer past any dispose and mlx may drop the LAST reference on
|
|
59
|
+
* the Metal completion thread. That's why this must be fromPointer (native
|
|
60
|
+
* free(NULL) dtor — safe from any thread), NOT the old fromView JSCallback
|
|
61
|
+
* pin (the 2026-07-06 deadlock class). The memory contract is trivially
|
|
62
|
+
* met: `mm` is module-level and never unmapped — the mapping owns the
|
|
63
|
+
* bytes for the process (clean file-backed pages, ~0 phys_footprint). */
|
|
54
64
|
export function expertOffloadArray(name: string): MlxArray | null {
|
|
55
65
|
if (!mm || !manifest) return null;
|
|
56
66
|
const e = manifest.get(name);
|
|
57
67
|
if (!e) return null;
|
|
58
|
-
|
|
68
|
+
// Bounds check the whole tensor (pointer() only checks the start).
|
|
69
|
+
if (e.offset + e.length > mm.size)
|
|
70
|
+
throw new Error(`expert-offload: ${name} [${e.offset}, +${e.length}) exceeds ${mm.path} (${mm.size} B)`);
|
|
71
|
+
return MlxArray.fromPointer(mm.pointer(e.offset), e.shape, SAFETENSORS_TO_MLX[e.dtype]);
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
if (process.env.MLX_BUN_EXPERT_OFFLOAD) activateExpertOffload(process.env.MLX_BUN_EXPERT_OFFLOAD);
|
package/src/flags.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Generic on/off resolver for the per-fork decode flags (--compiled-decode,
|
|
2
|
+
// --compiled-activations, --fused-sdpa, …). An explicit "1"/"0" in the env
|
|
3
|
+
// always wins; otherwise the caller's `defaultOn` applies. The tier aliases
|
|
4
|
+
// (--l1/--l2, applyDecodeRoute in cli.ts) set the env vars; model code
|
|
5
|
+
// reads them through this helper so a hand-set flag and a tier preset resolve
|
|
6
|
+
// identically.
|
|
7
|
+
|
|
8
|
+
/** Resolve an on/off env flag. An explicit "1"/"0" always wins; otherwise the
|
|
9
|
+
* flag takes `defaultOn`. */
|
|
10
|
+
export function flagOn(name: string, defaultOn: boolean): boolean {
|
|
11
|
+
const v = process.env[name];
|
|
12
|
+
return v === "1" ? true : v === "0" ? false : defaultOn;
|
|
13
|
+
}
|