File size: 10,914 Bytes
2de4138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
#!/usr/bin/env python3
"""Quantize a native ComfyUI single-file OmniRef checkpoint to native FP8 or NVFP4.

The input is the native ComfyUI layout produced by ``convert_omniref_comfy.py``:
Q/K/V are already fused into ``attn.qkv_proj`` and the PDD output heads use the
native first-head-plus-offset representation. The quantizer therefore sees one
fused QKV tensor and applies one Comfy weight scale to it, exactly as the
pinned ComfyUI runtime expects.

Only the per-block attention/MLP projections are quantized. The token refiner,
AdaLN curves and projections, patch/condition/output projections, norms, RoPE,
and FP32 PDD heads stay dense. This matches the native ComfyUI MiniMax-H3
quantized-layer policy used by the released V2/Trim Comfy checkpoints.

Writes a JSON receipt with source/output hashes, the exact format markers,
per-linear reconstruction metrics, and runtime versions.
"""
from __future__ import annotations

import argparse
import datetime as dt
import hashlib
import importlib.metadata
import json
from pathlib import Path
import re
import sys
import time

import torch

# Native Comfy MiniMax-H3 block linears. qkv_proj is the fused Q/K/V module.
QUANT_LINEAR = re.compile(
    r"^blocks\.\d+\.(?:attn\.(?:qkv_proj|out_proj|to_gate_compress)|mlp\.(?:fc1|fc2))\.weight$"
)

FORMATS = {
    "fp8": {
        "layout": "TensorCoreFP8Layout",
        "tag": {"format": "float8_e4m3fn"},
        "documented_name": "FP8 E4M3, per-tensor weight scale, dynamic per-tensor activation scale",
        "activation": "dynamic-per-tensor",
    },
    "nvfp4": {
        "layout": "TensorCoreNVFP4Layout",
        "tag": {"format": "nvfp4"},
        "documented_name": "NVFP4 E2M1, per-tensor global scale plus FP8 block scales (group 16), dynamic per-tensor activation scale",
        "activation": "dynamic-per-tensor",
    },
}


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        while True:
            block = handle.read(64 * 1024 * 1024)
            if not block:
                break
            digest.update(block)
    return digest.hexdigest()


def tensor_digest(tensor: torch.Tensor) -> str:
    return hashlib.sha256(tensor.detach().cpu().contiguous().view(torch.uint8).numpy().tobytes()).hexdigest()


def safetensors_header(path: Path) -> dict:
    from safetensors import safe_open

    with safe_open(str(path), framework="pt", device="cpu") as handle:
        keys = list(handle.keys())
        metadata = handle.metadata() or {}
    return {"tensor_count": len(keys), "metadata": metadata, "keys": keys}


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--src", required=True, type=Path, help="Native ComfyUI BF16 single-file checkpoint")
    parser.add_argument("--dst", required=True, type=Path, help="Output quantized single-file checkpoint")
    parser.add_argument("--format", required=True, choices=sorted(FORMATS))
    parser.add_argument("--receipt", required=True, type=Path)
    parser.add_argument("--comfyui-commit", default="b1a247c9cf9dbfda4b2c0a0c1d50950667eeaa0c")
    parser.add_argument("--expect-tensors", type=int, default=None)
    args = parser.parse_args()

    if args.src == args.dst:
        raise SystemExit("source and destination must differ")
    if args.dst.exists():
        raise SystemExit(f"refusing to overwrite existing output: {args.dst}")
    if not torch.cuda.is_available():
        raise SystemExit("CUDA is required for comfy-kitchen quantization")

    import comfy_kitchen
    import comfy_kitchen.tensor as ckt
    from safetensors import safe_open
    from safetensors.torch import load_file, save_file

    started = time.time()
    spec = FORMATS[args.format]
    layout_cls = getattr(ckt, spec["layout"])
    tag_bytes = json.dumps(spec["tag"], separators=(",", ":")).encode()

    print(f"[quantize] format={args.format} src={args.src} dst={args.dst}", flush=True)
    src_header = safetensors_header(args.src)
    if args.expect_tensors is not None and src_header["tensor_count"] != args.expect_tensors:
        raise SystemExit(f"expected {args.expect_tensors} tensors, found {src_header['tensor_count']}")
    print(f"[quantize] source tensors={src_header['tensor_count']} hashing source", flush=True)
    src_sha256 = sha256_file(args.src)

    # The native Comfy BF16 export is ~44 GB. Keeping the pass-through tensors
    # plus the quantized output in CPU RAM stays well under a GB200 node's RAM.
    source = load_file(str(args.src), device="cpu")
    metadata = dict(src_header["metadata"])
    metadata["quantization"] = json.dumps(
        {
            "format": spec["tag"]["format"],
            "layout": spec["layout"],
            "weights": "per-tensor global scale" if args.format == "fp8" else "per-tensor global scale plus FP8 block scales, group 16",
            "activations": spec["activation"],
            "quantized_layers": "all 50 DiT blocks: fused qkv_proj, out_proj, to_gate_compress, mlp.fc1, mlp.fc2",
            "dense_layers": "token_refiner, AdaLN, norms, embeddings, patch/condition projections, FP32 PDD heads",
            "qkv_treatment": "fused Q/K/V quantized as one tensor under one weight scale",
            "comfyui_commit": args.comfyui_commit,
        },
        separators=(",", ":"),
    )
    metadata["quantization_source_sha256"] = src_sha256
    metadata["quantization_recipe"] = "omniref-comfy-ptq-v1"

    output: dict[str, torch.Tensor] = {}
    linears = []
    for index, (name, tensor) in enumerate(source.items(), 1):
        if not QUANT_LINEAR.match(name):
            output[name] = tensor
            continue

        weight = tensor.to(device="cuda", dtype=torch.bfloat16).contiguous()
        qdata, params = layout_cls.quantize(weight)
        serialized = layout_cls.state_dict_tensors(qdata, params)
        for suffix, value in serialized.items():
            output[name + suffix] = value.detach().cpu().contiguous()
        output[name[: -len(".weight")] + ".comfy_quant"] = torch.tensor(list(tag_bytes), dtype=torch.uint8)

        dequantized = layout_cls.dequantize(qdata, params).float()
        reference = weight.float()
        diff = dequantized - reference
        rel_l2 = float(diff.norm() / reference.norm().clamp(min=1e-30))
        cosine = float(torch.nn.functional.cosine_similarity(dequantized.flatten(), reference.flatten(), dim=0))
        entry = {
            "name": name,
            "logical_shape": list(tensor.shape),
            "weight_storage_shape": list(serialized[""].shape),
            "weight_storage_dtype": str(serialized[""].dtype).replace("torch.", ""),
            "reconstruction_relative_l2": rel_l2,
            "reconstruction_cosine": cosine,
            "reconstruction_max_abs": float(diff.abs().max()),
            "source_amax": float(reference.abs().max()),
        }
        if args.format == "fp8":
            entry["weight_scale"] = float(params.scale.reshape(-1)[0])
        else:
            entry["weight_scale_2"] = float(params.scale.reshape(-1)[0])
            entry["weight_scale_shape"] = list(serialized["_scale"].shape)
            entry["weight_scale_dtype"] = str(serialized["_scale"].dtype).replace("torch.", "")
            entry["block_scale_min"] = float(serialized["_scale"].to(torch.float32).min())
            entry["block_scale_max"] = float(serialized["_scale"].to(torch.float32).max())
        linears.append(entry)
        print(
            f"[quantize] {index:4d}/{len(source)} {name} relL2={rel_l2:.6f} cos={cosine:.8f}",
            flush=True,
        )
        del weight, qdata, params, dequantized, reference, diff
        torch.cuda.empty_cache()

    expected_linears = 50 * 5
    if len(linears) != expected_linears:
        raise SystemExit(f"expected {expected_linears} quantized linears, got {len(linears)}")

    print(f"[quantize] writing {len(output)} tensors to {args.dst}", flush=True)
    args.dst.parent.mkdir(parents=True, exist_ok=True)
    save_file(output, str(args.dst), metadata=metadata)
    out_sha256 = sha256_file(args.dst)
    out_header = safetensors_header(args.dst)

    # Pass-through tensors must survive byte-for-byte. Compare every dense tensor
    # from the written file against the source file, not against the in-memory copy.
    pass_through = [name for name in source if not QUANT_LINEAR.match(name)]
    identical = 0
    with safe_open(str(args.src), framework="pt", device="cpu") as src_reader, safe_open(
        str(args.dst), framework="pt", device="cpu"
    ) as dst_reader:
        for name in pass_through:
            left = src_reader.get_tensor(name)
            right = dst_reader.get_tensor(name)
            if left.dtype != right.dtype or left.shape != right.shape or not torch.equal(left, right):
                raise SystemExit(f"pass-through tensor changed: {name}")
            identical += 1

    receipt = {
        "schema": "omniref-comfy-quant-receipt-v1",
        "created_utc": dt.datetime.now(dt.timezone.utc).isoformat(),
        "format": args.format,
        "format_description": spec["documented_name"],
        "source": {
            "path": str(args.src),
            "bytes": args.src.stat().st_size,
            "sha256": src_sha256,
            "tensor_count": src_header["tensor_count"],
            "metadata": src_header["metadata"],
        },
        "output": {
            "path": str(args.dst),
            "bytes": args.dst.stat().st_size,
            "sha256": out_sha256,
            "tensor_count": out_header["tensor_count"],
        },
        "runtime": {
            "torch": torch.__version__,
            "cuda": torch.version.cuda,
            "gpu": torch.cuda.get_device_name(0),
            "comfy_kitchen": importlib.metadata.version("comfy-kitchen"),
            "comfyui_commit": args.comfyui_commit,
        },
        "policy": {
            "quantized_linears": "all 50 DiT blocks: fused qkv_proj, out_proj, to_gate_compress, mlp.fc1, mlp.fc2",
            "dense_linears": "token refiner, AdaLN, norms, embeddings, patch/condition projections, FP32 PDD heads",
            "qkv": "fused by the native Comfy conversion, then quantized as one tensor with one weight scale",
            "activation_quantization": spec["activation"],
            "native_comfy_layout": spec["layout"],
        },
        "quantized_linear_count": len(linears),
        "quantized_linears": linears,
        "pass_through_tensor_count": identical,
        "pass_through_byte_identical": True,
        "elapsed_seconds": round(time.time() - started, 1),
    }
    args.receipt.parent.mkdir(parents=True, exist_ok=True)
    args.receipt.write_text(json.dumps(receipt, indent=2) + "\n")
    print(json.dumps({k: receipt[k] for k in ("format", "output", "quantized_linear_count", "elapsed_seconds")}, indent=2))
    return 0


if __name__ == "__main__":
    sys.exit(main())