@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.
- package/LICENSE +21 -0
- package/README.md +56 -2
- package/package.json +44 -4
- package/src/allocator.ts +628 -0
- package/src/atomic-output.ts +38 -0
- package/src/calibration.ts +182 -0
- package/src/config-writer.ts +109 -0
- package/src/convert.ts +79 -0
- package/src/drafter.ts +94 -0
- package/src/index.ts +86 -0
- package/src/quantizer.ts +691 -0
- package/src/qwen35-hf-fold.ts +103 -0
- package/src/rotate.ts +610 -0
- package/src/sensitivity.ts +216 -0
- package/src/trellis-allocation.ts +111 -0
- package/src/trellis-encoder.ts +328 -0
- package/src/trellis-quantizer.ts +405 -0
- package/src/trellis.ts +570 -0
- package/src/weight-transform.ts +299 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// Packed-Trellis quantizer for the HF Qwen3.5 layout (Qwen3.8-27B family):
|
|
2
|
+
// fold the checkpoint (γ + R1), code every MLP tensor with the bitshift trellis
|
|
3
|
+
// (QTIP TCQ, L=12, 1MAD, T=256, tail-biting) and write the on-disk packed
|
|
4
|
+
// format `@mlx-bun/inference` serves. Everything else follows the shipped
|
|
5
|
+
// compact allocation: 4-bit affine embed/lm_head/attention, 3-bit affine for the
|
|
6
|
+
// rest, vision tower bf16. Stock mlx-lm cannot load the result (config mode
|
|
7
|
+
// "trellis"); this engine can.
|
|
8
|
+
//
|
|
9
|
+
// STREAMING: source shard in → fold → code → incremental shard writer, releasing
|
|
10
|
+
// each source shard, so peak memory is one source shard plus one output shard
|
|
11
|
+
// plus the tensor in flight.
|
|
12
|
+
//
|
|
13
|
+
// Optional inputs, all of which change which codes are produced and never how:
|
|
14
|
+
// * `kMap` per-tensor bits from an allocation file (mixed k)
|
|
15
|
+
// * `ldlq` a directory of per-layer Hessian factors (layer-NNN-{mlp,down}.safetensors,
|
|
16
|
+
// tensor `L`), switching the objective to BlockLDLQ error feedback
|
|
17
|
+
// * `reuse` packed artifacts whose tensors of unchanged geometry are copied
|
|
18
|
+
// instead of re-encoded (a variant bank)
|
|
19
|
+
// * `interleave` reorder eligible k=3 axis-0 codes into the two-block layout
|
|
20
|
+
// the scatter kernel reads
|
|
21
|
+
//
|
|
22
|
+
// The Hessian factors and the k-map are inputs: collecting activation Hessians
|
|
23
|
+
// needs a forward pass over calibration text and is not part of this producer.
|
|
24
|
+
|
|
25
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { MlxArray as Arr, cpuStream, gpuStream, type MlxArray } from "@mlx-bun/mlx/array";
|
|
28
|
+
import { Dtype, activeMemory, cacheMemory, clearCache } from "@mlx-bun/mlx/ffi";
|
|
29
|
+
import * as ops from "@mlx-bun/mlx/ops";
|
|
30
|
+
import { quantize } from "@mlx-bun/mlx/ops";
|
|
31
|
+
import { TURBOQUANT_FOLD_FILE, Weights, ShardedWriter, type WriteResult } from "@mlx-bun/inference/artifacts";
|
|
32
|
+
import { writeAtomicDirectory } from "./atomic-output";
|
|
33
|
+
import { buildQuantizationBlock, writeQuantizedConfig, type PerLayerEntry } from "./config-writer";
|
|
34
|
+
import type { ProgressEvent } from "./quantizer";
|
|
35
|
+
import { QwenFoldContext } from "./rotate";
|
|
36
|
+
import { planQwen35HfFold } from "./qwen35-hf-fold";
|
|
37
|
+
import { TRELLIS_L, TRELLIS_T, interleaveTrellisCodes } from "./trellis";
|
|
38
|
+
import { TrellisEncoder, loadHessianFactor, type PackedTrellisTensor } from "./trellis-encoder";
|
|
39
|
+
import {
|
|
40
|
+
LM_PREFIX, TRELLIS_AFFINE_GROUP, TRELLIS_BASE_BITS, mlxLmPath, parseKMap, treatmentFor, trellisBpw,
|
|
41
|
+
type DownAxis, type Treatment, type TrellisKMap, type TreatmentPolicy,
|
|
42
|
+
} from "./trellis-allocation";
|
|
43
|
+
|
|
44
|
+
/** The k=3 axis-0 two-block interleave needs whole 48-word groups (2 blocks × 24 words). */
|
|
45
|
+
const INTERLEAVE_WORDS = 48;
|
|
46
|
+
/** The trellis encoder holds ~2 GB of its own, so shards stay below the writer's default ceiling. */
|
|
47
|
+
const DEFAULT_SHARD_BYTES = 2 * 1024 ** 3;
|
|
48
|
+
/** `final_gain_convention` in turboquant_fold.json, verbatim as the published artifact's sidecar records it. */
|
|
49
|
+
const FINAL_GAIN_CONVENTION = "effective final-norm gain gamma = stored + 1 of the source model`s final norm, " +
|
|
50
|
+
"the value folded into lm_head; a drafter that enters the folded head divides by it";
|
|
51
|
+
|
|
52
|
+
export interface TrellisQuantizeOptions {
|
|
53
|
+
/** Trellis bits per weight for every coded tensor without a k-map entry. Default 3. */
|
|
54
|
+
bits?: number;
|
|
55
|
+
/** Per-tensor bits (and affine overrides) from an allocation file's `budgets[budget]`. */
|
|
56
|
+
kMap?: { path: string; budget?: string };
|
|
57
|
+
/** Dim of down_proj the trellis runs along. Default `out` (the rotated dim). */
|
|
58
|
+
downAxis?: DownAxis;
|
|
59
|
+
/** Directory of per-layer Hessian factors; enables BlockLDLQ. */
|
|
60
|
+
ldlq?: string;
|
|
61
|
+
/** Packed artifacts to copy unchanged-geometry tensors from, first match wins. */
|
|
62
|
+
reuse?: string[];
|
|
63
|
+
/** Reorder eligible k=3 axis-0 codes into the two-block scatter layout. */
|
|
64
|
+
interleave?: boolean;
|
|
65
|
+
/** Trellis-code only the first N decoder layers (the rest ride the affine base tier). */
|
|
66
|
+
layers?: number;
|
|
67
|
+
/** Rotation seed (the MTP companion must use its trunk's). Default 42. */
|
|
68
|
+
seed?: number;
|
|
69
|
+
/** Coded rows per Viterbi batch; a memory knob that never changes the codes. Default 16384. */
|
|
70
|
+
batch?: number;
|
|
71
|
+
/** Output shard size in bytes. Default 2 GiB. */
|
|
72
|
+
shardBytes?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TrellisQuantizeResult {
|
|
76
|
+
outDir: string;
|
|
77
|
+
nTrellis: number;
|
|
78
|
+
nAffine: number;
|
|
79
|
+
/** Coded bits per weight over the coded modules (fp16 row scales / affine group overhead included). */
|
|
80
|
+
effectiveBpw: number;
|
|
81
|
+
/** Trellis tensors copied from `reuse` sources. */
|
|
82
|
+
reused: number;
|
|
83
|
+
ldlq: { applied: number; guardTrips: number; missingFactors: number; trippedTensors: string[] } | null;
|
|
84
|
+
write: WriteResult;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Quantize the full-precision HF-layout Qwen3.5-family checkpoint `srcDir`
|
|
88
|
+
* into a packed-Trellis artifact at `outDir` (published atomically). */
|
|
89
|
+
export async function quantizeTrellisModelDir(
|
|
90
|
+
srcDir: string,
|
|
91
|
+
outDir: string,
|
|
92
|
+
opts: TrellisQuantizeOptions = {},
|
|
93
|
+
onProgress?: (e: ProgressEvent) => void,
|
|
94
|
+
): Promise<TrellisQuantizeResult> {
|
|
95
|
+
const result = await writeAtomicDirectory(outDir, (staging) => produce(srcDir, staging, opts, onProgress));
|
|
96
|
+
return { ...result, outDir };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function produce(
|
|
100
|
+
srcDir: string,
|
|
101
|
+
outDir: string,
|
|
102
|
+
opts: TrellisQuantizeOptions,
|
|
103
|
+
onProgress?: (e: ProgressEvent) => void,
|
|
104
|
+
): Promise<TrellisQuantizeResult> {
|
|
105
|
+
const seed = opts.seed ?? 42;
|
|
106
|
+
const bits = opts.bits ?? 3;
|
|
107
|
+
const downAxis: DownAxis = opts.downAxis ?? "out";
|
|
108
|
+
const ldlqDir = opts.ldlq ?? "";
|
|
109
|
+
const kMapPath = opts.kMap?.path;
|
|
110
|
+
const kBudget = opts.kMap?.budget ?? "3.00";
|
|
111
|
+
if (!Number.isInteger(bits) || bits < 1 || bits > 8) throw new Error(`trellis bits must be an integer in [1, 8] (got ${bits})`);
|
|
112
|
+
if (!Number.isInteger(seed)) throw new Error(`rotation seed must be an integer (got ${seed})`);
|
|
113
|
+
const groupSize = TRELLIS_AFFINE_GROUP;
|
|
114
|
+
const progress = (stage: string, message: string, fraction: number) => onProgress?.({ stage, message, progress: fraction });
|
|
115
|
+
progress("loading", `Reading ${srcDir}`, 0);
|
|
116
|
+
|
|
117
|
+
// ------------------------------------------------------------- config ---
|
|
118
|
+
const raw = JSON.parse(readFileSync(join(srcDir, "config.json"), "utf8")) as Record<string, unknown>;
|
|
119
|
+
if (raw.quantization || raw.quantization_config)
|
|
120
|
+
throw new Error("source is already quantized — the fold needs full-precision weights");
|
|
121
|
+
const textCfg = (raw.text_config ?? raw) as Record<string, unknown>;
|
|
122
|
+
const hiddenSize = Number(textCfg.hidden_size);
|
|
123
|
+
const numHiddenLayers = Number(textCfg.num_hidden_layers);
|
|
124
|
+
const layerTypes = textCfg.layer_types as string[];
|
|
125
|
+
|
|
126
|
+
const kMap: TrellisKMap | undefined = kMapPath
|
|
127
|
+
? parseKMap(JSON.parse(readFileSync(kMapPath, "utf8")), kBudget, `k-map ${kMapPath}`)
|
|
128
|
+
: undefined;
|
|
129
|
+
const policy: TreatmentPolicy = { bits, kMap, downAxis, layers: opts.layers };
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------- run ---
|
|
132
|
+
const weights = await Weights.open(srcDir);
|
|
133
|
+
const names = weights.tensorNames;
|
|
134
|
+
const plan = (() => {
|
|
135
|
+
try { return planQwen35HfFold(weights, { numHiddenLayers, layerTypes }); }
|
|
136
|
+
catch (error) { weights.dispose(); throw error; }
|
|
137
|
+
})();
|
|
138
|
+
const { ops: foldOps, gammaNames, zeroNorms, headGamma } = plan;
|
|
139
|
+
|
|
140
|
+
const one = Arr.fromFloat32(new Float32Array(1).fill(1), [1]);
|
|
141
|
+
// Stored gains are γ−1: the fold context reads γ = stored + 1.
|
|
142
|
+
const gammaSource: Pick<Weights, "tensor"> = {
|
|
143
|
+
tensor(name: string): MlxArray {
|
|
144
|
+
const f32 = weights.tensor(name).astype(Dtype.float32, cpuStream);
|
|
145
|
+
const shifted = ops.add(f32, one, cpuStream);
|
|
146
|
+
shifted.eval();
|
|
147
|
+
f32.dispose();
|
|
148
|
+
return shifted;
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
const ctx = new QwenFoldContext(gammaSource, hiddenSize, seed, gammaNames, true);
|
|
152
|
+
const encoder = new TrellisEncoder({ batch: opts.batch });
|
|
153
|
+
|
|
154
|
+
const zerosLike = (src: MlxArray): MlxArray => {
|
|
155
|
+
const n = src.shape[0]!;
|
|
156
|
+
const z = Arr.fromFloat32(new Float32Array(n), [n]);
|
|
157
|
+
const out = z.astype(Dtype.bfloat16, cpuStream);
|
|
158
|
+
out.eval();
|
|
159
|
+
z.dispose();
|
|
160
|
+
return out;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (kMap) {
|
|
164
|
+
const hist = new Map<number, number>();
|
|
165
|
+
for (const k of kMap.k.values()) hist.set(k, (hist.get(k) ?? 0) + 1);
|
|
166
|
+
progress("planning", `k-map ${kMapPath} budget ${kBudget}: ` +
|
|
167
|
+
[...hist].sort((a, b) => a[0] - b[0]).map(([k, n]) => `k${k}×${n}`).join(" "), 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Calibration provenance, lifted verbatim from the Hessian stage's state.json
|
|
171
|
+
* so the artifact records WHICH corpus the LDLQ objective was fitted on. */
|
|
172
|
+
const ldlqCalib: unknown = ldlqDir && existsSync(join(ldlqDir, "state.json"))
|
|
173
|
+
? (JSON.parse(readFileSync(join(ldlqDir, "state.json"), "utf8")) as Record<string, unknown>).calibration
|
|
174
|
+
: null;
|
|
175
|
+
|
|
176
|
+
const reuseSources = await Promise.all((opts.reuse ?? []).map(async (dir) => ({
|
|
177
|
+
dir,
|
|
178
|
+
weights: await Weights.open(dir),
|
|
179
|
+
config: (JSON.parse(readFileSync(join(dir, "config.json"), "utf8")) as { quantization: Record<string, unknown> }).quantization,
|
|
180
|
+
})));
|
|
181
|
+
/** Find a trellis entry with the same k and axis in the ordered source bank. */
|
|
182
|
+
const reusable = (base: string, k: number, axis: 0 | 1) =>
|
|
183
|
+
reuseSources.find(({ weights: w, config }) => {
|
|
184
|
+
const e = config[base] as { mode?: string; bits?: number; trellis?: { axis?: number } } | undefined;
|
|
185
|
+
return !!e && e.mode === "trellis" && e.bits === k && e.trellis?.axis === axis &&
|
|
186
|
+
w.has(`${base}.weight`) && w.has(`${base}.scales`);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ----------------------------------------------------------- accounting ---
|
|
190
|
+
const perLayer = new Map<string, PerLayerEntry>();
|
|
191
|
+
let nAffine = 0, affineParams = 0, affineBits = 0;
|
|
192
|
+
let nTrellis = 0, trellisParams = 0, trellisBits = 0;
|
|
193
|
+
let ldlqApplied = 0, ldlqTrips = 0, ldlqMissing = 0;
|
|
194
|
+
const trippedTensors: string[] = [];
|
|
195
|
+
const kHist = new Map<number, number>();
|
|
196
|
+
let reused = 0;
|
|
197
|
+
const reusedBySource = new Map<string, number>();
|
|
198
|
+
let interleavedTensors = 0;
|
|
199
|
+
let trellisSeconds = 0;
|
|
200
|
+
|
|
201
|
+
const writer = new ShardedWriter(outDir, { shardBytes: opts.shardBytes ?? DEFAULT_SHARD_BYTES });
|
|
202
|
+
const t0 = performance.now();
|
|
203
|
+
try {
|
|
204
|
+
const byFile = new Map<string, string[]>();
|
|
205
|
+
for (const n of names) {
|
|
206
|
+
const f = weights.fileOf(n)!;
|
|
207
|
+
let list = byFile.get(f);
|
|
208
|
+
if (!list) byFile.set(f, (list = []));
|
|
209
|
+
list.push(n);
|
|
210
|
+
}
|
|
211
|
+
let done = 0;
|
|
212
|
+
for (const [file, list] of byFile) {
|
|
213
|
+
for (const name of list) {
|
|
214
|
+
const src = weights.tensor(name);
|
|
215
|
+
let folded: MlxArray;
|
|
216
|
+
if (zeroNorms.has(name)) folded = zerosLike(src);
|
|
217
|
+
else folded = ctx.apply(foldOps.get(name) ?? { kind: "passthrough" }, src);
|
|
218
|
+
|
|
219
|
+
const base = name.endsWith(".weight") ? name.slice(0, -".weight".length) : null;
|
|
220
|
+
const shape = folded.shape;
|
|
221
|
+
const eligible = base !== null && shape.length === 2 && shape[1]! % groupSize === 0;
|
|
222
|
+
const t: Treatment = eligible ? treatmentFor(base!, policy) : { kind: "bf16" };
|
|
223
|
+
|
|
224
|
+
if (!eligible) {
|
|
225
|
+
writer.add(name, folded);
|
|
226
|
+
} else if (t.kind === "bf16") {
|
|
227
|
+
writer.add(name, folded);
|
|
228
|
+
perLayer.set(base!, false);
|
|
229
|
+
} else if (t.kind === "trellis") {
|
|
230
|
+
const tt = performance.now();
|
|
231
|
+
let rec: PackedTrellisTensor;
|
|
232
|
+
const source = reusable(base!, t.k, t.axis);
|
|
233
|
+
if (source) {
|
|
234
|
+
rec = {
|
|
235
|
+
codes: ops.copyOf(source.weights.tensor(`${base}.weight`), gpuStream),
|
|
236
|
+
scales: ops.copyOf(source.weights.tensor(`${base}.scales`), gpuStream),
|
|
237
|
+
};
|
|
238
|
+
ops.evalAll([rec.codes, rec.scales]);
|
|
239
|
+
source.weights.release(`${base}.weight`); source.weights.release(`${base}.scales`);
|
|
240
|
+
reused++;
|
|
241
|
+
reusedBySource.set(source.dir, (reusedBySource.get(source.dir) ?? 0) + 1);
|
|
242
|
+
ldlqApplied++; // the reused codes carry the source artifact's LDLQ
|
|
243
|
+
} else if (ldlqDir) {
|
|
244
|
+
const rest = base!.slice(`${LM_PREFIX}layers.`.length);
|
|
245
|
+
const li = Number(rest.slice(0, rest.indexOf(".")));
|
|
246
|
+
const site = base!.endsWith("mlp.down_proj") ? "down" : "mlp"; // by MODULE, not axis (downAxis "in")
|
|
247
|
+
const lp = join(ldlqDir, `layer-${String(li).padStart(3, "0")}-${site}.safetensors`);
|
|
248
|
+
if (!existsSync(lp)) {
|
|
249
|
+
rec = encoder.encode(folded, t.axis, t.k);
|
|
250
|
+
ldlqMissing++;
|
|
251
|
+
} else {
|
|
252
|
+
const Lm = loadHessianFactor(lp);
|
|
253
|
+
const r = encoder.encodeLdlq(folded, t.axis, Lm, t.k);
|
|
254
|
+
Lm.dispose();
|
|
255
|
+
clearCache();
|
|
256
|
+
rec = { codes: r.codes, scales: r.scales };
|
|
257
|
+
if (r.tripped) {
|
|
258
|
+
ldlqTrips++;
|
|
259
|
+
trippedTensors.push(base!);
|
|
260
|
+
} else ldlqApplied++;
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
rec = encoder.encode(folded, t.axis, t.k);
|
|
264
|
+
}
|
|
265
|
+
trellisSeconds += (performance.now() - tt) / 1000;
|
|
266
|
+
folded.dispose();
|
|
267
|
+
if (opts.interleave && t.axis === 0 && t.k === 3 && rec.codes.ndim === 2 &&
|
|
268
|
+
rec.codes.shape[1]! % INTERLEAVE_WORDS === 0) {
|
|
269
|
+
const packed = interleaveTrellisCodes(rec.codes);
|
|
270
|
+
packed.eval(); rec.codes.dispose(); rec.codes = packed;
|
|
271
|
+
}
|
|
272
|
+
if (rec.codes.ndim === 3) interleavedTensors++;
|
|
273
|
+
writer.add(`${base}.weight`, rec.codes);
|
|
274
|
+
writer.add(`${base}.scales`, rec.scales);
|
|
275
|
+
perLayer.set(base!, {
|
|
276
|
+
bits: t.k, groupSize: TRELLIS_T, mode: "trellis",
|
|
277
|
+
trellis: { L: TRELLIS_L, code: "1mad", axis: t.axis },
|
|
278
|
+
});
|
|
279
|
+
const params = shape[0]! * shape[1]!;
|
|
280
|
+
const cols = t.axis === 1 ? shape[1]! : shape[0]!;
|
|
281
|
+
nTrellis++; trellisParams += params; trellisBits += params * trellisBpw(cols, t.k);
|
|
282
|
+
kHist.set(t.k, (kHist.get(t.k) ?? 0) + 1);
|
|
283
|
+
} else {
|
|
284
|
+
const q = quantize(folded, groupSize, t.bits, "affine", gpuStream);
|
|
285
|
+
folded.dispose();
|
|
286
|
+
writer.add(`${base}.weight`, q.packed);
|
|
287
|
+
writer.add(`${base}.scales`, q.scales);
|
|
288
|
+
if (q.biases) writer.add(`${base}.biases`, q.biases);
|
|
289
|
+
if (t.bits !== TRELLIS_BASE_BITS) perLayer.set(base!, { bits: t.bits, groupSize });
|
|
290
|
+
nAffine++;
|
|
291
|
+
const params = shape[0]! * shape[1]!;
|
|
292
|
+
affineParams += params; affineBits += params * (t.bits + 32 / groupSize);
|
|
293
|
+
}
|
|
294
|
+
done++;
|
|
295
|
+
progress("quantizing",
|
|
296
|
+
`${done}/${names.length} tensors · trellis ${nTrellis} ` +
|
|
297
|
+
`(${(trellisSeconds / 60).toFixed(1)} min, ` +
|
|
298
|
+
`${trellisSeconds > 0 ? (trellisParams / 1e6 / trellisSeconds).toFixed(2) : "—"} Mw/s) · ` +
|
|
299
|
+
`mlx ${(activeMemory() / 2 ** 30).toFixed(2)}+${(cacheMemory() / 2 ** 30).toFixed(2)} GiB`,
|
|
300
|
+
done / names.length);
|
|
301
|
+
}
|
|
302
|
+
weights.releaseShard(file);
|
|
303
|
+
clearCache();
|
|
304
|
+
}
|
|
305
|
+
const write = writer.finish();
|
|
306
|
+
const totalParams = affineParams + trellisParams, totalBits = affineBits + trellisBits;
|
|
307
|
+
|
|
308
|
+
const blockEntries = new Map<string, PerLayerEntry>();
|
|
309
|
+
for (const [path, entry] of perLayer) {
|
|
310
|
+
blockEntries.set(path, entry);
|
|
311
|
+
// Entries land in BOTH key spaces: the qwen3_5 graph resolves module paths
|
|
312
|
+
// in mlx-lm's, the artifact stores tensors in the HF space.
|
|
313
|
+
const p = mlxLmPath(path);
|
|
314
|
+
if (p && p !== path) blockEntries.set(p, entry);
|
|
315
|
+
}
|
|
316
|
+
const block = buildQuantizationBlock({ bits: TRELLIS_BASE_BITS, groupSize, mode: "affine" }, blockEntries);
|
|
317
|
+
await writeQuantizedConfig(raw, outDir, block, { srcDir });
|
|
318
|
+
await Bun.write(join(outDir, "optiq_metadata.json"), JSON.stringify({
|
|
319
|
+
method: "rotation+trellis_tcq(mlp,packed)+rtn(rest)",
|
|
320
|
+
base_model: srcDir,
|
|
321
|
+
bits: TRELLIS_BASE_BITS,
|
|
322
|
+
group_size: groupSize,
|
|
323
|
+
trellis: {
|
|
324
|
+
L: TRELLIS_L, V: 1, block: TRELLIS_T, code: "1mad",
|
|
325
|
+
k: kMap ? "mixed (see k_map)" : bits,
|
|
326
|
+
k_map: kMap
|
|
327
|
+
? {
|
|
328
|
+
file: kMapPath, budget: kBudget,
|
|
329
|
+
modules_by_k: Object.fromEntries([...kHist].sort((a, b) => a[0] - b[0]).map(([k, n]) => [`k${k}`, n])),
|
|
330
|
+
per_tensor: Object.fromEntries([...kMap.k].map(([n, k]) => [n.replace(LM_PREFIX, "model."), k])),
|
|
331
|
+
}
|
|
332
|
+
: null,
|
|
333
|
+
tail_biting: true,
|
|
334
|
+
reference: "arXiv:2406.11235v3 (QTIP); Cornell-RelaxML/qtip lib/codebook/bitshift.py",
|
|
335
|
+
distortion: ldlqDir
|
|
336
|
+
? "BlockLDLQ Hessian-weighted error feedback (QTIP lib/algo/ldlq.py, " +
|
|
337
|
+
`block ${TRELLIS_T} = trellis T, for_kernel=False path)`
|
|
338
|
+
: "unweighted squared error (no Hessian/LDLQ)",
|
|
339
|
+
ldlq: ldlqDir
|
|
340
|
+
? {
|
|
341
|
+
hessians: ldlqDir,
|
|
342
|
+
applied: ldlqApplied, guard_trips: ldlqTrips, missing_L: ldlqMissing,
|
|
343
|
+
tripped_tensors: trippedTensors,
|
|
344
|
+
guard: "xn peak > 4x the unweighted normalized max -> " +
|
|
345
|
+
"fall back to unweighted Viterbi for that tensor",
|
|
346
|
+
calibration: ldlqCalib,
|
|
347
|
+
}
|
|
348
|
+
: null,
|
|
349
|
+
scale: "per coded row, fp16 (QTIP uses one per-tensor Wscale after TWO-sided IP)",
|
|
350
|
+
down_axis: downAxis === "in" ? "input (un-rotated intermediate dim; plain reduce kernel)" : "output (rotated dim; scatter kernel)",
|
|
351
|
+
reused_from: reuseSources.length ? {
|
|
352
|
+
tensors: reused,
|
|
353
|
+
sources: Object.fromEntries(reusedBySource),
|
|
354
|
+
} : null,
|
|
355
|
+
interleaved_modules: interleavedTensors,
|
|
356
|
+
packaging: "packed: uint32 bit-stream (reversed-time symbols, tail-biting window) + fp16 row scales; " +
|
|
357
|
+
"config mode \"trellis\" (packages/quantize/src/trellis.ts, packages/inference/src/layers/trellis-linear.ts); mlx-lm cannot load it",
|
|
358
|
+
modules: nTrellis, params: trellisParams,
|
|
359
|
+
effective_bpw: trellisParams > 0 ? trellisBits / trellisParams : 0,
|
|
360
|
+
},
|
|
361
|
+
achieved_bpw_coded: totalBits / totalParams,
|
|
362
|
+
affine_modules: nAffine,
|
|
363
|
+
total_bytes: write.totalSize,
|
|
364
|
+
allocation: "shipped-compact (base 3 g64; 4-bit embed/lm_head/attn; vision bf16) with the 3-bit MLP tier replaced by the trellis",
|
|
365
|
+
weight_transforms: [{
|
|
366
|
+
id: "rotation.qwen3_5", seed, family: "qwen3_5",
|
|
367
|
+
deviations: [
|
|
368
|
+
"no-R2 (attn_output_gate does not commute with per-head rotation)",
|
|
369
|
+
"no-embedding-mean-centering", "no-R4-downproj-input-fold", "no-R3",
|
|
370
|
+
"gamma-folded; norms written as ZEROS (mlx-lm qwen3_5 sanitize adds 1.0 for this checkpoint family)",
|
|
371
|
+
"fold-precision-f32",
|
|
372
|
+
"vision folded at merger.linear_fc2 only (deepstack empty); vision tower left bf16",
|
|
373
|
+
"mtp companion folded with the trunk seed (unverified — mlx-lm drops mtp)",
|
|
374
|
+
"one-sided incoherence processing only (R1 folds into adjacent matrices; QTIP's SU/SV pair does not)",
|
|
375
|
+
],
|
|
376
|
+
}],
|
|
377
|
+
}, null, 2));
|
|
378
|
+
for (const f of ["preprocessor_config.json", "video_preprocessor_config.json"])
|
|
379
|
+
if (existsSync(join(srcDir, f))) copyFileSync(join(srcDir, f), join(outDir, f));
|
|
380
|
+
writeFileSync(join(outDir, TURBOQUANT_FOLD_FILE), JSON.stringify({
|
|
381
|
+
source: srcDir, generatedAt: new Date().toISOString(), seed,
|
|
382
|
+
r1: true, r2: false, hiddenSize,
|
|
383
|
+
gamma_convention: "stored+1 folded in; folded norms written as zeros",
|
|
384
|
+
// The γ the fold scaled lm_head by (its fold op names it), read back from
|
|
385
|
+
// the fold context; a drafter entering the folded head divides by it.
|
|
386
|
+
final_gain: Array.from(ctx.gain(headGamma)),
|
|
387
|
+
final_gain_convention: FINAL_GAIN_CONVENTION,
|
|
388
|
+
final_gain_source: `${srcDir} ${headGamma} (stored + 1 in float32, as folded into lm_head.weight)`,
|
|
389
|
+
}, null, 2));
|
|
390
|
+
progress("done", `${nAffine} affine + ${nTrellis} trellis modules · ${(totalBits / totalParams).toFixed(4)} coded bpw · ` +
|
|
391
|
+
`${(write.totalSize / 2 ** 30).toFixed(3)} GiB · ${((performance.now() - t0) / 60000).toFixed(1)} min`, 1);
|
|
392
|
+
return {
|
|
393
|
+
outDir, nTrellis, nAffine, effectiveBpw: totalBits / totalParams, reused,
|
|
394
|
+
ldlq: ldlqDir ? { applied: ldlqApplied, guardTrips: ldlqTrips, missingFactors: ldlqMissing, trippedTensors } : null,
|
|
395
|
+
write,
|
|
396
|
+
};
|
|
397
|
+
} finally {
|
|
398
|
+
ctx.dispose();
|
|
399
|
+
one.dispose();
|
|
400
|
+
encoder.dispose();
|
|
401
|
+
for (const source of reuseSources) source.weights.dispose();
|
|
402
|
+
weights.dispose();
|
|
403
|
+
clearCache();
|
|
404
|
+
}
|
|
405
|
+
}
|