Upload convert_nvfp4.py with huggingface_hub
Browse files- convert_nvfp4.py +108 -0
convert_nvfp4.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Convert the MiniMax H3 bf16 DiT to NVFP4 in ComfyUI's native quant layout.
|
| 3 |
+
|
| 4 |
+
Runs ON the instance (needs /workspace/ComfyUI + comfy_kitchen + a GPU).
|
| 5 |
+
|
| 6 |
+
Mirrors the quantization policy of the released int8_convrot DiT exactly:
|
| 7 |
+
only the 50 main blocks' attn.qkv_proj / attn.out_proj / mlp.fc1 / mlp.fc2
|
| 8 |
+
are quantized (200 layers); norms, adaln, patch/condition projections,
|
| 9 |
+
token_refiner and final layers stay bf16.
|
| 10 |
+
|
| 11 |
+
Per quantized layer (identical tensor layout to the released nvfp4_awq TE):
|
| 12 |
+
.weight U8 [out, in/2] packed FP4 E2M1 pairs
|
| 13 |
+
.weight_scale F8_E4M3 [out, in/16] per-16-block scales
|
| 14 |
+
.weight_scale_2 F32 [] global scale (amax / (448*6))
|
| 15 |
+
.comfy_quant U8 [n] JSON layer config
|
| 16 |
+
|
| 17 |
+
Usage:
|
| 18 |
+
python3 convert_nvfp4.py --src models/diffusion_models/minimax_h3_ref2va_bf16.safetensors \
|
| 19 |
+
--dst models/diffusion_models/minimax_h3_ref2va_nvfp4.safetensors [--fpmm]
|
| 20 |
+
|
| 21 |
+
--fpmm adds {"full_precision_matrix_mult": true} (dequant->bf16 GEMM, the
|
| 22 |
+
quality-safe path the official TE uses). Without it, comfy_kitchen's
|
| 23 |
+
native FP4 tensor-core GEMM path is used (faster, more quality risk).
|
| 24 |
+
"""
|
| 25 |
+
import argparse
|
| 26 |
+
import json
|
| 27 |
+
import sys
|
| 28 |
+
import time
|
| 29 |
+
|
| 30 |
+
sys.path.insert(0, "/workspace/ComfyUI")
|
| 31 |
+
|
| 32 |
+
import torch # noqa: E402
|
| 33 |
+
from safetensors import safe_open # noqa: E402
|
| 34 |
+
from safetensors.torch import save_file # noqa: E402
|
| 35 |
+
from comfy.quant_ops import ( # noqa: E402
|
| 36 |
+
TensorCoreConvRotW4A4Layout,
|
| 37 |
+
TensorCoreNVFP4Layout,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
TARGET_SUFFIXES = (".attn.qkv_proj.weight", ".attn.out_proj.weight",
|
| 41 |
+
".mlp.fc1.weight", ".mlp.fc2.weight")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def should_quantize(key, shape):
|
| 45 |
+
return (key.startswith("blocks.")
|
| 46 |
+
and key.endswith(TARGET_SUFFIXES)
|
| 47 |
+
and len(shape) == 2)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def main():
|
| 51 |
+
ap = argparse.ArgumentParser()
|
| 52 |
+
ap.add_argument("--src", required=True)
|
| 53 |
+
ap.add_argument("--dst", required=True)
|
| 54 |
+
ap.add_argument("--algo", choices=["nvfp4", "convrot_w4a4"], default="nvfp4")
|
| 55 |
+
ap.add_argument("--fpmm", action="store_true",
|
| 56 |
+
help="nvfp4 only: full_precision_matrix_mult=true "
|
| 57 |
+
"(dequant->bf16 GEMM)")
|
| 58 |
+
args = ap.parse_args()
|
| 59 |
+
|
| 60 |
+
if args.algo == "convrot_w4a4":
|
| 61 |
+
# int4 weights + int4 activations, rotation-assisted (groupsize 256)
|
| 62 |
+
cfg = {"format": "convrot_w4a4", "convrot_groupsize": 256,
|
| 63 |
+
"linear_dtype": "int4"}
|
| 64 |
+
else:
|
| 65 |
+
cfg = {"format": "nvfp4"}
|
| 66 |
+
if args.fpmm:
|
| 67 |
+
cfg["full_precision_matrix_mult"] = True
|
| 68 |
+
cfg_tensor = torch.tensor(list(json.dumps(cfg).encode("utf-8")),
|
| 69 |
+
dtype=torch.uint8)
|
| 70 |
+
|
| 71 |
+
out, n_q, n_keep, t0 = {}, 0, 0, time.time()
|
| 72 |
+
with safe_open(args.src, framework="pt", device="cpu") as f:
|
| 73 |
+
keys = list(f.keys())
|
| 74 |
+
for i, k in enumerate(keys):
|
| 75 |
+
t = f.get_tensor(k)
|
| 76 |
+
if should_quantize(k, t.shape):
|
| 77 |
+
layer = k[:-len(".weight")]
|
| 78 |
+
w = t.cuda()
|
| 79 |
+
if args.algo == "convrot_w4a4":
|
| 80 |
+
qdata, params = TensorCoreConvRotW4A4Layout.quantize(
|
| 81 |
+
w, convrot_groupsize=256, linear_dtype="int4")
|
| 82 |
+
out[layer + ".weight"] = qdata.contiguous().cpu()
|
| 83 |
+
out[layer + ".weight_scale"] = params.scale.contiguous().cpu()
|
| 84 |
+
else:
|
| 85 |
+
qdata, params = TensorCoreNVFP4Layout.quantize(w)
|
| 86 |
+
out[layer + ".weight"] = qdata.contiguous().cpu()
|
| 87 |
+
out[layer + ".weight_scale"] = params.block_scale.contiguous().cpu()
|
| 88 |
+
out[layer + ".weight_scale_2"] = params.scale.to(torch.float32).cpu()
|
| 89 |
+
out[layer + ".comfy_quant"] = cfg_tensor.clone()
|
| 90 |
+
del w, qdata, params
|
| 91 |
+
n_q += 1
|
| 92 |
+
if n_q % 20 == 0:
|
| 93 |
+
torch.cuda.empty_cache()
|
| 94 |
+
print(f"[{time.time()-t0:6.0f}s] quantized {n_q} layers "
|
| 95 |
+
f"({i+1}/{len(keys)} tensors)", flush=True)
|
| 96 |
+
else:
|
| 97 |
+
out[k] = t
|
| 98 |
+
n_keep += 1
|
| 99 |
+
|
| 100 |
+
print(f"quantized {n_q} layers, kept {n_keep} tensors; saving {args.dst}")
|
| 101 |
+
save_file(out, args.dst)
|
| 102 |
+
size = sum(v.numel() * v.element_size() for v in out.values())
|
| 103 |
+
print(f"done in {time.time()-t0:.0f}s — ~{size/1e9:.1f} GB "
|
| 104 |
+
f"(config: {json.dumps(cfg)})")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
main()
|