Skip to content

Commit c4e965d

Browse files
committed
WIP PTQ tool
1 parent 9d9f98c commit c4e965d

13 files changed

Lines changed: 2273 additions & 0 deletions

File tree

tools/ptq/checkpoint_merger.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import argparse
2+
import logging
3+
import sys
4+
import yaml
5+
import re
6+
from typing import Dict, Tuple
7+
import torch
8+
from safetensors.torch import save_file
9+
import json
10+
11+
# Add comfyui to path if needed
12+
import os
13+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
14+
15+
import comfy.utils
16+
from comfy.ops import QUANT_FORMAT_MIXINS
17+
from comfy.quant_ops import F8_E4M3_MAX, F4_E2M1_MAX
18+
19+
class QuantizationConfig:
20+
def __init__(self, config_path: str):
21+
with open(config_path, 'r') as f:
22+
self.config = yaml.safe_load(f)
23+
24+
# Compile disable list patterns
25+
self.disable_patterns = []
26+
for pattern in self.config.get('disable_list', []):
27+
# Convert glob-style patterns to regex
28+
regex_pattern = pattern.replace('*', '.*')
29+
self.disable_patterns.append(re.compile(regex_pattern))
30+
31+
# Parse per-layer dtype config
32+
self.per_layer_dtype = self.config.get('per_layer_dtype', {})
33+
self.dtype_patterns = []
34+
for pattern, dtype in self.per_layer_dtype.items():
35+
regex_pattern = pattern.replace('*', '.*')
36+
self.dtype_patterns.append((re.compile(regex_pattern), dtype))
37+
38+
logging.info(f"Loaded config with {len(self.disable_patterns)} disable patterns")
39+
logging.info(f"Per-layer dtype rules: {self.per_layer_dtype}")
40+
41+
def should_quantize(self, layer_name: str) -> bool:
42+
for pattern in self.disable_patterns:
43+
if pattern.match(layer_name):
44+
logging.debug(f"Layer {layer_name} disabled by pattern {pattern.pattern}")
45+
return False
46+
return True
47+
48+
def get_dtype(self, layer_name: str) -> str:
49+
for pattern, dtype in self.dtype_patterns:
50+
if pattern.match(layer_name):
51+
return dtype
52+
return None
53+
54+
def load_amax_artefact(artefact_path: str) -> Dict:
55+
logging.info(f"Loading amax artefact from {artefact_path}")
56+
57+
with open(artefact_path, 'r') as f:
58+
data = json.load(f)
59+
60+
if 'amax_values' not in data:
61+
raise ValueError("Invalid artefact format: missing 'amax_values' key")
62+
63+
metadata = data.get('metadata', {})
64+
amax_values = data['amax_values']
65+
66+
logging.info(f"Loaded {len(amax_values)} amax values from artefact")
67+
logging.info(f"Artefact metadata: {metadata}")
68+
69+
return data
70+
71+
def get_scale_fp8(amax: float, dtype: torch.dtype) -> torch.Tensor:
72+
scale = amax / torch.finfo(dtype).max
73+
scale_tensor = torch.tensor(scale, dtype=torch.float32)
74+
return scale_tensor
75+
76+
def get_scale_nvfp4(amax: float, dtype: torch.dtype) -> torch.Tensor:
77+
scale = amax / (F8_E4M3_MAX * F4_E2M1_MAX)
78+
scale_tensor = torch.tensor(scale, dtype=torch.float32)
79+
return scale_tensor
80+
81+
def get_scale(amax: float, dtype: torch.dtype):
82+
if dtype in [torch.float8_e4m3fn, torch.float8_e5m2]:
83+
return get_scale_fp8(amax, dtype)
84+
elif dtype in [torch.float4_e2m1fn_x2]:
85+
return get_scale_nvfp4(amax, dtype)
86+
else:
87+
raise ValueError(f"Unsupported dtype {dtype} ")
88+
89+
def apply_quantization(
90+
checkpoint: Dict,
91+
amax_values: Dict[str, float],
92+
config: QuantizationConfig
93+
) -> Tuple[Dict, Dict]:
94+
quantized_dict = {}
95+
layer_metadata = {}
96+
97+
for key, amax in amax_values.items():
98+
if key.endswith(".input_quantizer"):
99+
continue
100+
101+
layer_name = ".".join(key.split(".")[:-1])
102+
103+
if not config.should_quantize(layer_name):
104+
logging.debug(f"Layer {layer_name} disabled by config")
105+
continue
106+
107+
dtype_str = config.get_dtype(layer_name)
108+
dtype = getattr(torch, dtype_str)
109+
device = torch.device("cuda") # Required for NVFP4
110+
111+
weight = checkpoint.pop(f"{layer_name}.weight").to(device)
112+
scale_tensor = get_scale(amax, dtype)
113+
114+
input_amax = amax_values.get(f"{layer_name}.input_quantizer", None)
115+
if input_amax is not None:
116+
input_scale = get_scale(input_amax, dtype)
117+
quantized_dict[f"{layer_name}.input_scale"] = input_scale.clone()
118+
119+
# logging.info(f"Quantizing {layer_name}: amax={amax}, scale={scale_tensor:.6f}")
120+
tensor_layout = QUANT_FORMAT_MIXINS[dtype_str]["layout_type"]
121+
quantized_weight, layout_params = tensor_layout.quantize(
122+
weight,
123+
scale=scale_tensor,
124+
dtype=dtype
125+
)
126+
quantized_dict[f"{layer_name}.weight_scale"] = scale_tensor.clone()
127+
quantized_dict[f"{layer_name}.weight"] = quantized_weight.clone()
128+
129+
if "block_scale" in layout_params:
130+
quantized_dict[f"{layer_name}.weight_block_scale"] = layout_params["block_scale"].clone()
131+
132+
# Build metadata
133+
layer_metadata[layer_name] = {
134+
"format": dtype_str,
135+
"params": {}
136+
}
137+
138+
logging.info(f"Quantized {len(layer_metadata)} layers")
139+
140+
quantized_dict = quantized_dict | checkpoint
141+
142+
metadata_dict = {
143+
"_quantization_metadata": json.dumps({
144+
"format_version": "1.0",
145+
"layers": layer_metadata
146+
})
147+
}
148+
return quantized_dict, metadata_dict
149+
150+
151+
def main():
152+
"""Main entry point for checkpoint merger."""
153+
154+
parser = argparse.ArgumentParser(
155+
description="Merge calibration artifacts with checkpoint to create quantized model",
156+
formatter_class=argparse.RawDescriptionHelpFormatter,
157+
)
158+
159+
parser.add_argument(
160+
"--artefact",
161+
required=True,
162+
help="Path to calibration artefact JSON file (amax values)"
163+
)
164+
parser.add_argument(
165+
"--checkpoint",
166+
required=True,
167+
help="Path to original checkpoint to quantize"
168+
)
169+
parser.add_argument(
170+
"--config",
171+
required=True,
172+
help="Path to YAML quantization config file"
173+
)
174+
parser.add_argument(
175+
"--output",
176+
required=True,
177+
help="Output path for quantized checkpoint"
178+
)
179+
parser.add_argument(
180+
"--debug",
181+
action="store_true",
182+
help="Enable debug logging"
183+
)
184+
185+
args = parser.parse_args()
186+
187+
# Configure logging
188+
if args.debug:
189+
logging.basicConfig(
190+
level=logging.DEBUG,
191+
format='[%(levelname)s] %(name)s: %(message)s'
192+
)
193+
else:
194+
logging.basicConfig(
195+
level=logging.INFO,
196+
format='[%(levelname)s] %(message)s'
197+
)
198+
199+
# Print header
200+
201+
# Step 1: Load calibration artefact
202+
logging.info("[1/5] Loading calibration artefact...")
203+
try:
204+
artefact_data = load_amax_artefact(args.artefact)
205+
amax_values = artefact_data['amax_values']
206+
except Exception as e:
207+
logging.error(f"Failed to load artefact: {e}")
208+
sys.exit(1)
209+
210+
# Step 2: Load quantization config
211+
logging.info("[2/5] Loading quantization config...")
212+
try:
213+
config = QuantizationConfig(args.config)
214+
except Exception as e:
215+
logging.error(f"Failed to load config: {e}")
216+
sys.exit(1)
217+
218+
# Step 3: Load checkpoint
219+
logging.info("[3/5] Loading checkpoint...")
220+
try:
221+
checkpoint = comfy.utils.load_torch_file(args.checkpoint)
222+
logging.info(f"Loaded checkpoint with {len(checkpoint)} keys")
223+
except Exception as e:
224+
logging.error(f"Failed to load checkpoint: {e}")
225+
sys.exit(1)
226+
227+
# Step 4: Apply quantization
228+
logging.info("[4/5] Applying quantization...")
229+
try:
230+
quantized_dict, metadata_json = apply_quantization(
231+
checkpoint,
232+
amax_values,
233+
config
234+
)
235+
except Exception as e:
236+
logging.error(f"Failed to apply quantization: {e}")
237+
import traceback
238+
traceback.print_exc()
239+
sys.exit(1)
240+
241+
# Step 5: Export quantized checkpoint
242+
logging.info("[5/5] Exporting quantized checkpoint...")
243+
try:
244+
save_file(quantized_dict, args.output, metadata=metadata_json)
245+
246+
except Exception as e:
247+
logging.error(f"Failed to export checkpoint: {e}")
248+
import traceback
249+
traceback.print_exc()
250+
sys.exit(1)
251+
252+
253+
if __name__ == "__main__":
254+
main()
255+

tools/ptq/configs/flux_fp8.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# FLUX Quantization Config: Transformer Blocks Only
2+
#
3+
# Quantize only double and single transformer blocks,
4+
# leave input/output projections in higher precision.
5+
6+
disable_list: [
7+
# Disable input projections
8+
"*img_in*",
9+
"*txt_in*",
10+
"*time_in*",
11+
"*vector_in*",
12+
"*guidance_in*",
13+
14+
# Disable output layers
15+
"*final_layer*",
16+
17+
# Disable positional embeddings
18+
"*pe_embedder*",
19+
]
20+
21+
per_layer_dtype: {
22+
"*": "float8_e4m3fn",
23+
}

tools/ptq/configs/flux_nvfp4.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# FLUX Quantization Config: Transformer Blocks Only
2+
#
3+
# Quantize only double and single transformer blocks,
4+
# leave input/output projections in higher precision.
5+
6+
disable_list: [
7+
# Disable input projections
8+
"*img_in*",
9+
"*txt_in*",
10+
"*time_in*",
11+
"*vector_in*",
12+
"*guidance_in*",
13+
14+
# Disable output layers
15+
"*final_layer*",
16+
17+
# Disable positional embeddings
18+
"*pe_embedder*",
19+
20+
"*modulation*",
21+
"*txt_mod*",
22+
"*img_mod*",
23+
]
24+
25+
per_layer_dtype: {
26+
"*": "float4_e2m1fn_x2",
27+
}

0 commit comments

Comments
 (0)