File size: 6,587 Bytes
7871e64 | 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 | """Generate the reduced-precision variants of the SHARP ONNX model.
Each variant is written to its own folder next to `fp32/`, as `model.onnx` plus
`model.onnx.data` (the external weight file).
python quantize.py fp16
python quantize.py int8
python quantize.py int4
python quantize.py fp8
Requires: onnx, onnxruntime, onnxconverter-common (fp16), ml_dtypes (fp8).
"""
import argparse
import os
import numpy as np
import onnx
from onnx import TensorProto, helper, numpy_helper
SRC_MODEL = os.path.join("fp32", "model.onnx")
DATA_FILE = "model.onnx.data"
def save_model(model: onnx.ModelProto, folder: str) -> str:
os.makedirs(folder, exist_ok=True)
path = os.path.join(folder, "model.onnx")
# onnx.save appends to an existing external-data file instead of replacing it,
# so any leftovers from an earlier run have to go first
for stale in (path, os.path.join(folder, DATA_FILE)):
if os.path.exists(stale):
os.remove(stale)
onnx.save(
model,
path,
save_as_external_data=True,
all_tensors_to_one_file=True,
location=DATA_FILE,
size_threshold=1024,
convert_attribute=False,
)
return path
def _cast_resize_fp16_inputs(model: onnx.ModelProto) -> int:
"""Insert Cast nodes so Resize gets float32 roi/scales.
Operator block lists do not apply to `Constant` nodes folded into the graph, so the
fp16 scales have to be cast back explicitly.
"""
initializers = {t.name: t for t in model.graph.initializer}
producers = {out: node for node in model.graph.node for out in node.output}
patched = 0
nodes = []
for node in model.graph.node:
if node.op_type != "Resize":
nodes.append(node)
continue
for index in range(1, len(node.input)):
name = node.input[index]
if not name:
continue
producer = producers.get(name)
is_fp16 = initializers.get(name) is not None and initializers[name].data_type == TensorProto.FLOAT16
if producer is not None and producer.op_type == "Constant":
tensor = [a.t for a in producer.attribute if a.name == "value"][0]
is_fp16 = tensor.data_type == TensorProto.FLOAT16
if not is_fp16:
continue
cast_out = f"{name}_float32"
nodes.append(helper.make_node("Cast", [name], [cast_out], name=f"{cast_out}_cast", to=TensorProto.FLOAT))
node.input[index] = cast_out
patched += 1
nodes.append(node)
if patched:
model.graph.ClearField("node")
model.graph.node.extend(nodes)
return patched
def convert_fp16() -> str:
# onnxruntime's converter inserts the casts needed to keep every node's operand
# dtypes consistent, which onnxconverter-common does not do reliably.
from onnxruntime.transformers.float16 import convert_float_to_float16
model = onnx.load(SRC_MODEL) # resolves the external .data file
# use_symbolic_shape_infer=False: the in-memory proto is >2 GB and cannot be serialized
model = convert_float_to_float16(model, keep_io_types=True, disable_shape_infer=True)
print(f"fp16: cast {_cast_resize_fp16_inputs(model)} Resize inputs back to float32")
return save_model(model, "fp16")
def convert_int8() -> str:
from onnxruntime.quantization import QuantType, quantize_dynamic
out = os.path.join("int8", "model.onnx")
os.makedirs("int8", exist_ok=True)
quantize_dynamic(
SRC_MODEL,
out,
weight_type=QuantType.QInt8,
per_channel=True,
reduce_range=False,
use_external_data_format=True,
extra_options={"MatMulConstBOnly": False},
)
return out
def convert_int4() -> str:
from onnxruntime.quantization.matmul_nbits_quantizer import MatMulNBitsQuantizer
model = onnx.load(SRC_MODEL)
quantizer = MatMulNBitsQuantizer(model, block_size=32, is_symmetric=True, bits=4)
quantizer.process()
# newer onnxruntime wraps the ModelProto in onnx_ir.Model
quantized = quantizer.model.model if hasattr(quantizer.model, "model") else quantizer.model
return save_model(quantized, "int4")
def convert_fp8() -> str:
"""Store weights as float8_e4m3fn with a per-output-channel scale.
Each weight becomes `<name>`_fp8` plus a float32 scale, and a DequantizeLinear node
restores `<name>`. float8 needs opset 19 and ONNX Runtime >= 1.25 for the CPU kernel.
"""
import ml_dtypes
model = onnx.load(SRC_MODEL)
for opset in model.opset_import:
if opset.domain in ("", "ai.onnx"):
opset.version = max(opset.version, 19)
dequantize = []
new_initializers = []
converted = 0
for init in model.graph.initializer:
array = numpy_helper.to_array(init)
if array.dtype != np.float32 or array.ndim < 2 or array.size < 1024:
continue
name = init.name
amax = np.abs(array).reshape(array.shape[0], -1).max(axis=1)
scale = np.maximum(amax, 1e-9).astype(np.float32) / 448.0 # 448 = E4M3 max
shape = (array.shape[0],) + (1,) * (array.ndim - 1)
quantized = (array / scale.reshape(shape)).astype(ml_dtypes.float8_e4m3fn)
# the fp8 tensor takes a new name; DequantizeLinear rebuilds the original one
init.CopyFrom(numpy_helper.from_array(quantized, name + "_fp8"))
new_initializers.append(numpy_helper.from_array(scale, name + "_scale"))
dequantize.append(
helper.make_node("DequantizeLinear", [name + "_fp8", name + "_scale"], [name],
name=f"{name}_dequantize", axis=0)
)
converted += 1
# the DequantizeLinear nodes read only initializers, so they can run first
nodes = list(dequantize) + list(model.graph.node)
model.graph.ClearField("node")
model.graph.node.extend(nodes)
model.graph.initializer.extend(new_initializers)
print(f"fp8: converted {converted} weight tensors")
return save_model(model, "fp8")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("variant", choices=["fp16", "int8", "int4", "fp8", "all"])
args = parser.parse_args()
steps = (
[convert_fp16, convert_int8, convert_int4, convert_fp8]
if args.variant == "all"
else [globals()[f"convert_{args.variant}"]]
)
for step in steps:
print(f"-> {step.__name__}")
print(" ", step())
if __name__ == "__main__":
main()
|