siyiweigeHEW opened a new issue, #20231:
URL: https://github.com/apache/tvm/issues/20231

   
   ### Expected behavior
   
   PyTorch's `torch.round` follows round-half-to-even (banker's rounding):
   
   ```python
   >>> torch.round(torch.tensor([0.5, 1.5, 2.5, -0.5, -2.5]))
   tensor([0., 2., 2., -0., -2.])
   ```
   
   `decimals` is an official parameter (positive = round to decimal places, 
negative = round to tens/hundreds, e.g. `torch.round(torch.tensor(25.), 
decimals=-1) == tensor(20.)`).
   
   A valid PyTorch model using `torch.round` — with or without `decimals` — 
should be convertible by `tvm.relax.frontend.torch.from_exported_program` / 
`from_fx` and produce results identical to native PyTorch.
   
   ### Actual behavior
   
   `tvm.relax.frontend.torch` mishandles `torch.round` in three ways:
   
   **1. Half values are rounded away from zero, not to even 
(`from_exported_program`, `aten.round.default`).**
   `_round` 
(python/tvm/relax/frontend/torch/base_fx_graph_translator.py:396-409) delegates 
`decimals == 0` to `relax.op.round`, which lowers to `topi.round` 
(python/tvm/topi/math.py:450, `te.round`) → `tir.round` → `llvm.round`, i.e. 
**round-half-away-from-zero**. Plain `torch.round(tensor([0.5, 2.5, -2.5, 
4.5]))` returns `[1, 3, -3, 5]` instead of `[0, 2, -2, 4]` (max diff 1.0). This 
also affects the ONNX frontend `onnx.Round` (shared `relax.op.round`), which 
violates the ONNX Round spec ("In case of halves, the rule is to round them to 
the nearest even integer").
   
   **2. Any explicit `decimals` argument makes the whole model fail to import 
(`from_exported_program`).**
   `torch.export` lowers `torch.round(x, decimals=k)` (even `decimals=0`) to 
the ATen op `aten.round.decimals`, but the exported-program dispatch map 
registers only `"round.default"` 
(python/tvm/relax/frontend/torch/exported_program_translator.py:1240). 
Conversion aborts with:
   
   ```
   AssertionError: Unsupported function types ['round.decimals']
   ```
   
   So the `decimals != 0` branch in `_round` is unreachable on the recommended 
export path.
   
   **3. Via `from_fx`, the `decimals` path computes wrong values.**
   `_round` uses `round(x * 10**decimals) / 10**decimals` with `scale = 
10**decimals`. Because the inner `relax.op.round` is ties-away-from-zero, and 
the multiply/divide is numerically sensitive in float64:
   
   ```python
   torch.round(torch.tensor([25., 125., 165.]), decimals=-1)   # -> [20., 120., 
160.]
   # TVM (from_fx)                                                -> [30., 
130., 170.]   max|diff|=10
   torch.round(torch.tensor(2.25), decimals=1)                 # -> 2.2   TVM 
-> 2.3
   torch.round(torch.tensor(250.), decimals=-2)                # -> 200.  TVM 
-> 300.  max|diff|=100
   ```
   
   (Note: even after switching the inner round to ties-to-even, the float64 
case `25. * 0.1 = 2.5000000000000004` rounds up to `30` instead of `20`, so the 
`x*10^d / 10^d` transform itself is not a correct implementation of `round(x, 
decimals)`.)
   
   ### Environment
   
   - OS: Linux
   - TVM: v0.24.dev0 (commit `176c77388`, branch main)
   - Python: 3.11
   - torch: 2.10.0
   - onnx: 1.20.1 / onnxruntime: 1.24.1 (for the ONNX-side observation)
   
   ### Steps to reproduce
   
   ```python
   """Repro: TVM relax torch frontend torch.round defects."""
   import warnings; warnings.filterwarnings("ignore")
   import numpy as np
   import torch
   import torch.nn as nn
   import tvm
   from tvm import relax
   from tvm.relax.frontend.torch import from_exported_program, from_fx
   
   
   def run_tvm(mod, x):
       ex = relax.build(mod, target="llvm")
       vm = relax.VirtualMachine(ex, tvm.cpu())
       out = vm["main"](x.numpy())
       return out.numpy() if hasattr(out, "numpy") else [o.numpy() for o in 
out][0]
   
   
   # Case 1: plain torch.round(x) on half values (from_exported_program)
   class M1(nn.Module):
       def forward(self, t):
           return torch.round(t)
   
   x = torch.tensor([-2.5, -0.5, 0.5, 2.5, 4.5], dtype=torch.float32)
   exp = torch.export.export(M1().eval(), (x,))
   print("Case1 ref:", torch.round(x).numpy().tolist())
   print("Case1 tvm:", run_tvm(from_exported_program(exp), x).tolist())
   
   # Case 2: torch.round(x, decimals=...) fails to import 
(from_exported_program)
   class M2(nn.Module):
       def forward(self, t):
           return torch.round(t, decimals=-1)
   
   exp2 = torch.export.export(M2().eval(), (x,))
   print("Case2 exported op:", [n.target for n in exp2.graph.nodes if "round" 
in str(n.target)])
   try:
       from_exported_program(exp2)
       print("Case2 tvm: OK")
   except Exception as e:
       print(f"Case2 tvm: {type(e).__name__}: {e}")
   
   # Case 3: decimals path wrong results (from_fx)
   class M3(nn.Module):
       def forward(self, t):
           return torch.round(t, decimals=-1)
   
   x3 = torch.tensor([25.0, 125.0, 165.0], dtype=torch.float32)
   gm = torch.fx.symbolic_trace(M3().eval())
   mod3 = from_fx(gm, [(list(x3.shape), "float32")])
   print("Case3 ref:", torch.round(x3, decimals=-1).numpy().tolist())
   print("Case3 tvm:", run_tvm(mod3, x3).tolist())
   ```
   
   Actual output:
   
   ```
   Case1 ref: [-2.0, -0.0, 0.0, 2.0, 4.0]
   Case1 tvm: [-3.0, -1.0, 1.0, 3.0, 5.0]      # half values rounded away from 
zero
   Case2 exported op: [aten.round.decimals]
   Case2 tvm: AssertionError: Unsupported function types ['round.decimals']
   Case3 ref: [20.0, 120.0, 160.0]
   Case3 tvm: [30.0, 130.0, 170.0]              # error = 10^decimals
   ```
   
   ### Notes
   
   - The root-cause rounding mode (`topi.round`/`tir.round` being 
ties-away-from-zero) is already fixed upstream for the ONNX path by #19367 
("[BugFix][ONNX] Fix Round op to use ties-to-even", topi.round → 
`te.nearbyint`) and #19368 (aligning `tir.round` across backends). The tested 
commit predates both fixes.
   - The **torch-frontend** defects above are not addressed by those fixes: 
`aten.round.decimals` is still missing from the dispatch map on current `main` 
(only `"round.default"` is registered), and the `_round` `decimals` branch is 
unchanged. The float64 precision issue of the `round(x*10^d)/10^d` transform 
also remains once the inner rounding is corrected.
   - Impact includes silent wrong results (Case 1, Case 3) and a hard import 
failure (Case 2) for valid, common PyTorch models (`torch.round` is frequently 
used in quantization-style preprocessing, including with negative `decimals`).
   
   ### Triage
   
   * needs-triage
   * bug
   * relax
   * frontend/torch
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to