mlx-bun 0.0.13 → 0.2.0
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 +31 -21
- package/bin/mlx-bun.mjs +1 -1
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- package/docs/design/colibri-glm52-port.md +1 -1
- package/docs/design/compat-cli-surface-design.md +1 -1
- package/docs/design/decode-speed-program.md +1 -1
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +1 -1
- package/docs/design/dspark-speculative-decoding.md +53 -3
- package/docs/design/grammar-spec-batching-integration.md +1 -1
- package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
- package/docs/design/orpo-dynamic-lambda.md +1 -1
- package/docs/design/orpo-future-enhancements.md +1 -1
- package/docs/design/orpo-training.md +2 -2
- package/docs/design/parity-tier-dag.md +1 -1
- package/docs/design/repo-cleanup-plan.md +46 -4
- package/docs/design/segmented-backward-training.md +2 -2
- package/docs/design/ssd-kv-cold-tier.md +127 -0
- package/docs/design/turboquant-weights.md +467 -0
- package/docs/design/w4a16-compute-precision-spike.md +1 -1
- package/docs/design/web-chat-redesign.md +5 -5
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/curve-sampler-research-plan.md +4 -4
- package/docs/planning/memory-docs-and-dag-plan.md +1 -1
- package/docs/planning/release-notes-v0.2.0.md +49 -0
- package/docs/planning/video-series.md +421 -0
- package/docs/reference/cli.md +15 -4
- package/docs/reference/distribution.md +1 -0
- package/docs/reference/features-matrix.md +3 -2
- package/docs/reference/library-api.md +41 -1
- package/docs/reference/models.md +33 -0
- package/docs/reference/server-api.md +69 -14
- package/docs/reference/server-config.md +57 -33
- package/package.json +2 -2
- package/src/anthropic.ts +92 -3
- package/src/cli.ts +131 -36
- package/src/eval/kl.ts +27 -20
- package/src/eval/runner.ts +23 -4
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- package/src/expert-trace.ts +3 -1
- package/src/fit.ts +24 -43
- package/src/flags.ts +7 -9
- package/src/generate.ts +107 -92
- package/src/grammar.ts +2 -1
- package/src/harness-pi.ts +1 -1
- package/src/index.ts +20 -0
- package/src/jobs/job-entry.ts +3 -2
- package/src/kv-scheme.ts +199 -0
- package/src/kv-store.ts +370 -205
- package/src/media-fetch.ts +19 -3
- package/src/memory/model.ts +2 -1
- package/src/memory/vault.ts +2 -1
- package/src/memory-plan.ts +18 -0
- package/src/mlx/ffi.ts +8 -11
- package/src/mlx/ops.ts +31 -6
- package/src/model/batched-mask.ts +1 -1
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +43 -97
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +56 -105
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +3 -2
- package/src/model/factory.ts +44 -49
- package/src/model/gemma4-base.ts +163 -10
- package/src/model/generated/gemma4-12b.ts +50 -146
- package/src/model/generated/gemma4-26b.ts +32 -92
- package/src/model/generated/gemma4-e4b.ts +26 -74
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/minicpm5.ts +2 -1
- package/src/model/profile.ts +433 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5.ts +217 -25
- package/src/native/frame_extract.swift +140 -0
- package/src/native-pack.ts +13 -6
- package/src/pi-web.ts +6 -5
- package/src/prompt-cache.ts +2 -2
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +6 -0
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +102 -2
- package/src/runtime-config.ts +67 -0
- package/src/sampler.ts +272 -0
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +200 -87
- package/src/serve/completion-executor.ts +346 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +205 -0
- package/src/serve/generation-gateway.ts +116 -109
- package/src/serve/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +338 -0
- package/src/serve/model-admin-routes.ts +153 -0
- package/src/serve/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +158 -0
- package/src/serve/static-routes.ts +101 -0
- package/src/server.ts +704 -1400
- package/src/spec/dflash-source.ts +2 -1
- package/src/spec/dspark/module-dflash.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +8 -0
- package/src/spec/qwen-mtp-source.ts +431 -0
- package/src/spec/serve-loop.ts +36 -68
- package/src/ssd-cache.ts +17 -14
- package/src/ssd-durability.ts +195 -0
- package/src/train/flash-cce.ts +12 -11
- package/src/train/forward.ts +32 -1
- package/src/train/loss.ts +2 -1
- package/src/train/segmented.ts +4 -3
- package/src/train/trainer.ts +3 -2
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +47 -1
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +513 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.js +10 -3
- package/src/web/src/chat.ts +12 -5
- package/src/weights.ts +81 -26
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
package/src/cli.ts
CHANGED
|
@@ -13,7 +13,8 @@ import { existsSync } from "node:fs";
|
|
|
13
13
|
import { basename, join, resolve } from "node:path";
|
|
14
14
|
import { Registry } from "./registry";
|
|
15
15
|
import { loadModelConfig, parseTurboQuantScheme, type TurboQuantScheme } from "./config";
|
|
16
|
-
import { fit, skuMatrix, thisMachine } from "./fit";
|
|
16
|
+
import { fit, skuMatrix, thisMachine, type FitKvScheme } from "./fit";
|
|
17
|
+
import { resolveKvScheme } from "./kv-scheme";
|
|
17
18
|
import { EvalDB } from "./evaldb";
|
|
18
19
|
import pkg from "../package.json" with { type: "json" };
|
|
19
20
|
import { renderHelp } from "./tui";
|
|
@@ -27,6 +28,11 @@ import {
|
|
|
27
28
|
planGlm52MemoryForArtifact,
|
|
28
29
|
} from "./model/glm52-memory";
|
|
29
30
|
import type { JobEvent } from "./jobs/types";
|
|
31
|
+
import {
|
|
32
|
+
configureRuntime,
|
|
33
|
+
runtimeValue,
|
|
34
|
+
type RuntimeKey,
|
|
35
|
+
} from "./runtime-config";
|
|
30
36
|
|
|
31
37
|
const argv = process.argv.slice(2);
|
|
32
38
|
// The appliance path: naked `mlx-bun` (or only options, e.g.
|
|
@@ -118,10 +124,10 @@ const SERVER_FLAGS = `Server options:
|
|
|
118
124
|
--unix <path> (internal) listen on a unix socket instead of a
|
|
119
125
|
TCP port — the engine half of --isolate.
|
|
120
126
|
--batch <n> Max concurrent requests decoded together
|
|
121
|
-
[default: 8]. A
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
[default: 8]. A lone scheduler request uses the
|
|
128
|
+
parity- and latency-gated B=1 fast path; concurrent
|
|
129
|
+
requests can form B=N (agent fan-out). 1 pins the
|
|
130
|
+
preserved strict serial executor.
|
|
125
131
|
--decode-concurrency is accepted as the
|
|
126
132
|
mlx_lm.server alias (semantics differ; see
|
|
127
133
|
docs/reference/server-config.md)
|
|
@@ -152,9 +158,13 @@ Model & quality:
|
|
|
152
158
|
Serial lane only — with --batch N a mounted draft
|
|
153
159
|
routes every request serial, like mlx_lm.server.
|
|
154
160
|
--draft-kind <kind> Override draft-artifact detection:
|
|
155
|
-
two-model | assistant | dspark | deepspec | ngram
|
|
161
|
+
two-model | assistant | dspark | deepspec | mtp | ngram
|
|
156
162
|
(deepspec = DeepSeek's released Gemma4DSparkModel
|
|
157
|
-
drafters;
|
|
163
|
+
drafters; mtp = a NATIVE multi-token-prediction
|
|
164
|
+
head split from the target's own release, e.g.
|
|
165
|
+
mlx-community/Qwen3.8-27B-MTP-bf16 — auto-detected
|
|
166
|
+
from model_type *_mtp;
|
|
167
|
+
ngram = MODEL-FREE prompt lookup — no
|
|
158
168
|
--draft-model, drafts copied from the request's
|
|
159
169
|
own context, lossless) [default: auto]
|
|
160
170
|
--num-draft-tokens <n> Drafts per verify round [default: 3; ngram: 10;
|
|
@@ -343,6 +353,8 @@ Usage: mlx-bun fit <query> [options]
|
|
|
343
353
|
|
|
344
354
|
Options:
|
|
345
355
|
--ctx <tokens> Context size to assess [default: 8192]
|
|
356
|
+
--kv-quant <mode> Bill the KV cache quantized: 4 | 8 | config | off
|
|
357
|
+
(mirrors serve --kv-quant; default bf16)
|
|
346
358
|
--skus Also print the Apple silicon SKU matrix
|
|
347
359
|
|
|
348
360
|
Solves weights + KV growth + prefill transient against wired memory and
|
|
@@ -576,6 +588,9 @@ knapsack per-layer bit allocation; implies quantization, no -q needed):
|
|
|
576
588
|
--candidate-bits <l> Comma list the knapsack may pick from [default: 4,8]
|
|
577
589
|
--calibration-mix <m> "optiq" or a JSONL path [default: optiq]
|
|
578
590
|
--n-calibration <n> Calibration samples [default: 2]
|
|
591
|
+
--rotate-weights Fold the model's offline TurboQuant rotation before
|
|
592
|
+
quantization (auto-detects Llama/Qwen3.5/Qwen MTP)
|
|
593
|
+
--rotation-seed <n> Deterministic rotation seed [default: 42]
|
|
579
594
|
|
|
580
595
|
Not supported (mlx_lm.convert flags we don't implement — the command
|
|
581
596
|
exits with an error rather than guessing): --dtype, -d/--dequantize,
|
|
@@ -805,11 +820,11 @@ function openChatUi(url: string, hostPort: string): void {
|
|
|
805
820
|
}
|
|
806
821
|
|
|
807
822
|
/** Server/runtime flags shared by every mode that loads a model
|
|
808
|
-
* (serve, pi).
|
|
823
|
+
* (serve, pi). Runtime overrides are installed here before the
|
|
809
824
|
* generate/compiled-decode modules read them. */
|
|
810
825
|
/** Resolve the decode ROUTE: a tier alias (--l1/--l2) sets the whole route,
|
|
811
826
|
* and an explicit per-fork flag (--kv-quant/--fused-sdpa/…) overrides the
|
|
812
|
-
* alias.
|
|
827
|
+
* alias. Installs decode runtime options and returns the kv-quant mode. See
|
|
813
828
|
* docs/design/parity-tier-dag.md + unified-engine-frontier-plan.md.
|
|
814
829
|
* Each tier is a GUARANTEE about which reference you reproduce bit-for-bit:
|
|
815
830
|
* --l1 bit-for-bit IDENTICAL to mlx-lm — drop-in replacement for mlx-lm
|
|
@@ -863,8 +878,9 @@ function applyDecodeRoute(): { kvQuant?: "off" | "config" | number; turboQuant?:
|
|
|
863
878
|
const pick = (name: string, base: boolean | undefined): boolean | null => {
|
|
864
879
|
const ex = onOff(name); return ex !== null ? ex : (base ?? null); // explicit flag wins, else tier preset
|
|
865
880
|
};
|
|
866
|
-
const set = (
|
|
867
|
-
if (val !== null)
|
|
881
|
+
const set = (name: RuntimeKey, val: boolean | null, invert = false) => {
|
|
882
|
+
if (val !== null)
|
|
883
|
+
configureRuntime({ [name]: (invert ? !val : val) ? "1" : "0" });
|
|
868
884
|
};
|
|
869
885
|
// Explicit --kv-quant picks the composition its ORACLE uses: config (the
|
|
870
886
|
// optiq mixed scheme) runs optiq's fused SDPA (the L2 golden composition);
|
|
@@ -874,7 +890,7 @@ function applyDecodeRoute(): { kvQuant?: "off" | "config" | number; turboQuant?:
|
|
|
874
890
|
set("MLX_BUN_COMPILED_DECODE", pick("compiled-decode", p.compiled));
|
|
875
891
|
set("MLX_BUN_NO_FUSED_SDPA", pick("fused-sdpa", fusedSdpaBase), true); // inverted env
|
|
876
892
|
// Compiled activations (the faithful geglu/swiglu — mlx-lm's @mx.compile kernel).
|
|
877
|
-
// One fork drives both
|
|
893
|
+
// One fork drives both runtime options; qwen3/qwen3.5/universal compile unconditionally,
|
|
878
894
|
// so this toggles gemma geglu + minicpm5 swiglu (the only unfused-capable sites).
|
|
879
895
|
set("MLX_BUN_COMPILED_GEGLU", pick("compiled-activations", p.compiledAct));
|
|
880
896
|
set("MLX_BUN_COMPILED_SWIGLU", pick("compiled-activations", p.compiledAct));
|
|
@@ -909,11 +925,12 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
909
925
|
process.exit(1);
|
|
910
926
|
};
|
|
911
927
|
const route = applyDecodeRoute(); // --l1/--l2 tier alias, with per-fork flags overriding
|
|
912
|
-
if (flag("force-wire"))
|
|
928
|
+
if (flag("force-wire")) configureRuntime({ MLX_BUN_FORCE_WIRE: "1" });
|
|
913
929
|
// Remote image_url/audio_url media fetches block private/loopback/link-
|
|
914
930
|
// local destinations by default (SSRF — src/media-fetch.ts); this is the
|
|
915
931
|
// LAN-hosts escape hatch.
|
|
916
|
-
if (flag("allow-private-media"))
|
|
932
|
+
if (flag("allow-private-media"))
|
|
933
|
+
configureRuntime({ MLX_BUN_ALLOW_PRIVATE_MEDIA: "1" });
|
|
917
934
|
|
|
918
935
|
const serverOptions: import("./server").ServerOptions = {};
|
|
919
936
|
const budgetGB = Number(opt("memory-budget", "0"));
|
|
@@ -945,7 +962,7 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
945
962
|
console.warn("--ssd-cache-verify has no effect without --ssd-cache — ignored");
|
|
946
963
|
}
|
|
947
964
|
// --batch N: max concurrent requests batched through the mlx-lm-parity
|
|
948
|
-
// engine (N=1
|
|
965
|
+
// engine (N=1 pins the strict serial executor). --decode-concurrency is accepted for
|
|
949
966
|
// drop-in compatibility with mlx_lm.server, but the semantics differ: there
|
|
950
967
|
// it caps per-BatchGenerator decode parallelism (default 32); in mlx-bun it
|
|
951
968
|
// caps continuous batching (default 8; `--batch 1` pins the serial path —
|
|
@@ -965,7 +982,7 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
965
982
|
// storage, default off (docs/design/paged-kv-cache.md). Serial-only in v1:
|
|
966
983
|
// with no explicit --batch, pin --batch 1 (the default is 8); an explicit
|
|
967
984
|
// --batch N>1 is refused loudly by createServer rather than downgraded.
|
|
968
|
-
if (flag("paged-kv") ||
|
|
985
|
+
if (flag("paged-kv") || runtimeValue("MLX_BUN_PAGED_KV") === "1") {
|
|
969
986
|
const bsRaw = opt("paged-kv-block-size");
|
|
970
987
|
serverOptions.pagedKv = bsRaw !== null ? { blockSize: Number(bsRaw) } : {};
|
|
971
988
|
if (batchRaw === null) {
|
|
@@ -1086,7 +1103,7 @@ async function mountStartupAdapter(
|
|
|
1086
1103
|
function runtimeSummary(o: import("./server").ServerOptions): string {
|
|
1087
1104
|
const kv = o.turboQuant ? `turbo-k${o.turboQuant.kBits}v${o.turboQuant.vBits}`
|
|
1088
1105
|
: o.kvQuant === "off" ? "off" : typeof o.kvQuant === "number" ? `kv${o.kvQuant}` : "config";
|
|
1089
|
-
const lever = (
|
|
1106
|
+
const lever = (name: RuntimeKey, dflt: string) => runtimeValue(name) ?? dflt;
|
|
1090
1107
|
return `kv-quant ${kv} · compiled-decode ${lever("MLX_BUN_COMPILED_DECODE", "1") === "1" ? "on" : "off"}` +
|
|
1091
1108
|
(lever("MLX_BUN_COMPILED_GEGLU", "1") === "0" ? " · compiled-activations off" : "") +
|
|
1092
1109
|
(o.batch && o.batch > 1 ? ` · batch ${o.batch}` : "") +
|
|
@@ -1376,7 +1393,7 @@ switch (cmd) {
|
|
|
1376
1393
|
|
|
1377
1394
|
case "fit": {
|
|
1378
1395
|
const query = positional(0);
|
|
1379
|
-
if (!query) { console.error("usage: mlx-bun fit <query> [--ctx N] [--skus]"); process.exit(1); }
|
|
1396
|
+
if (!query) { console.error("usage: mlx-bun fit <query> [--ctx N] [--kv-quant 4|8|config] [--skus]"); process.exit(1); }
|
|
1380
1397
|
const reg = new Registry();
|
|
1381
1398
|
if (reg.list().length === 0) await reg.scan();
|
|
1382
1399
|
const m = reg.resolve(query);
|
|
@@ -1443,9 +1460,32 @@ switch (cmd) {
|
|
|
1443
1460
|
console.log();
|
|
1444
1461
|
break;
|
|
1445
1462
|
}
|
|
1446
|
-
|
|
1463
|
+
// --kv-quant mirrors serve: bill the quantized cache's true bytes so
|
|
1464
|
+
// the reported window matches what serving with the same flag admits.
|
|
1465
|
+
const kvQuantOpt = opt("kv-quant");
|
|
1466
|
+
let fitKvScheme: FitKvScheme | undefined;
|
|
1467
|
+
if (kvQuantOpt === "4" || kvQuantOpt === "8") {
|
|
1468
|
+
fitKvScheme = resolveKvScheme({ override: Number(kvQuantOpt) }).fitOptions;
|
|
1469
|
+
} else if (kvQuantOpt === "config") {
|
|
1470
|
+
if (!config.kvQuant?.length) {
|
|
1471
|
+
console.error(`${m.repoId} ships no per-layer kv-quant config — use --kv-quant 4|8`);
|
|
1472
|
+
process.exit(1);
|
|
1473
|
+
}
|
|
1474
|
+
fitKvScheme = resolveKvScheme({
|
|
1475
|
+
override: "config",
|
|
1476
|
+
config: config.kvQuant,
|
|
1477
|
+
missingConfig: "error",
|
|
1478
|
+
}).fitOptions;
|
|
1479
|
+
} else if (kvQuantOpt != null && kvQuantOpt !== "off") {
|
|
1480
|
+
console.error(`--kv-quant ${kvQuantOpt}: expected 4, 8, config, or off`);
|
|
1481
|
+
process.exit(1);
|
|
1482
|
+
}
|
|
1483
|
+
const r = fit(config, m.sizeBytes, ctx, thisMachine(), undefined, m.expertsBytes, undefined, fitKvScheme);
|
|
1447
1484
|
h1("will it fit?");
|
|
1448
|
-
|
|
1485
|
+
const kvNote = fitKvScheme
|
|
1486
|
+
? style.dim(` · kv-quant ${fitKvScheme.kvBits ? `${fitKvScheme.kvBits}-bit` : "config"}`)
|
|
1487
|
+
: "";
|
|
1488
|
+
console.log(` ${style.bold(m.repoId)} ${style.dim(`@ ${ctx.toLocaleString()} context · this machine`)}${kvNote}`);
|
|
1449
1489
|
console.log();
|
|
1450
1490
|
const expertsNote = config.text.enableMoeBlock && m.expertsBytes > 0
|
|
1451
1491
|
? style.dim(` (experts ${gb(m.expertsBytes)}; top ${config.text.topKExperts}/${config.text.numExperts} read per token)`)
|
|
@@ -1470,7 +1510,7 @@ switch (cmd) {
|
|
|
1470
1510
|
{ header: "fits", paint: (c) => (c.includes("fits") ? style.green(c) : style.dim(c)) },
|
|
1471
1511
|
{ header: "max context", align: "right" }, { header: "decode", align: "right" },
|
|
1472
1512
|
],
|
|
1473
|
-
skuMatrix(config, m.sizeBytes, ctx, m.expertsBytes).map((row) => [
|
|
1513
|
+
skuMatrix(config, m.sizeBytes, ctx, m.expertsBytes, fitKvScheme).map((row) => [
|
|
1474
1514
|
row.sku, `${row.ramGB} GB`, row.fits ? "fits" : "—",
|
|
1475
1515
|
row.fits ? row.maxContext.toLocaleString() : "—",
|
|
1476
1516
|
row.fits ? `~${row.decodeTps.toFixed(0)} tok/s` : "—",
|
|
@@ -1573,10 +1613,29 @@ switch (cmd) {
|
|
|
1573
1613
|
const config = await loadModelConfig(m.path);
|
|
1574
1614
|
const report = fit(config, m.sizeBytes, 8192, undefined, undefined, m.expertsBytes);
|
|
1575
1615
|
sFit.done(`${style.bold(m.repoId)} ${style.dim(`· ${gb(m.sizeBytes)} · ~${report.predictedDecodeTps.toFixed(0)} tok/s predicted`)}${picked ? style.dim(" · auto-picked (override: mlx-bun serve <query>)") : ""}`);
|
|
1616
|
+
// Near-ceiling advisory (admission doctrine: advise, never refuse). A
|
|
1617
|
+
// model near the DEFAULT Metal wired ceiling (~75% of RAM) dies with an
|
|
1618
|
+
// UNCATCHABLE async-GPU-OOM on long prefills (M4 24 GB × 17 GB model,
|
|
1619
|
+
// 2026-08-20) — surface the standard remedy up front instead.
|
|
1620
|
+
{
|
|
1621
|
+
const { maxRecommendedWorkingSetSize } = await import("./mlx/ffi");
|
|
1622
|
+
const ceiling = maxRecommendedWorkingSetSize();
|
|
1623
|
+
const sysctlRaw = Bun.spawnSync(["sysctl", "-n", "iogpu.wired_limit_mb"]).stdout.toString().trim();
|
|
1624
|
+
const limitIsDefault = sysctlRaw === "0" || sysctlRaw === "";
|
|
1625
|
+
if (limitIsDefault && ceiling > 0 && m.sizeBytes > 0.8 * ceiling) {
|
|
1626
|
+
const ramGb = Number(Bun.spawnSync(["sysctl", "-n", "hw.memsize"]).stdout.toString().trim()) / 2 ** 30;
|
|
1627
|
+
const suggestMb = Math.floor((ramGb - 2.5) * 1024);
|
|
1628
|
+
console.error(style.dim(
|
|
1629
|
+
` ! ${gb(m.sizeBytes)} model vs ~${gb(ceiling)} default GPU ceiling — long prompts can\n` +
|
|
1630
|
+
` hit an uncatchable Metal OOM. If that happens, raise the limit first:\n` +
|
|
1631
|
+
` sudo sysctl iogpu.wired_limit_mb=${suggestMb} (resets on reboot)`,
|
|
1632
|
+
));
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1576
1635
|
const sNative = step("native runtime");
|
|
1577
1636
|
await ensureNative(sNative);
|
|
1578
1637
|
sNative.done("native runtime ready");
|
|
1579
|
-
const { createServer, loadContext } = await import("./server");
|
|
1638
|
+
const { createServer, loadContext, shutdownServer } = await import("./server");
|
|
1580
1639
|
if (flag("expert-offload")) {
|
|
1581
1640
|
if (m.expertsBytes === 0) {
|
|
1582
1641
|
console.error(style.dim("--expert-offload ignored: this model has no experts (dense)"));
|
|
@@ -1609,9 +1668,9 @@ switch (cmd) {
|
|
|
1609
1668
|
}
|
|
1610
1669
|
const draftKindRaw = opt("draft-kind");
|
|
1611
1670
|
const draftKind = (draftKindRaw ?? undefined) as
|
|
1612
|
-
| "dspark" | "deepspec" | "assistant" | "two-model" | "ngram" | undefined;
|
|
1613
|
-
if (draftKind !== undefined && !["dspark", "deepspec", "assistant", "two-model", "ngram"].includes(draftKind)) {
|
|
1614
|
-
console.error(`--draft-kind expects two-model|assistant|dspark|deepspec|ngram (got "${draftKindRaw}")`);
|
|
1671
|
+
| "dspark" | "deepspec" | "assistant" | "two-model" | "ngram" | "mtp" | undefined;
|
|
1672
|
+
if (draftKind !== undefined && !["dspark", "deepspec", "assistant", "two-model", "ngram", "mtp"].includes(draftKind)) {
|
|
1673
|
+
console.error(`--draft-kind expects two-model|assistant|dspark|deepspec|mtp|ngram (got "${draftKindRaw}")`);
|
|
1615
1674
|
process.exit(1);
|
|
1616
1675
|
}
|
|
1617
1676
|
// Prompt-lookup window bounds (ngram kind only; validated here so a typo
|
|
@@ -1639,7 +1698,10 @@ switch (cmd) {
|
|
|
1639
1698
|
const ctx = await loadContext(m.path, m.repoId, {
|
|
1640
1699
|
memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes,
|
|
1641
1700
|
glm: glm52RuntimeFlags(rt.serverOptions),
|
|
1642
|
-
|
|
1701
|
+
// ngram is model-free; mtp resolves the bundled <model>/mtp/ companion
|
|
1702
|
+
// in loadContext when no --draft-model is given (cli.md contract —
|
|
1703
|
+
// this gate silently dropped that case until 2026-08-20).
|
|
1704
|
+
...(draftModelDir || draftKind === "ngram" || draftKind === "mtp"
|
|
1643
1705
|
? {
|
|
1644
1706
|
...(draftModelDir ? { draftModelDir } : {}),
|
|
1645
1707
|
numDraftTokens,
|
|
@@ -1652,6 +1714,30 @@ switch (cmd) {
|
|
|
1652
1714
|
sLoad.done(`weights loaded ${style.dim(`in ${(performance.now() - t0).toFixed(0)} ms`)}${draftModelDir ? style.dim(` · draft: ${draftModelDir.split("/").filter(Boolean).at(-1)}`) : draftKind === "ngram" ? style.dim(" · draft: ngram (prompt lookup)") : ""}`);
|
|
1653
1715
|
await mountStartupAdapter(ctx, rt.serverOptions);
|
|
1654
1716
|
const server = createServer(ctx, rt.port, { ...rt.serverOptions, owner: "serve" });
|
|
1717
|
+
let shutdownStarted = false;
|
|
1718
|
+
const onServeSignal = (signal: "SIGINT" | "SIGTERM"): void => {
|
|
1719
|
+
if (shutdownStarted) return;
|
|
1720
|
+
shutdownStarted = true;
|
|
1721
|
+
const rawTimeout = Number(runtimeValue("MLX_BUN_SHUTDOWN_TIMEOUT_MS"));
|
|
1722
|
+
const timeoutMs = Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 120_000;
|
|
1723
|
+
console.log(`\n[server] ${signal}: flushing prompt-cache durability`);
|
|
1724
|
+
void shutdownServer(server, timeoutMs).then((result) => {
|
|
1725
|
+
if (!result.durability.durable) {
|
|
1726
|
+
const d = result.durability;
|
|
1727
|
+
console.warn(
|
|
1728
|
+
`[server] cache flush incomplete after ${Math.round(d.elapsedMs)} ms: ` +
|
|
1729
|
+
`${d.pendingSnapshots} snapshots, ${d.pendingSpills} spills, ` +
|
|
1730
|
+
`${d.droppedSpills} dropped, ${d.failedSpills} failed`,
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
process.exit(0);
|
|
1734
|
+
}).catch((error) => {
|
|
1735
|
+
console.warn(`[server] shutdown flush failed: ${(error as Error).message}`);
|
|
1736
|
+
process.exit(0);
|
|
1737
|
+
});
|
|
1738
|
+
};
|
|
1739
|
+
process.once("SIGINT", () => onServeSignal("SIGINT"));
|
|
1740
|
+
process.once("SIGTERM", () => onServeSignal("SIGTERM"));
|
|
1655
1741
|
const shownHost = rt.serverOptions.hostname ?? "localhost";
|
|
1656
1742
|
console.log();
|
|
1657
1743
|
box([
|
|
@@ -1682,7 +1768,7 @@ switch (cmd) {
|
|
|
1682
1768
|
// three entry points (raw / OpenAI API / chat UI) differ only in how params
|
|
1683
1769
|
// are POPULATED; here every param is explicit. Decode-path levers mirror
|
|
1684
1770
|
// serve so you can pin the route (mlx-lm compat = --l1).
|
|
1685
|
-
// --l1/--l2 + per-fork overrides set the decode
|
|
1771
|
+
// --l1/--l2 + per-fork overrides set the decode runtime options; the
|
|
1686
1772
|
// returned KV scheme is applied below so `generate --l2` runs the same
|
|
1687
1773
|
// quantized-KV route (and produces the same tokens) as `serve --l2`.
|
|
1688
1774
|
const route = applyDecodeRoute();
|
|
@@ -1706,13 +1792,12 @@ switch (cmd) {
|
|
|
1706
1792
|
// bf16, same silent fallback as serve), N = uniform bits from decode
|
|
1707
1793
|
// start. Unset (no tier, no --kv-quant) keeps generate's historical
|
|
1708
1794
|
// default: bf16 bit-exact greedy (generateText's parity path).
|
|
1709
|
-
const kvScheme =
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
? { kvBits: route.kvQuant, quantizedKvStart: 0 }
|
|
1795
|
+
const kvScheme = route.turboQuant || route.kvQuant !== undefined
|
|
1796
|
+
? resolveKvScheme({
|
|
1797
|
+
override: route.kvQuant,
|
|
1798
|
+
turboQuant: route.turboQuant,
|
|
1799
|
+
config: tm.config.kvQuant,
|
|
1800
|
+
}).generationOptions
|
|
1716
1801
|
: undefined;
|
|
1717
1802
|
const num = (n: string): number | undefined => { const v = opt(n); return v == null ? undefined : Number(v); };
|
|
1718
1803
|
const text = await generateText(tm, prompt, {
|
|
@@ -1794,7 +1879,7 @@ switch (cmd) {
|
|
|
1794
1879
|
case "benchmark": {
|
|
1795
1880
|
const { banner, step, box, style } = await import("./tui");
|
|
1796
1881
|
banner(pkg.version);
|
|
1797
|
-
serverRuntimeFlags(); //
|
|
1882
|
+
serverRuntimeFlags(); // decode options (--compiled-decode, --fused-sdpa, ...) apply to the run
|
|
1798
1883
|
const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
|
|
1799
1884
|
const tokens = Number(opt("tokens", "256"));
|
|
1800
1885
|
const runs = Number(opt("runs", "3"));
|
|
@@ -2446,6 +2531,12 @@ switch (cmd) {
|
|
|
2446
2531
|
console.error(`--candidate-bits expects a comma list of integers in [2, 8] (got "${opt("candidate-bits")}")`);
|
|
2447
2532
|
process.exit(1);
|
|
2448
2533
|
}
|
|
2534
|
+
const rotateWeights = flag("rotate-weights");
|
|
2535
|
+
const rotationSeed = Number(opt("rotation-seed", "42"));
|
|
2536
|
+
if (!Number.isInteger(rotationSeed)) {
|
|
2537
|
+
console.error(`--rotation-seed expects an integer (got "${opt("rotation-seed")}")`);
|
|
2538
|
+
process.exit(1);
|
|
2539
|
+
}
|
|
2449
2540
|
const mlxPath = opt("mlx-path", "mlx_model")!;
|
|
2450
2541
|
const { existsSync } = await import("node:fs");
|
|
2451
2542
|
if (existsSync(mlxPath)) {
|
|
@@ -2487,7 +2578,7 @@ switch (cmd) {
|
|
|
2487
2578
|
? `quantizing (mixed, target ${targetBpw} bpw — sensitivity sweep, ~minutes)`
|
|
2488
2579
|
: `quantizing (${qBits}-bit, group ${qGroup})`,
|
|
2489
2580
|
);
|
|
2490
|
-
const { quantizeModelDir } = await import("./quantize");
|
|
2581
|
+
const { quantizeModelDir, automaticRotationWeightTransform } = await import("./quantize");
|
|
2491
2582
|
try {
|
|
2492
2583
|
const r = await quantizeModelDir(
|
|
2493
2584
|
srcDir,
|
|
@@ -2500,6 +2591,9 @@ switch (cmd) {
|
|
|
2500
2591
|
...(candidateBits ? { candidateBits } : {}),
|
|
2501
2592
|
...(opt("calibration-mix") ? { calibrationMix: opt("calibration-mix")! } : {}),
|
|
2502
2593
|
...(opt("n-calibration") ? { nCalibration: Number(opt("n-calibration")) } : {}),
|
|
2594
|
+
...(rotateWeights
|
|
2595
|
+
? { weightTransform: automaticRotationWeightTransform({ seed: rotationSeed }) }
|
|
2596
|
+
: {}),
|
|
2503
2597
|
},
|
|
2504
2598
|
(e) => sQ.update(e.message),
|
|
2505
2599
|
);
|
|
@@ -2511,6 +2605,7 @@ switch (cmd) {
|
|
|
2511
2605
|
`source ${style.dim(srcDir)}`,
|
|
2512
2606
|
`model ${style.bold(r.outDir)} ${style.dim(`· ${gb(r.write.totalSize)}`)}`,
|
|
2513
2607
|
`quant ${style.dim(targetBpw !== undefined ? `mixed ${r.achievedBpw.toFixed(2)} bpw (target ${targetBpw})` : `${qBits}-bit g${qGroup} affine`)}`,
|
|
2608
|
+
...(rotateWeights ? [`transform ${style.dim(`TurboQuant rotation seed ${rotationSeed}`)}`] : []),
|
|
2514
2609
|
"",
|
|
2515
2610
|
`serve it ${style.accent(`mlx-bun serve ${r.outDir}`)}`,
|
|
2516
2611
|
]);
|
package/src/eval/kl.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Two reference modes:
|
|
11
11
|
// * self-flag (DEFAULT, the kernel-drift gate): one model, one weight
|
|
12
|
-
// load; forward each prompt twice with
|
|
12
|
+
// load; forward each prompt twice with a runtime lever set to its
|
|
13
13
|
// reference value vs its candidate value. Directly measures the drift
|
|
14
14
|
// a perf lever introduces. Fits in RAM trivially (no second model).
|
|
15
15
|
// * two-model (optiq-style absolute quality): a separate reference
|
|
@@ -30,6 +30,10 @@ import { loadModelConfig, type ModelConfig, type TurboQuantScheme } from "../con
|
|
|
30
30
|
import { createModel, type RuntimeModel } from "../model/factory";
|
|
31
31
|
import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
|
|
32
32
|
import { maybeQuantizeKv } from "../generate";
|
|
33
|
+
import {
|
|
34
|
+
configureRuntime,
|
|
35
|
+
runtimeKey,
|
|
36
|
+
} from "../runtime-config";
|
|
33
37
|
|
|
34
38
|
export interface KLResult {
|
|
35
39
|
nPrompts: number;
|
|
@@ -135,23 +139,28 @@ export async function evaluateKlSelfFlag(opts: {
|
|
|
135
139
|
const { model, tokenizer } = await loadRunnable(opts.candidate);
|
|
136
140
|
const tokenized = prepPrompts(opts.prompts, tokenizer, seqLen, n);
|
|
137
141
|
|
|
138
|
-
const
|
|
142
|
+
const key = runtimeKey(opts.flag);
|
|
139
143
|
const perPrompt: Float32Array[] = [];
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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();
|
|
144
|
+
for (const ids of tokenized) {
|
|
145
|
+
let p: MlxArray;
|
|
146
|
+
const restoreRef = configureRuntime({ [key]: opts.refValue });
|
|
147
|
+
try {
|
|
148
|
+
p = forwardLogits(model, ids);
|
|
149
|
+
} finally {
|
|
150
|
+
restoreRef();
|
|
151
151
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
let q: MlxArray;
|
|
153
|
+
const restoreCandidate = configureRuntime({ [key]: opts.candValue });
|
|
154
|
+
try {
|
|
155
|
+
q = forwardLogits(model, ids);
|
|
156
|
+
} finally {
|
|
157
|
+
restoreCandidate();
|
|
158
|
+
}
|
|
159
|
+
const T = Math.min(p.shape[1]!, q.shape[1]!);
|
|
160
|
+
const pT = p.slice([0, 0, 0], [1, T, p.shape[2]!]);
|
|
161
|
+
const qT = q.slice([0, 0, 0], [1, T, q.shape[2]!]);
|
|
162
|
+
perPrompt.push(klPerToken(pT, qT));
|
|
163
|
+
for (const a of [p, q, pT, qT]) a.dispose();
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
const agg = aggregate(perPrompt);
|
|
@@ -232,8 +241,7 @@ function decodeArm(
|
|
|
232
241
|
flag: string, value: string,
|
|
233
242
|
kvOverride?: Parameters<typeof maybeQuantizeKv>[1],
|
|
234
243
|
): Float32Array[] {
|
|
235
|
-
const
|
|
236
|
-
process.env[flag] = value;
|
|
244
|
+
const restore = configureRuntime({ [runtimeKey(flag)]: value });
|
|
237
245
|
try {
|
|
238
246
|
const cache = model.makeCache();
|
|
239
247
|
try {
|
|
@@ -256,8 +264,7 @@ function decodeArm(
|
|
|
256
264
|
for (const c of cache) c.dispose();
|
|
257
265
|
}
|
|
258
266
|
} finally {
|
|
259
|
-
|
|
260
|
-
else process.env[flag] = prev;
|
|
267
|
+
restore();
|
|
261
268
|
}
|
|
262
269
|
}
|
|
263
270
|
|
package/src/eval/runner.ts
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
8
|
import { generate, type GenerateOptions } from "../generate";
|
|
9
|
+
import { clearCache } from "../mlx/ffi";
|
|
9
10
|
import * as ops from "../mlx/ops";
|
|
10
11
|
import { ChatTemplate } from "../chat-template";
|
|
11
12
|
import { loadModelConfig, type ModelConfig } from "../config";
|
|
13
|
+
import { runtimeValue } from "../runtime-config";
|
|
12
14
|
import {
|
|
13
15
|
openModel,
|
|
14
16
|
type Glm52RuntimeOpenOptions,
|
|
@@ -19,7 +21,7 @@ import { DiffusionGemmaModel } from "../model/diffusion-gemma";
|
|
|
19
21
|
import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
|
|
20
22
|
import { resolveModelDir } from "./kl";
|
|
21
23
|
|
|
22
|
-
export const EVAL_DATA_DIR =
|
|
24
|
+
export const EVAL_DATA_DIR = runtimeValue("MLX_BUN_EVAL_DATA") ?? `${homedir()}/.cache/mlx-bun/eval-data`;
|
|
23
25
|
|
|
24
26
|
/** Read a jsonl dataset exported by scripts/eval/export-datasets.py. */
|
|
25
27
|
export function loadJsonl<T = Record<string, unknown>>(name: string): T[] {
|
|
@@ -124,7 +126,23 @@ export function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: nu
|
|
|
124
126
|
if (tm.activeAdapters) tm.model.loraState.active = tm.activeAdapters; // apply the mounted adapter
|
|
125
127
|
const cache = tm.model.makeCache();
|
|
126
128
|
try {
|
|
127
|
-
|
|
129
|
+
// Chunked, LM-head-free prefill. The old single-shot `forward(prompt)`
|
|
130
|
+
// materialized [1, L, vocab] logits just to discard them — at a 248k
|
|
131
|
+
// vocab that's ~1 GB per 1k prompt tokens, and with no allocator-cache
|
|
132
|
+
// clears the Metal buffer cache retained every giant freed tensor
|
|
133
|
+
// (the 27B capability-suite 73 GB swap-thrash). Prefill only needs the
|
|
134
|
+
// cache state, which forwardHidden advances identically (the LM head
|
|
135
|
+
// never touches the cache) — bit-exact by construction. Chunk size and
|
|
136
|
+
// clear cadence mirror mlx-lm generate_step / our generate().
|
|
137
|
+
const PREFILL_CHUNK = 2048;
|
|
138
|
+
const prompt = ids.slice(0, -1);
|
|
139
|
+
for (let s = 0; s < prompt.length; s += PREFILL_CHUNK) {
|
|
140
|
+
const chunk = prompt.slice(s, s + PREFILL_CHUNK);
|
|
141
|
+
const cIds = ops.fromInt32(chunk, [1, chunk.length]);
|
|
142
|
+
tm.model.forwardHidden(cIds, cache).dispose();
|
|
143
|
+
cIds.dispose();
|
|
144
|
+
clearCache();
|
|
145
|
+
}
|
|
128
146
|
let last = ids[ids.length - 1]!;
|
|
129
147
|
const eos = new Set(tm.config.eosTokenIds);
|
|
130
148
|
const out: number[] = [];
|
|
@@ -134,6 +152,7 @@ export function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: nu
|
|
|
134
152
|
lg.dispose();
|
|
135
153
|
const best = ops.itemUint32(am);
|
|
136
154
|
am.dispose();
|
|
155
|
+
if (i % 256 === 0) clearCache(); // mlx-lm decode cadence
|
|
137
156
|
if (eos.has(best)) break; // mlx-lm halts on EOS and excludes it from the output
|
|
138
157
|
out.push(best);
|
|
139
158
|
last = best;
|
|
@@ -148,7 +167,7 @@ export function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: nu
|
|
|
148
167
|
* pass `opts.sampler` to drive temperature/HLG/etc. */
|
|
149
168
|
export async function generateText(tm: TaskModel, body: string, opts: GenOpts = {}): Promise<string> {
|
|
150
169
|
const maxTokens = opts.maxTokens ?? 256;
|
|
151
|
-
const enableThinking = opts.enableThinking ??
|
|
170
|
+
const enableThinking = opts.enableThinking ?? runtimeValue("MLX_BUN_EVAL_THINK") === "1";
|
|
152
171
|
const templated = opts.useChat !== false && tm.template !== null;
|
|
153
172
|
const text = templated
|
|
154
173
|
? tm.template!.render([{ role: "user", content: body }], { addGenerationPrompt: true, enableThinking })
|
|
@@ -165,7 +184,7 @@ export async function generateText(tm: TaskModel, body: string, opts: GenOpts =
|
|
|
165
184
|
// MLX_BUN_EVAL_KV_QUANT=1 generates through the model's quantized KV (serving
|
|
166
185
|
// config); an explicit opts.kvScheme (cli generate's resolved tier route) wins.
|
|
167
186
|
const envKv: GenOpts["kvScheme"] | undefined =
|
|
168
|
-
(
|
|
187
|
+
(runtimeValue("MLX_BUN_EVAL_KV_QUANT") === "1" && tm.config.kvQuant?.length)
|
|
169
188
|
? { kvConfig: tm.config.kvQuant, quantizedKvStart: 0 } : undefined;
|
|
170
189
|
const kvScheme = opts.kvScheme ?? envKv;
|
|
171
190
|
const kvActive = !!(kvScheme && (kvScheme.kvBits || kvScheme.kvConfig?.length || kvScheme.turboQuant));
|
package/src/eval/tasks/bfcl.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
|
|
18
18
|
import { type ToolDefinition } from "../../chat-template";
|
|
19
|
+
import { runtimeValue } from "../../runtime-config";
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Data shapes (the merged jsonl: each line = {query, answer}).
|
|
@@ -465,7 +466,7 @@ export async function evaluateBfcl(
|
|
|
465
466
|
|
|
466
467
|
// Optiq-parity mode (DEFAULT): optiq's EXACT 200 BFCL-simple questions, scored
|
|
467
468
|
// through OUR pipeline. MLX_BUN_BFCL_FROZEN=0 reverts to our copy + sampling.
|
|
468
|
-
const useFrozen = opts.frozen ?? (
|
|
469
|
+
const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_BFCL_FROZEN") !== "0");
|
|
469
470
|
let items: BfclItem[];
|
|
470
471
|
if (useFrozen) {
|
|
471
472
|
items = loadJsonl<BfclFrozenRow>("bfcl_optiq_frozen").map((r) => ({
|
package/src/eval/tasks/gsm8k.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// 3-shot CoT, greedy decode, extract the numeric answer, compare.
|
|
3
3
|
|
|
4
4
|
import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
|
|
5
|
+
import { runtimeValue } from "../../runtime-config";
|
|
5
6
|
|
|
6
7
|
// 3-shot exemplars (verbatim from gsm8k.py).
|
|
7
8
|
const FEW_SHOT: { question: string; answer: string }[] = [
|
|
@@ -73,7 +74,7 @@ export async function evaluateGsm8k(
|
|
|
73
74
|
// his published GSM8K used) but through OUR OWN pipeline — our few-shot builder,
|
|
74
75
|
// our chat template, our scorer. If our number reproduces his, our runtime is
|
|
75
76
|
// faithful. MLX_BUN_GSM8K_FROZEN=0 falls back to our own sampling of the full set.
|
|
76
|
-
const useFrozen = opts.frozen ?? (
|
|
77
|
+
const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_GSM8K_FROZEN") !== "0");
|
|
77
78
|
let rows: Gsm8kRow[];
|
|
78
79
|
if (useFrozen) {
|
|
79
80
|
rows = loadJsonl<Gsm8kRow>("gsm8k_optiq_frozen"); // optiq's exact draw; we use {question, answer}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// fused-attention kernel (opportunity A) has the most to prove.
|
|
11
11
|
|
|
12
12
|
import { generateText, loadJsonl, type TaskModel } from "../runner";
|
|
13
|
+
import { runtimeValue } from "../../runtime-config";
|
|
13
14
|
|
|
14
15
|
const INSTR =
|
|
15
16
|
"You will be given a list of hash assignments and a starting hash. " +
|
|
@@ -96,7 +97,7 @@ export async function evaluateHashhop(
|
|
|
96
97
|
// RECONSTRUCTION producing DIFFERENT problems, so it cannot reproduce optiq's
|
|
97
98
|
// number — the frozen problems are the only way to match. MLX_BUN_HASHHOP_FROZEN=0
|
|
98
99
|
// uses our generator.
|
|
99
|
-
const useFrozen = opts.frozen ?? (
|
|
100
|
+
const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_HASHHOP_FROZEN") !== "0");
|
|
100
101
|
if (useFrozen) {
|
|
101
102
|
const rows = loadJsonl<HashhopFrozen>("hashhop_optiq_frozen");
|
|
102
103
|
const hopCorrect: Record<number, number> = {};
|
|
@@ -16,11 +16,12 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
16
16
|
import { tmpdir } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
|
|
19
|
+
import { runtimeValue } from "../../runtime-config";
|
|
19
20
|
|
|
20
21
|
// Oracle venv python — the same interpreter the Python reference uses.
|
|
21
22
|
// Machine-specific: overridable via MLX_BUN_ORACLE_PYTHON (the committed default is
|
|
22
23
|
// the reference box; other laptops set the env, never re-commit the path).
|
|
23
|
-
const ORACLE_PYTHON =
|
|
24
|
+
const ORACLE_PYTHON = runtimeValue("MLX_BUN_ORACLE_PYTHON") ?? "/Users/joshrossi/Code/mlx-lm/.venv/bin/python";
|
|
24
25
|
|
|
25
26
|
interface HumanevalRow {
|
|
26
27
|
task_id: string;
|
|
@@ -185,7 +186,7 @@ function runProgram(program: string, timeoutSec: number, memoryMb: number): RunR
|
|
|
185
186
|
env,
|
|
186
187
|
timeout: timeoutSec * 1000, // hard wall-clock kill
|
|
187
188
|
killSignal: "SIGKILL",
|
|
188
|
-
stdio: ["ignore", "ignore",
|
|
189
|
+
stdio: ["ignore", "ignore", runtimeValue("MLX_BUN_EVAL_DEBUG") === "1" ? "pipe" : "ignore"],
|
|
189
190
|
encoding: "utf8",
|
|
190
191
|
});
|
|
191
192
|
|
|
@@ -219,7 +220,7 @@ export async function evaluateHumaneval(
|
|
|
219
220
|
// Optiq-parity mode (DEFAULT): optiq's EXACT 164-problem set (he runs the full
|
|
220
221
|
// HumanEval test split), scored through OUR pipeline. MLX_BUN_HUMANEVAL_FROZEN=0
|
|
221
222
|
// reverts to our own copy + optional sampling.
|
|
222
|
-
const useFrozen = opts.frozen ?? (
|
|
223
|
+
const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_HUMANEVAL_FROZEN") !== "0");
|
|
223
224
|
const rows = loadJsonl<HumanevalRow>(useFrozen ? "humaneval_optiq_frozen" : "humaneval");
|
|
224
225
|
const idx = useFrozen
|
|
225
226
|
? Array.from({ length: rows.length }, (_, i) => i)
|
|
@@ -252,7 +253,7 @@ export async function evaluateHumaneval(
|
|
|
252
253
|
|
|
253
254
|
const rr = runProgram(program, timeoutSec, memoryMb);
|
|
254
255
|
if (rr.ok) nPass++;
|
|
255
|
-
else if (
|
|
256
|
+
else if (runtimeValue("MLX_BUN_EVAL_DEBUG") === "1") {
|
|
256
257
|
const err = rr.stderr ?? "";
|
|
257
258
|
const reason = rr.timedOut ? "TIMEOUT"
|
|
258
259
|
: completion.trim() === "" ? "EMPTY"
|
package/src/eval/tasks/ifeval.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// default in both implementations, so behavior is identical.
|
|
22
22
|
|
|
23
23
|
import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
|
|
24
|
+
import { runtimeValue } from "../../runtime-config";
|
|
24
25
|
|
|
25
26
|
export interface IfevalInstance {
|
|
26
27
|
prompt: string;
|
|
@@ -472,7 +473,7 @@ export async function evaluateIfeval(
|
|
|
472
473
|
// Optiq-parity mode (DEFAULT): score optiq's EXACT prompt set (he runs the full
|
|
473
474
|
// IFEval split — 541 prompts) through OUR verifiers + OUR chat template.
|
|
474
475
|
// MLX_BUN_IFEVAL_FROZEN=0 reverts to our own copy + optional sampling.
|
|
475
|
-
const useFrozen = opts.frozen ?? (
|
|
476
|
+
const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_IFEVAL_FROZEN") !== "0");
|
|
476
477
|
const rows = loadJsonl<IfevalRow>(useFrozen ? "ifeval_optiq_frozen" : "ifeval");
|
|
477
478
|
const nSamples = opts.nSamples ?? rows.length;
|
|
478
479
|
const idx = (!useFrozen && nSamples < rows.length)
|
package/src/eval/tasks/mmlu.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// table. MLX_BUN_MMLU_FROZEN=0 falls back to our own sampling of the full 14k set.
|
|
11
11
|
|
|
12
12
|
import { loadJsonl, sampleIndices, type TaskModel } from "../runner";
|
|
13
|
+
import { runtimeValue } from "../../runtime-config";
|
|
13
14
|
|
|
14
15
|
interface MmluRow {
|
|
15
16
|
question: string;
|
|
@@ -85,7 +86,7 @@ export async function evaluateMmlu(
|
|
|
85
86
|
// parity, not sampling. Set MLX_BUN_MMLU_FROZEN=0 (or frozen:false) to fall back to
|
|
86
87
|
// our own stratified sampling of the full 14k test set (a DIFFERENT subset: JS RNG
|
|
87
88
|
// ≠ numpy RandomState, so it does not match optiq's questions even at the same seed).
|
|
88
|
-
const useFrozen = opts.frozen ?? (
|
|
89
|
+
const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_MMLU_FROZEN") !== "0");
|
|
89
90
|
|
|
90
91
|
const testRows = loadJsonl<MmluRow>(useFrozen ? "mmlu_optiq_frozen" : "mmlu_test");
|
|
91
92
|
const devRows = loadJsonl<MmluRow>(useFrozen ? "mmlu_optiq_dev" : "mmlu_dev");
|