Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions onnxruntime/python/tools/quantization/operators/matmul.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
import logging

import onnx
from onnx import onnx_pb as onnx_proto

from ..quant_utils import TENSOR_NAME_QUANT_SUFFIX, QuantizedValue, QuantizedValueType, find_by_name, get_mul_node
from ..quant_utils import (
FLOAT8_TYPES,
TENSOR_NAME_QUANT_SUFFIX,
QuantizedValue,
QuantizedValueType,
find_by_name,
get_mul_node,
)
from .base_operator import QuantOperatorBase
from .qdq_base_operator import QDQOperatorBase

Expand Down Expand Up @@ -172,17 +178,7 @@ def quantize(self):
qlinear_matmul_inputs.append(output_scale_name)
qlinear_matmul_inputs.append(output_zp_name)

domain = (
"com.microsoft"
if self.quantizer.weight_qType
in {
onnx_proto.TensorProto.FLOAT8E4M3FN,
onnx_proto.TensorProto.FLOAT8E4M3FNUZ,
onnx_proto.TensorProto.FLOAT8E5M2,
onnx_proto.TensorProto.FLOAT8E5M2FNUZ,
}
else ""
)
domain = "com.microsoft" if self.quantizer.weight_qType in FLOAT8_TYPES else ""
Comment thread
tianleiwu marked this conversation as resolved.
qlinear_matmul_node = onnx.helper.make_node(
"QLinearMatMul",
qlinear_matmul_inputs,
Expand Down
12 changes: 11 additions & 1 deletion onnxruntime/python/tools/quantization/operators/maxpool.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from ..quant_utils import FLOAT8_TYPES
from .base_operator import QuantOperatorBase
from .direct_q8 import Direct8BitOp, QDQDirect8BitOp


Expand All @@ -11,9 +13,13 @@

# if version is less than 12, go to normal quantize.
if self.quantizer.opset_version < 12:
super(Direct8BitOp, self).quantize()
QuantOperatorBase.quantize(self)
return

# FP8 types are not supported for MaxPool; emit node unquantized.
if self.quantizer.activation_qType in FLOAT8_TYPES:
return QuantOperatorBase.quantize(self)

Check warning

Code scanning / CodeQL

Use of the return value of a procedure Warning

The result of
QuantOperatorBase.quantize
is used even though it is always None.
Comment thread
tianleiwu marked this conversation as resolved.
Dismissed

# Direct 8bits op
return super().quantize()

Expand All @@ -30,5 +36,9 @@
if self.quantizer.opset_version < 12:
return

# FP8 types are not supported for MaxPool; leave node unquantized.
if self.quantizer.activation_qType in FLOAT8_TYPES:
return

# Direct 8bits op
return super().quantize()
6 changes: 6 additions & 0 deletions onnxruntime/python/tools/quantization/quant_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
MODEL_SIZE_THRESHOLD = 2147483648 # Quant model should use external data if >= 2GB

FLOAT8_DISTRIBUTIONS = {}
FLOAT8_TYPES = (
Comment thread
tianleiwu marked this conversation as resolved.
onnx_proto.TensorProto.FLOAT8E4M3FN,
onnx_proto.TensorProto.FLOAT8E4M3FNUZ,
onnx_proto.TensorProto.FLOAT8E5M2,
onnx_proto.TensorProto.FLOAT8E5M2FNUZ,
Comment thread
tianleiwu marked this conversation as resolved.
)

type_to_name = {getattr(TensorProto, k): k for k in dir(TensorProto) if isinstance(getattr(TensorProto, k), int)}

Expand Down
156 changes: 155 additions & 1 deletion onnxruntime/test/python/quantization/test_op_maxpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# license information.
# --------------------------------------------------------------------------

import os
import tempfile
import unittest

import numpy as np
Expand All @@ -18,7 +20,8 @@
check_qtype_by_node_type,
)

from onnxruntime.quantization import QuantFormat, QuantType, quantize_static
from onnxruntime.quantization import CalibrationMethod, QuantFormat, QuantType, quantize_static
from onnxruntime.quantization.quant_utils import FLOAT8_TYPES


class TestOpMaxPool(unittest.TestCase):
Expand Down Expand Up @@ -171,3 +174,154 @@ def test_quantize_maxpool_s8s8(self):

if __name__ == "__main__":
unittest.main()


class TestOpMaxPoolFP8(unittest.TestCase):
"""Tests for MaxPool with FP8 quantization (issue #21090)."""

def setUp(self):
np.random.seed(1)

def construct_model_conv_maxpool(self, output_model_path):
conv_input_shape = [1, 2, 26, 42]
conv_weight_shape = [3, 2, 3, 3]
maxpool_input_shape = [1, 3, 24, 40]
output_shape = [1, 3, 22, 38]

input_tensor = helper.make_tensor_value_info("input", TensorProto.FLOAT, conv_input_shape)
conv_weight_arr = np.random.randint(-1, 2, conv_weight_shape).astype(np.float32)
conv_weight_initializer = onnx.numpy_helper.from_array(conv_weight_arr, name="conv1_weight")
conv_node = onnx.helper.make_node("Conv", ["input", "conv1_weight"], ["conv_output"], name="conv_node")
identity_out = helper.make_tensor_value_info("identity_out", TensorProto.FLOAT, maxpool_input_shape)
identity_node = helper.make_node("Identity", ["conv_output"], ["identity_out"], name="IdentityNode")
output_tensor = helper.make_tensor_value_info("output", TensorProto.FLOAT, output_shape)
maxpool_node = helper.make_node(
"MaxPool", ["conv_output"], ["output"], name="maxpool_node", kernel_shape=[3, 3]
Comment thread
tianleiwu marked this conversation as resolved.
)
graph = helper.make_graph(
[conv_node, identity_node, maxpool_node],
"TestMaxPoolFP8",
[input_tensor],
[identity_out, output_tensor],
initializer=[conv_weight_initializer],
)
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 14)])
model.ir_version = 7
onnx.save(model, output_model_path)

def test_quantize_maxpool_fp8(self):
"""MaxPool must be left unquantized (pass-through) when FP8 types are used.

Verifies graph structure only — no InferenceSession, since QLinearConv-FP8
is not runnable on the CPU EP (separate concern from issue #21090).
"""

with tempfile.TemporaryDirectory() as tmp:
model_fp32_path = os.path.join(tmp, "maxpool_fp32.onnx")
model_fp8_qdq_path = os.path.join(tmp, "maxpool_fp8_qdq.onnx")

self.construct_model_conv_maxpool(model_fp32_path)

input_data_list = [{"input": np.random.randint(-1, 2, [1, 2, 26, 42]).astype(np.float32)}]
data_reader = TestDataFeeds(input_data_list)

# quantize_static must complete without raising ValueError
quantize_static(
model_fp32_path,
model_fp8_qdq_path,
data_reader,
quant_format=QuantFormat.QDQ,
activation_type=QuantType.QFLOAT8E4M3FN,
weight_type=QuantType.QFLOAT8E4M3FN,
calibrate_method=CalibrationMethod.Distribution,
)

# Inspect graph structure: MaxPool must not have FP8 tensors on its
# inputs or outputs, confirming it was skipped during quantization.
qdq_model = onnx.load(model_fp8_qdq_path)
tensor_types = {}
for vi in qdq_model.graph.value_info:
tensor_types[vi.name] = vi.type.tensor_type.elem_type
for inp in qdq_model.graph.input:
tensor_types[inp.name] = inp.type.tensor_type.elem_type
for out in qdq_model.graph.output:
tensor_types[out.name] = out.type.tensor_type.elem_type

for node in qdq_model.graph.node:
if node.op_type == "MaxPool":
for tensor_name in list(node.input) + list(node.output):
if tensor_name in tensor_types:
self.assertNotIn(
tensor_types[tensor_name],
FLOAT8_TYPES,
f"MaxPool tensor {tensor_name!r} must not be FP8 type",
)
Comment thread
tianleiwu marked this conversation as resolved.

# Assert no QuantizeLinear node whose output feeds directly into MaxPool
# (i.e., no FP8 QDQ pair was inserted around MaxPool).
maxpool_inputs = set()
for node in qdq_model.graph.node:
if node.op_type == "MaxPool":
maxpool_inputs.update(node.input)

for node in qdq_model.graph.node:
if node.op_type == "QuantizeLinear":
for out in node.output:
self.assertNotIn(
out,
maxpool_inputs,
f"QuantizeLinear output {out!r} must not feed directly into MaxPool",
)

def test_quantize_maxpool_fp8_qoperator(self):
"""QMaxPool (QOperator format) guard: MaxPool must remain unquantized under FP8.

The guard lives in QMaxPool.quantize() in operators/maxpool.py. When
activation_qType is in FLOAT8_TYPES the method falls back to
QuantOperatorBase.quantize(), which copies the node unchanged. This
test exercises that path and confirms the output graph still contains
exactly one plain MaxPool node with no FP8-typed inputs/outputs.
"""
with tempfile.TemporaryDirectory() as tmp:
model_fp32_path = os.path.join(tmp, "maxpool_fp32.onnx")
model_fp8_qop_path = os.path.join(tmp, "maxpool_fp8_qoperator.onnx")

self.construct_model_conv_maxpool(model_fp32_path)

input_data_list = [{"input": np.random.randint(-1, 2, [1, 2, 26, 42]).astype(np.float32)}]
data_reader = TestDataFeeds(input_data_list)

# quantize_static with QOperator format must complete without raising.
quantize_static(
model_fp32_path,
model_fp8_qop_path,
data_reader,
quant_format=QuantFormat.QOperator,
activation_type=QuantType.QFLOAT8E4M3FN,
weight_type=QuantType.QFLOAT8E4M3FN,
calibrate_method=CalibrationMethod.Distribution,
)

qop_model = onnx.load(model_fp8_qop_path)

# Exactly one MaxPool node must survive (unquantized).
maxpool_nodes = [n for n in qop_model.graph.node if n.op_type == "MaxPool"]
self.assertEqual(len(maxpool_nodes), 1, "Expected exactly one MaxPool node in QOperator FP8 model")

# No FP8-typed tensor may appear on MaxPool inputs or outputs.
tensor_types = {}
for vi in qop_model.graph.value_info:
tensor_types[vi.name] = vi.type.tensor_type.elem_type
for inp in qop_model.graph.input:
tensor_types[inp.name] = inp.type.tensor_type.elem_type
for out in qop_model.graph.output:
tensor_types[out.name] = out.type.tensor_type.elem_type

for node in maxpool_nodes:
for tensor_name in list(node.input) + list(node.output):
if tensor_name in tensor_types:
self.assertNotIn(
tensor_types[tensor_name],
FLOAT8_TYPES,
f"MaxPool tensor {tensor_name!r} must not be FP8 type in QOperator model",
)
Loading