[Mlir-commits] [mlir] [mlir][tosa] Fix integer bilinear (quantized) tosa.resize lowering to use floordivsi (PR #193821)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Thu Apr 23 11:57:44 PDT 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-mlir-tosa
@llvm/pr-subscribers-mlir-linalg
Author: Henry Wu (HanchengWu)
<details>
<summary>Changes</summary>
## Background
`tosa.resize` in bilinear integer (quantized) mode lowers to a `linalg.generic`
body that, for each output pixel, computes a corresponding input coordinate and
blends the four neighboring input pixels. The mapping is:
```
val = out_coord * scale_d + offset
index = val / scale_n // integer part — which input pixel to start from
delta = val - index * scale_n // fractional part, scaled to [0, scale_n)
```
`delta` is the interpolation weight toward the next pixel. The bilinear formula
(integer path) is:
```
topAcc = pixel[y0,x0] * (scale_x - dx) + pixel[y0,x1] * dx
bottomAcc = pixel[y1,x0] * (scale_x - dx) + pixel[y1,x1] * dx
result = topAcc * (scale_y - dy) + bottomAcc * dy
```
For this to be a valid convex combination (interpolation, not extrapolation),
`dx` and `dy` must be in `[0, scale_n]`.
The pixel indices `y0`, `y1`, `x0`, `x1` are computed by `getClampedIdxs`:
```cpp
y0 = clamp(iy, 0, H-1)
y1 = clamp(iy+1, 0, H-1)
```
---
## The Bug
The integer path uses `DivSIOp` (truncation toward zero):
```cpp
// getIndexAndDeltaInt — mlir/lib/Conversion/TosaToLinalg/TosaToLinalg.cpp
index = arith::DivSIOp::create(b, val, scaleN); // truncates toward zero
delta = arith::MulIOp::create(b, index, scaleN);
delta = arith::SubIOp::create(b, val, delta); // = val - (val/scaleN)*scaleN
```
When `val < 0` (which happens at boundary output pixels when `offset` is
negative), `DivSIOp` truncates toward zero instead of toward -∞. This produces
a negative remainder, i.e. a negative `delta`, which causes extrapolation.
Note: the code comment on line 2058 already says `// ix = floor(x / scale_n)`,
but the code uses truncation — this mismatch is the root cause of the bug.
The float path (`getIndexAndDeltaFp`) uses `FloorDivSIOp` and is unaffected:
with floor division, `r = val - floor(val/scaleN)*scaleN` is always in
`[0, scaleN-1]`.
---
## Concrete Example
**Setup:** 2×2 input upsampled to 4×4, `scale=[4,2,4,2]`, `offset=[-1,-1]`
- `scale_y_n=4`, `scale_y_d=2`, `scale_x_n=4`, `scale_x_d=2`
- `offset_y=-1`, `offset_x=-1`
- Input: `tensor<1x2x2x1xi8>` with `input[0,0,0,0]=100`, all others `0`
**At output pixel (0,0):**
```
val = 0 * 2 + (-1) = -1
```
### Without any fix (DivSIOp, buggy)
```
iy = DivSIOp(-1, 4) = 0 // truncates -0.25 toward zero
dy = -1 - 0*4 = -1 // OUT OF RANGE: should be in [0, 4]
y0 = clamp(0, 0, 1) = 0
y1 = clamp(0+1, 0, 1) = 1 // different rows
ix = DivSIOp(-1, 4) = 0
dx = -1 // same issue
x0 = clamp(0, 0, 1) = 0
x1 = clamp(0+1, 0, 1) = 1
// pixels:
y0x0 = input[0,0,0,0] = 100
y0x1 = input[0,0,1,0] = 0
y1x0 = input[0,1,0,0] = 0
y1x1 = input[0,1,1,0] = 0
topAcc = 100*(4-(-1)) + 0*(-1) = 500 // EXTRAPOLATION
bottomAcc = 0*(4-(-1)) + 0*(-1) = 0
result = 500*(4-(-1)) + 0*(-1) = 2500 // WRONG
```
### Fix w/ FloorDivSIOp
```
iy = FloorDivSIOp(-1, 4) = -1 // floors -0.25 toward -∞
dy = -1 - (-1)*4 = 3 // naturally in [0, scale_n-1]
y0 = clamp(-1, 0, 1) = 0
y1 = clamp(-1+1, 0, 1) = 0 // SAME row — both snap to boundary
dx = 3 (same by symmetry)
// all four neighbors collapse to the same boundary pixel:
y0x0 = y0x1 = y1x0 = y1x1 = input[0,0,0,0] = 100
topAcc = 100*(4-3) + 100*3 = 400
bottomAcc = 100*(4-3) + 100*3 = 400
result = 400*(4-3) + 400*3 = 1600 // correct
```
`y0=y1=0` means boundary replication is enforced by `getClampedIdxs`, making
`dy` irrelevant.
## Semantic Analysis of the fix
- `iy=-1` correctly signals "this position is before the first input pixel."
- `getClampedIdxs` does its intended job: both `y0` and `y1` snap to the
boundary pixel, enforcing replication explicitly.
- `dy=3` **appears** valid (it's in `[0, scale_n-1]`) but is semantically
meaningless: the true position is `-0.25`, which is outside the image — there
is no "3/4 toward the next pixel" to interpolate toward. It is harmless only
because `y0=y1`.
- Same analysis for dx=3 by symmetry.
- Fixes the root cause (wrong division op, matching the existing code comment
and mirroring the float path), but `delta` still carries a misleading value
at out-of-bounds positions.
---
Full diff: https://github.com/llvm/llvm-project/pull/193821.diff
2 Files Affected:
- (modified) mlir/lib/Conversion/TosaToLinalg/TosaToLinalg.cpp (+1-1)
- (modified) mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-resize.mlir (+5-4)
``````````diff
diff --git a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalg.cpp b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalg.cpp
index 11b3aabcbfeb4..e9c9e17fe6274 100644
--- a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalg.cpp
+++ b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalg.cpp
@@ -2059,7 +2059,7 @@ class GenericResizeConverter : public OpRewritePattern<tosa::ResizeOp> {
// dx = x - ix * scale_n;
Value val = arith::MulIOp::create(b, in, scaleD);
val = arith::AddIOp::create(b, val, offset);
- index = arith::DivSIOp::create(b, val, scaleN);
+ index = arith::FloorDivSIOp::create(b, val, scaleN);
delta = arith::MulIOp::create(b, index, scaleN);
delta = arith::SubIOp::create(b, val, delta);
};
diff --git a/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-resize.mlir b/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-resize.mlir
index 4900476b25dc5..2959cf59e953a 100644
--- a/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-resize.mlir
+++ b/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-resize.mlir
@@ -218,13 +218,13 @@ func.func @resize_nearest_int(%arg0: tensor<1x15x13x1xi8>) -> () {
// CHECK: %[[TEMP_Y:.*]] = arith.muli %[[Y]], %[[SCALE_Y_D]]
// CHECK: %[[Y:.*]] = arith.addi %[[TEMP_Y]], %[[OFFSET_Y]]
- // CHECK: %[[I_Y:.*]] = arith.divsi %[[Y]], %[[SCALE_Y_N]]
+ // CHECK: %[[I_Y:.*]] = arith.floordivsi %[[Y]], %[[SCALE_Y_N]]
// CHECK: %[[TEMP_Y:.*]] = arith.muli %[[I_Y]], %[[SCALE_Y_N]]
// CHECK: %[[D_Y:.*]] = arith.subi %[[Y]], %[[TEMP_Y]]
// CHECK: %[[TEMP_X:.*]] = arith.muli %[[X]], %[[SCALE_X_D]]
// CHECK: %[[X:.*]] = arith.addi %[[TEMP_X]], %[[OFFSET_X]]
- // CHECK: %[[I_X:.*]] = arith.divsi %[[X]], %[[SCALE_X_N]]
+ // CHECK: %[[I_X:.*]] = arith.floordivsi %[[X]], %[[SCALE_X_N]]
// CHECK: %[[TEMP_X:.*]] = arith.muli %[[I_X]], %[[SCALE_X_N]]
// CHECK: %[[D_X:.*]] = arith.subi %[[X]], %[[TEMP_X]]
@@ -285,13 +285,13 @@ func.func @resize_bilinear_int(%arg0: tensor<1x19x20x1xi8>) {
// CHECK: %[[TEMP_Y:.*]] = arith.muli %[[Y]], %[[SCALE_Y_D]]
// CHECK: %[[Y:.*]] = arith.addi %[[TEMP_Y]], %[[OFFSET_Y]]
- // CHECK: %[[I_Y:.*]] = arith.divsi %[[Y]], %[[SCALE_Y_N]]
+ // CHECK: %[[I_Y:.*]] = arith.floordivsi %[[Y]], %[[SCALE_Y_N]]
// CHECK: %[[TEMP_Y:.*]] = arith.muli %[[I_Y]], %[[SCALE_Y_N]]
// CHECK: %[[D_Y:.*]] = arith.subi %[[Y]], %[[TEMP_Y]]
// CHECK: %[[TEMP_X:.*]] = arith.muli %[[X]], %[[SCALE_X_D]]
// CHECK: %[[X:.*]] = arith.addi %[[TEMP_X]], %[[OFFSET_X]]
- // CHECK: %[[I_X:.*]] = arith.divsi %[[X]], %[[SCALE_X_N]]
+ // CHECK: %[[I_X:.*]] = arith.floordivsi %[[X]], %[[SCALE_X_N]]
// CHECK: %[[TEMP_X:.*]] = arith.muli %[[I_X]], %[[SCALE_X_N]]
// CHECK: %[[D_X:.*]] = arith.subi %[[X]], %[[TEMP_X]]
@@ -605,3 +605,4 @@ func.func @skip_interpolate_bilinear_f32(%arg0 : tensor<3x1x2x7xf32>) -> tensor<
// CHECK: return %[[GENERIC]]
return %resize : tensor<3x1x4x7xf32>
}
+
``````````
</details>
https://github.com/llvm/llvm-project/pull/193821
More information about the Mlir-commits
mailing list