kylinfires's picture
Sync main: low-VRAM live test results (624x360)
b9fc164 verified
Raw History Blame Contribute Delete
5.15 kB
#!/usr/bin/env python3
"""Group-wise symmetric int4 quantization of the JoyAI-Video-Edit DiT.
Reads the official diffusers shards, renames `double_blocks.N.attn.X` ->
`double_blocks.N.X` (the runtime's layout), quantizes every large 2D Linear
weight to int4 (group_size=64) and writes packed weights + bf16 group scales.
out/int4-0000X-of-00002.safetensors : packed uint8 [out, in/2] + scales
out/meta.json : group size, key list, dtype map
"""
from __future__ import annotations
import json
import os
import time
from pathlib import Path
import torch
from safetensors import safe_open
from safetensors.torch import save_file
SRC = Path("/home/ubuntu/jve-diff")
OUT = SRC / "int4"
OUT.mkdir(exist_ok=True)
GROUP = 64
MIN_NUMEL = 8_000_000 # quantize the big Linears only
def map_key(k: str) -> str:
if k.startswith("double_blocks."):
parts = k.split(".")
if parts[2] == "attn":
return ".".join(parts[:2] + parts[3:])
return k
def quantize(w: torch.Tensor):
"""w: [out, in] -> (packed uint8 [out, in/2], scales bf16 [out, in/64])"""
out_f, in_f = w.shape
assert in_f % GROUP == 0 and in_f % 2 == 0, w.shape
wr = w.float().reshape(out_f, in_f // GROUP, GROUP)
scale = wr.abs().amax(dim=-1).clamp_min(1e-8) / 7.0
q = torch.round(wr / scale.unsqueeze(-1)).clamp_(-8, 7).to(torch.int8)
q = q.reshape(out_f, in_f) + 8 # 0..15
q8 = q.to(torch.uint8)
packed = (q8[:, 0::2] | (q8[:, 1::2] << 4)).contiguous()
return packed, scale.to(torch.bfloat16).contiguous()
def main() -> None:
t0 = time.time()
idx = json.load(open(SRC / "transformer_diffusion_pytorch_model.safetensors.index.json"))
weight_map = idx["weight_map"]
shards = sorted(set(weight_map.values()))
print(f"{len(weight_map)} tensors in {len(shards)} shards", flush=True)
# group keys by shard so we stream one shard at a time (RAM stays ~5 GB)
per_shard: dict[str, list[str]] = {}
for k, s in weight_map.items():
per_shard.setdefault(s, []).append(k)
packed_t: dict[str, torch.Tensor] = {}
meta: dict[str, dict] = {}
stats = []
for si, shard in enumerate(shards, 1):
path = SRC / "transformer" / shard
with safe_open(str(path), framework="pt", device="cpu") as f:
for k in per_shard[shard]:
t = f.get_tensor(k).to(torch.bfloat16)
nk = map_key(k)
if t.dim() == 2 and t.numel() >= MIN_NUMEL:
packed, scales = quantize(t)
packed_t[nk + ".qweight"] = packed
packed_t[nk + ".scales"] = scales
# reconstruction error
deq = (packed & 0x0F).to(torch.float32) - 8
deq2 = (packed >> 4).to(torch.float32) - 8
w_deq = torch.empty(t.shape, dtype=torch.float32)
w_deq[:, 0::2] = deq
w_deq[:, 1::2] = deq2
w_deq = (w_deq.reshape(t.shape[0], -1, GROUP) *
scales.float().unsqueeze(-1)).reshape(t.shape)
tf = t.float()
mse = (tf - w_deq).pow(2).mean().item()
var = tf.pow(2).mean().item()
cos = torch.nn.functional.cosine_similarity(
tf.flatten(), w_deq.flatten(), dim=0).item()
stats.append((nk, mse / max(var, 1e-12), cos, t.numel()))
meta[nk] = {"group": GROUP, "shape": list(t.shape),
"dtype": "int4", "rel_mse": mse / max(var, 1e-12)}
else:
packed_t[nk] = t.contiguous()
meta[nk] = {"dtype": "bf16", "shape": list(t.shape)}
print(f" shard {si}/{len(shards)} done tensors={len(packed_t)} "
f"{time.time() - t0:.0f}s", flush=True)
# split the output into two files to keep each under ~5 GB
keys = sorted(packed_t)
half = len(keys) // 2
parts = [keys[:half], keys[half:]]
for pi, ks in enumerate(parts, 1):
save_file({k: packed_t[k] for k in ks},
str(OUT / f"int4-{pi:05d}-of-{len(parts):05d}.safetensors"))
print(f"wrote part {pi} ({len(ks)} tensors)", flush=True)
json.dump({"group_size": GROUP, "tensors": meta},
open(OUT / "meta.json", "w"), indent=1)
n_q = len(stats)
params_q = sum(s[3] for s in stats)
rel = sorted(s[1] for s in stats)
cosl = sorted(s[2] for s in stats)
print(f"\nquantized {n_q} tensors, {params_q/1e9:.2f} B params")
print(f"rel_mse mean={sum(rel)/len(rel):.5f} p50={rel[len(rel)//2]:.5f} max={rel[-1]:.5f}")
print(f"cosine mean={sum(cosl)/len(cosl):.6f} min={cosl[0]:.6f}")
worst = sorted(stats, key=lambda s: -s[1])[:3]
for nk, m, c, n in worst:
print(f" worst {nk:<48} rel_mse={m:.5f} cos={c:.5f}")
tot = sum(p.numel() * p.element_size() for p in packed_t.values())
print(f"output size {tot/2**30:.2f} GiB")
print(f"done in {time.time() - t0:.0f}s")
if __name__ == "__main__":
main()