Files
Soowon Jeong 2e6ee08eaf [BugFix] Align tir.round to ties-to-even across all backends (#19368)
## Problem

`tir.round` constant-folds using `std::nearbyint` (IEEE 754
ties-to-even), but all backends lower it to platform `round()` which
uses ties-away-from-zero. This means compiled code can produce different
results from constant-folded code for midpoint values:

| Input | Constant-fold (ties-to-even) | Compiled (ties-away) |
|-------|-----|------|
| 0.5   | 0.0 | 1.0  |
| 2.5   | 2.0 | 3.0  |
| -0.5  | 0.0 | -1.0 |

This was identified as a follow-up to #19367 — see [this
comment](https://github.com/apache/tvm/pull/19367#issuecomment-4201800320).

## Fix

Align all backends to use ties-to-even intrinsics, matching the
constant-folding behavior:

| Backend | Before | After |
|---------|--------|-------|
| LLVM/ROCm/Hexagon | `llvm::Intrinsic::round` |
`llvm::Intrinsic::nearbyint` |
| NVPTX | `__nv_round[f]` | `__nv_nearbyint[f]` |
| CUDA | `round`/`roundf` | `nearbyint`/`nearbyintf` (f16/bf16 already
used `hrint`) |
| Metal/OpenCL | `round` | `rint` |
| Vulkan/SPIR-V | `GLSLstd450Round` | `GLSLstd450RoundEven` |

Also fixes OpenCL codegen where `tir.nearbyint` was incorrectly mapped
to OpenCL `round()` instead of `rint()`.

Updates `op.h` documentation to explicitly state ties-to-even semantics
for both `round()` and `nearbyint()`.

## Testing

```
python -m pytest tests/python/tirx-base/test_tir_intrin.py -xvs
```

New `test_round_ties_to_even` verifies midpoint inputs `[0.5, 1.5, 2.5,
3.5, -0.5, -1.5, -2.5, -3.5]` produce ties-to-even results on the LLVM
backend. All 12 tests pass (10 passed, 2 skipped for CUDA).

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:35:22 -04:00

69 lines
2.8 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=invalid-name, too-many-nested-blocks
"Roi pool in python"
import math
import numpy as np
def roi_pool_nchw_python(a_np, rois_np, pooled_size, spatial_scale):
"""Roi pool in python"""
_, channel, height, width = a_np.shape
num_roi = rois_np.shape[0]
b_np = np.zeros((num_roi, channel, pooled_size, pooled_size), dtype=a_np.dtype)
if isinstance(pooled_size, int):
pooled_size_h = pooled_size_w = pooled_size
else:
pooled_size_h, pooled_size_w = pooled_size
for i in range(num_roi):
roi = rois_np[i]
batch_index = int(roi[0])
# Use ties-away-from-zero rounding to match ONNX runtime (std::round semantics).
# Python's built-in round() uses ties-to-even, so use floor(x + 0.5) explicitly.
roi_start_w = math.floor(roi[1] * spatial_scale + 0.5)
roi_start_h = math.floor(roi[2] * spatial_scale + 0.5)
roi_end_w = math.floor(roi[3] * spatial_scale + 0.5)
roi_end_h = math.floor(roi[4] * spatial_scale + 0.5)
roi_h = max(roi_end_h - roi_start_h + 1, 1)
roi_w = max(roi_end_w - roi_start_w + 1, 1)
bin_h = float(roi_h) / pooled_size_h
bin_w = float(roi_w) / pooled_size_w
for ph in range(pooled_size_h):
for pw in range(pooled_size_w):
hstart = math.floor(ph * bin_h)
wstart = math.floor(pw * bin_w)
hend = math.ceil((ph + 1) * bin_h)
wend = math.ceil((pw + 1) * bin_w)
hstart = min(max(hstart + roi_start_h, 0), height)
hend = min(max(hend + roi_start_h, 0), height)
wstart = min(max(wstart + roi_start_w, 0), width)
wend = min(max(wend + roi_start_w, 0), width)
is_empty = (hend <= hstart) or (wend <= wstart)
for c in range(channel):
if is_empty:
b_np[i, c, ph, pw] = 0.0
else:
b_np[i, c, ph, pw] = np.max(a_np[batch_index, c, hstart:hend, wstart:wend])
return b_np