Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
cb07e5e
[Relax][ONNX] Fix Resize coordinate error with non-integer scales
cchung100m Jun 9, 2026
3c30393
Fix lint error
cchung100m Jun 10, 2026
551d3ac
Fix TypeError: not supported type in emit_te
cchung100m Jun 10, 2026
7d0f27c
Fix ndims==5 use relax.op.image.resize3d instead of bb.emit_te
cchung100m Jun 10, 2026
bf46574
[Relax][ONNX] Add test case: test_resize_noninteger_scales_2d
cchung100m Jun 10, 2026
8424576
[Relax][ONNX] Add test case: test_resize_noninteger_scales_1d
cchung100m Jun 10, 2026
36ff8ff
[Relax][ONNX] Add test case: test_resize_noninteger_scales_3d
cchung100m Jun 10, 2026
ca4a42c
[Relax][ONNX] Add test case: test_resize_integer_scales_regression
cchung100m Jun 11, 2026
3822930
Use topi_mode instead of relax_mode in the 3D resize implementation f…
cchung100m Jun 11, 2026
39944a2
Add test cases: test_resize_asymmetric_nearest_noninteger_scales_2d a…
cchung100m Jul 20, 2026
37bda58
Add test cases: test_resize_dynamic_roi_noninteger_scales_1d
cchung100m Jul 22, 2026
df4abac
Add test cases: test_resize_dynamic_roi_noninteger_scales_2d
cchung100m Jul 22, 2026
5e90808
Add test cases: test_resize_dynamic_roi_noninteger_scales_3d_anisotropic
cchung100m Jul 23, 2026
ea8b3b5
[Relax][ONNX] Add three TOPI-level tests to verify resize behavior wi…
cchung100m Aug 14, 2026
17f8aba
Trigger CI pipeline
cchung100m Aug 21, 2026
836e5f9
Merge branch 'main' into issue-19570
cchung100m Sep 14, 2026
0964a93
Preserve the resize2d integer-division fast path when an explicit sca…
cchung100m Sep 14, 2026
1de2be8
Merge branch 'main' into issue-19570
cchung100m Sep 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion python/tvm/relax/frontend/onnx/onnx_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3755,6 +3755,7 @@ def _emit_resize_topi_dynamic_roi(
cubic_coeff_a: float,
exclude_outside: int,
extrapolation_value: float,
scales: list | None,
) -> relax.Expr:
"""Lower Resize with runtime ROI via TOPI, which supports Expr ROI."""
if rank == 3:
Expand All @@ -3771,6 +3772,7 @@ def resize1d_dyn(d, r, s0):
cubic_coeff_a,
exclude_outside,
extrapolation_value,
scales=scales,
)

return bb.emit_te(resize1d_dyn, data, roi_spatial_vec, sizes_spatial[0])
Expand All @@ -3789,12 +3791,14 @@ def resize2d_dyn(d, r, s0, s1):
bicubic_alpha=cubic_coeff_a,
bicubic_exclude=exclude_outside,
extrapolation_value=extrapolation_value,
scales=scales,
)

return bb.emit_te(resize2d_dyn, data, roi_spatial_vec, sizes_spatial[0], sizes_spatial[1])

def resize3d_dyn(d, r, s0, s1, s2):
# r is ONNX order (D,H,W) x2; TOPI expects (W,H,D) x2.
# NOTE: scales order must stay ONNX (D,H,W), unlike r which is reordered
return topi.image.resize3d(
d,
(r[2], r[1], r[0], r[5], r[4], r[3]),
Expand All @@ -3806,6 +3810,7 @@ def resize3d_dyn(d, r, s0, s1, s2):
bicubic_alpha=cubic_coeff_a,
bicubic_exclude=exclude_outside,
extrapolation_value=extrapolation_value,
scales=scales,
)

return bb.emit_te(
Expand Down Expand Up @@ -3872,14 +3877,18 @@ def _impl_v18(cls, bb, inputs, attr, params):

use_dynamic_roi = roi_dynamic_vec is not None

# Convert scales to sizes if needed.
# Convert scales to sizes if needed, preserving the original spatial scales so
# the coordinate transformation uses the exact ONNX scale value rather than the
# lossy ratio derived from floor(input * scale) / input.
original_spatial_scales = None
if scales is not None:
if isinstance(scales, tvm.ir.GenericConst):
scales = scales.value.numpy()
elif isinstance(scales, relax.expr.ShapeExpr):
scales = [int(val.value) for val in scales.values]
else:
raise ValueError(f"Type {type(scales)} for scale is currently unsupported.")
original_spatial_scales = [float(s) for s in scales[2:]]
sizes = []

for i, dim in enumerate(x.ty.shape):
Expand All @@ -3906,6 +3915,7 @@ def _impl_v18(cls, bb, inputs, attr, params):
cubic_coeff_a,
exclude_outside,
extrapolation_value,
original_spatial_scales,
)

if ndims == 3:
Expand All @@ -3921,8 +3931,24 @@ def _impl_v18(cls, bb, inputs, attr, params):
cubic_coeff_a,
exclude_outside,
extrapolation_value,
scales=original_spatial_scales,
)
elif ndims == 4:
if original_spatial_scales is not None:
return bb.emit_te(
topi.image.resize2d,
x,
roi_static,
sizes,
"NCHW",
topi_mode,
coord_mode,
rounding_method,
cubic_coeff_a,
exclude_outside,
extrapolation_value,
scales=original_spatial_scales,
)
return relax.op.image.resize2d(
x,
size=relax.ShapeExpr(sizes),
Expand All @@ -3937,6 +3963,21 @@ def _impl_v18(cls, bb, inputs, attr, params):
)
else: # ndims == 5
roi3d = _topi_resize3d_roi_from_onnx_ncdhw_spatial(roi_static)
if original_spatial_scales is not None:
return bb.emit_te(
topi.image.resize3d,
x,
roi3d,
sizes,
"NCDHW",
topi_mode,
coord_mode,
rounding_method,
cubic_coeff_a,
exclude_outside,
extrapolation_value,
scales=original_spatial_scales,
)
return relax.op.image.resize3d(
x,
size=relax.ShapeExpr(sizes),
Expand Down
113 changes: 106 additions & 7 deletions python/tvm/topi/image/resize.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ def can_convert_multiply_to_intdiv(origin_size, scaled_size):
return True


def _explicit_scale_matches_intdiv(origin_size, scaled_size, scale_override):
"""Check whether an explicit scale (image_size / target_size) agrees with the exact
integer ratio the sizes alone would imply.

When it does, integer division stays numerically exact and should still be used;
falling back to floating-point reciproal multiplication in that case only adds
rounding error for no benefit.
"""
if not can_convert_multiply_to_intdiv(origin_size, scaled_size):
return False
int_ratio = (scaled_size / origin_size.astype("float")).value
epsilon = 1e-5
return abs(scale_override * int_ratio - 1.0) < epsilon


def get_1d_indices(indices, layout="NCW"):
"""Get 1d indices"""
(cc, inum, ic) = (0, 0, 0)
Expand Down Expand Up @@ -145,9 +160,13 @@ def get_inx(
start_x=0,
end_x=-1,
use_int_div=False,
scale_x_override=None,
):
"""Infer input x from output x with various coordinate transformation methods"""
scale_x = te.div(image_width.astype("float"), target_width.astype("float"))
if scale_x_override is not None:
scale_x = scale_x_override
else:
scale_x = te.div(image_width.astype("float"), target_width.astype("float"))
if coordinate_transformation_mode == "half_pixel":
in_x = (x + 0.5) * scale_x - 0.5
elif coordinate_transformation_mode == "align_corners":
Expand Down Expand Up @@ -237,6 +256,7 @@ def _resize_1d(
alpha=-0.5,
exclude_outside=0,
out_dtype=None,
scale_x=None,
):
"""Perform resize operation on the data with selected method and options.

Expand Down Expand Up @@ -315,7 +335,15 @@ def _cast_output(value, data_dtype="float32", out_dtype=None):
if boxes is not None:
# TODO(mbrookhart): Find an example of this
raise NotImplementedError("resize1d with image boxes not yet implemented")
in_x = get_inx(x, image_width, target_width, coordinate_transformation_mode, roi[0], roi[1])
in_x = get_inx(
x,
image_width,
target_width,
coordinate_transformation_mode,
roi[0],
roi[1],
scale_x_override=scale_x,
)

if method == "nearest_neighbor":
if rounding_method == "":
Expand Down Expand Up @@ -383,6 +411,7 @@ def resize1d(
extrapolation_value=0.0,
out_dtype=None,
output_shape=None,
scales=None,
):
"""Perform resize operation on the data.

Expand Down Expand Up @@ -438,6 +467,12 @@ def resize1d(
Shape to return. If left None will be inferred
(If shape is determined dynamically, pass out_dtype.shape as output_shape)

scales: tuple or None
Explicit scale factors for coordinate transformation.
If not None, scales=[scale_w] for 1D.
When provided, overrides the scale derived from input and output sizes.
Used by the ONNX frontend when a Resize node is driven by a "scales" input.

Returns
-------
output : tvm.te.Tensor
Expand Down Expand Up @@ -472,6 +507,8 @@ def resize1d(
if isinstance(size[i], int):
size[i] = tvm.tirx.IntImm("int32", size[i])

scale_x = (1.0 / scales[0]) if scales is not None else None

def compute_func(*indices):
return _resize_1d(
indices,
Expand All @@ -487,6 +524,7 @@ def compute_func(*indices):
exclude_outside=bicubic_exclude,
extrapolation_value=extrapolation_value,
out_dtype=out_dtype,
scale_x=scale_x,
)

return te.compute(output_shape, compute_func, name="resize", tag=tag.INJECTIVE)
Expand All @@ -510,6 +548,8 @@ def _resize_2d(
alpha=-0.5,
exclude_outside=0,
out_dtype=None,
scale_h=None,
scale_w=None,
):
"""Perform resize operation on the data with selected method and options.

Expand Down Expand Up @@ -593,8 +633,18 @@ def _cast_output(value, data_dtype="float32", out_dtype=None):
width_use_int_div = False
if method == "nearest_neighbor" and coordinate_transformation_mode == "asymmetric":
if rounding_method == "floor" or rounding_method == "":
height_use_int_div = can_convert_multiply_to_intdiv(image_height, target_height)
width_use_int_div = can_convert_multiply_to_intdiv(image_width, target_width)
if scale_h is None:
height_use_int_div = can_convert_multiply_to_intdiv(image_height, target_height)
else:
height_use_int_div = _explicit_scale_matches_intdiv(
image_height, target_height, scale_h
)
if scale_w is None:
width_use_int_div = can_convert_multiply_to_intdiv(image_width, target_width)
else:
width_use_int_div = _explicit_scale_matches_intdiv(
image_width, target_width, scale_w
)

n, c, y, x, cc, inum, ic = get_2d_indices(indices, layout)
box_idx = box_indices(n) if box_indices is not None else n
Expand All @@ -618,6 +668,7 @@ def _cast_output(value, data_dtype="float32", out_dtype=None):
roi[1],
roi[3],
width_use_int_div,
scale_x_override=scale_w,
)
in_y = get_inx(
y,
Expand All @@ -627,6 +678,7 @@ def _cast_output(value, data_dtype="float32", out_dtype=None):
roi[0],
roi[2],
height_use_int_div,
scale_x_override=scale_h,
)

if method == "nearest_neighbor":
Expand Down Expand Up @@ -756,6 +808,7 @@ def resize2d(
extrapolation_value=0.0,
out_dtype=None,
output_shape=None,
scales=None,
):
"""Perform resize operation on the data.

Expand Down Expand Up @@ -805,6 +858,12 @@ def resize2d(
Shape to return. If left None will be inferred
(If shape is determined dynamically, pass out_dtype.shape as output_shape)

scales: tuple or None
Explicit scale factors for coordinate transformation.
If not None, scales=[scale_h, scale_w] for 2D.
When provided, overrides the scale derived from input and output sizes.
Used by the ONNX frontend when a Resize node is driven by a "scales" input.

Returns
-------
output : tvm.te.Tensor
Expand Down Expand Up @@ -839,6 +898,9 @@ def resize2d(
if isinstance(size[i], int):
size[i] = tvm.tirx.IntImm("int32", size[i])

scale_h = (1.0 / scales[0]) if scales is not None else None
scale_w = (1.0 / scales[1]) if scales is not None else None

def compute_func(*indices):
return _resize_2d(
indices,
Expand All @@ -856,6 +918,8 @@ def compute_func(*indices):
exclude_outside=bicubic_exclude,
extrapolation_value=extrapolation_value,
out_dtype=out_dtype,
scale_h=scale_h,
scale_w=scale_w,
)

return te.compute(output_shape, compute_func, name="resize", tag=tag.INJECTIVE)
Expand Down Expand Up @@ -976,6 +1040,9 @@ def _resize_3d(
alpha=-0.5,
exclude_outside=0,
out_dtype=None,
scale_d=None,
scale_h=None,
scale_w=None,
):
"""Perform resize operation on the data with selected method and options.

Expand Down Expand Up @@ -1066,9 +1133,33 @@ def _cast_output(value, data_dtype="float32", out_dtype=None):
if boxes is not None:
# TODO(mbrookhart): Find an example of this
raise NotImplementedError("resize1d with image boxes not yet implemented")
in_z = get_inx(z, image_depth, target_depth, coordinate_transformation_mode, roi[2], roi[5])
in_y = get_inx(y, image_height, target_height, coordinate_transformation_mode, roi[1], roi[4])
in_x = get_inx(x, image_width, target_width, coordinate_transformation_mode, roi[0], roi[3])
in_z = get_inx(
z,
image_depth,
target_depth,
coordinate_transformation_mode,
roi[2],
roi[5],
scale_x_override=scale_d,
)
in_y = get_inx(
y,
image_height,
target_height,
coordinate_transformation_mode,
roi[1],
roi[4],
scale_x_override=scale_h,
)
in_x = get_inx(
x,
image_width,
target_width,
coordinate_transformation_mode,
roi[0],
roi[3],
scale_x_override=scale_w,
)

if method == "nearest_neighbor":
if rounding_method == "":
Expand Down Expand Up @@ -1225,6 +1316,7 @@ def resize3d(
extrapolation_value=0.0,
out_dtype=None,
output_shape=None,
scales=None,
):
"""Perform resize operation on the data.

Expand Down Expand Up @@ -1302,6 +1394,10 @@ def resize3d(
if isinstance(size[i], int):
size[i] = tvm.tirx.IntImm("int32", size[i])

scale_d = (1.0 / scales[0]) if scales is not None else None
scale_h = (1.0 / scales[1]) if scales is not None else None
scale_w = (1.0 / scales[2]) if scales is not None else None

def compute_func(*indices):
return _resize_3d(
indices,
Expand All @@ -1321,6 +1417,9 @@ def compute_func(*indices):
exclude_outside=bicubic_exclude,
extrapolation_value=extrapolation_value,
out_dtype=out_dtype,
scale_d=scale_d,
scale_h=scale_h,
scale_w=scale_w,
)

return te.compute(output_shape, compute_func, name="resize", tag=tag.INJECTIVE)
Loading
Loading