Desktop app 0.4.0 (Ask the assistant about this report), audit-aware chat search, v0.4 pipeline, converter for 1.5B
fbb1758 verified Download training/mlc_quantize.py from Devseis/Arx: direct link, hf CLI and curl.
- Browser
- Download file 9.86 kB
-
https://huggingface.co/spaces/Devseis/Arx/resolve/main/training/mlc_quantize.py
- Command line
-
hf download hf://spaces/Devseis/Arx/training/mlc_quantize.py
-
curl -L -o mlc_quantize.py https://huggingface.co/spaces/Devseis/Arx/resolve/main/training/mlc_quantize.py
9.86 kB
| #!/usr/bin/env python3 | |
| """Convert a Qwen2.5-0.5B-architecture model (e.g. the merged Devseis auditor) to WebLLM/MLC weights, q4f16_1 or q4f32_1. | |
| Why this exists: MLC's own converter could not run on this Mac (the Intel-Mac nightly mlc_llm and TVM builds do not | |
| match, and the stable Linux release needs an apache-tvm-ffi build that was never published). This script writes the | |
| same format MLC's group quantization produces (4-bit symmetric, groups of 32 along the input dimension, 8 values per | |
| uint32, one scale per group), reusing the tensor layout of MLC's published Qwen2.5-0.5B-Instruct build so WebLLM's | |
| prebuilt runtime library for that model can run it. | |
| It is verified, not assumed: `--verify` converts the original Qwen2.5-0.5B-Instruct and compares it with | |
| mlc-ai/Qwen2.5-0.5B-Instruct-<quant>-MLC: identical layout, bit-identical scales, 99.6% identical 4-bit values (the rest | |
| differ by one step at exact rounding ties, most likely because mlc-ai quantized slightly different source weights). | |
| Usage: | |
| training/.venv/bin/python training/mlc_quantize.py --verify --quant q4f16_1 | |
| training/.venv/bin/python training/mlc_quantize.py --model training/models/devseis-auditor-0.5b-v0.1 --quant q4f16_1 \\ | |
| --out training/models/mlc/devseis-auditor-0.5b-v0.1-q4f16_1-MLC | |
| """ | |
| import argparse | |
| import hashlib | |
| import json | |
| import os | |
| import shutil | |
| import numpy as np | |
| import torch | |
| from safetensors.torch import load_file | |
| ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| MODELS = os.path.join(ROOT, "training", "models") | |
| GROUP = 32 | |
| MAX_INT = 7 # 4-bit symmetric: values 0..14 stored, 7 = zero | |
| COPY = ["mlc-chat-config.json", "tokenizer.json", "tokenizer_config.json", "vocab.json", "merges.txt"] | |
| def load_hf(model_dir): | |
| weights = load_file(os.path.join(model_dir, "model.safetensors")) | |
| return {k: v.to(torch.float32).numpy() for k, v in weights.items()} | |
| def quantize(w, dtype, mode=None): | |
| """Group quantization along the last axis. Returns (q_weight uint32 [out, in/8], q_scale [out, in/32]). | |
| Matches MLC: scale = max|w| * (1/7) in the model dtype (bit-exact against mlc-ai's build), values = round(w / scale) | |
| computed in float32 (99.6% identical to mlc-ai's build; the rest differ by one step at exact rounding ties).""" | |
| w = w.astype(dtype) | |
| out, inp = w.shape | |
| g = w.reshape(out, inp // GROUP, GROUP) | |
| scale = (np.abs(g).max(axis=-1, keepdims=True) * dtype(1 / MAX_INT)).astype(dtype) | |
| with np.errstate(divide="ignore", invalid="ignore"): | |
| x = np.where(scale == 0, 0, g.astype(np.float32) / scale.astype(np.float32)) | |
| x = np.sign(x) * np.floor(np.abs(x) + 0.5) # round half away from zero | |
| q = np.clip(x + MAX_INT, 0, 2 * MAX_INT).astype(np.uint32).reshape(out, inp // 8, 8) | |
| packed = np.zeros((out, inp // 8), dtype=np.uint32) | |
| for k in range(8): | |
| packed |= q[..., k] << np.uint32(4 * k) | |
| return packed, scale.reshape(out, inp // GROUP).astype(dtype) | |
| def build(hf, dtype, mode=None, quantized=True): | |
| """Tensors named as in MLC's Qwen2 model (q0f16: unquantized float16, same fused names).""" | |
| t = {} | |
| def linear(name, w): | |
| if quantized: | |
| t[name + ".q_weight"], t[name + ".q_scale"] = quantize(w, dtype, mode) | |
| else: | |
| t[name + ".weight"] = w.astype(dtype) | |
| linear("model.embed_tokens", hf["model.embed_tokens.weight"]) | |
| t["model.norm.weight"] = hf["model.norm.weight"].astype(dtype) | |
| layer = 0 | |
| while f"model.layers.{layer}.input_layernorm.weight" in hf: | |
| p = f"model.layers.{layer}." | |
| t[p + "input_layernorm.weight"] = hf[p + "input_layernorm.weight"].astype(dtype) | |
| t[p + "post_attention_layernorm.weight"] = hf[p + "post_attention_layernorm.weight"].astype(dtype) | |
| qkv = np.concatenate([hf[p + f"self_attn.{x}_proj.weight"] for x in "qkv"], axis=0) | |
| linear(p + "self_attn.c_attn", qkv) | |
| t[p + "self_attn.c_attn.bias"] = np.concatenate([hf[p + f"self_attn.{x}_proj.bias"] for x in "qkv"]).astype(dtype) | |
| linear(p + "self_attn.o_proj", hf[p + "self_attn.o_proj.weight"]) | |
| gate_up = np.concatenate([hf[p + "mlp.gate_proj.weight"], hf[p + "mlp.up_proj.weight"]], axis=0) | |
| linear(p + "mlp.gate_up_proj", gate_up) | |
| linear(p + "mlp.down_proj", hf[p + "mlp.down_proj.weight"]) | |
| layer += 1 | |
| return t | |
| def stored(rec): | |
| """MLC stores float32 tensors as bfloat16 when the record's format is f32-to-bf16 (WebLLM expands them on load).""" | |
| return rec["dtype"] == "float32" and rec.get("format") == "f32-to-bf16" | |
| def to_bytes(arr, rec): | |
| if stored(rec): | |
| return torch.from_numpy(np.ascontiguousarray(arr, dtype=np.float32)).to(torch.bfloat16).view(torch.int16).numpy().tobytes() | |
| return np.ascontiguousarray(arr).tobytes() | |
| def from_bytes(data, rec): | |
| if stored(rec): | |
| return (np.frombuffer(data, dtype=np.uint16).astype(np.uint32) << np.uint32(16)).view(np.float32).reshape(rec["shape"]) | |
| return np.frombuffer(data, dtype=rec["dtype"]).reshape(rec["shape"]) | |
| def records(ref_dir): | |
| cache = json.load(open(os.path.join(ref_dir, "tensor-cache.json"))) | |
| return cache, [(shard, rec) for shard in cache["records"] for rec in shard["records"]] | |
| def check_layout(tensors, recs): | |
| for _, rec in recs: | |
| arr = tensors[rec["name"]] | |
| assert list(arr.shape) == rec["shape"], (rec["name"], arr.shape, rec["shape"]) | |
| assert str(arr.dtype) == rec["dtype"], (rec["name"], arr.dtype, rec["dtype"]) | |
| assert (arr.nbytes // 2 if stored(rec) else arr.nbytes) == rec["nbytes"], rec["name"] | |
| assert len(tensors) == len(recs), (len(tensors), len(recs)) | |
| def write(tensors, ref_dir, out_dir): | |
| cache, recs = records(ref_dir) | |
| check_layout(tensors, recs) | |
| os.makedirs(out_dir, exist_ok=True) | |
| for shard in cache["records"]: | |
| data = bytearray(shard["nbytes"]) | |
| for rec in shard["records"]: | |
| data[rec["byteOffset"]:rec["byteOffset"] + rec["nbytes"]] = to_bytes(tensors[rec["name"]], rec) | |
| with open(os.path.join(out_dir, shard["dataPath"]), "wb") as handle: | |
| handle.write(data) | |
| shard["md5sum"] = hashlib.md5(data).hexdigest() | |
| for name in ("tensor-cache.json", "ndarray-cache.json"): | |
| with open(os.path.join(out_dir, name), "w") as handle: | |
| json.dump(cache, handle) | |
| for name in COPY: | |
| shutil.copy(os.path.join(ref_dir, name), out_dir) | |
| def verify(quant, ref_root=None, base=None): | |
| """Convert the base model and compare with mlc-ai's build: layout must match exactly, scales bit-exactly, values >= 99%.""" | |
| ref = os.path.join(ref_root or os.path.join(MODELS, "mlc-ref"), quant) | |
| dtype = np.float32 if quant == "q4f32_1" else np.float16 | |
| _, recs = records(ref) | |
| tensors = build(load_hf(base or os.path.join(MODELS, "Qwen2.5-0.5B-Instruct")), dtype, quantized=not quant.startswith("q0")) | |
| check_layout(tensors, recs) | |
| if quant.startswith("q0"): | |
| present = [(s, r) for s, r in recs if os.path.exists(os.path.join(ref, s["dataPath"]))] | |
| same = sum(open(os.path.join(ref, s["dataPath"]), "rb").read()[r["byteOffset"]:r["byteOffset"] + r["nbytes"]] == to_bytes(tensors[r["name"]], r) for s, r in present) | |
| print(f"{quant}: layout identical ({len(recs)} tensors); {same}/{len(present)} tensors in the downloaded shards byte-identical") | |
| return same == len(present) and len(present) > 0 | |
| shards, scale_same, scale_total, val_same, val_total, exact = {}, 0, 0, 0, 0, 0 | |
| for shard, rec in recs: | |
| if shard["dataPath"] not in shards: | |
| shards[shard["dataPath"]] = open(os.path.join(ref, shard["dataPath"]), "rb").read() | |
| want = from_bytes(shards[shard["dataPath"]][rec["byteOffset"]:rec["byteOffset"] + rec["nbytes"]], rec) | |
| got = from_bytes(to_bytes(tensors[rec["name"]], rec), rec) | |
| exact += bool(np.array_equal(got, want)) | |
| if rec["name"].endswith("q_scale"): | |
| scale_same += int((got == want).sum()); scale_total += got.size | |
| elif rec["name"].endswith("q_weight"): | |
| a = np.stack([(got >> np.uint32(4 * k)) & np.uint32(15) for k in range(8)], -1) | |
| b = np.stack([(want >> np.uint32(4 * k)) & np.uint32(15) for k in range(8)], -1) | |
| val_same += int((a == b).sum()); val_total += a.size | |
| assert int(np.abs(a.astype(np.int32) - b.astype(np.int32)).max()) <= 1, rec["name"] | |
| print(f"{quant}: layout identical ({len(recs)} tensors, {exact} byte-identical); scales {scale_same / scale_total:.4%} identical; " | |
| f"4-bit values {val_same / val_total:.4%} identical, all others within one step") | |
| return scale_same == scale_total and val_same / val_total >= 0.99 | |
| def main(): | |
| parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| parser.add_argument("--quant", choices=["q4f16_1", "q4f32_1", "q0f16"], required=True) | |
| parser.add_argument("--verify", action="store_true", help="compare a conversion of the base model with mlc-ai's build") | |
| parser.add_argument("--model", help="merged model folder (model.safetensors)") | |
| parser.add_argument("--out", help="output folder") | |
| parser.add_argument("--ref", help="folder with mlc-ai reference builds per quantization (default training/models/mlc-ref, 0.5B)") | |
| parser.add_argument("--base", help="--verify: original base model folder matching --ref") | |
| args = parser.parse_args() | |
| if args.verify: | |
| raise SystemExit(0 if verify(args.quant, args.ref, args.base) else 1) | |
| dtype = np.float32 if args.quant == "q4f32_1" else np.float16 | |
| tensors = build(load_hf(args.model), dtype, quantized=not args.quant.startswith("q0")) | |
| write(tensors, os.path.join(args.ref or os.path.join(MODELS, "mlc-ref"), args.quant), args.out) | |
| print(f"wrote {args.out}") | |
| if __name__ == "__main__": | |
| main() | |