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 1575515972 [Fix][Relax][ONNX] Fold Min/Max/Sum/Mean constants 
elementwise (#20119)
1575515972 is described below

commit 15755159720cb5acefb00a152493da58dfd4f944
Author: Aryan Putta <[email protected]>
AuthorDate: Mon Aug 17 17:12:06 2026 -0400

    [Fix][Relax][ONNX] Fold Min/Max/Sum/Mean constants elementwise (#20119)
    
    Fixes #20117.
    
    ## Problem
    
    `MultiInputBase._impl_v1` folds all-constant operands with:
    
    ```python
    output = cls.numpy_op(*np_inputs)
    ```
    
    `numpy_op` is a reduction (`np.min`, `np.max`, `np.sum`, `np.mean`),
    whose signature is `op(a, axis=None, ...)`. Passing the operands
    positionally binds the second constant to `axis` instead of combining it
    with the first.
    
    Importing a model where `Min`, `Max`, `Sum` or `Mean` has only constant
    inputs therefore raises:
    
    ```
    TypeError: only integer scalar arrays can be converted to a scalar index
    ```
    
    There is a quieter case. When the second operand is a rank-0 integer
    that is a valid axis, numpy accepts it and nothing raises. The fold
    returns a reduction of the first operand, with the wrong shape and the
    wrong values:
    
    ```python
    a = np.arange(1, 7).reshape(3, 2)   # the first constant
    b = np.array(0)                     # a rank-0 constant, a valid axis
    np.min(a, b)                        # -> [1, 2]        shape (2,)
                                        # elementwise min  -> 
[[0,0],[0,0],[0,0]]  shape (3, 2)
    ```
    
    The issue reports `Min` with two rank-1 constants, but the defect is in
    the shared base class, so `Max`, `Sum` and `Mean` are affected
    identically.
    
    ## Fix
    
    Broadcast the operands, stack them on a new leading axis, then reduce
    over it. That is exactly what the non-constant path immediately below
    already builds with `broadcast_to`, `stack` and `relax_op`, so both
    paths now compute one definition.
    
    ## Tests
    
    `test_multi_input_all_constant_inputs` covers all four operators through
    `check_correctness`, which compares the imported module against
    onnxruntime.
    
    `Sum` and `Mean` accept only floating point operands in ONNX, so the
    integer cases use `Min` and `Max`. Those cases pass a rank-0 operand
    holding a valid axis index (`0` and `1`), since an out-of-range value
    would raise and would not reach the silent path.
    
    Every case fails before this change and passes after: the four float
    cases raise `TypeError`, and the two integer cases return a wrong
    result.
    
    ## Verification
    
    Across 444 combinations of the four operators, six shapes including
    rank-0 and broadcasting pairs, three dtypes, and operand counts of one,
    three and four, the new fold agrees with the broadcast + stack + reduce
    path in every case. Over the same set the old fold raised in 425 and
    returned a wrong answer in 7.
    
    The six cases added here were each checked against onnxruntime directly,
    and the folded values match its output.
---
 python/tvm/relax/frontend/onnx/onnx_frontend.py | 10 ++++--
 tests/python/relax/test_frontend_onnx.py        | 41 +++++++++++++++++++++++++
 2 files changed, 49 insertions(+), 2 deletions(-)

diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py 
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index 6bbf220dfe..65bd5bfe1a 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -2454,8 +2454,14 @@ 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]):
-            np_inputs = [inp.data.numpy() for inp in inputs]
-            output = cls.numpy_op(*np_inputs)  # pylint: disable=not-callable
+            # 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
+            )
             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 4adc3a0ab3..32dd4b0bef 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -815,6 +815,47 @@ def test_multi_input_broadcasting():
             )
 
 
[email protected](
+    "op_name, dtype, shapes, values",
+    [
+        # Two rank-1 constants, the shape reported in apache/tvm#20117.
+        ("Min", TensorProto.FLOAT, [[2], [2]], [[1.0, 2.0], [3.0, 4.0]]),
+        ("Max", TensorProto.FLOAT, [[2], [2]], [[1.0, 2.0], [3.0, 4.0]]),
+        ("Sum", TensorProto.FLOAT, [[2], [2]], [[1.0, 2.0], [3.0, 4.0]]),
+        ("Mean", TensorProto.FLOAT, [[2], [2]], [[1.0, 2.0], [3.0, 4.0]]),
+        # Sum and Mean accept only floating point operands in ONNX, so the
+        # integer cases cover Min and Max. A rank-0 operand holding a valid
+        # axis index is the input that returned a wrong answer rather than
+        # raising, so it needs 0 or 1 here and not an out-of-range value.
+        ("Min", TensorProto.INT64, [[3, 2], []], [[1, 2, 3, 4, 5, 6], [0]]),
+        ("Max", TensorProto.INT64, [[3, 2], []], [[1, 2, 3, 4, 5, 6], [1]]),
+    ],
+)
+def test_multi_input_all_constant_inputs(op_name, dtype, shapes, values):
+    """Folding constant operands must match the broadcast + stack + reduce 
path."""
+    nodes, names = [], []
+    for i, (shape, value) in enumerate(zip(shapes, values)):
+        nodes.append(
+            helper.make_node(
+                "Constant",
+                inputs=[],
+                outputs=[f"c{i}"],
+                value=helper.make_tensor(f"c{i}_v", dtype, shape, value),
+            )
+        )
+        names.append(f"c{i}")
+
+    nodes.append(helper.make_node(op_name, names, ["output"]))
+    output_shape = list(np.broadcast_shapes(*[tuple(shape) for shape in 
shapes]))
+    graph = helper.make_graph(
+        nodes,
+        f"all_constant_{op_name}",
+        inputs=[],
+        outputs=[helper.make_tensor_value_info("output", dtype, output_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