This is an automated email from the ASF dual-hosted git repository.

tlopex pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new 1729c726bf [Relax][Frontend][ONNX] Support Modern QDQ opset attributes 
(#19993)
1729c726bf is described below

commit 1729c726bf068011fec4a921b9d1ffff5014ec12
Author: Ronald Nap <[email protected]>
AuthorDate: Mon Jul 13 15:11:42 2026 -0700

    [Relax][Frontend][ONNX] Support Modern QDQ opset attributes (#19993)
    
    ## Summary
    
    Adds support for newer `QuantizeLinear` and `DequantizeLinear`
    attributes in the Relax ONNX frontend.
    
    This includes `output_dtype`, `saturate`, and newer opset behavior,
    while rejecting unsupported blocked quantization and `precision` cases.
    
    For `QuantizeLinear` and `DequantizeLinear`, opsets 24 and 25 use the
    existing converter for currently supported types. Support for
    `float8e8m0`, `int2`, and `uint2` are outside this PR’s scope.
    
    ## Testing
    
    Added structural and rejection tests for opsets 19, 21, 23, 24, and 25.
---
 python/tvm/relax/frontend/onnx/onnx_frontend.py | 113 +++++++++++
 tests/python/relax/test_frontend_onnx.py        | 249 ++++++++++++++++++++++++
 2 files changed, 362 insertions(+)

diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py 
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index 70e90d3731..0ca7ef9e0c 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -356,6 +356,78 @@ class QuantizeLinear(OnnxOpConverter):
             zp = relax.const(0, out_dtype)
         return relax.op.quantize(x, scale, zp, axis=axis, out_dtype=out_dtype)
 
+    @classmethod
+    def _impl_v19(cls, bb, inputs, attr, params):
+        x, scale = inputs[0], inputs[1]
+        zp = inputs[2] if len(inputs) > 2 and inputs[2] is not None else None
+        axis = attr.get("axis", 1)
+        if hasattr(x.ty, "ndim") and x.ty.ndim <= 1 and axis == 1:
+            axis = 0
+        out_dtype = "uint8" if zp is None else zp.ty.dtype.dtype
+        if attr.get("saturate", 1) != 1 and 
str(out_dtype).startswith("float8"):
+            raise ValueError(
+                "QuantizeLinear float8 quantization with saturate=0 is not 
supported yet."
+            )
+        if zp is None:
+            zp = relax.const(0, out_dtype)
+        return relax.op.quantize(x, scale, zp, axis=axis, out_dtype=out_dtype)
+
+    @classmethod
+    def _impl_v21(cls, bb, inputs, attr, params):
+        if attr.get("block_size", 0) != 0:
+            raise ValueError("QuantizeLinear blocked quantization is not 
supported yet.")
+        x, scale = inputs[0], inputs[1]
+        zp = inputs[2] if len(inputs) > 2 and inputs[2] is not None else None
+        axis = attr.get("axis", 1)
+        if hasattr(x.ty, "ndim") and x.ty.ndim <= 1 and axis == 1:
+            axis = 0
+        output_dtype = attr.get("output_dtype", 0)
+        out_dtype = (
+            get_type(output_dtype) if output_dtype else "uint8" if zp is None 
else zp.ty.dtype.dtype
+        )
+        if attr.get("saturate", 1) != 1 and 
str(out_dtype).startswith("float8"):
+            raise ValueError(
+                "QuantizeLinear float8 quantization with saturate=0 is not 
supported yet."
+            )
+        if zp is not None and zp.ty.dtype.dtype != out_dtype:
+            raise ValueError(
+                "QuantizeLinear output_dtype must match the zero-point dtype, "
+                f"but got {out_dtype} and {zp.ty.dtype.dtype}."
+            )
+        if zp is None:
+            zp = relax.const(0, out_dtype)
+        return relax.op.quantize(x, scale, zp, axis=axis, out_dtype=out_dtype)
+
+    @classmethod
+    def _impl_v23(cls, bb, inputs, attr, params):
+        if attr.get("block_size", 0) != 0:
+            raise ValueError("QuantizeLinear blocked quantization is not 
supported yet.")
+        if attr.get("precision", 0) != 0:
+            raise ValueError(
+                "QuantizeLinear with a non-default precision attribute is not 
supported yet."
+            )
+        x, scale = inputs[0], inputs[1]
+        zp = inputs[2] if len(inputs) > 2 and inputs[2] is not None else None
+        axis = attr.get("axis", 1)
+        if hasattr(x.ty, "ndim") and x.ty.ndim <= 1 and axis == 1:
+            axis = 0
+        output_dtype = attr.get("output_dtype", 0)
+        out_dtype = (
+            get_type(output_dtype) if output_dtype else "uint8" if zp is None 
else zp.ty.dtype.dtype
+        )
+        if attr.get("saturate", 1) != 1 and 
str(out_dtype).startswith("float8"):
+            raise ValueError(
+                "QuantizeLinear float8 quantization with saturate=0 is not 
supported yet."
+            )
+        if zp is not None and zp.ty.dtype.dtype != out_dtype:
+            raise ValueError(
+                "QuantizeLinear output_dtype must match the zero-point dtype, "
+                f"but got {out_dtype} and {zp.ty.dtype.dtype}."
+            )
+        if zp is None:
+            zp = relax.const(0, out_dtype)
+        return relax.op.quantize(x, scale, zp, axis=axis, out_dtype=out_dtype)
+
 
 class DequantizeLinear(OnnxOpConverter):
     @classmethod
@@ -380,6 +452,47 @@ class DequantizeLinear(OnnxOpConverter):
             zp = relax.const(0, x.ty.dtype.dtype)
         return relax.op.dequantize(x, scale, zp, axis=axis, 
out_dtype="float32")
 
+    @classmethod
+    def _impl_v19(cls, bb, inputs, attr, params):
+        x, scale = inputs[0], inputs[1]
+        zp = inputs[2] if len(inputs) > 2 and inputs[2] is not None else None
+        axis = attr.get("axis", 1)
+        if hasattr(x.ty, "ndim") and x.ty.ndim <= 1 and axis == 1:
+            axis = 0
+        out_dtype = scale.ty.dtype.dtype
+        if zp is None:
+            zp = relax.const(0, x.ty.dtype.dtype)
+        return relax.op.dequantize(x, scale, zp, axis=axis, 
out_dtype=out_dtype)
+
+    @classmethod
+    def _impl_v21(cls, bb, inputs, attr, params):
+        if attr.get("block_size", 0) != 0:
+            raise ValueError("DequantizeLinear blocked quantization is not 
supported yet.")
+        x, scale = inputs[0], inputs[1]
+        zp = inputs[2] if len(inputs) > 2 and inputs[2] is not None else None
+        axis = attr.get("axis", 1)
+        if hasattr(x.ty, "ndim") and x.ty.ndim <= 1 and axis == 1:
+            axis = 0
+        out_dtype = scale.ty.dtype.dtype
+        if zp is None:
+            zp = relax.const(0, x.ty.dtype.dtype)
+        return relax.op.dequantize(x, scale, zp, axis=axis, 
out_dtype=out_dtype)
+
+    @classmethod
+    def _impl_v23(cls, bb, inputs, attr, params):
+        if attr.get("block_size", 0) != 0:
+            raise ValueError("DequantizeLinear blocked quantization is not 
supported yet.")
+        x, scale = inputs[0], inputs[1]
+        zp = inputs[2] if len(inputs) > 2 and inputs[2] is not None else None
+        axis = attr.get("axis", 1)
+        if hasattr(x.ty, "ndim") and x.ty.ndim <= 1 and axis == 1:
+            axis = 0
+        output_dtype = attr.get("output_dtype", 0)
+        out_dtype = get_type(output_dtype) if output_dtype else 
scale.ty.dtype.dtype
+        if zp is None:
+            zp = relax.const(0, x.ty.dtype.dtype)
+        return relax.op.dequantize(x, scale, zp, axis=axis, 
out_dtype=out_dtype)
+
 
 class DynamicQuantizeLinear(OnnxOpConverter):
     @classmethod
diff --git a/tests/python/relax/test_frontend_onnx.py 
b/tests/python/relax/test_frontend_onnx.py
index c1bf8802f0..a21b3d4e7a 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -12291,5 +12291,254 @@ def test_dequantizelinear_default_axis_opset10():
     check_correctness(model, inputs={"x": x}, opset=10, check_dtypes=True)
 
 
[email protected]("opset", [21, 23, 24, 25])
+def test_quantizelinear_output_dtype(opset):
+    node = helper.make_node("QuantizeLinear", ["x", "scale"], ["y"], 
output_dtype=TensorProto.INT16)
+    graph = helper.make_graph(
+        [node],
+        "quantizelinear_output_dtype",
+        [
+            helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 3]),
+            helper.make_tensor_value_info("scale", TensorProto.FLOAT, []),
+        ],
+        [helper.make_tensor_value_info("y", TensorProto.INT16, [2, 3])],
+    )
+    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 
opset)])
+
+    tvm_model = from_onnx(model, opset=opset, keep_params_in_input=True)
+
+    @I.ir_module
+    class Expected:
+        @R.function
+        def main(
+            x: R.Tensor((2, 3), dtype="float32"),
+            scale: R.Tensor((), dtype="float32"),
+        ) -> R.Tensor((2, 3), dtype="int16"):
+            R.func_attr({"num_input": 2})
+            with R.dataflow():
+                gv: R.Tensor((2, 3), dtype="int16") = R.quantize(
+                    x, scale, R.const(0, "int16"), out_dtype="int16", axis=1
+                )
+                R.output(gv)
+            return gv
+
+    tvm.ir.assert_structural_equal(tvm_model, Expected)
+
+
[email protected]("opset", [19, 21, 23, 24, 25])
+def test_dequantizelinear_scale_dtype(opset):
+    node = helper.make_node("DequantizeLinear", ["x", "scale", "zero_point"], 
["y"])
+    graph = helper.make_graph(
+        [node],
+        "dequantizelinear_scale_dtype",
+        [
+            helper.make_tensor_value_info("x", TensorProto.INT8, [2, 3]),
+            helper.make_tensor_value_info("scale", TensorProto.FLOAT16, []),
+            helper.make_tensor_value_info("zero_point", TensorProto.INT8, []),
+        ],
+        [helper.make_tensor_value_info("y", TensorProto.FLOAT16, [2, 3])],
+    )
+    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 
opset)])
+
+    tvm_model = from_onnx(model, opset=opset, keep_params_in_input=True)
+
+    @I.ir_module
+    class Expected:
+        @R.function
+        def main(
+            x: R.Tensor((2, 3), dtype="int8"),
+            scale: R.Tensor((), dtype="float16"),
+            zero_point: R.Tensor((), dtype="int8"),
+        ) -> R.Tensor((2, 3), dtype="float16"):
+            R.func_attr({"num_input": 3})
+            with R.dataflow():
+                gv: R.Tensor((2, 3), dtype="float16") = R.dequantize(
+                    x, scale, zero_point, out_dtype="float16", axis=1
+                )
+                R.output(gv)
+            return gv
+
+    tvm.ir.assert_structural_equal(tvm_model, Expected)
+
+
[email protected]("opset", [23, 24, 25])
+def test_dequantizelinear_output_dtype(opset):
+    node = helper.make_node(
+        "DequantizeLinear",
+        ["x", "scale", "zero_point"],
+        ["y"],
+        output_dtype=TensorProto.FLOAT,
+    )
+    graph = helper.make_graph(
+        [node],
+        "dequantizelinear_output_dtype",
+        [
+            helper.make_tensor_value_info("x", TensorProto.INT8, [2, 3]),
+            helper.make_tensor_value_info("scale", TensorProto.FLOAT16, []),
+            helper.make_tensor_value_info("zero_point", TensorProto.INT8, []),
+        ],
+        [helper.make_tensor_value_info("y", TensorProto.FLOAT, [2, 3])],
+    )
+    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 
opset)])
+
+    tvm_model = from_onnx(model, opset=opset, keep_params_in_input=True)
+
+    @I.ir_module
+    class Expected:
+        @R.function
+        def main(
+            x: R.Tensor((2, 3), dtype="int8"),
+            scale: R.Tensor((), dtype="float16"),
+            zero_point: R.Tensor((), dtype="int8"),
+        ) -> R.Tensor((2, 3), dtype="float32"):
+            R.func_attr({"num_input": 3})
+            with R.dataflow():
+                gv: R.Tensor((2, 3), dtype="float32") = R.dequantize(
+                    x, scale, zero_point, out_dtype="float32", axis=1
+                )
+                R.output(gv)
+            return gv
+
+    tvm.ir.assert_structural_equal(tvm_model, Expected)
+
+
[email protected]("opset", [19, 21, 23, 24, 25])
+def test_quantizelinear_integer_saturate(opset):
+    node = helper.make_node("QuantizeLinear", ["x", "scale"], ["y"], 
saturate=0)
+    graph = helper.make_graph(
+        [node],
+        "quantizelinear_integer_saturate",
+        [
+            helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 3]),
+            helper.make_tensor_value_info("scale", TensorProto.FLOAT, []),
+        ],
+        [helper.make_tensor_value_info("y", TensorProto.UINT8, [2, 3])],
+    )
+    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 
opset)])
+
+    tvm_model = from_onnx(model, opset=opset, keep_params_in_input=True)
+
+    @I.ir_module
+    class Expected:
+        @R.function
+        def main(
+            x: R.Tensor((2, 3), dtype="float32"),
+            scale: R.Tensor((), dtype="float32"),
+        ) -> R.Tensor((2, 3), dtype="uint8"):
+            R.func_attr({"num_input": 2})
+            with R.dataflow():
+                gv: R.Tensor((2, 3), dtype="uint8") = R.quantize(
+                    x, scale, R.const(0, "uint8"), out_dtype="uint8", axis=1
+                )
+                R.output(gv)
+            return gv
+
+    tvm.ir.assert_structural_equal(tvm_model, Expected)
+
+
[email protected]("opset", [19, 21, 23, 24, 25])
+def test_quantizelinear_float8_saturate_rejected(opset):
+    node = helper.make_node(
+        "QuantizeLinear",
+        ["x", "scale", "zero_point"],
+        ["y"],
+        saturate=0,
+    )
+    graph = helper.make_graph(
+        [node],
+        "quantizelinear_float8_saturate",
+        [
+            helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 3]),
+            helper.make_tensor_value_info("scale", TensorProto.FLOAT, []),
+            helper.make_tensor_value_info("zero_point", 
TensorProto.FLOAT8E4M3FN, []),
+        ],
+        [helper.make_tensor_value_info("y", TensorProto.FLOAT8E4M3FN, [2, 3])],
+    )
+    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 
opset)])
+
+    with pytest.raises(ValueError, match="saturate=0"):
+        from_onnx(model, opset=opset, keep_params_in_input=True)
+
+
[email protected]("opset", [21, 23, 24, 25])
[email protected](
+    "op_name,input_dtype,output_dtype",
+    [
+        ("QuantizeLinear", TensorProto.FLOAT, TensorProto.INT8),
+        ("DequantizeLinear", TensorProto.INT8, TensorProto.FLOAT),
+    ],
+)
+def test_qdq_blocked_quantization_rejected(opset, op_name, input_dtype, 
output_dtype):
+    node = helper.make_node(
+        op_name,
+        ["x", "scale", "zero_point"],
+        ["y"],
+        axis=1,
+        block_size=2,
+    )
+    graph = helper.make_graph(
+        [node],
+        "qdq_blocked_quantization",
+        [
+            helper.make_tensor_value_info("x", input_dtype, [1, 4]),
+            helper.make_tensor_value_info("scale", TensorProto.FLOAT, [1, 2]),
+            helper.make_tensor_value_info("zero_point", TensorProto.INT8, [1, 
2]),
+        ],
+        [helper.make_tensor_value_info("y", output_dtype, [1, 4])],
+    )
+    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 
opset)])
+
+    with pytest.raises(ValueError, match="blocked quantization"):
+        from_onnx(model, opset=opset, keep_params_in_input=True)
+
+
[email protected]("opset", [23, 24, 25])
+def test_quantizelinear_precision_rejected(opset):
+    node = helper.make_node(
+        "QuantizeLinear",
+        ["x", "scale"],
+        ["y"],
+        output_dtype=TensorProto.INT8,
+        precision=TensorProto.FLOAT16,
+    )
+    graph = helper.make_graph(
+        [node],
+        "quantizelinear_precision",
+        [
+            helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 3]),
+            helper.make_tensor_value_info("scale", TensorProto.FLOAT, []),
+        ],
+        [helper.make_tensor_value_info("y", TensorProto.INT8, [2, 3])],
+    )
+    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 
opset)])
+
+    with pytest.raises(ValueError, match="precision attribute"):
+        from_onnx(model, opset=opset, keep_params_in_input=True)
+
+
[email protected]("opset", [21, 23, 24, 25])
+def test_quantizelinear_output_dtype_mismatch(opset):
+    node = helper.make_node(
+        "QuantizeLinear",
+        ["x", "scale", "zero_point"],
+        ["y"],
+        output_dtype=TensorProto.UINT8,
+    )
+    graph = helper.make_graph(
+        [node],
+        "quantizelinear_output_dtype_mismatch",
+        [
+            helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 3]),
+            helper.make_tensor_value_info("scale", TensorProto.FLOAT, []),
+            helper.make_tensor_value_info("zero_point", TensorProto.INT8, []),
+        ],
+        [helper.make_tensor_value_info("y", TensorProto.UINT8, [2, 3])],
+    )
+    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 
opset)])
+
+    with pytest.raises(ValueError, match="must match the zero-point dtype"):
+        from_onnx(model, opset=opset, keep_params_in_input=True)
+
+
 if __name__ == "__main__":
     tvm.testing.main()

Reply via email to