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 7356265096 [Fix][Relax][ONNX] Cast BatchNorm params to input dtype 
(#19979)
7356265096 is described below

commit 7356265096cba7196673b09f90b023e600171452
Author: Vic Wen <[email protected]>
AuthorDate: Sat Jul 11 14:54:13 2026 +0800

    [Fix][Relax][ONNX] Cast BatchNorm params to input dtype (#19979)
    
    Fixes #19977.
    
    ONNX `BatchNormalization` allows the input/output tensor dtype,
    scale/bias dtype, and mean/variance dtype to be separate floating-point
    type parameters.
    
    For example, a valid ONNX model may use `float16` data with `float32`
    gamma, beta, mean, and variance tensors.
    
    The Relax `batch_norm` operator currently requires all five input
    tensors to have the same dtype. The ONNX frontend previously forwarded
    the ONNX inputs directly to `relax.nn.batch_norm`, causing import to
    fail during normalization
    for mixed-dtype ONNX models.
    
    This patch casts the ONNX BatchNormalization parameter tensors (`scale`,
    `bias`, `mean`, and `var`) to the data tensor dtype before calling Relax
    `batch_norm`.
    
    This preserves the ONNX output dtype, which follows the input data
    dtype, while keeping the fix localized to the frontend compatibility
    layer.
    
    The regression test builds a minimal ONNX BatchNormalization graph with
    `float16` data and `float32` parameters, imports it through the Relax
    ONNX frontend, and checks that the generated Relax `batch_norm` call
    receives same-dtype inputs.
    
    Verification:
    
    - `python -m pytest
    tests/python/relax/test_frontend_onnx.py::test_batch_norm_mixed_dtype_params
    
tests/python/relax/test_frontend_onnx.py::test_batch_norm_defaults_to_inference_mode
    -q`
    
    Signed-off-by: viiccwen <[email protected]>
---
 python/tvm/relax/frontend/onnx/onnx_frontend.py | 69 ++++++++++++++++--
 tests/python/relax/test_frontend_onnx.py        | 95 +++++++++++++++++++++++++
 2 files changed, 158 insertions(+), 6 deletions(-)

diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py 
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index be3dd5d4e4..da65cc7bee 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -3576,18 +3576,75 @@ class BatchNormalization(OnnxOpConverter):
         epsilon = attr.get("epsilon", 1e-05)
         momentum = attr.get("momentum", 0.9)
         training_mode = attr.get("training_mode", 0)
-        return relax.op.nn.batch_norm(
-            data,
-            gamma=scale,
-            beta=bias,
-            moving_mean=mean,
-            moving_var=var,
+
+        data_dtype = data.ty.dtype
+        scale_dtype = scale.ty.dtype
+        bias_dtype = bias.ty.dtype
+        mean_dtype = mean.ty.dtype
+        var_dtype = var.ty.dtype
+
+        if scale_dtype != bias_dtype:
+            raise ValueError(
+                "ONNX BatchNormalization requires scale and bias to have the 
same "
+                f"dtype, but received {scale_dtype} and {bias_dtype}."
+            )
+
+        if mean_dtype != var_dtype:
+            raise ValueError(
+                "ONNX BatchNormalization requires mean and var to have the 
same "
+                f"dtype, but received {mean_dtype} and {var_dtype}."
+            )
+
+        if data_dtype == scale_dtype == mean_dtype:
+            compute_dtype = data_dtype
+        elif (
+            data_dtype == "float16"
+            and scale_dtype in ("float16", "float32")
+            and mean_dtype in ("float16", "float32")
+        ):
+            compute_dtype = "float32"
+        else:
+            raise NotImplementedError(
+                "ONNX BatchNormalization with mixed input dtypes is currently "
+                "supported only for float16 data with float16/float32 
parameters "
+                "and statistics, but received "
+                f"data={data_dtype}, scale/bias={scale_dtype}, 
mean/var={mean_dtype}."
+            )
+
+        # ONNX requires float computation for float16 training statistics to 
avoid overflow.
+        if training_mode and data_dtype == "float16":
+            compute_dtype = "float32"
+
+        def cast_for_compute(expr, source_dtype):
+            if source_dtype == compute_dtype:
+                return expr
+            return relax.op.astype(expr, compute_dtype)
+
+        output = relax.op.nn.batch_norm(
+            cast_for_compute(data, data_dtype),
+            gamma=cast_for_compute(scale, scale_dtype),
+            beta=cast_for_compute(bias, bias_dtype),
+            moving_mean=cast_for_compute(mean, mean_dtype),
+            moving_var=cast_for_compute(var, var_dtype),
             axis=1,
             epsilon=epsilon,
             momentum=momentum,
             training=bool(training_mode),
         )
 
+        y = relax.TupleGetItem(output, 0)
+        running_mean = relax.TupleGetItem(output, 1)
+        running_var = relax.TupleGetItem(output, 2)
+
+        if compute_dtype != data_dtype:
+            y = relax.op.astype(y, data_dtype)
+        if compute_dtype != mean_dtype:
+            running_mean = relax.op.astype(running_mean, mean_dtype)
+        if compute_dtype != var_dtype:
+            running_var = relax.op.astype(running_var, var_dtype)
+
+        return relax.Tuple([y, running_mean, running_var])
+
 
 class MeanVarianceNormalization(OnnxOpConverter):
     """Converts an onnx MeanVarianceNormalization node into an equivalent 
Relax expression."""
diff --git a/tests/python/relax/test_frontend_onnx.py 
b/tests/python/relax/test_frontend_onnx.py
index 5aab4f558f..35cf699dba 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -7546,6 +7546,101 @@ def test_batch_norm_defaults_to_inference_mode():
     assert batch_norm_attrs[0].training is False
 
 
+def test_batch_norm_mixed_dtype_params():
+    data = helper.make_tensor_value_info("data", TensorProto.FLOAT16, [1, 3, 
2, 2])
+    output = helper.make_tensor_value_info("output", TensorProto.FLOAT16, [1, 
3, 2, 2])
+    params = [
+        numpy_helper.from_array(np.array([1.0, 1.5, 2.0], dtype=np.float32), 
name="gamma"),
+        numpy_helper.from_array(np.array([0.0, 0.1, -0.1], dtype=np.float32), 
name="beta"),
+        numpy_helper.from_array(np.array([0.2, -0.3, 0.4], dtype=np.float32), 
name="mean"),
+        numpy_helper.from_array(np.array([1.0, 1.5, 2.0], dtype=np.float32), 
name="var"),
+    ]
+    batch_norm_node = helper.make_node(
+        "BatchNormalization",
+        ["data", "gamma", "beta", "mean", "var"],
+        ["output"],
+        epsilon=1e-5,
+        momentum=0.9,
+        training_mode=0,
+    )
+    graph = helper.make_graph(
+        [batch_norm_node],
+        "mixed_dtype_batchnorm",
+        [data],
+        [output],
+        initializer=params,
+    )
+    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 
15)])
+
+    tvm_model = from_onnx(model, keep_params_in_input=False)
+
+    assert tuple(dim.value for dim in tvm_model["main"].ret_ty.shape.values) 
== (1, 3, 2, 2)
+    assert tvm_model["main"].ret_ty.dtype == "float16"
+
+    batch_norm_calls = []
+
+    def visit(expr):
+        if isinstance(expr, relax.Call) and expr.op == 
tvm.ir.Op.get("relax.nn.batch_norm"):
+            batch_norm_calls.append(expr)
+
+    relax.analysis.post_order_visit(tvm_model["main"], visit)
+
+    assert len(batch_norm_calls) == 1
+    arg_dtypes = [
+        str(getattr(arg, "struct_info", getattr(arg, "ty", None)).dtype)
+        for arg in batch_norm_calls[0].args
+    ]
+    assert arg_dtypes == ["float32"] * 5
+
+
+def test_batch_norm_training_preserves_output_dtypes():
+    data = helper.make_tensor_value_info("data", TensorProto.FLOAT16, [1, 3, 
2, 2])
+    outputs = [
+        helper.make_tensor_value_info("output", TensorProto.FLOAT16, [1, 3, 2, 
2]),
+        helper.make_tensor_value_info("running_mean", TensorProto.FLOAT16, 
[3]),
+        helper.make_tensor_value_info("running_var", TensorProto.FLOAT16, [3]),
+    ]
+    inputs = [
+        data,
+        helper.make_tensor_value_info("gamma", TensorProto.FLOAT16, [3]),
+        helper.make_tensor_value_info("beta", TensorProto.FLOAT16, [3]),
+        helper.make_tensor_value_info("mean", TensorProto.FLOAT16, [3]),
+        helper.make_tensor_value_info("var", TensorProto.FLOAT16, [3]),
+    ]
+    batch_norm_node = helper.make_node(
+        "BatchNormalization",
+        [value.name for value in inputs],
+        [value.name for value in outputs],
+        training_mode=1,
+    )
+    graph = helper.make_graph(
+        [batch_norm_node],
+        "mixed_dtype_training_batchnorm",
+        inputs,
+        outputs,
+    )
+    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 
15)])
+
+    tvm_model = from_onnx(model, keep_params_in_input=True)
+
+    assert [str(field.dtype) for field in tvm_model["main"].ret_ty.fields] == [
+        "float16",
+        "float16",
+        "float16",
+    ]
+
+    batch_norm_calls = []
+
+    def visit(expr):
+        if isinstance(expr, relax.Call) and expr.op == 
tvm.ir.Op.get("relax.nn.batch_norm"):
+            batch_norm_calls.append(expr)
+
+    relax.analysis.post_order_visit(tvm_model["main"], visit)
+
+    assert len(batch_norm_calls) == 1
+    assert [str(arg.ty.dtype) for arg in batch_norm_calls[0].args] == 
["float32"] * 5
+
+
 def get_pool_padding(shape, auto_pad, kernel_shape, strides, pads):
     def get_pad_pair(input1d, kernel1d, stride1d, mode):
         if input1d % stride1d == 0:

Reply via email to