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 acfc483738 [BugFix][ONNX] Fix Round op to use ties-to-even (#19367)
acfc483738 is described below
commit acfc4837389323f952feabe602f10e665f3dd59e
Author: Soowon Jeong <[email protected]>
AuthorDate: Wed Apr 8 04:52:47 2026 +0900
[BugFix][ONNX] Fix Round op to use ties-to-even (#19367)
## Problem
The ONNX `Round` operator specification requires **ties-to-even**
(banker's) rounding:
> "For cases where number is exactly halfway between two integers, it
rounds to the nearest even integer."
> — https://onnx.ai/onnx/operators/onnx__Round.html
However, the current TVM implementation produces **ties-away-from-zero**
results on midpoint values:
| Input | Expected (ties-to-even) | Actual (ties-away) |
|-------|------------------------|--------------------|
| 0.5 | 0.0 | 1.0 |
| 1.5 | 2.0 | 2.0 |
| 2.5 | 2.0 | 3.0 |
| -0.5 | 0.0 | -1.0 |
| -2.5 | -2.0 | -3.0 |
This was reported in issue #18590.
## Root Cause
The lowering chain for `relax.op.round`:
```
relax.op.round -> (LegalizeOps) -> topi.round() -> te.round -> tir.round ->
llvm::round
```
`llvm::round` is defined as ties-away-from-zero (C99 `round()`), while
`llvm::nearbyint` uses the IEEE 754 default rounding mode
(ties-to-even).
## Fix
**`python/tvm/topi/math.py`**: Switch `topi.round()` from `te.round` to
`te.nearbyint`. This lowers to `tir.nearbyint` -> `llvm::nearbyint`,
which respects IEEE 754 ties-to-even.
**`src/target/source/intrin_rule_webgpu.cc`**: Register `tir.nearbyint`
for the WebGPU backend. WGSL `round()` is already ties-to-even per the
WGSL spec, so `tir.nearbyint` -> `round` is the correct mapping.
**`tests/python/relax/test_frontend_onnx.py`**: Add
`test_round_ties_to_even()` with explicit midpoint inputs to prevent
regression.
## Testing
```
python -m pytest
tests/python/relax/test_frontend_onnx.py::test_round_ties_to_even -xvs
python -m pytest
"tests/python/relax/test_frontend_onnx.py::test_unary[Round]" -xvs
```
Both pass. The new test compares TVM output against onnxruntime (which
correctly implements ties-to-even) for inputs `[0.5, 1.5, 2.5, -0.5,
-1.5, -2.5]`.
Fixes #18590
---
python/tvm/topi/math.py | 7 +++++--
src/target/source/intrin_rule_webgpu.cc | 8 ++++++++
tests/python/relax/test_frontend_onnx.py | 21 +++++++++++++++++++++
3 files changed, 34 insertions(+), 2 deletions(-)
diff --git a/python/tvm/topi/math.py b/python/tvm/topi/math.py
index 146009e3ba..d3e8991c85 100644
--- a/python/tvm/topi/math.py
+++ b/python/tvm/topi/math.py
@@ -447,7 +447,10 @@ def isinf(x):
@tvm.te.tag_scope(tag=tag.ELEMWISE)
def round(x):
- """Round elements of x to nearest integer.
+ """Round elements of x to nearest integer using ties-to-even (banker's
rounding).
+
+ Ties are broken by rounding to the nearest even integer, matching the ONNX
Round
+ specification and IEEE 754 default rounding mode.
Parameters
----------
@@ -459,7 +462,7 @@ def round(x):
y : tvm.te.Tensor
The result.
"""
- return te.compute(x.shape, lambda *i: te.round(x(*i)))
+ return te.compute(x.shape, lambda *i: te.nearbyint(x(*i)))
def log(x):
diff --git a/src/target/source/intrin_rule_webgpu.cc
b/src/target/source/intrin_rule_webgpu.cc
index 86658d8e28..bc48395468 100644
--- a/src/target/source/intrin_rule_webgpu.cc
+++ b/src/target/source/intrin_rule_webgpu.cc
@@ -113,6 +113,14 @@ TVM_REGISTER_OP("tirx.log2")
TVM_REGISTER_OP("tirx.pow")
.set_attr<FLowerIntrinsic>("webgpu.FLowerIntrinsic",
DispatchPureExtern<Direct>);
+struct ReturnRound {
+ std::string operator()(DataType t, std::string name) const { return "round";
}
+};
+
+// WGSL round() uses ties-to-even (banker's rounding), matching IEEE 754 and
ONNX Round spec.
+TVM_REGISTER_OP("tirx.nearbyint")
+ .set_attr<FLowerIntrinsic>("webgpu.FLowerIntrinsic",
DispatchPureExtern<ReturnRound>);
+
TVM_REGISTER_OP("tirx.round")
.set_attr<FLowerIntrinsic>("webgpu.FLowerIntrinsic",
DispatchPureExtern<Direct>);
diff --git a/tests/python/relax/test_frontend_onnx.py
b/tests/python/relax/test_frontend_onnx.py
index 29e2d9499d..534161ce6d 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -699,6 +699,27 @@ def test_unary(op_name: str):
verify_unary(op_name, [8, 8, 8], input_dtype=input_dtype,
output_dtype=output_dtype)
+def test_round_ties_to_even():
+ """ONNX Round must use ties-to-even (banker's rounding), not
ties-away-from-zero.
+
+ Per the ONNX spec: "For cases where number is exactly halfway between two
+ integers, it rounds to the nearest even integer."
+ https://onnx.ai/onnx/operators/onnx__Round.html
+ """
+ round_node = helper.make_node("Round", ["x"], ["y"])
+ graph = helper.make_graph(
+ [round_node],
+ "round_ties_to_even_test",
+ inputs=[helper.make_tensor_value_info("x", TensorProto.FLOAT, [6])],
+ outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT, [6])],
+ )
+ model = helper.make_model(graph, producer_name="round_ties_to_even_test")
+ # Midpoint values: 0.5->0, 1.5->2, 2.5->2, -0.5->0, -1.5->-2, -2.5->-2
(ties-to-even)
+ # Ties-away would give: 0.5->1, 1.5->2, 2.5->3, -0.5->-1, -1.5->-2,
-2.5->-3
+ inputs = {"x": np.array([0.5, 1.5, 2.5, -0.5, -1.5, -2.5],
dtype="float32")}
+ check_correctness(model, inputs=inputs, opset=11)
+
+
@pytest.mark.parametrize("from_type", [TensorProto.INT32, TensorProto.FLOAT,
TensorProto.FLOAT16])
@pytest.mark.parametrize("to_type", [TensorProto.INT32, TensorProto.FLOAT,
TensorProto.FLOAT16])
def test_cast(from_type, to_type):