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 802e1b784a [Fix][Relax][Frontend][ONNX] Fix Mean/Sum/Min/Max with 
all-constant inputs (#20147)
802e1b784a is described below

commit 802e1b784aa1bad91b6d24af55de7175834f001e
Author: HuEnwei <[email protected]>
AuthorDate: Wed Aug 26 06:48:02 2026 +0800

    [Fix][Relax][Frontend][ONNX] Fix Mean/Sum/Min/Max with all-constant inputs 
(#20147)
    
    Fixes: #20146
    
    ## Summary
    
    The Relax ONNX frontend mishandles `Mean` / `Sum` / `Min` / `Max` nodes
    whose
    inputs are all constants (model initializers): a **single** constant
    input
    returns a 0-d scalar (e.g. the *global* mean `3.5` for a `(2, 3)` input)
    instead of the input unchanged, and **multiple** constant inputs raise
    `TypeError: only integer scalar arrays can be converted to a scalar
    index`.
    onnxruntime returns correct results in both cases.
    
    ## Root cause
    
    `MultiInputBase._impl_v1`'s constant-fold path at
    `python/tvm/relax/frontend/onnx/onnx_frontend.py:2456-2459` called
    
    ```python
    output = cls.numpy_op(*np_inputs)
    ```
    
    For `Mean` / `Sum` / `Min` / `Max`, `numpy_op` is `np.mean` / `np.sum` /
    `np.min` / `np.max`. These numpy reductions reduce their **first**
    argument and
    interpret the 2nd and later positional arguments as the `axis`
    parameter, not as
    additional data tensors. So `np.mean(x)` reduces the whole tensor to a
    0-d
    scalar, and `np.mean(a, b, ...)` passes an array into `axis` →
    TypeError.
    
    ## Fix
    
    Mirror the non-constant path: broadcast each constant to the common
    shape,
    stack along a new leading axis, then reduce along it.
    
    ```python
    input_shapes = [inp.ty.shape for inp in inputs]
    target_shape = tuple(
        int(dim)
        for dim in functools.reduce(compute_broadcast_shape, input_shapes)
    )
    stacked = _np.stack(
        [_np.broadcast_to(x, target_shape) for x in np_inputs], axis=0
    )
    output = cls.numpy_op(stacked, axis=0)
    ```
    
    A single constant input then reduces a `(1, *shape)` stack along axis 0,
    which
    is the identity — matching the ONNX semantics. (`relax.Constant` shape
    elements
    are `tvm.tir.IntImm`, so they are converted to plain ints for
    `np.broadcast_to`.)
    
    ## Validation
    
    Differential test: Relax (build + `VirtualMachine`) vs onnxruntime (and
    `onnx.reference`) on the same model, comparing output shapes and max
    |diff|.
    
    | Case | onnxruntime | TVM | max\|diff\| | Result |
    |---|---|---|---|---|
    | Mean, single const `(2,3)` | `(2, 3)` | `(2, 3)` | 0.00e+00 | OK |
    | Mean, single const `(5,)` | `(5,)` | `(5,)` | 0.00e+00 | OK |
    | Mean, single const scalar `()` | `()` | `()` | 0.00e+00 | OK |
    | Mean, two consts `(2,3)` | `(2, 3)` | `(2, 3)` | 0.00e+00 | OK |
    | Mean, two consts `(2,3,4)` | `(2, 3, 4)` | `(2, 3, 4)` | 0.00e+00 | OK
    |
    | Mean, const broadcast `(2,3)+(3,)` | `(2, 3)` | `(2, 3)` | 0.00e+00 |
    OK |
    | Mean, 3 consts broadcast `(2,3)+(3,)+(2,1)` | `(2, 3)` | `(2, 3)` |
    2.38e-07 | OK |
    | Sum, two consts `(2,3)` | `(2, 3)` | `(2, 3)` | 0.00e+00 | OK |
    | Min, consts int64 `(2,3)` | `(2, 3)` | `(2, 3)` | 0.00e+00 | OK |
    | Max, single const int32 `(2,3)` | `(2, 3)` | `(2, 3)` | 0.00e+00 | OK
    |
    | Mean, non-const broadcast `(2,3)+(3,)` | `(2, 3)` | `(2, 3)` |
    0.00e+00 | OK |
    | Mean, mixed const + graph input | `(2, 3)` | `(2, 3)` | 0.00e+00 | OK
    |
    
    The two failing cases from the issue now match onnxruntime exactly:
    
    ```
    onnxruntime -> (2, 3) [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
    TVM        -> (2, 3) [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
    [two inputs] onnxruntime -> (2, 3) [6.0, 7.0, 8.0, 9.0, 10.0, 11.0]
    [two inputs] TVM        -> (2, 3) [6.0, 7.0, 8.0, 9.0, 10.0, 11.0]
    ```
    
    Run:
    
    ```bash
    python results/TVM/deepseek-v4-flash/prove_hum/onnx_Mean/5修复_差分验证.py
    ```
    
    ## Files changed
    
    - `python/tvm/relax/frontend/onnx/onnx_frontend.py` —
    `MultiInputBase._impl_v1`
    constant-fold path: broadcast + stack + reduce along the leading axis
    instead
      of `numpy_op(*np_inputs)` (fixes `Mean` / `Sum` / `Min` / `Max`).
    
    ---------
    
    Co-authored-by: FFChopon <[email protected]>
    Co-authored-by: Claude <[email protected]>
---
 python/tvm/relax/frontend/onnx/onnx_frontend.py | 21 +++++---
 tests/python/relax/test_frontend_onnx.py        | 67 +++++++++++++++++++++++++
 2 files changed, 81 insertions(+), 7 deletions(-)

diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py 
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index 7e8616f65f..b63518f6c0 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -2468,14 +2468,21 @@ class MultiInputBase(OnnxOpConverter):
         if cls.numpy_op is None or cls.relax_op is None:
             raise NotImplementedError("numpy_op and relax_op must be defined 
for MultiInputBase")
         if all([isinstance(inp, relax.Constant) for inp in inputs]):
-            # numpy_op is a reduction, so the operands cannot be passed
-            # positionally: the second constant would be taken as ``axis``.
-            # Broadcast and stack first, then reduce over the stack axis, which
-            # is what the non-constant path below builds.
-            np_inputs = _np.broadcast_arrays(*[inp.data.numpy() for inp in 
inputs])
-            output = cls.numpy_op(  # pylint: disable=not-callable
-                _np.stack(np_inputs, axis=0), axis=0
+            np_inputs = [inp.data.numpy() for inp in inputs]
+            # numpy_op (np.mean/np.sum/np.min/np.max) reduces its first arg,
+            # treating any further positional args as `axis`, so calling it as
+            # numpy_op(*np_inputs) is wrong for the variadic ONNX semantics.
+            # Broadcast to the common shape, stack along a new leading axis,
+            # then reduce along it — mirrors the non-constant path below.
+            input_shapes = [inp.ty.shape for inp in inputs]
+            target_shape = tuple(
+                int(dim)
+                for dim in functools.reduce(compute_broadcast_shape, 
input_shapes)
+            )
+            stacked = _np.stack(
+                [_np.broadcast_to(x, target_shape) for x in np_inputs], axis=0
             )
+            output = cls.numpy_op(stacked, axis=0)  # pylint: 
disable=not-callable
             return relax.const(output, output.dtype)
 
         input_shapes = [inp.ty.shape for inp in inputs]
diff --git a/tests/python/relax/test_frontend_onnx.py 
b/tests/python/relax/test_frontend_onnx.py
index 09f3a7b01d..39e067d5d7 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -856,6 +856,73 @@ def test_multi_input_all_constant_inputs(op_name, dtype, 
shapes, values):
     check_correctness(helper.make_model(graph), opset=13)
 
 
[email protected]("op_name", ["Min", "Max", "Sum", "Mean"])
[email protected]("rank", [0, 1, 2, 3])
+def test_multi_input_constant_rank_axis_bounds(op_name, rank):
+    """All-constant inputs reduce over the new leading stack axis.
+
+    Stacking the inputs along ``axis=0`` yields a tensor of rank ``rank+1``,
+    so the reduction axis must lie in ``[-(rank+1), rank]``. ``axis=0`` equals
+    the lower bound ``-(rank+1)`` and is therefore valid for every rank, while
+    a positive ``axis=rank+1`` is out of bounds and must raise. The importer
+    relies on this in ``MultiInputBase._impl_v1``, so both the numpy invariant
+    and the end-to-end folding result are asserted here.
+    """
+    shape = (2,) * rank
+    stacked = np.stack([np.ones(shape, np.float32), np.ones(shape, 
np.float32)], axis=0)
+    reduce = {"Min": np.min, "Max": np.max, "Sum": np.sum, "Mean": 
np.mean}[op_name]
+    assert stacked.ndim == rank + 1
+    for axis in (0, -(rank + 1), rank, -rank):
+        reduce(stacked, axis=axis)  # must not raise
+    for axis in (rank + 1, -(rank + 2)):
+        with pytest.raises((IndexError, ValueError)):
+            reduce(stacked, axis=axis)
+
+    # End-to-end: two identical rank-``rank`` constants fold to the identity.
+    const_nodes = []
+    for name in ("c0", "c1"):
+        const_nodes.append(
+            helper.make_node(
+                "Constant",
+                inputs=[],
+                outputs=[name],
+                value=helper.make_tensor(
+                    f"{name}_v", TensorProto.FLOAT, list(shape), 
np.ones(shape, np.float32).flatten().tolist()
+                ),
+            )
+        )
+    graph = helper.make_graph(
+        const_nodes + [helper.make_node(op_name, ["c0", "c1"], ["output"])],
+        f"const_rank_{op_name}",
+        inputs=[],
+        outputs=[helper.make_tensor_value_info("output", TensorProto.FLOAT, 
list(shape))],
+    )
+    check_correctness(helper.make_model(graph), opset=13)
+
+
[email protected]("op_name", ["Min", "Max", "Sum", "Mean"])
[email protected]("shape", [[], [5], [2, 3]])
+def test_multi_input_constant_single_input(op_name, shape):
+    """A single constant input is returned unchanged (shape preserved)."""
+    if shape:
+        vals = np.arange(np.prod(shape), dtype=np.float32).reshape(shape)
+    else:
+        vals = np.array(1.0, np.float32)
+    node = helper.make_node(
+        "Constant",
+        inputs=[],
+        outputs=["c0"],
+        value=helper.make_tensor("c0_v", TensorProto.FLOAT, shape, 
vals.flatten().tolist()),
+    )
+    graph = helper.make_graph(
+        [node, helper.make_node(op_name, ["c0"], ["output"])],
+        f"const_single_{op_name}",
+        inputs=[],
+        outputs=[helper.make_tensor_value_info("output", TensorProto.FLOAT, 
shape)],
+    )
+    check_correctness(helper.make_model(graph), opset=13)
+
+
 @pytest.mark.parametrize("op_name", ["And", "Or", "Xor"])
 def test_binary_bool(op_name: str):
     verify_binary(op_name, [32, 32], [32, 32], [32, 32], 
dtype=TensorProto.BOOL)

Reply via email to