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/quantize/index.ts
CHANGED
|
@@ -2,13 +2,40 @@
|
|
|
2
2
|
// import from here: quantize a model directory, write sharded safetensors,
|
|
3
3
|
// build the config block, and resolve/inspect models for the wizard UI.
|
|
4
4
|
|
|
5
|
-
export { quantizeModelDir, isQuantizable } from "./quantizer";
|
|
5
|
+
export { quantizeModelDir, isQuantizable, withPreparedProbe } from "./quantizer";
|
|
6
6
|
export type {
|
|
7
|
+
PreparedProbe,
|
|
8
|
+
ProbeSource,
|
|
7
9
|
QuantizeOptions,
|
|
8
10
|
QuantizeResult,
|
|
9
11
|
ProgressEvent,
|
|
10
12
|
} from "./quantizer";
|
|
11
13
|
|
|
14
|
+
export {
|
|
15
|
+
planQwen35Fold,
|
|
16
|
+
planQwenMtpFold,
|
|
17
|
+
} from "./rotate";
|
|
18
|
+
export type {
|
|
19
|
+
FoldOptions,
|
|
20
|
+
QwenFoldOptions,
|
|
21
|
+
} from "./rotate";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
automaticRotationWeightTransform,
|
|
25
|
+
llamaWeightTransform,
|
|
26
|
+
planLlamaWeightTransform,
|
|
27
|
+
qwen35WeightTransform,
|
|
28
|
+
qwenMtpWeightTransform,
|
|
29
|
+
} from "./weight-transform";
|
|
30
|
+
export type {
|
|
31
|
+
LlamaWeightTransformPlan,
|
|
32
|
+
QwenWeightTransformPlan,
|
|
33
|
+
WeightTransform,
|
|
34
|
+
WeightTransformContext,
|
|
35
|
+
WeightTransformMetadata,
|
|
36
|
+
WeightTransformPlan,
|
|
37
|
+
} from "./weight-transform";
|
|
38
|
+
|
|
12
39
|
export {
|
|
13
40
|
writeShardedSafetensors,
|
|
14
41
|
DEFAULT_SHARD_BYTES,
|
package/src/quantize/job.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { registerRunner } from "../jobs/runner";
|
|
|
13
13
|
import { Registry } from "../registry";
|
|
14
14
|
import { loadModelConfig } from "../config";
|
|
15
15
|
import { quantizeModelDir, type QuantizeOptions } from "./quantizer";
|
|
16
|
+
import { automaticRotationWeightTransform } from "./weight-transform";
|
|
16
17
|
|
|
17
18
|
/** Resolve the source model directory from a job config. Accepts an explicit
|
|
18
19
|
* filesystem path (src_dir) or a registry query / model id (model_id). */
|
|
@@ -57,6 +58,9 @@ export const quantizeRunner: JobRunner = async (emit: Emit, config) => {
|
|
|
57
58
|
// MUST be forwarded into opts or quantizeModelDir silently runs uniform.
|
|
58
59
|
const targetBpw = config.target_bpw != null ? Number(config.target_bpw) : undefined;
|
|
59
60
|
const mixed = targetBpw !== undefined;
|
|
61
|
+
const rotationSeed = Number(config.rotation_seed ?? 42);
|
|
62
|
+
if (config.rotate_weights && !Number.isInteger(rotationSeed))
|
|
63
|
+
throw new Error(`quantize job: rotation_seed must be an integer (got ${String(config.rotation_seed)})`);
|
|
60
64
|
|
|
61
65
|
emit({
|
|
62
66
|
type: "stage",
|
|
@@ -74,6 +78,13 @@ export const quantizeRunner: JobRunner = async (emit: Emit, config) => {
|
|
|
74
78
|
...(config.reference ? { reference: String(config.reference) } : {}),
|
|
75
79
|
...(config.calibration_mix ? { calibrationMix: String(config.calibration_mix) } : {}),
|
|
76
80
|
...(config.n_calibration != null ? { nCalibration: Number(config.n_calibration) } : {}),
|
|
81
|
+
...(config.rotate_weights
|
|
82
|
+
? {
|
|
83
|
+
weightTransform: automaticRotationWeightTransform({
|
|
84
|
+
seed: rotationSeed,
|
|
85
|
+
}),
|
|
86
|
+
}
|
|
87
|
+
: {}),
|
|
77
88
|
};
|
|
78
89
|
|
|
79
90
|
const r = await quantizeModelDir(srcDir, outDir, opts, (e) =>
|
|
@@ -37,6 +37,12 @@ import {
|
|
|
37
37
|
import { loadLlmCalibration } from "./calibration";
|
|
38
38
|
import { analyzeSensitivityExact, type SensitivityResult } from "./sensitivity";
|
|
39
39
|
import { optimizeMixedPrecision } from "./allocator";
|
|
40
|
+
import type {
|
|
41
|
+
WeightTransform,
|
|
42
|
+
WeightTransformContext,
|
|
43
|
+
WeightTransformPlan,
|
|
44
|
+
} from "./weight-transform";
|
|
45
|
+
import { writeAtomicDirectory } from "./atomic-output";
|
|
40
46
|
|
|
41
47
|
/** Quantize the allocator cache this often (in modules processed). */
|
|
42
48
|
const CACHE_CLEAR_EVERY = 16;
|
|
@@ -56,6 +62,8 @@ export interface QuantizeOptions {
|
|
|
56
62
|
* block (mlx's "not quantized" convention). Shape-ineligible modules
|
|
57
63
|
* never reach this. */
|
|
58
64
|
quantizePredicate?: (base: string, fullShape: number[]) => boolean;
|
|
65
|
+
/** Optional offline basis transform applied before quantization. */
|
|
66
|
+
weightTransform?: WeightTransform;
|
|
59
67
|
|
|
60
68
|
// --- mixed-precision (OptiQ sensitivity + knapsack) -------------------
|
|
61
69
|
// Setting targetBpw switches quantizeModelDir to the mixed path: it loads the
|
|
@@ -73,6 +81,23 @@ export interface QuantizeOptions {
|
|
|
73
81
|
calibrationMix?: string;
|
|
74
82
|
/** Number of calibration samples (forward passes per layer×bit probe). */
|
|
75
83
|
nCalibration?: number;
|
|
84
|
+
/** Dependency used to provide the loadable model for sensitivity sweeps. */
|
|
85
|
+
probeSource?: ProbeSource;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PreparedProbe {
|
|
89
|
+
modelDir: string;
|
|
90
|
+
dispose(): void | Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Supplies a loadable sensitivity model without re-entering the top-level
|
|
94
|
+
* quantization orchestrator. Tests and callers may inject an existing probe. */
|
|
95
|
+
export interface ProbeSource {
|
|
96
|
+
prepare(
|
|
97
|
+
srcDir: string,
|
|
98
|
+
opts: QuantizeOptions,
|
|
99
|
+
onProgress?: (e: ProgressEvent) => void,
|
|
100
|
+
): Promise<PreparedProbe>;
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
export interface ProgressEvent {
|
|
@@ -124,9 +149,6 @@ export async function quantizeModelDir(
|
|
|
124
149
|
opts: QuantizeOptions,
|
|
125
150
|
onProgress?: (e: ProgressEvent) => void,
|
|
126
151
|
): Promise<QuantizeResult> {
|
|
127
|
-
const groupSize = opts.groupSize;
|
|
128
|
-
const mode = opts.mode ?? "affine";
|
|
129
|
-
|
|
130
152
|
// Mixed-precision path: derive a per-module bit allocation first, then write.
|
|
131
153
|
let perLayerBits: Map<string, number> | undefined;
|
|
132
154
|
let mixedMeta: MixedMeta | undefined;
|
|
@@ -136,6 +158,34 @@ export async function quantizeModelDir(
|
|
|
136
158
|
mixedMeta = alloc.meta;
|
|
137
159
|
}
|
|
138
160
|
|
|
161
|
+
return writeAtomicDirectory(outDir, (stagingDir) =>
|
|
162
|
+
writeQuantizedModelDir(
|
|
163
|
+
srcDir,
|
|
164
|
+
stagingDir,
|
|
165
|
+
opts,
|
|
166
|
+
perLayerBits,
|
|
167
|
+
mixedMeta,
|
|
168
|
+
onProgress,
|
|
169
|
+
outDir,
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Lower-level writer used by the public orchestrator and its probe source.
|
|
175
|
+
* Deliberately does not perform sensitivity/allocation or call back into
|
|
176
|
+
* quantizeModelDir. */
|
|
177
|
+
async function writeQuantizedModelDir(
|
|
178
|
+
srcDir: string,
|
|
179
|
+
outDir: string,
|
|
180
|
+
opts: QuantizeOptions,
|
|
181
|
+
perLayerBits?: Map<string, number>,
|
|
182
|
+
mixedMeta?: MixedMeta,
|
|
183
|
+
onProgress?: (e: ProgressEvent) => void,
|
|
184
|
+
publishedOutDir = outDir,
|
|
185
|
+
): Promise<QuantizeResult> {
|
|
186
|
+
const groupSize = opts.groupSize;
|
|
187
|
+
const mode = opts.mode ?? "affine";
|
|
188
|
+
|
|
139
189
|
const bits = opts.bits;
|
|
140
190
|
|
|
141
191
|
mkdirSync(outDir, { recursive: true });
|
|
@@ -148,13 +198,31 @@ export async function quantizeModelDir(
|
|
|
148
198
|
const config = await loadModelConfig(srcDir);
|
|
149
199
|
const srcQuant: QuantizationConfig | null = config.quantization;
|
|
150
200
|
const weights = await Weights.open(srcDir);
|
|
201
|
+
let transformPlan: WeightTransformPlan | undefined;
|
|
202
|
+
let transformContext: WeightTransformContext | undefined;
|
|
203
|
+
try {
|
|
204
|
+
if (opts.weightTransform && srcQuant)
|
|
205
|
+
throw new Error(
|
|
206
|
+
`weight transform ${opts.weightTransform.id} requires full-precision source weights`,
|
|
207
|
+
);
|
|
208
|
+
transformPlan = opts.weightTransform?.plan(weights.tensorNames, config);
|
|
209
|
+
if (transformPlan) {
|
|
210
|
+
validateTransformPlan(transformPlan, weights.tensorNames);
|
|
211
|
+
transformContext = opts.weightTransform!.createContext(weights, transformPlan);
|
|
212
|
+
progress("transforming", `Planned ${transformPlan.id}`, 0);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
transformContext?.dispose();
|
|
216
|
+
weights.dispose();
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
151
219
|
|
|
152
220
|
// Preserve the source tensor order so the output diff is mlx-lm-shaped.
|
|
153
221
|
// tensorNames is sorted; we walk modules in that order. For each module's
|
|
154
222
|
// `.weight` we decide quantize / pass-through, then emit its tensors. We
|
|
155
223
|
// skip the source `.scales`/`.biases` of already-quantized modules (they're
|
|
156
224
|
// regenerated) and pass through everything else verbatim.
|
|
157
|
-
const names = weights.tensorNames;
|
|
225
|
+
const names = transformPlan?.outputNames ?? weights.tensorNames;
|
|
158
226
|
|
|
159
227
|
// Group names by module base so we can detect "already quantized" (has a
|
|
160
228
|
// sibling `.scales`) and process a module atomically.
|
|
@@ -194,26 +262,28 @@ export async function quantizeModelDir(
|
|
|
194
262
|
if (!hasWeight) {
|
|
195
263
|
// Module with only scales/biases and no weight — shouldn't happen, but
|
|
196
264
|
// pass any present tensors through unchanged.
|
|
197
|
-
for (const suf of suffixes)
|
|
265
|
+
for (const suf of suffixes)
|
|
266
|
+
out.push(outputTensor(weights, transformPlan, transformContext, `${base}.${suf}`));
|
|
198
267
|
bumpProgress();
|
|
199
268
|
continue;
|
|
200
269
|
}
|
|
201
270
|
|
|
202
271
|
// Materialize the full-precision weight for this module.
|
|
203
272
|
const weightName = `${base}.weight`;
|
|
204
|
-
|
|
273
|
+
let fullWeight = outputArray(weights, transformPlan, transformContext, weightName);
|
|
274
|
+
const shape = fullWeight.shape;
|
|
205
275
|
|
|
206
276
|
// Determine the *original* (full-precision) shape: if already quantized,
|
|
207
277
|
// the packed weight's last dim is compressed — recover the real width
|
|
208
278
|
// from the scales (groups × srcGroupSize) when dequantizing.
|
|
209
|
-
let fullWeight = weights.tensor(weightName);
|
|
210
279
|
let materialized: MlxArray | null = null;
|
|
211
280
|
|
|
212
281
|
if (alreadyQuant) {
|
|
213
282
|
const srcSpec = quantFor(srcQuant, base);
|
|
214
283
|
if (!srcSpec) {
|
|
215
284
|
// Has scales but config says unquantized — fall back to passthrough.
|
|
216
|
-
for (const suf of suffixes)
|
|
285
|
+
for (const suf of suffixes)
|
|
286
|
+
out.push(outputTensor(weights, transformPlan, transformContext, `${base}.${suf}`));
|
|
217
287
|
bumpProgress();
|
|
218
288
|
continue;
|
|
219
289
|
}
|
|
@@ -263,6 +333,7 @@ export async function quantizeModelDir(
|
|
|
263
333
|
// bf16 matches the reference dtype and the scales/biases dtype on disk).
|
|
264
334
|
const bf16 = fullWeight.astype(Dtype.bfloat16, cpuStream);
|
|
265
335
|
if (materialized) materialized.dispose();
|
|
336
|
+
else if (transformContext) fullWeight.dispose();
|
|
266
337
|
|
|
267
338
|
const q = quantize(bf16, groupSize, moduleBits, mode, cpuStream);
|
|
268
339
|
bf16.dispose();
|
|
@@ -283,9 +354,10 @@ export async function quantizeModelDir(
|
|
|
283
354
|
}
|
|
284
355
|
|
|
285
356
|
// Pass through non-weight/scale/bias tensors (e.g. anything unusual).
|
|
286
|
-
for (const name of passthroughNames)
|
|
357
|
+
for (const name of passthroughNames)
|
|
358
|
+
out.push(outputTensor(weights, transformPlan, transformContext, name));
|
|
287
359
|
|
|
288
|
-
progress("writing", `Writing ${out.length} tensors to ${
|
|
360
|
+
progress("writing", `Writing ${out.length} tensors to ${publishedOutDir}`, 1);
|
|
289
361
|
const write = writeShardedSafetensors(outDir, out);
|
|
290
362
|
|
|
291
363
|
const achievedBpw = quantizedParams > 0 ? quantizedBits / quantizedParams : 0;
|
|
@@ -302,7 +374,9 @@ export async function quantizeModelDir(
|
|
|
302
374
|
// emit its standard optiq_metadata.json sidecar. For mixed mode we write a
|
|
303
375
|
// richer OptiQ-style sidecar ourselves below (method "mixed_precision").
|
|
304
376
|
const block = buildQuantizationBlock({ bits: defaultBits, groupSize, mode }, perLayer);
|
|
305
|
-
|
|
377
|
+
const outputConfig = transformedConfig(config.raw, transformPlan);
|
|
378
|
+
const weightTransforms = transformPlan ? [transformPlan.metadata] : undefined;
|
|
379
|
+
await writeQuantizedConfig(outputConfig, outDir, block, {
|
|
306
380
|
srcDir,
|
|
307
381
|
optiq: mixedMeta
|
|
308
382
|
? undefined
|
|
@@ -313,6 +387,7 @@ export async function quantizeModelDir(
|
|
|
313
387
|
group_size: groupSize,
|
|
314
388
|
achieved_bpw: achievedBpw,
|
|
315
389
|
per_layer_count: nQuantized,
|
|
390
|
+
weight_transforms: weightTransforms,
|
|
316
391
|
},
|
|
317
392
|
});
|
|
318
393
|
|
|
@@ -331,6 +406,7 @@ export async function quantizeModelDir(
|
|
|
331
406
|
n_high: mixedMeta.nHigh,
|
|
332
407
|
n_low: mixedMeta.nLow,
|
|
333
408
|
per_layer_count: nQuantized,
|
|
409
|
+
weight_transforms: weightTransforms,
|
|
334
410
|
per_layer: Object.fromEntries(
|
|
335
411
|
[...perLayer].map(([p, e]) => [
|
|
336
412
|
p,
|
|
@@ -344,13 +420,14 @@ export async function quantizeModelDir(
|
|
|
344
420
|
);
|
|
345
421
|
}
|
|
346
422
|
|
|
347
|
-
progress("done", `Quantized ${nQuantized} modules → ${
|
|
348
|
-
return { outDir, achievedBpw, nQuantized, write };
|
|
423
|
+
progress("done", `Quantized ${nQuantized} modules → ${publishedOutDir}`, 1);
|
|
424
|
+
return { outDir: publishedOutDir, achievedBpw, nQuantized, write };
|
|
349
425
|
} finally {
|
|
350
426
|
// Dispose every emitted array (their bytes are now on disk) and the source.
|
|
351
427
|
for (const t of out) {
|
|
352
428
|
try { t.array.dispose(); } catch {}
|
|
353
429
|
}
|
|
430
|
+
transformContext?.dispose();
|
|
354
431
|
weights.dispose();
|
|
355
432
|
clearCache();
|
|
356
433
|
}
|
|
@@ -367,8 +444,57 @@ export async function quantizeModelDir(
|
|
|
367
444
|
}
|
|
368
445
|
|
|
369
446
|
/** Pass-through tensor: the lazy mlx array straight from the source weights. */
|
|
370
|
-
function
|
|
371
|
-
|
|
447
|
+
function outputTensor(
|
|
448
|
+
weights: Weights,
|
|
449
|
+
plan: WeightTransformPlan | undefined,
|
|
450
|
+
context: WeightTransformContext | undefined,
|
|
451
|
+
name: string,
|
|
452
|
+
): NamedTensor {
|
|
453
|
+
return { name, array: outputArray(weights, plan, context, name) };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function outputArray(
|
|
457
|
+
weights: Weights,
|
|
458
|
+
plan: WeightTransformPlan | undefined,
|
|
459
|
+
context: WeightTransformContext | undefined,
|
|
460
|
+
name: string,
|
|
461
|
+
): MlxArray {
|
|
462
|
+
if (!plan || !context) return weights.tensor(name);
|
|
463
|
+
const sourceName = plan.sourceByOutput.get(name);
|
|
464
|
+
if (!sourceName) throw new Error(`weight transform ${plan.id}: no source for ${name}`);
|
|
465
|
+
return context.apply(name, weights.tensor(sourceName));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function validateTransformPlan(
|
|
469
|
+
plan: WeightTransformPlan,
|
|
470
|
+
sourceNames: readonly string[],
|
|
471
|
+
): void {
|
|
472
|
+
const seen = new Set<string>();
|
|
473
|
+
const sources = new Set(sourceNames);
|
|
474
|
+
for (const name of plan.outputNames) {
|
|
475
|
+
if (seen.has(name)) throw new Error(`weight transform ${plan.id}: duplicate output ${name}`);
|
|
476
|
+
seen.add(name);
|
|
477
|
+
const sourceName = plan.sourceByOutput.get(name);
|
|
478
|
+
if (!sourceName)
|
|
479
|
+
throw new Error(`weight transform ${plan.id}: no source for ${name}`);
|
|
480
|
+
if (!sources.has(sourceName))
|
|
481
|
+
throw new Error(`weight transform ${plan.id}: missing source tensor ${sourceName}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function transformedConfig(
|
|
486
|
+
raw: Record<string, unknown>,
|
|
487
|
+
plan: WeightTransformPlan | undefined,
|
|
488
|
+
): Record<string, unknown> {
|
|
489
|
+
if (!plan?.untieWordEmbeddings) return raw;
|
|
490
|
+
const out: Record<string, unknown> = { ...raw, tie_word_embeddings: false };
|
|
491
|
+
if (raw.text_config && typeof raw.text_config === "object") {
|
|
492
|
+
out.text_config = {
|
|
493
|
+
...(raw.text_config as Record<string, unknown>),
|
|
494
|
+
tie_word_embeddings: false,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
372
498
|
}
|
|
373
499
|
|
|
374
500
|
/** Summary of a mixed-precision allocation, for the sidecar + return value. */
|
|
@@ -382,6 +508,57 @@ interface MixedMeta {
|
|
|
382
508
|
allocBpw: number;
|
|
383
509
|
}
|
|
384
510
|
|
|
511
|
+
const defaultProbeSource: ProbeSource = {
|
|
512
|
+
async prepare(srcDir, opts, onProgress) {
|
|
513
|
+
const config = await loadModelConfig(srcDir);
|
|
514
|
+
if (config.quantization) return { modelDir: srcDir, dispose() {} };
|
|
515
|
+
|
|
516
|
+
const { mkdtempSync, rmSync } = await import("node:fs");
|
|
517
|
+
const { tmpdir } = await import("node:os");
|
|
518
|
+
const tempDir = mkdtempSync(join(tmpdir(), "mlx-bun-qbaseline-"));
|
|
519
|
+
onProgress?.({
|
|
520
|
+
stage: "sensitivity",
|
|
521
|
+
message: "Unquantized source — building an 8-bit baseline for the sweep",
|
|
522
|
+
progress: 0,
|
|
523
|
+
});
|
|
524
|
+
try {
|
|
525
|
+
await writeQuantizedModelDir(
|
|
526
|
+
srcDir,
|
|
527
|
+
tempDir,
|
|
528
|
+
{
|
|
529
|
+
...opts,
|
|
530
|
+
bits: 8,
|
|
531
|
+
targetBpw: undefined,
|
|
532
|
+
probeSource: undefined,
|
|
533
|
+
},
|
|
534
|
+
);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
modelDir: tempDir,
|
|
541
|
+
dispose: () => rmSync(tempDir, { recursive: true, force: true }),
|
|
542
|
+
};
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
/** Acquire/use/release helper that pins the injected ProbeSource lifecycle. */
|
|
547
|
+
export async function withPreparedProbe<T>(
|
|
548
|
+
source: ProbeSource,
|
|
549
|
+
srcDir: string,
|
|
550
|
+
opts: QuantizeOptions,
|
|
551
|
+
run: (modelDir: string) => Promise<T>,
|
|
552
|
+
onProgress?: (e: ProgressEvent) => void,
|
|
553
|
+
): Promise<T> {
|
|
554
|
+
const prepared = await source.prepare(srcDir, opts, onProgress);
|
|
555
|
+
try {
|
|
556
|
+
return await run(prepared.modelDir);
|
|
557
|
+
} finally {
|
|
558
|
+
await prepared.dispose();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
385
562
|
/**
|
|
386
563
|
* Run the OptiQ mixed-precision pipeline for `srcDir`:
|
|
387
564
|
* load model + tokenizer → calibration → per-layer KL sensitivity →
|
|
@@ -407,95 +584,83 @@ async function computeMixedAllocation(
|
|
|
407
584
|
|
|
408
585
|
progress("sensitivity", "Loading model for sensitivity analysis", 0.0);
|
|
409
586
|
|
|
410
|
-
// The
|
|
411
|
-
//
|
|
412
|
-
// source
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
progress("sensitivity", "Building calibration set", 0.02);
|
|
438
|
-
const calibrationFn = loadLlmCalibration(tokenizer, {
|
|
439
|
-
nSamples: nCalibration,
|
|
440
|
-
seqLen: 128,
|
|
441
|
-
mix: calibrationMix,
|
|
442
|
-
});
|
|
443
|
-
const calIds = calibrationFn();
|
|
587
|
+
// The source dependency owns preparation and cleanup. The default builds a
|
|
588
|
+
// temporary uniform 8-bit baseline from bf16 through the lower-level writer;
|
|
589
|
+
// an injected source can point at a prebuilt probe without orchestrator
|
|
590
|
+
// recursion.
|
|
591
|
+
return withPreparedProbe(
|
|
592
|
+
opts.probeSource ?? defaultProbeSource,
|
|
593
|
+
srcDir,
|
|
594
|
+
opts,
|
|
595
|
+
async (probeDir) => {
|
|
596
|
+
const config = await loadModelConfig(probeDir);
|
|
597
|
+
const weights = await Weights.open(probeDir);
|
|
598
|
+
try {
|
|
599
|
+
const model = createModel(weights, config);
|
|
600
|
+
const tokenizer = await loadTokenizer(srcDir);
|
|
601
|
+
|
|
602
|
+
// Calibration: short sequences keep per-probe forward memory modest.
|
|
603
|
+
progress("sensitivity", "Building calibration set", 0.02);
|
|
604
|
+
const calibrationFn = loadLlmCalibration(tokenizer, {
|
|
605
|
+
nSamples: nCalibration,
|
|
606
|
+
seqLen: 128,
|
|
607
|
+
mix: calibrationMix,
|
|
608
|
+
});
|
|
609
|
+
const calIds = calibrationFn();
|
|
610
|
+
|
|
611
|
+
// Quantizable linears to probe: the model's LoRA-target linears
|
|
612
|
+
// (attention + MLP projections), keyed by full module path.
|
|
613
|
+
const layers = model.loraTargets();
|
|
444
614
|
|
|
445
|
-
// Quantizable linears to probe: the model's LoRA-target linears (attention
|
|
446
|
-
// + MLP projections), keyed by full module path.
|
|
447
|
-
const layers = model.loraTargets();
|
|
448
|
-
|
|
449
|
-
progress(
|
|
450
|
-
"sensitivity",
|
|
451
|
-
`Probing ${layers.size} layers × ${candidateBits.length} bit-widths`,
|
|
452
|
-
0.05,
|
|
453
|
-
);
|
|
454
|
-
const sensitivity: SensitivityResult[] = analyzeSensitivityExact(
|
|
455
|
-
model,
|
|
456
|
-
layers,
|
|
457
|
-
calIds,
|
|
458
|
-
{ candidateBits, groupSize },
|
|
459
|
-
(done, total, layerName) =>
|
|
460
615
|
progress(
|
|
461
616
|
"sensitivity",
|
|
462
|
-
`
|
|
463
|
-
0.05
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
617
|
+
`Probing ${layers.size} layers × ${candidateBits.length} bit-widths`,
|
|
618
|
+
0.05,
|
|
619
|
+
);
|
|
620
|
+
const sensitivity: SensitivityResult[] = analyzeSensitivityExact(
|
|
621
|
+
model,
|
|
622
|
+
layers,
|
|
623
|
+
calIds,
|
|
624
|
+
{ candidateBits, groupSize },
|
|
625
|
+
(done, total, layerName) =>
|
|
626
|
+
progress(
|
|
627
|
+
"sensitivity",
|
|
628
|
+
`Sensitivity ${done}/${total}: ${layerName}`,
|
|
629
|
+
0.05 + 0.85 * (done / Math.max(total, 1)),
|
|
630
|
+
),
|
|
631
|
+
);
|
|
477
632
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
633
|
+
// Greedy knapsack → per-layer bit allocation.
|
|
634
|
+
progress("allocating", "Optimizing per-layer bit allocation", 0.92);
|
|
635
|
+
const opt = optimizeMixedPrecision(sensitivity, {
|
|
636
|
+
targetBpw,
|
|
637
|
+
candidateBits,
|
|
638
|
+
groupSize,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
const perLayerBits = new Map<string, number>();
|
|
642
|
+
for (const c of opt.configs) perLayerBits.set(c.layerName, c.bits);
|
|
643
|
+
|
|
644
|
+
const meta: MixedMeta = {
|
|
645
|
+
targetBpw,
|
|
646
|
+
candidateBits,
|
|
647
|
+
nHigh: opt.nHighBits,
|
|
648
|
+
nLow: opt.nLowBits,
|
|
649
|
+
allocBpw: opt.achievedBpw,
|
|
650
|
+
};
|
|
485
651
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
652
|
+
progress(
|
|
653
|
+
"allocating",
|
|
654
|
+
`Allocated ${opt.nHighBits} high / ${opt.nLowBits} low (alloc bpw ${opt.achievedBpw.toFixed(2)})`,
|
|
655
|
+
0.95,
|
|
656
|
+
);
|
|
491
657
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
658
|
+
return { perLayerBits, meta };
|
|
659
|
+
} finally {
|
|
660
|
+
weights.dispose();
|
|
661
|
+
clearCache();
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
onProgress,
|
|
665
|
+
);
|
|
501
666
|
}
|