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 c7a5f4f388 [Fix][Relax][ONNX] Correct fmod mapping in Mod constant 
folding (#20170)
c7a5f4f388 is described below

commit c7a5f4f388b3680da7a9289847e2e269e16a1235
Author: Jeremy Schoemaker <[email protected]>
AuthorDate: Fri Aug 28 01:55:48 2026 -0500

    [Fix][Relax][ONNX] Correct fmod mapping in Mod constant folding (#20170)
    
    The Mod converter
    (python/tvm/relax/frontend/onnx/onnx_frontend.py:730-736 on main)
    assigns `numpy_op` with the two conventions swapped: `fmod=0` (integer
    mod, sign follows divisor per the ONNX spec) folds with `np.fmod`, and
    `fmod=1` (C fmod, sign follows dividend) folds with `np.mod`. The
    runtime path is correct (`floor_mod` for `fmod=0`, `mod` for `fmod=1`),
    so the bug fires exactly when both operands are constants or
    initializers and `BinaryBase.base_impl` folds the result at import time,
    e.g. shape-arithmetic subgraphs. `Mod(-5, 3)` with `fmod=0` bakes
    `R.const(-2)` into the graph where onnxruntime returns 1; `Mod(-5.5,
    3.0)` with `fmod=1` folds to 0.5 where onnxruntime returns -2.5.
    
    The suite could not catch this: the numpy reference in
    `verify_binary_scalar` (tests/python/relax/test_frontend_onnx.py:365 on
    main) encoded the identical swap, and the only folded value exercised
    was 4 mod 8, where both conventions agree.
    
    This is a reintroduction of a fixed bug: #6160 corrected this exact
    fmod=0/fmod=1 mapping in the old Relay ONNX frontend (issue #6106); the
    Relax frontend brought it back in the numpy constant-fold path.
    
    Changes:
    - Swap the assignments so `fmod=0` pairs `np.mod` with
    `relax.op.floor_mod` and `fmod=1` pairs `np.fmod` with `relax.op.mod`.
    - Correct the inverted test reference in `verify_binary_scalar`.
    - Add `test_mod_constant_fold_negative_operands`: Mod over two constant
    tensors with negative operands (int32 fmod=0, int32 fmod=1, float32
    fmod=1), folded at import and checked against onnxruntime. All three
    cases fail before the fix and pass after; the full ONNX frontend test
    file shows no other delta.
    
    Not changed: the dead class-level defaults on `Mod` (`numpy_op =
    _np.mod` / `relax_op = relax.op.mod`), which `_impl_v10` always
    overwrites since ONNX Mod only exists from opset 10.
---
 python/tvm/relax/frontend/onnx/onnx_frontend.py |  4 ++--
 tests/python/relax/test_frontend_onnx.py        | 27 ++++++++++++++++++++++++-
 2 files changed, 28 insertions(+), 3 deletions(-)

diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py 
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index fdcf26d170..f1cf2957c6 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -728,10 +728,10 @@ class Mod(BinaryBase):
     @classmethod
     def _impl_v10(cls, bb, inputs, attr, params):
         if attr.get("fmod", 0) == 0:
-            cls.numpy_op = _np.fmod
+            cls.numpy_op = _np.mod
             cls.relax_op = relax.op.floor_mod
         else:
-            cls.numpy_op = _np.mod
+            cls.numpy_op = _np.fmod
             cls.relax_op = relax.op.mod
         return cls.base_impl(bb, inputs, attr, params)
 
diff --git a/tests/python/relax/test_frontend_onnx.py 
b/tests/python/relax/test_frontend_onnx.py
index 6a417d56f1..9d1a6d8aaa 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -362,7 +362,7 @@ def verify_binary_scalar(op_name, attrs={}, domain=None, 
dtype=TensorProto.INT32
         "Mul": np.multiply,
         "Div": np.divide,
         "Pow": np.power,
-        "Mod": np.mod if attrs.get("fmod", 0) else np.fmod,
+        "Mod": np.fmod if attrs.get("fmod", 0) else np.mod,
     }[op_name]
     expected_value = op(lhs, rhs).astype(dtype_str)
 
@@ -697,6 +697,31 @@ def test_mod(int_mode: bool):
     verify_binary_scalar("Mod", attrs={"fmod": fmod}, dtype=dtype)
 
 
[email protected](
+    "fmod, dtype, a_vals, b_vals",
+    [
+        (0, TensorProto.INT32, [-5, 5, -5, 5], [3, 3, -3, -3]),
+        (1, TensorProto.INT32, [-5, 5, -5, 5], [3, 3, -3, -3]),
+        (1, TensorProto.FLOAT, [-5.5, 5.5, -5.5, 5.5], [3.0, 3.0, -3.0, -3.0]),
+    ],
+)
+def test_mod_constant_fold_negative_operands(fmod, dtype, a_vals, b_vals):
+    """Mod over two constants is folded at import time. The folded value must
+    match onnxruntime for negative operands, where integer mod (sign follows
+    divisor) and fmod (sign follows dividend) disagree."""
+    a = make_constant_node("a", dtype, [4], a_vals)
+    b = make_constant_node("b", dtype, [4], b_vals)
+    mod_node = helper.make_node("Mod", ["a", "b"], ["c"], fmod=fmod)
+    graph = helper.make_graph(
+        [a, b, mod_node],
+        "mod_constant_fold_test",
+        inputs=[],
+        outputs=[helper.make_tensor_value_info("c", dtype, [4])],
+    )
+    model = helper.make_model(graph, producer_name="mod_constant_fold_test")
+    check_correctness(model)
+
+
 SHAPE_PARAMS = [
     ([[32, 32], [32, 32]], [32, 32]),
     ([[32, 1], [1, 2]], [32, 2]),

Reply via email to