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]),