hiyufan opened a new pull request, #20373:
URL: https://github.com/apache/tvm/pull/20373
**Stacked on #20372** — the first commit here is that PR; this PR is the
second commit. It reuses the scalar-promotion helpers from #20372, so it should
land after it (or I can rebase onto `main` once that merges).
### Problem
Three converters disagree with torch on what a division returns.
| expression | tensor dtype | torch | frontend on `main` |
| --- | --- | --- | --- |
| `x / 2` | int64 `[3, 4, 5]` | float32 `[1.5, 2.0, 2.5]` | int64 **`[1, 2,
2]`** |
| `x / (x + 1)` | int64 | float32 `[0.75, 0.8, 0.83]` | int64 `[0, 0, 0]` |
| `x / 2` | bool | float32 `[0.5, 0.5, 0.0]` | bool |
| `2 / x` | int64 `[3, 4, 5]` | float32 `[0.67, 0.5, 0.4]` | int64 `[0, 0,
0]` |
| `torch.reciprocal(x)` | int64 | float32 | int64 |
| `x // 2` | int64 / float32 | int64 / float32 | **`TypeError`** (same-dtype
check) |
| `torch.div(x, 2, rounding_mode="trunc")` | int64 | int64 `[-3, -1, 1, 3]`
| `TypeError` |
| `torch.div(x, 2, rounding_mode="floor")` | int64 | int64 `[-4, -2, 1, 3]`
| `TypeError` |
Three causes:
1. **`div.Tensor` / `div.Scalar`** went through the generic `_binary_op`,
which keeps the promoted dtype. torch's true division always yields a floating
result — `int64 / 2` and `int64 / int64` are float32 — which is a rule *on top
of* `torch.result_type` (an int scalar alone does not widen an int tensor), so
it needs its own converter.
2. **`div.Tensor_mode`** — what `x // 2` and `torch.div(x, s,
rounding_mode=…)` decompose to under `run_decompositions()` — built its
constant with `relax.const(inp_2)` and no dtype, i.e. int32. Every float tensor
and every non-int32 integer tensor then failed relax's same-dtype check. The
int32 case passed only because relax.const's default dtype happens to be int32.
3. **`reciprocal.default`**, what `scalar / x` decomposes to, divided
`const(1, x.dtype)` by `x`, so `2 / int_tensor` was an integer quotient too.
The converter was duplicated in `fx_translator.py` and
`exported_program_translator.py`; there is now one in the base class.
### Fix
The two promotion closures inside `_binary_op` become methods
(`_promote_binary_operands`, `_promote_scalar_operand`) so the division
converters can share them. `_true_division_operands` applies torch's one extra
rule for `/`:
```python
lhs, rhs = self._promote_binary_operands(lhs, rhs)
if dtype is integral or bool:
lhs, rhs = astype(lhs, "float32"), astype(rhs, "float32")
```
- `div.Tensor` / `div.Scalar` (and fx `truediv`) → new `_true_divide`.
- `_div` (rounding modes) promotes the same way and then keeps the promoted
dtype: `floor` → `floor_divide`; `trunc` on an integer pair → plain `divide`,
since integer division in relax truncates toward zero (checked: `[-7, -3, 3, 7]
/ 2` → `[-3, -1, 1, 3]`); floats keep divide + `trunc`.
- `_reciprocal` → `_true_division_operands(1, x)` then divide, in the base
class.
### Verification
The same 864-program sweep as #20372 (18 binary ops × 8 tensor dtypes × 6
Python scalars, built with `relax.build(llvm)`, **result dtype and values**
compared with torch), measured against #20372's head as the base:
| | matched | wrong dtype or values | raised |
| --- | --- | --- | --- |
| #20372 | 645 | 52 | 132 |
| this PR | **799** | **0** | 30 |
Per-case diff of the failure lists: **no case fails with this change that
did not fail before it; 154 repaired.** Every remaining `wrong` case is gone.
The 30 that still raise are the same pre-existing edges noted in #20372 — relax
rejecting arithmetic on a bool tensor with a bool scalar, a uint8 tensor
against a negative scalar (torch wraps modulo 256, `relax.const(-3, "uint8")`
raises), and `x ** True` on an integer tensor — plus the four division ops on
that same `uint8 / -3` input.
`test_frontend_from_exported_program.py` and `test_frontend_from_fx.py`:
failure sets identical before and after apart from the new tests (8 and 15
pre-existing in my environment). `ruff check` / `ruff format --check` clean on
all four touched files.
### Tests
- `test_true_division_of_integers_gives_float` — IR-level: `int64 / 2` casts
both operands to float32 before the divide.
- `test_true_division_values` — `x / s`, `s / x`, `x / (x + 1)` and
`torch.reciprocal(x)` over int64, int32, uint8 and bool tensors (and a float
scalar), asserting dtype and values.
- `test_division_with_rounding_mode` — `x // 2`, `torch.div(…, "floor")`,
`torch.div(…, "trunc")` and a tensor divisor on `[-7, -3, 3, 7]` for int64,
int32 and float32; the negative inputs are what separate floor from trunc.
8 of the 9 fail against #20372's head:
```text
test_true_division_values[int64-2] result dtype int64, torch gives
torch.float32
test_division_with_rounding_mode[int64] TypeError: ... R.floor_divide(x,
R.const(2, "int32")) ... T.int64 on the LHS
test_division_with_rounding_mode[float32] TypeError: ... T.float32 on the
LHS ... T.int32 on the RHS
```
The ninth, `test_division_with_rounding_mode[int32]`, passes there for the
int32-default reason above; it stays in to pin that the path keeps working.
---
This change was prepared with AI assistance (Claude). I have reviewed and
verified it, and can speak to it in review.
--
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]