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):

Reply via email to