christopher-kapic's picture
Upload folder using huggingface_hub
fdc6474 verified
Raw
History Blame Contribute Delete
6.1 kB
#!/usr/bin/env python3
"""Assemble the hybrid NVFP4+AQLM GLM-5.2 checkpoint at /data/glm52.
Sources:
- /tmp/glm52-dl/nvfp4-full: lukealonso/GLM-5.2-NVFP4 (all non-expert
tensors verbatim; routed-expert tensors only for plan.nvfp4_layers)
- /data/glm52-aqlm-parts/layer_N.pt: AQLM codes/codebooks/scales for
the remaining expert layers
Output: sharded safetensors (~4GB each) + index + config.json with the
nvfp4_aqlm_hybrid quantization_config + tokenizer/aux files.
"""
import json
import os
import re
import shutil
import time
import torch
from safetensors import safe_open
from safetensors.torch import save_file
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SRC = "/tmp/glm52-dl/nvfp4-full"
PARTS = "/data/glm52-aqlm-parts"
DST = "/data/glm52"
SHARD_BYTES = 4 << 30
plan = json.load(open(os.path.join(ROOT, "hybrid_plan.json")))
NVFP4_LAYERS = set(plan["nvfp4_layers"])
AQLM_LAYERS = sorted(plan["aqlm_mixed_layers"]) + sorted(plan["aqlm_cold_layers"])
def keep(name: str) -> bool:
m = re.match(r"model\.layers\.(\d+)\.mlp\.experts\.\d+\.", name)
if m:
return int(m.group(1)) in NVFP4_LAYERS
return True
class ShardWriter:
def __init__(self, dst: str):
self.dst = dst
self.cur: dict[str, torch.Tensor] = {}
self.cur_bytes = 0
self.n = 0
self.weight_map: dict[str, str] = {}
self.total = 0
self.files: list[str] = []
def add(self, name: str, tensor: torch.Tensor):
nb = tensor.numel() * tensor.element_size()
if self.cur_bytes + nb > SHARD_BYTES and self.cur:
self.flush()
self.cur[name] = tensor
self.cur_bytes += nb
self.total += nb
def flush(self):
if not self.cur:
return
self.n += 1
fname = f"model-{self.n:05d}.safetensors"
save_file(self.cur, os.path.join(self.dst, fname))
for k in self.cur:
self.weight_map[k] = fname
print(f" wrote {fname} ({self.cur_bytes/1e9:.2f} GB)", flush=True)
self.files.append(fname)
self.cur = {}
self.cur_bytes = 0
def finalize(self):
self.flush()
# rename to canonical model-XXXXX-of-NNNNN scheme
total_n = self.n
wm = {}
for i, fname in enumerate(self.files, 1):
new = f"model-{i:05d}-of-{total_n:05d}.safetensors"
os.rename(
os.path.join(self.dst, fname), os.path.join(self.dst, new)
)
for k, v in self.weight_map.items():
if v == fname:
wm[k] = new
idx = {
"metadata": {"total_size": self.total},
"weight_map": wm,
}
json.dump(
idx,
open(os.path.join(self.dst, "model.safetensors.index.json"), "w"),
indent=0,
)
print(f"index: {len(wm)} tensors, {self.total/1e9:.1f} GB total")
def main():
os.makedirs(DST, exist_ok=True)
idx = json.load(open(os.path.join(SRC, "model.safetensors.index.json")))
weight_map = idx["weight_map"]
shards = sorted(set(weight_map.values()))
writer = ShardWriter(DST)
# 1. copy kept tensors from the NVFP4 repo, shard by shard
for shard in shards:
with safe_open(os.path.join(SRC, shard), framework="pt") as f:
names = [n for n in f.keys() if keep(n)]
if not names:
continue
print(f"{shard}: keeping {len(names)} tensors", flush=True)
for n in names:
writer.add(n, f.get_tensor(n))
# 2. append AQLM expert tensors
layer_books = {}
mse_report = {}
for li in AQLM_LAYERS:
part = os.path.join(PARTS, f"layer_{li}.pt")
# the quantizer writes in place: wait for existence + 60s of quiescence
while (
not os.path.exists(part) or time.time() - os.path.getmtime(part) < 60
):
print(f"waiting for quantizer: layer {li} ...", flush=True)
time.sleep(30)
d = torch.load(part, map_location="cpu", weights_only=True)
p = f"model.layers.{li}.mlp.experts"
writer.add(f"{p}.w13_codes", d["w13_codes"])
writer.add(f"{p}.w13_codebooks", d["w13_codebooks"])
writer.add(f"{p}.w13_scales", d["w13_scales"])
writer.add(f"{p}.w2_codes", d["w2_codes"])
writer.add(f"{p}.w2_codebooks", d["w2_codebooks"])
writer.add(f"{p}.w2_scales", d["w2_scales"])
layer_books[str(li)] = {
"w13": d["books"]["w13"],
"w2": d["books"]["w2"],
}
mse_report[li] = (d["w13_rel_mse"], d["w2_rel_mse"])
print(
f"layer {li}: aqlm books={d['books']} "
f"rel_mse w13={d['w13_rel_mse']:.4f} w2={d['w2_rel_mse']:.4f}",
flush=True,
)
writer.finalize()
# 3. config.json: replace quantization_config with the hybrid config,
# dropping quantized expert layers from the modelopt view is NOT
# needed -- AQLM layers are dispatched before the nvfp4 config is
# consulted, and their tensors do not match NVFP4 patterns.
cfg = json.load(open(os.path.join(SRC, "config.json")))
nvfp4_qc = cfg["quantization_config"]
cfg["quantization_config"] = {
"quant_method": "nvfp4_aqlm_hybrid",
"nvfp4": nvfp4_qc,
"aqlm": {"entries": 65536, "group_size": 8},
"aqlm_layer_books": layer_books,
}
json.dump(cfg, open(os.path.join(DST, "config.json"), "w"), indent=2)
# 4. aux files
for f in os.listdir(SRC):
if (
f.endswith(".json")
and f not in ("config.json", "model.safetensors.index.json")
or f.endswith((".txt", ".jinja", ".py", ".md"))
):
shutil.copy2(os.path.join(SRC, f), os.path.join(DST, f))
print(f"copied {f}")
json.dump(
{str(k): v for k, v in mse_report.items()},
open(os.path.join(DST, "aqlm_mse_report.json"), "w"),
indent=2,
)
print("DONE:", DST)
if __name__ == "__main__":
main()