@mlx-bun/quantize 0.0.0-stage → 1.0.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.
@@ -0,0 +1,182 @@
1
+ // Calibration data loading for sensitivity analysis.
2
+ //
3
+ // Port of optiq/calibration/datasets.py `load_llm_calibration` /
4
+ // `_load_optiq_calibration`: load a multi-domain JSONL mix, tokenize, build one
5
+ // long token stream, and chunk it into fixed-length sequences. The
6
+ // `calibrationFn` contract is the TS analogue of OptIQ's
7
+ // `list[(args_tuple, kwargs_dict)]`: here, just a list of `number[]` token-id
8
+ // prompts (each a (1, seq_len) row the model forwards).
9
+ //
10
+ // Two deviations from the Python, both forced by our pure-JS tokenizer surface
11
+ // (packages/inference/src/input/tokenizer.ts exposes plain `encode` only, no
12
+ // `apply_chat_template`):
13
+ // - `messages` samples use the role-prefixed dumb-concatenation fallback —
14
+ // exactly OptIQ's `no chat template` branch.
15
+ // - `add_special_tokens=False` is the OptIQ encode flag; our `encode` already
16
+ // omits special tokens for raw substrings.
17
+ //
18
+ // Source mix: the bundled `optiq.jsonl` if locatable on disk (the oracle venv's
19
+ // optiq/calibration/data/optiq.jsonl), else a small built-in fallback corpus so
20
+ // the path works with no external data.
21
+
22
+ import { existsSync } from "node:fs";
23
+ import type { LoadedTokenizer } from "@mlx-bun/inference/input";
24
+ import { runtimeValue } from "@mlx-bun/inference/runtime/config";
25
+
26
+ /** One JSONL calibration sample (subset of the OptIQ schema we can consume). */
27
+ interface CalibrationSample {
28
+ domain?: string;
29
+ text?: string;
30
+ messages?: Array<{ role: string; content: string }>;
31
+ tools?: unknown;
32
+ }
33
+
34
+ /** Candidate on-disk locations for the bundled optiq.jsonl mix. */
35
+ function bundledMixCandidates(): string[] {
36
+ const home = process.env.HOME ?? "";
37
+ const rel = "lib/python3.14/site-packages/optiq/calibration/data/optiq.jsonl";
38
+ return [
39
+ runtimeValue("MLX_BUN_CALIBRATION_JSONL"),
40
+ `${home}/Code/mlx-lm-example/.venv/${rel}`,
41
+ `${home}/Code/mlx-lm/.venv/${rel}`,
42
+ ].filter((p): p is string => !!p);
43
+ }
44
+
45
+ /** A tiny built-in fallback corpus (used only when no JSONL mix is found).
46
+ * Multi-domain prose so groups have meaningful activation diversity. */
47
+ const FALLBACK_CORPUS: string[] = [
48
+ "The history of computing stretches back to mechanical calculators and the abacus. Charles Babbage designed the Analytical Engine, a general-purpose mechanical computer, in the nineteenth century. Ada Lovelace wrote what is often considered the first algorithm intended for such a machine. The theoretical foundations were later laid by Alan Turing, whose model of computation defined the limits of what machines can compute.",
49
+ "In physics, the conservation of energy states that the total energy of an isolated system remains constant over time. Energy can neither be created nor destroyed; rather, it transforms from one form to another. Kinetic energy becomes potential energy as a ball rises, and back again as it falls. Thermodynamics extends these ideas to heat, work, and entropy in large systems.",
50
+ "Functions in programming are reusable blocks of code that perform a specific task. They take inputs, called arguments, and may return a value. Pure functions have no side effects and always return the same output for the same input, which makes them easier to test and reason about. Recursion is a technique where a function calls itself to solve smaller subproblems.",
51
+ "The water cycle describes the continuous movement of water on, above, and below the surface of the Earth. Water evaporates from oceans and lakes, condenses into clouds, and precipitates as rain or snow. Rivers carry water back to the sea, and groundwater slowly percolates through soil and rock. Plants release water vapor through transpiration, contributing to atmospheric moisture.",
52
+ "Economic markets coordinate the production and exchange of goods through prices. When demand for a product rises and supply stays fixed, prices tend to increase, signaling producers to make more. Competition drives firms to lower costs and innovate. Externalities, such as pollution, are costs not reflected in market prices and often require regulation to address.",
53
+ "A neural network is composed of layers of interconnected nodes, each applying a weighted sum followed by a nonlinear activation. During training, the network adjusts its weights to minimize a loss function via gradient descent. Backpropagation efficiently computes gradients by applying the chain rule layer by layer. Deeper networks can represent more complex functions but are harder to train.",
54
+ ];
55
+
56
+ /**
57
+ * Build a calibration callable from a tokenizer.
58
+ *
59
+ * @returns a zero-arg function producing `number[][]` — N token-id rows, each
60
+ * of length `seqLen`, ready to forward through a model.
61
+ */
62
+ export function loadLlmCalibration(
63
+ tokenizer: LoadedTokenizer,
64
+ opts: {
65
+ nSamples?: number;
66
+ seqLen?: number;
67
+ seed?: number;
68
+ mix?: string;
69
+ } = {},
70
+ ): () => number[][] {
71
+ const nSamples = opts.nSamples ?? 32;
72
+ const seqLen = opts.seqLen ?? 1024;
73
+ const seed = opts.seed ?? 42;
74
+ const mix = opts.mix ?? "optiq";
75
+
76
+ const samples = loadSamples(mix);
77
+
78
+ // Concatenate every sample's tokens into one long stream.
79
+ const allTokens: number[] = [];
80
+ for (const s of samples) {
81
+ let text: string | undefined;
82
+ if (s.messages) {
83
+ // No chat template available in our JS tokenizer — role-prefixed
84
+ // concatenation (OptIQ's no-chat-template fallback branch).
85
+ text = s.messages.map((m) => `[${m.role}]: ${m.content}`).join("\n\n");
86
+ } else if (s.text !== undefined) {
87
+ text = s.text;
88
+ } else {
89
+ continue;
90
+ }
91
+ const toks = tokenizer.encode(text);
92
+ for (const t of toks) allTokens.push(t);
93
+ }
94
+
95
+ if (allTokens.length < seqLen) {
96
+ throw new Error(
97
+ `Calibration mix produced only ${allTokens.length} tokens (< seqLen=${seqLen}). ` +
98
+ `Try a smaller seqLen or a larger mix.`,
99
+ );
100
+ }
101
+
102
+ // Chunk start offsets, shuffled deterministically (seeded), like OptIQ.
103
+ const starts: number[] = [];
104
+ for (let s = 0; s < allTokens.length - seqLen; s += seqLen) starts.push(s);
105
+ shuffleInPlace(starts, seed);
106
+
107
+ const chunks: number[][] = [];
108
+ for (const start of starts.slice(0, nSamples)) {
109
+ chunks.push(allTokens.slice(start, start + seqLen));
110
+ }
111
+
112
+ if (chunks.length === 0) {
113
+ throw new Error("Could not create any calibration sequences.");
114
+ }
115
+
116
+ return () => chunks;
117
+ }
118
+
119
+ /** Load raw samples from the mix spec ("optiq" → bundled, else a JSONL path,
120
+ * else the built-in fallback corpus). */
121
+ function loadSamples(mix: string): CalibrationSample[] {
122
+ let jsonlPath: string | null = null;
123
+ if (mix === "optiq") {
124
+ jsonlPath = bundledMixCandidates().find((p) => existsSync(p)) ?? null;
125
+ } else if (existsSync(mix)) {
126
+ jsonlPath = mix;
127
+ } else {
128
+ throw new Error(
129
+ `calibration mix not found at ${mix}. Use mix='optiq' (default) or a ` +
130
+ `path to a JSONL file.`,
131
+ );
132
+ }
133
+
134
+ if (jsonlPath) return parseJsonl(jsonlPath);
135
+
136
+ // Built-in fallback: synthesize one prose sample per corpus entry.
137
+ return FALLBACK_CORPUS.map((text) => ({ domain: "prose", text }));
138
+ }
139
+
140
+ /** Parse a JSONL file into CalibrationSample[] (one object per non-blank line). */
141
+ function parseJsonl(path: string): CalibrationSample[] {
142
+ // Synchronous read so the caller can stay synchronous; the file is small.
143
+ const text = readFileSyncUtf8(path);
144
+ const out: CalibrationSample[] = [];
145
+ for (const line of text.split("\n")) {
146
+ const trimmed = line.trim();
147
+ if (trimmed) out.push(JSON.parse(trimmed) as CalibrationSample);
148
+ }
149
+ return out;
150
+ }
151
+
152
+ function readFileSyncUtf8(path: string): string {
153
+ // Bun's readFileSync via node:fs is available in this runtime.
154
+ // (Kept local so the rest of the module is import-light.)
155
+ const { readFileSync } = require("node:fs") as typeof import("node:fs");
156
+ return readFileSync(path, "utf8");
157
+ }
158
+
159
+ /** Deterministic Fisher–Yates shuffle (mulberry32 PRNG seeded by `seed`).
160
+ * NOTE: this does NOT reproduce numpy's RandomState shuffle order — it only
161
+ * needs to be deterministic for our own reproducibility; the exact sample
162
+ * ordering does not affect the allocator's 1:1 parity (that test feeds
163
+ * synthetic SensitivityResults directly). */
164
+ function shuffleInPlace<T>(arr: T[], seed: number): void {
165
+ const rand = mulberry32(seed >>> 0);
166
+ for (let i = arr.length - 1; i > 0; i--) {
167
+ const j = Math.floor(rand() * (i + 1));
168
+ const tmp = arr[i]!;
169
+ arr[i] = arr[j]!;
170
+ arr[j] = tmp;
171
+ }
172
+ }
173
+
174
+ function mulberry32(a: number): () => number {
175
+ return () => {
176
+ a |= 0;
177
+ a = (a + 0x6d2b79f5) | 0;
178
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
179
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
180
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
181
+ };
182
+ }
@@ -0,0 +1,109 @@
1
+ // Config + sidecar writer for a quantized model directory.
2
+ //
3
+ // Produces the `config.json` block the loader (src/config.ts → parseQuantization)
4
+ // reads back: a top-level `quantization` object AND a mirror `quantization_config`
5
+ // (HF compatibility — config.ts reads `raw.quantization ?? raw.quantization_config`).
6
+ // The block carries default {group_size, bits, mode} plus per-module overrides
7
+ // keyed by module path (e.g. "model.embed_tokens") — exactly the OptiQ layout.
8
+ //
9
+ // Aux files (tokenizer, chat template, generation config, …) are copied through
10
+ // verbatim so the output directory is a complete, loadable model snapshot.
11
+
12
+ import { join } from "node:path";
13
+ import { copyAuxFiles } from "@mlx-bun/inference/artifacts/auxiliary-files";
14
+
15
+ /** Compatibility re-export; `@mlx-bun/inference/artifacts/auxiliary-files` owns it. */
16
+ export { copyAuxFiles };
17
+
18
+ /** A per-module quantization override. `false` means "left unquantized". */
19
+ export type PerLayerEntry =
20
+ | { bits: number; groupSize: number; mode?: string; trellis?: { L: number; code: string; axis: 0 | 1 } }
21
+ | false;
22
+
23
+ /** The default quantization definition for the model. */
24
+ export interface QuantDef {
25
+ bits: number;
26
+ groupSize: number;
27
+ mode?: string;
28
+ }
29
+
30
+ /** The serialized quantization block, ready to drop into config.json. */
31
+ export interface QuantizationBlock {
32
+ group_size: number;
33
+ bits: number;
34
+ mode: string;
35
+ // plus per-module entries: { [modulePath]: {bits, group_size} | false }
36
+ [modulePath: string]: number | string | { bits: number; group_size: number; mode?: string; trellis?: { L: number; code: string; axis: 0 | 1 } } | false;
37
+ }
38
+
39
+ /**
40
+ * Build the `quantization` config object: a `{group_size, bits, mode}` default
41
+ * plus one entry per module in `perLayer`. A module mapped to `false` is
42
+ * serialized as `false` (mlx's "not quantized" convention); otherwise it
43
+ * becomes `{bits, group_size}`.
44
+ */
45
+ export function buildQuantizationBlock(
46
+ def: QuantDef,
47
+ perLayer: Map<string, PerLayerEntry>,
48
+ ): QuantizationBlock {
49
+ const block: QuantizationBlock = {
50
+ group_size: def.groupSize,
51
+ bits: def.bits,
52
+ mode: def.mode ?? "affine",
53
+ };
54
+ for (const [path, entry] of perLayer) {
55
+ if (entry === false) { block[path] = false; continue; }
56
+ const v: { bits: number; group_size: number; mode?: string; trellis?: { L: number; code: string; axis: 0 | 1 } } =
57
+ { bits: entry.bits, group_size: entry.groupSize };
58
+ if (entry.mode) v.mode = entry.mode;
59
+ if (entry.trellis) v.trellis = entry.trellis;
60
+ block[path] = v;
61
+ }
62
+ return block;
63
+ }
64
+
65
+ /** Metadata for the OptiQ-style sidecar describing the achieved quantization. */
66
+ export interface OptiqMetadata {
67
+ method: string;
68
+ base_model: string;
69
+ bits: number;
70
+ group_size: number;
71
+ achieved_bpw: number;
72
+ per_layer_count: number;
73
+ weight_transforms?: Array<{
74
+ id: string;
75
+ seed: number;
76
+ family: string;
77
+ deviations: string[];
78
+ }>;
79
+ }
80
+
81
+ /**
82
+ * Write `config.json` (deep copy of `srcConfigRaw` with both `quantization`
83
+ * and `quantization_config` set to `block`) into `outDir`, copy through any
84
+ * present aux files, and write `optiq_metadata.json` describing the run.
85
+ *
86
+ * `*.model` tokenizer files beyond the known names are also swept in so SPM /
87
+ * tiktoken sidecars survive the round-trip.
88
+ */
89
+ export async function writeQuantizedConfig(
90
+ srcConfigRaw: Record<string, unknown>,
91
+ outDir: string,
92
+ block: QuantizationBlock,
93
+ opts: { srcDir?: string; optiq?: OptiqMetadata } = {},
94
+ ): Promise<void> {
95
+ // Deep copy so we never mutate the caller's config object.
96
+ const config = JSON.parse(JSON.stringify(srcConfigRaw)) as Record<string, unknown>;
97
+ config.quantization = block;
98
+ config.quantization_config = block;
99
+ await Bun.write(join(outDir, "config.json"), JSON.stringify(config, null, 2));
100
+
101
+ if (opts.srcDir) await copyAuxFiles(opts.srcDir, outDir);
102
+
103
+ if (opts.optiq) {
104
+ await Bun.write(
105
+ join(outDir, "optiq_metadata.json"),
106
+ JSON.stringify(opts.optiq, null, 2),
107
+ );
108
+ }
109
+ }
package/src/convert.ts ADDED
@@ -0,0 +1,79 @@
1
+ // Non-quantizing conversion (mlx_lm.convert without -q): rewrite a checkpoint
2
+ // with its floating tensors cast to one dtype and/or its quantized modules
3
+ // dequantized to dense weights. Tensor names, shard layout rules, and aux files
4
+ // follow the quantizer's writer; the CPU stream discipline is the same.
5
+
6
+ import { mkdirSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { clearCache } from "@mlx-bun/mlx/ffi";
9
+ import { cpuStream } from "@mlx-bun/mlx/array";
10
+ import { dequantize } from "@mlx-bun/mlx/ops";
11
+ import { Weights, loadModelConfig, quantFor, writeShardedSafetensors, type NamedTensor, type WriteResult } from "@mlx-bun/inference/artifacts";
12
+ import { copyAuxFiles } from "@mlx-bun/inference/artifacts/auxiliary-files";
13
+ import { writeAtomicDirectory } from "./atomic-output";
14
+ import { castTensor, type ConvertDtype, type ProgressEvent } from "./quantizer";
15
+
16
+ export interface ConvertOptions {
17
+ /** Cast every floating tensor (quantization scales/biases included) to this dtype. */
18
+ dtype?: ConvertDtype;
19
+ /** Replace each quantized module with its dense weight and drop the config's quantization block. */
20
+ dequantize?: boolean;
21
+ }
22
+
23
+ export interface ConvertResult {
24
+ outDir: string;
25
+ /** Modules that were dequantized to dense weights. */
26
+ nDequantized: number;
27
+ write: WriteResult;
28
+ }
29
+
30
+ /** Rewrite `srcDir` into `outDir` (published atomically). Without options the
31
+ * tensors are copied unchanged, as `mlx_lm.convert` without -q/--dtype does. */
32
+ export async function convertModelDir(
33
+ srcDir: string,
34
+ outDir: string,
35
+ opts: ConvertOptions = {},
36
+ onProgress?: (e: ProgressEvent) => void,
37
+ ): Promise<ConvertResult> {
38
+ return writeAtomicDirectory(outDir, async (staging) => {
39
+ const progress = (stage: string, message: string, fraction: number) => onProgress?.({ stage, message, progress: fraction });
40
+ progress("loading", `Reading ${srcDir}`, 0);
41
+ const config = await loadModelConfig(srcDir);
42
+ const weights = await Weights.open(srcDir);
43
+ const out: NamedTensor[] = [];
44
+ try {
45
+ const names = weights.tensorNames;
46
+ const present = new Set(names), scaled = new Set<string>();
47
+ for (const name of names) if (name.endsWith(".scales")) scaled.add(name.slice(0, -".scales".length));
48
+ // Modules to dequantize: a `.scales` sibling and a quantization spec for the module.
49
+ const dense = new Set([...scaled].filter(base => opts.dequantize && quantFor(config.quantization, base)));
50
+ let processed = 0;
51
+ for (const name of names) {
52
+ const base = /^(.*)\.(weight|scales|biases)$/.exec(name)?.[1];
53
+ if (base && dense.has(base)) {
54
+ if (!name.endsWith(".weight")) continue; // scales/biases are folded into the dense weight
55
+ const spec = quantFor(config.quantization, base)!;
56
+ const packed = weights.tensor(name), scales = weights.tensor(`${base}.scales`);
57
+ const biases = present.has(`${base}.biases`) ? weights.tensor(`${base}.biases`) : null;
58
+ const weight = dequantize(packed, scales, biases, spec, cpuStream);
59
+ out.push({ name, array: castTensor(name, weight, opts.dtype) });
60
+ } else out.push({ name, array: castTensor(name, weights.tensor(name), opts.dtype) });
61
+ if (++processed % 16 === 0) clearCache();
62
+ progress("converting", `Tensor ${processed}/${names.length}`, processed / names.length);
63
+ }
64
+ progress("writing", `Writing ${out.length} tensors to ${outDir}`, 1);
65
+ mkdirSync(staging, { recursive: true });
66
+ const write = writeShardedSafetensors(staging, out);
67
+ const raw = JSON.parse(JSON.stringify(config.raw)) as Record<string, unknown>;
68
+ if (opts.dequantize) { delete raw.quantization; delete raw.quantization_config; }
69
+ await Bun.write(join(staging, "config.json"), JSON.stringify(raw, null, 2));
70
+ await copyAuxFiles(srcDir, staging);
71
+ progress("done", `Converted ${out.length} tensors → ${outDir}`, 1);
72
+ return { outDir, nDequantized: dense.size, write };
73
+ } finally {
74
+ for (const tensor of out) { try { tensor.array.dispose(); } catch { /* already released */ } }
75
+ weights.dispose();
76
+ clearCache();
77
+ }
78
+ });
79
+ }
package/src/drafter.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { rowOperationFor } from "@mlx-bun/inference/models/graph";
2
+ // Quantize a DeepSpec-format drafter checkpoint (DeepSeek's released
3
+ // `Gemma4DSparkModel`; a locally trained dspark.json drafter is small enough to
4
+ // serve as trained).
5
+ //
6
+ // Thin policy layer over this package's quantizer: the generic
7
+ // shape-eligibility rule already captures every 2-D matmul weight — the
8
+ // layer projections, fc, lm_head, markov_w2, and the two gather tables
9
+ // (embed_tokens, markov_w1: quantized-embedding gather-dequant at load,
10
+ // mlx QuantizedEmbedding semantics). The drafter-specific overlay:
11
+ //
12
+ // KEEP bf16: confidence_head.* — threshold-sensitive (the truncation
13
+ // comparison runs on its sigmoid; borderline flips change proposal
14
+ // lengths) and tiny (~4k params), so quantizing it buys nothing.
15
+ // Norms / layer_scalar / biases are 1-D and never shape-eligible.
16
+ //
17
+ // Drafter numerics only move ACCEPTANCE, never correctness (the target
18
+ // verifies every draft) — the quality gate for any scheme swept through
19
+ // here is the drafter acceptance A/B (`scripts/drafter-ab.ts`), not a KL battery.
20
+
21
+ import { quantizeModelDir, type QuantizeResult } from "./quantizer";
22
+ import { isDeepspecDrafterConfig } from "@mlx-bun/inference/models/drafters";
23
+ import { loadDeepspecDrafter } from "@mlx-bun/inference/models/drafter-loaders";
24
+ import { MlxArray } from "@mlx-bun/mlx/array";
25
+ import { Dtype } from "@mlx-bun/mlx/ffi";
26
+
27
+ /** Module bases kept full-precision regardless of shape eligibility. */
28
+ const KEEP_BF16 = [/^confidence_head\./];
29
+
30
+ /** The drafter per-tensor policy: quantize everything shape-eligible
31
+ * except the KEEP_BF16 set. */
32
+ export function drafterQuantizePredicate(base: string): boolean {
33
+ return !KEEP_BF16.some((re) => re.test(base));
34
+ }
35
+
36
+ export interface QuantizeDrafterOptions {
37
+ bits: 4 | 8;
38
+ groupSize: 32 | 64;
39
+ /** Skip the post-write load smoke (tests exercise loading separately). */
40
+ skipLoadSmoke?: boolean;
41
+ onProgress?: (stage: string, message: string, progress: number) => void;
42
+ }
43
+
44
+ /**
45
+ * Quantize the DeepSpec drafter at `srcDir` into `outDir` and smoke the
46
+ * result: reload through the DeepSpec drafter loader (quantized detection + graph
47
+ * build) and eval one projectContext row through the quantized fc path.
48
+ */
49
+ export async function quantizeDrafterDir(
50
+ srcDir: string,
51
+ outDir: string,
52
+ opts: QuantizeDrafterOptions,
53
+ ): Promise<QuantizeResult> {
54
+ const raw = (await Bun.file(`${srcDir}/config.json`).json()) as Record<string, any>;
55
+ if (!isDeepspecDrafterConfig(raw))
56
+ throw new Error(
57
+ `quantizeDrafterDir: expected a DeepSpec checkpoint (architectures[0]==="Gemma4DSparkModel"), got ${JSON.stringify(raw.architectures)} — for regular models use \`mlx-bun convert -q\``,
58
+ );
59
+ if (raw.quantization ?? raw.quantization_config)
60
+ throw new Error("quantizeDrafterDir: source is already quantized — start from the bf16 checkpoint");
61
+
62
+ const result = await quantizeModelDir(
63
+ srcDir,
64
+ outDir,
65
+ {
66
+ bits: opts.bits,
67
+ groupSize: opts.groupSize,
68
+ quantizePredicate: drafterQuantizePredicate,
69
+ },
70
+ opts.onProgress ? (e) => opts.onProgress!(e.stage, e.message, e.progress) : undefined,
71
+ );
72
+
73
+ if (!opts.skipLoadSmoke) {
74
+ // Load smoke: quantized detection + one real quantized matmul (fc) so a
75
+ // wrong spec/layout fails HERE, not at serve time.
76
+ const d = await loadDeepspecDrafter(outDir);
77
+ try {
78
+ const width = d.tapLayers.length * d.hidden;
79
+ const zeros = MlxArray.fromFloat32(new Float32Array(width), [1, 1, width]);
80
+ const x = zeros.astype(Dtype.bfloat16);
81
+ zeros.dispose();
82
+ const proj = rowOperationFor(d.projectContext,1)(x); // one context row
83
+ x.dispose();
84
+ const got = proj.shape;
85
+ proj.dispose();
86
+ if (got.length !== 3 || got[2] !== d.hidden)
87
+ throw new Error(`load smoke: projectContext shape ${JSON.stringify(got)}, expected [1,1,${d.hidden}]`);
88
+ } finally {
89
+ d.dispose();
90
+ }
91
+ }
92
+
93
+ return result;
94
+ }
package/src/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ // Public checkpoint-quantization API: allocation, weight transforms, sharded
2
+ // output, and quantization metadata. Model discovery belongs to @mlx-bun/hub.
3
+
4
+ export { quantizeModelDir, isQuantizable, withPreparedProbe, CONVERT_DTYPES } from "./quantizer";
5
+ export { convertModelDir } from "./convert";
6
+ export { quantizeDrafterDir, drafterQuantizePredicate } from "./drafter";
7
+ export type { QuantizeDrafterOptions } from "./drafter";
8
+ export type { ConvertOptions, ConvertResult } from "./convert";
9
+ export type {
10
+ ConvertDtype,
11
+ PreparedProbe,
12
+ ProbeSource,
13
+ QuantizeOptions,
14
+ QuantizeResult,
15
+ ProgressEvent,
16
+ } from "./quantizer";
17
+
18
+ export {
19
+ planQwen35Fold,
20
+ planQwenMtpFold,
21
+ } from "./rotate";
22
+ export type {
23
+ FoldOptions,
24
+ QwenFoldOptions,
25
+ } from "./rotate";
26
+
27
+ export {
28
+ automaticRotationWeightTransform,
29
+ llamaWeightTransform,
30
+ planLlamaWeightTransform,
31
+ qwen35WeightTransform,
32
+ qwenMtpWeightTransform,
33
+ } from "./weight-transform";
34
+ export type {
35
+ LlamaWeightTransformPlan,
36
+ QwenWeightTransformPlan,
37
+ WeightTransform,
38
+ WeightTransformContext,
39
+ WeightTransformMetadata,
40
+ WeightTransformPlan,
41
+ } from "./weight-transform";
42
+
43
+ export { quantizeTrellisModelDir } from "./trellis-quantizer";
44
+ export type { TrellisQuantizeOptions, TrellisQuantizeResult } from "./trellis-quantizer";
45
+ export { TrellisEncoder, loadHessianFactor } from "./trellis-encoder";
46
+ export type { PackedTrellisTensor, LdlqEncoding, TrellisEncoderOptions } from "./trellis-encoder";
47
+ export { parseKMap, treatmentFor, trellisBpw, mlxLmPath } from "./trellis-allocation";
48
+ export type { DownAxis, Treatment, TreatmentPolicy, TrellisKMap } from "./trellis-allocation";
49
+ export { planQwen35HfFold } from "./qwen35-hf-fold";
50
+ export type { Qwen35HfFoldPlan, Qwen35HfFoldSource } from "./qwen35-hf-fold";
51
+
52
+ export { writeShardedSafetensors, DEFAULT_SHARD_BYTES } from "@mlx-bun/inference/artifacts";
53
+ export type { NamedTensor, ShardInfo, SafetensorsIndex, WriteResult, WriteOpts } from "@mlx-bun/inference/artifacts";
54
+
55
+ export {
56
+ buildQuantizationBlock,
57
+ writeQuantizedConfig,
58
+ } from "./config-writer";
59
+ export type {
60
+ QuantDef,
61
+ PerLayerEntry,
62
+ QuantizationBlock,
63
+ OptiqMetadata,
64
+ } from "./config-writer";
65
+
66
+ // Mixed-precision (OptiQ sensitivity + knapsack) surface.
67
+ export {
68
+ optimizeMixedPrecision,
69
+ computeBpw,
70
+ klReduction,
71
+ UpgradeHeap,
72
+ } from "./allocator";
73
+ export type {
74
+ LayerQuantConfig,
75
+ OptimizationResult,
76
+ OptimizeMixedPrecisionOptions,
77
+ UpgradeEntry,
78
+ } from "./allocator";
79
+
80
+ export { analyzeSensitivityExact, klFromRef } from "./sensitivity";
81
+ export type {
82
+ SensitivityResult,
83
+ AnalyzeSensitivityOptions,
84
+ } from "./sensitivity";
85
+
86
+ export { loadLlmCalibration } from "./calibration";