Spaces:
Running
Running
| """ | |
| Quantize vit_large_patch16_224_fp32.onnx using Nvidia Model Optimizer. | |
| Quantization modes attempted: | |
| - FP16: convert_to_f16 | |
| - INT8: entropy / max calibration | |
| - FP8: entropy / max calibration | |
| - INT4: awq_clip / awq_lite (symmetric + asymmetric) / awq_full / rtn_dq | |
| Calibration data: vit_large_patch16_224_calibration.npy (20 images, shape 20x3x224x224) | |
| Usage: | |
| python3 vit_large_patch16_224_quantize_all.py # run all modes | |
| python3 vit_large_patch16_224_quantize_all.py --mode fp16 # run FP16 only | |
| python3 vit_large_patch16_224_quantize_all.py --mode int8 # run INT8 entropy + max | |
| python3 vit_large_patch16_224_quantize_all.py --mode fp8 # run FP8 entropy + max | |
| python3 vit_large_patch16_224_quantize_all.py --mode int4 # run all INT4 variants | |
| Running one mode at a time avoids OOM kills on memory-constrained systems. | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| import time | |
| import numpy as np | |
| import onnx | |
| import onnxruntime as ort | |
| import gc | |
| # --------------------------------------------------------------------------- | |
| # Post-quantization repair for INT4 models | |
| # --------------------------------------------------------------------------- | |
| def _fix_int4_dq_axis(output_path): | |
| """Fix ModelOpt bug: classifier.weight DequantizeLinear has axis=0 but | |
| scale shape is correct for axis=1 with block_size=128. | |
| ORT validates ceil(Di/block_size) on the declared axis and rejects the | |
| model at runtime. Patching axis 0 -> 1 makes the scale shape valid. | |
| """ | |
| model = onnx.load(output_path) | |
| fixed = False | |
| for node in model.graph.node: | |
| if node.op_type != "DequantizeLinear": | |
| continue | |
| if "classifier.weight" not in node.name: | |
| continue | |
| axis = None | |
| block_size = None | |
| for attr in node.attribute: | |
| if attr.name == "axis": | |
| axis = attr.i | |
| elif attr.name == "block_size": | |
| block_size = attr.i | |
| if axis != 0 or block_size is None: | |
| continue | |
| weight_name = node.input[0] | |
| scale_name = node.input[1] | |
| weight_shape = scale_shape = None | |
| for init in model.graph.initializer: | |
| if init.name == weight_name: | |
| weight_shape = list(init.dims) | |
| if init.name == scale_name: | |
| scale_shape = list(init.dims) | |
| if weight_shape is None or scale_shape is None: | |
| continue | |
| expected_axis1 = list(weight_shape) | |
| expected_axis1[1] = (expected_axis1[1] + block_size - 1) // block_size | |
| if scale_shape == expected_axis1: | |
| for attr in node.attribute: | |
| if attr.name == "axis": | |
| attr.i = 1 | |
| fixed = True | |
| print(f" [fix] Patched {node.name}: axis 0 -> 1 " | |
| f"(scale {scale_shape} matches axis=1, block_size={block_size})") | |
| if fixed: | |
| onnx.save(model, output_path) | |
| print(f" [fix] Saved repaired model to {output_path}") | |
| else: | |
| print(f" [fix] No classifier DQ axis issues found — model unchanged") | |
| # --------------------------------------------------------------------------- | |
| # Configuration | |
| # --------------------------------------------------------------------------- | |
| ONNX_PATH = "vit_large_patch16_224_fp32.onnx" | |
| CALIB_PATH = "vit_large_patch16_224_calibration.npy" | |
| MODEL_NAME = "vit_large_patch16_224" | |
| # Load calibration data | |
| calib_data = np.load(CALIB_PATH) | |
| print(f"Calibration data: shape={calib_data.shape}, dtype={calib_data.dtype}") | |
| # --------------------------------------------------------------------------- | |
| # FP16 Quantization | |
| # --------------------------------------------------------------------------- | |
| def run_fp16(): | |
| print("\n" + "=" * 70) | |
| print("FP16 Quantization") | |
| print("=" * 70) | |
| from modelopt.onnx.quantization.int8 import convert_to_f16 | |
| onnx_model = onnx.load(ONNX_PATH, load_external_data=True) | |
| fp16_model = convert_to_f16(onnx_model, keep_io_types=True) | |
| output_path = "fp16/vit_large_patch16_224_fp16.onnx" | |
| if not os.path.exists("fp16"): | |
| os.mkdir("fp16") | |
| onnx.save(fp16_model, output_path) | |
| size_mb = os.path.getsize(output_path) / 1e6 | |
| print(f" Saved: {output_path} ({size_mb:.1f} MB)") | |
| return output_path | |
| # --------------------------------------------------------------------------- | |
| # ViT-specific: find nodes that must stay in FP32 for accurate quantization | |
| # --------------------------------------------------------------------------- | |
| def _find_vit_nodes_to_exclude(onnx_path): | |
| """Find all MatMul and Add nodes that must stay in FP32 for ViT. | |
| ViT-Large has 192 MatMuls per 24 transformer layers: | |
| - 72 Q/K/V projections (fed by LayerNorm, 3/layer) | |
| - 24 Q@K^T attention score MatMuls (feed into Softmax) | |
| - 24 attn@V attention apply MatMuls | |
| - 24 attention output projection MatMuls | |
| - 24 MLP input MatMuls (fed by LayerNorm) | |
| - 24 MLP hidden MatMuls (SAFE to quantize) | |
| Additionally, 49 Add nodes feed directly into LayerNorm (residual→Pre-LN). | |
| Quantizing these inserts Q/DQ before LayerNorm, amplifying noise via x/std. | |
| Only the 24 MLP hidden MatMuls are safe for INT8 quantization. | |
| All 168 attention-path MatMuls must stay FP32 because: | |
| - LayerNorm→MatMul: Q/DQ on LN output amplifies noise via x/std | |
| - Q@K^T: INT8 error in scores gets exponentially amplified by Softmax | |
| - attn@V: error in attention weights corrupts the value aggregation | |
| - Output proj: receives attn@V result which must stay accurate | |
| """ | |
| model = onnx.load(onnx_path, load_external_data=False) | |
| # Build maps | |
| output_to_producer = {} | |
| output_to_consumers = {} | |
| for node in model.graph.node: | |
| for out in node.output: | |
| output_to_producer[out] = node | |
| for inp in node.input: | |
| output_to_consumers.setdefault(inp, []).append(node) | |
| all_matmuls = [n for n in model.graph.node if n.op_type == "MatMul"] | |
| excluded = set() | |
| # 1. MatMuls fed directly by LayerNorm (Q/K/V projections + MLP input) | |
| for mm in all_matmuls: | |
| for inp in mm.input: | |
| prod = output_to_producer.get(inp) | |
| if prod and prod.op_type == "LayerNormalization": | |
| excluded.add(mm.name) | |
| break | |
| # 2. MatMuls that feed into Softmax (Q@K^T attention scores) | |
| # Trace backwards from each Softmax through arithmetic/shape ops | |
| _passthrough = {"Mul", "Div", "Reshape", "Transpose", "Cast", "Add", | |
| "Slice", "Where", "IsNaN", "Concat", "Shape", "Expand"} | |
| for sm in (n for n in model.graph.node if n.op_type == "Softmax"): | |
| visited, queue = set(), [sm] | |
| while queue: | |
| node = queue.pop(0) | |
| if node.name in visited: | |
| continue | |
| visited.add(node.name) | |
| for inp in node.input: | |
| prod = output_to_producer.get(inp) | |
| if prod: | |
| if prod.op_type == "MatMul": | |
| excluded.add(prod.name) | |
| elif prod.op_type in _passthrough: | |
| queue.append(prod) | |
| # 3. MatMuls that apply attention weights (attn@V) | |
| # Trace forwards from each Softmax through arithmetic/shape ops | |
| for sm in (n for n in model.graph.node if n.op_type == "Softmax"): | |
| visited, queue = set(), list(sm.output) | |
| for _ in range(10): | |
| next_q = [] | |
| for out_name in queue: | |
| for consumer in output_to_consumers.get(out_name, []): | |
| if consumer.name in visited: | |
| continue | |
| visited.add(consumer.name) | |
| if consumer.op_type == "MatMul": | |
| excluded.add(consumer.name) | |
| elif consumer.op_type in _passthrough: | |
| next_q.extend(consumer.output) | |
| queue = next_q | |
| if not queue: | |
| break | |
| # 4. Attention output projection MatMuls | |
| # Trace forward from each attn@V MatMul to find the next MatMul | |
| attn_v_names = {n.name for n in all_matmuls if n.name in excluded | |
| and n.name not in {mm.name for mm in all_matmuls | |
| if any(output_to_producer.get(inp, None) is not None | |
| and output_to_producer[inp].op_type == "LayerNormalization" | |
| for inp in mm.input)}} | |
| for mm in all_matmuls: | |
| if mm.name in attn_v_names: | |
| visited, queue = set(), list(mm.output) | |
| for _ in range(5): | |
| next_q = [] | |
| for out_name in queue: | |
| for consumer in output_to_consumers.get(out_name, []): | |
| if consumer.name in visited: | |
| continue | |
| visited.add(consumer.name) | |
| if consumer.op_type == "MatMul" and consumer.name not in excluded: | |
| excluded.add(consumer.name) | |
| elif consumer.op_type in _passthrough: | |
| next_q.extend(consumer.output) | |
| queue = next_q | |
| if not queue: | |
| break | |
| # 5. Add nodes whose output feeds into LayerNorm (residual→Pre-LN path) | |
| # Quantizing these inserts Q/DQ before LayerNorm, same problem as #1. | |
| add_to_ln = 0 | |
| for node in model.graph.node: | |
| if node.op_type == "Add": | |
| for out in node.output: | |
| for consumer in output_to_consumers.get(out, []): | |
| if consumer.op_type == "LayerNormalization": | |
| excluded.add(node.name) | |
| add_to_ln += 1 | |
| break | |
| del model | |
| matmul_excluded = len(excluded & {n.name for n in all_matmuls}) | |
| matmul_safe = len(all_matmuls) - matmul_excluded | |
| print(f" Excluded: {matmul_excluded} attention MatMuls + {add_to_ln} residual→LN Adds " | |
| f"= {len(excluded)} nodes total") | |
| print(f" Safe to quantize: {matmul_safe} MLP MatMuls + remaining Adds + Conv") | |
| return sorted(excluded) | |
| # --------------------------------------------------------------------------- | |
| # INT8 Quantization | |
| # --------------------------------------------------------------------------- | |
| def run_int8(method, label=None): | |
| """Run INT8 quantization with a given calibration method.""" | |
| if label is None: | |
| label = f"int8_{method}{zp_tag}" | |
| print("\n" + "=" * 70) | |
| print(f"INT8 Quantization — method={method}") | |
| print("=" * 70) | |
| from modelopt.onnx.quantization import quantize | |
| # Find attention-path MatMul nodes that must stay in FP32 | |
| nodes_to_exclude = _find_vit_nodes_to_exclude(ONNX_PATH) | |
| output_path = f"int8/vit_large_patch16_224_{label}.onnx" | |
| if not os.path.exists("int8"): | |
| os.mkdir("int8") | |
| t0 = time.time() | |
| quantize( | |
| onnx_path=ONNX_PATH, | |
| quantize_mode="int8", | |
| calibration_data=calib_data, | |
| calibration_method=method, | |
| output_path=output_path, | |
| use_external_data_format=True, | |
| calibration_eps=["cuda:0", "cpu"], | |
| opset=19, | |
| # ViT-specific fixes: the decomposed attention mechanism and LayerNorm | |
| # are extremely sensitive to INT8 quantization. | |
| # Without these exclusions, Top-1 drops from 82% to 0%. | |
| # | |
| # Root cause: ModelOpt's QDQ pattern inserts Q/DQ on the activation | |
| # inputs of quantized MatMuls. For attention-path MatMuls, this | |
| # quantizes LayerNorm outputs (amplified by x/std normalization) and | |
| # attention scores (exponentially amplified by Softmax). | |
| # | |
| # Fix: allowlist only MatMul+Add+Conv, and exclude all 168 attention- | |
| # path MatMuls via nodes_to_exclude. Only 24 MLP hidden MatMuls are | |
| # quantized. | |
| op_types_to_quantize=["MatMul", "Add", "Conv"], | |
| nodes_to_exclude=nodes_to_exclude, | |
| ) | |
| elapsed = time.time() - t0 | |
| # Calculate total size (onnx + .data) | |
| size_mb = os.path.getsize(output_path) / 1e6 | |
| data_path = output_path + ".data" | |
| if os.path.exists(data_path): | |
| size_mb += os.path.getsize(data_path) / 1e6 | |
| print(f" Saved: {output_path} ({size_mb:.1f} MB) in {elapsed:.1f}s") | |
| print("ir_version: " + str(onnx.load(output_path).ir_version)) | |
| return output_path | |
| # --------------------------------------------------------------------------- | |
| # FP8 Quantization | |
| # --------------------------------------------------------------------------- | |
| def run_fp8(method): | |
| print("\n" + "=" * 70) | |
| print(f"FP8 Quantization — method={method}") | |
| print("=" * 70) | |
| from modelopt.onnx.quantization import quantize | |
| # Find attention-path MatMul nodes that must stay in FP32 | |
| nodes_to_exclude = _find_vit_nodes_to_exclude(ONNX_PATH) | |
| output_path = f"fp8/vit_large_patch16_224_fp8_{method}.onnx" | |
| if not os.path.exists("fp8"): | |
| os.mkdir("fp8") | |
| t0 = time.time() | |
| quantize( | |
| onnx_path=ONNX_PATH, | |
| quantize_mode="fp8", | |
| calibration_data=calib_data, | |
| calibration_method=method, | |
| output_path=output_path, | |
| use_external_data_format=True, | |
| calibration_eps=["cuda:0", "cpu"], | |
| opset=19, | |
| # ViT-specific fixes (same rationale as INT8) op_types_to_quantize=["MatMul", "Add", "Conv"], | |
| nodes_to_exclude=nodes_to_exclude, | |
| ) | |
| elapsed = time.time() - t0 | |
| size_mb = os.path.getsize(output_path) / 1e6 | |
| data_path = output_path + ".data" | |
| if os.path.exists(data_path): | |
| size_mb += os.path.getsize(data_path) / 1e6 | |
| print(f" Saved: {output_path} ({size_mb:.1f} MB) in {elapsed:.1f}s") | |
| return output_path | |
| # --------------------------------------------------------------------------- | |
| # INT4 Quantization | |
| # --------------------------------------------------------------------------- | |
| def run_int4(method, use_zero_point=False): | |
| print("\n" + "=" * 70) | |
| print(f"INT4 Quantization — method={method}, zero_point={use_zero_point}") | |
| print("=" * 70) | |
| from modelopt.onnx.quantization import quantize | |
| zp_tag = "_asym" if use_zero_point else "" | |
| output_path = f"int4/vit_large_patch16_224_int4_{method}{zp_tag}.onnx" | |
| if not os.path.exists("int4"): | |
| os.mkdir("int4") | |
| t0 = time.time() | |
| quantize( | |
| onnx_path=ONNX_PATH, | |
| quantize_mode="int4", | |
| calibration_data=calib_data, | |
| calibration_method=method, | |
| use_zero_point=use_zero_point, | |
| output_path=output_path, | |
| use_external_data_format=True, | |
| calibration_eps=["cuda:0", "cpu"], | |
| opset=21, | |
| ) | |
| elapsed = time.time() - t0 | |
| size_mb = os.path.getsize(output_path) / 1e6 | |
| data_path = output_path + ".data" | |
| if os.path.exists(data_path): | |
| size_mb += os.path.getsize(data_path) / 1e6 | |
| print(f" Saved: {output_path} ({size_mb:.1f} MB) in {elapsed:.1f}s") | |
| # Post-quantization repair: fix classifier DequantizeLinear axis bug | |
| _fix_int4_dq_axis(output_path) | |
| return output_path | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| ALL_MODES = ["fp16", "int8", "fp8", "int4"] | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser( | |
| description="Quantize vit_large_patch16_224 using NVIDIA ModelOpt" | |
| ) | |
| parser.add_argument( | |
| "--mode", type=str, nargs="*", default=ALL_MODES, | |
| choices=ALL_MODES, | |
| help=f"Quantization mode(s) to run (default: all). " | |
| f"Run one at a time to avoid OOM on large models. " | |
| f"Choices: {ALL_MODES}", | |
| ) | |
| args = parser.parse_args() | |
| modes = args.mode | |
| print(f"Running modes: {modes}") | |
| results = {} | |
| # --- FP16 --- | |
| if "fp16" in modes: | |
| try: | |
| results["fp16"] = run_fp16() | |
| except Exception as e: | |
| print(f" FP16 FAILED: {e}") | |
| finally: | |
| gc.collect() | |
| # --- INT8 variants --- | |
| if "int8" in modes: | |
| for method in ["entropy", "max"]: | |
| label = f"int8_{method}" | |
| try: | |
| results[label] = run_int8(method, label=label) | |
| except Exception as e: | |
| print(f" {label} FAILED: {e}") | |
| finally: | |
| gc.collect() | |
| # --- FP8 variants --- | |
| if "fp8" in modes: | |
| for method in ["entropy", "max"]: | |
| label = f"fp8_{method}" | |
| try: | |
| results[label] = run_fp8(method) | |
| except Exception as e: | |
| print(f" {label} FAILED: {e}") | |
| finally: | |
| gc.collect() | |
| # --- INT4 variants --- | |
| if "int4" in modes: | |
| for method in ["awq_clip", "awq_full", "rtn_dq"]: | |
| label = f"int4_{method}" | |
| try: | |
| results[label] = run_int4(method, use_zero_point=False) | |
| except Exception as e: | |
| print(f" {label} FAILED: {e}") | |
| finally: | |
| gc.collect() | |
| # special treat for awq_lite | |
| method = "awq_lite" | |
| for zp in [False, True]: | |
| label = f"int4_{method}" + ("_asym" if zp else "") | |
| try: | |
| results[label] = run_int4(method, use_zero_point=zp) | |
| except Exception as e: | |
| print(f" {label} FAILED: {e}") | |
| finally: | |
| gc.collect() | |
| # --- Summary --- | |
| print("\n" + "=" * 70) | |
| print("Quantization Summary") | |
| print("=" * 70) | |
| for label, path in results.items(): | |
| size_mb = os.path.getsize(path) / 1e6 | |
| data_path = path + ".data" | |
| if os.path.exists(data_path): | |
| size_mb += os.path.getsize(data_path) / 1e6 | |
| print(f" {label:30s} → {path:50s} ({size_mb:.1f} MB)") | |