Skip to content

Commit b992012

Browse files
committed
[Relax][ONNX] Add three TOPI-level tests to verify resize behavior without scales
1 parent 8e28cfa commit b992012

2 files changed

Lines changed: 210 additions & 3 deletions

File tree

‎python/tvm/topi/image/resize.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,12 @@ def resize1d(
452452
Shape to return. If left None will be inferred
453453
(If shape is determined dynamically, pass out_dtype.shape as output_shape)
454454
455+
scales: tuple or None
456+
Explicit scale factors for coordinate transformation.
457+
If not None, scales=[scale_w] for 1D.
458+
When provided, overrides the scale derived from input and output sizes.
459+
Used by the ONNX frontend when a Resize node is driven by a "scales" input.
460+
455461
Returns
456462
-------
457463
output : tvm.te.Tensor
@@ -829,6 +835,12 @@ def resize2d(
829835
Shape to return. If left None will be inferred
830836
(If shape is determined dynamically, pass out_dtype.shape as output_shape)
831837
838+
scales: tuple or None
839+
Explicit scale factors for coordinate transformation.
840+
If not None, scales=[scale_h, scale_w] for 2D.
841+
When provided, overrides the scale derived from input and output sizes.
842+
Used by the ONNX frontend when a Resize node is driven by a "scales" input.
843+
832844
Returns
833845
-------
834846
output : tvm.te.Tensor

‎tests/python/relax/test_frontend_onnx.py‎

Lines changed: 198 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141
import tvm
4242
import tvm.testing
43-
from tvm import relax
43+
from tvm import relax, te, topi
4444
from tvm.relax.frontend.onnx import from_onnx
4545
from tvm.script import ir as I
4646
from tvm.script import relax as R
@@ -8360,8 +8360,9 @@ def test_resize_asymmetric_nearest_noninteger_scales_2d():
83608360
def test_resize_asymmetric_nearest_integer_scales_baseline():
83618361
"""Baseline: asymmetric+nearest+floor with integer scale should work correctly.
83628362
8363-
This ensures the guarded optimization (when scale_override is None) still produces
8364-
correct results for integer scales and doesn't regress existing behavior.
8363+
This verifies the correctness of the float fallback path when an integer scale override
8364+
is provided (scale_override is not None). The optimization case (when scale_override is
8365+
None) is tested separately in TOPI-level tests.
83658366
"""
83668367
resize_node = helper.make_node(
83678368
"Resize",
@@ -8407,6 +8408,200 @@ def test_resize_integer_scales_regression(input_shape, scales, output_shape):
84078408
check_correctness(helper.make_model(graph), opset=18)
84088409

84098410

8411+
def _build_topi_resize2d_nearest_asymmetric_prim_func(input_shape, size, scales=None):
8412+
"""
8413+
Parameters
8414+
----------
8415+
input_shape : tuple
8416+
NCHW input shape, e.g. (1, 1, 2, 2).
8417+
size: tuple
8418+
Target (out_h, out_w) spatial size
8419+
scales: tuple or None
8420+
If not None, an explicit (scale_h, scale_w) override -- mirroring
8421+
what the ONNX frontend passes when the ONNX "scales" input (rather
8422+
than "sizes") drives the resize. Per the guard in `_resize_2d`,
8423+
supplying this disable the integer-division fast path regardless
8424+
of whether size[i] / input_shape[i] is itself an integer.
8425+
8426+
Returns
8427+
-------
8428+
(resized_te_tensor, prim_func) : tuple
8429+
The TE output tensor (used to read the concrete output shape) and
8430+
the lowered TIR PrimFunc (used both to run the kernel and to
8431+
inspect which code path was generated).
8432+
"""
8433+
data = te.placeholder(input_shape, dtype="float32", name="data")
8434+
roi = (0.0, 0.0, 0.0, 0.0)
8435+
resized = topi.image.resize2d(
8436+
data,
8437+
roi,
8438+
size=size,
8439+
layout="NCHW",
8440+
method="nearest_neighbor",
8441+
coordinate_transformation_mode="asymmetric",
8442+
rounding_method="",
8443+
scales=scales,
8444+
)
8445+
prim_func = te.create_prim_func([data, resized])
8446+
return resized, prim_func
8447+
8448+
8449+
def _run_prim_func(prim_func, input_np, output_shape):
8450+
"""Compile a 1-input/1-output PrimFunc for LLVM and run it on `input_np`."""
8451+
built = tvm.compile(prim_func, target="llvm")
8452+
data_nd = tvm.runtime.tensor(input_np.astype("float32"))
8453+
out_nd = tvm.runtime.tensor(np.zeros(output_shape, dtype="float32"))
8454+
built(data_nd, out_nd)
8455+
return out_nd.numpy()
8456+
8457+
8458+
def _fast_path_used(prim_func):
8459+
"""Return True if the lowered TIR took the integer-division fast path.
8460+
8461+
The fast path computes the source index purely with integer division
8462+
(`T.Div`, no rounding needed) and never calls `T.floor`. The float
8463+
fallback always computes `floor(scale * out_index + eps)` and casts it
8464+
to int, so it always contains `T.floor` and never emits a bare `T.Div`
8465+
for the index computation. Checking both signals (rather than only the
8466+
absence of one) keeps the assertion meaningful in both directions.
8467+
"""
8468+
script = prim_func.script()
8469+
has_int_div = "T.Div(" in script
8470+
has_float_floor = "T.floor(" in script
8471+
assert has_int_div != has_float_floor, (
8472+
"expected exactly one of the integer-division or floating-point "
8473+
f"code paths to appear in the lowered TIR, got:\n{script}"
8474+
)
8475+
return has_int_div
8476+
8477+
8478+
@pytest.mark.parametrize(
8479+
"input_hw,output_hw",
8480+
[
8481+
((2, 2), (4, 4)),
8482+
((3, 3), (6, 6)),
8483+
],
8484+
)
8485+
def test_topi_resize2d_int_div_optimization_fires_without_scale_override(input_hw, output_hw):
8486+
"""scale_override=None + integer size ratio => int-div fast path fires.
8487+
8488+
This is the primary positive case for the optimization: when
8489+
`topi.image.resize2d` is called without an explicit `scales` override
8490+
(i.e. `scales=None`, as happens whenever the ONNX Resize node's output
8491+
shape is derived from a "sizes" input rather than a "scales" input) and
8492+
the output/input size ratio is a whole number, `_resize_2d` should
8493+
select the integer-division fast path (`can_convert_multiply_to_intdiv`
8494+
returns True and `scale_h`/`scale_w` are both None).
8495+
8496+
We verify this two ways:
8497+
1. The lowered TIR contains `T.Div` and not `T.floor`, proving the
8498+
fast path -- and not the float fallback -- was actually chosen.
8499+
2. The numeric output matches ONNX's nearest/asymmetric/floor
8500+
reference formula `in_x = floor(out_x * (in_size / out_size))`,
8501+
so the optimization is not merely "fast" but also correct.
8502+
"""
8503+
in_h, in_w = input_hw
8504+
out_h, out_w = output_hw
8505+
input_shape = (1, 1, in_h, in_w)
8506+
8507+
resized, prim_func = _build_topi_resize2d_nearest_asymmetric_prim_func(
8508+
input_shape, (out_h, out_w), scales=None
8509+
)
8510+
8511+
assert _fast_path_used(prim_func), (
8512+
"expected the integer-division fast path to fire when scale_override "
8513+
"is None and the size ratio is an integer"
8514+
)
8515+
8516+
input_np = np.arange(np.prod(input_shape), dtype="float32").reshape(input_shape)
8517+
actual = _run_prim_func(prim_func, input_np, tuple(int(s) for s in resized.shape))
8518+
8519+
# Reference: ONNX asymmetric + nearest_neighbor + floor rounding is
8520+
# `in_idx = floor(out_idx * in_size / out_size)`, clamped to
8521+
# [0, in_size - 1]. Since in_size / out_size is an integer ratio here,
8522+
# this is exactly `out_idx // (out_size // in_size)`.
8523+
def ref_index(out_size, in_size):
8524+
ratio = out_size // in_size
8525+
idx = np.arange(out_size) // ratio
8526+
return np.clip(idx, 0, in_size - 1)
8527+
8528+
y_idx = ref_index(out_h, in_h)
8529+
x_idx = ref_index(out_w, in_w)
8530+
expected = input_np[:, :, y_idx][:, :, :, x_idx]
8531+
8532+
np.testing.assert_array_equal(actual, expected)
8533+
8534+
8535+
@pytest.mark.parametrize(
8536+
"input_hw,output_hw,scale_override",
8537+
[
8538+
((2, 2), (4, 4), (2.4, 2.4)),
8539+
((3, 3), (6, 6), (2.5, 2.5)),
8540+
],
8541+
)
8542+
def test_topi_resize2d_int_div_optimization_disabled_by_scale_override(
8543+
input_hw, output_hw, scale_override
8544+
):
8545+
"""A non-integer scale_override disables the int-div fast path."""
8546+
in_h, in_w = input_hw
8547+
out_h, out_w = output_hw
8548+
input_shape = (1, 1, in_h, in_w)
8549+
8550+
resized, prim_func = _build_topi_resize2d_nearest_asymmetric_prim_func(
8551+
input_shape, (out_h, out_w), scales=scale_override
8552+
)
8553+
8554+
assert not _fast_path_used(prim_func), (
8555+
"expected the integer-division fast path to be disabled when an "
8556+
"explicit (non-integer) scale_override is supplied"
8557+
)
8558+
8559+
input_np = np.arange(np.prod(input_shape), dtype="float32").reshape(input_shape)
8560+
actual = _run_prim_func(prim_func, input_np, tuple(int(s) for s in resized.shape))
8561+
8562+
epsilon = 1e-5
8563+
8564+
def ref_index(out_size, in_size, scale):
8565+
idx = np.floor(np.arange(out_size) / scale + epsilon).astype(np.int64)
8566+
return np.clip(idx, 0, in_size - 1)
8567+
8568+
y_idx = ref_index(out_h, in_h, scale_override[0])
8569+
x_idx = ref_index(out_w, in_w, scale_override[1])
8570+
expected = input_np[:, :, y_idx][:, :, :, x_idx]
8571+
8572+
np.testing.assert_array_equal(actual, expected)
8573+
8574+
ratio = out_h // in_h
8575+
naive_idx = np.clip(np.arange(out_h) // ratio, 0, in_h - 1)
8576+
assert not np.array_equal(naive_idx, y_idx), (
8577+
"test setup issue: chosen scale_override does not actually differ "
8578+
"from the naive integer-ratio index mapping"
8579+
)
8580+
8581+
8582+
def test_topi_resize2d_int_div_fast_path_matches_integer_scale_override():
8583+
"""An *integer* scale_override disables the fast path but agrees with it."""
8584+
input_shape = (1, 1, 3, 3)
8585+
output_hw = (6, 6)
8586+
input_np = np.arange(np.prod(input_shape), dtype="float32").reshape(input_shape)
8587+
8588+
fast_resized, fast_prim_func = _build_topi_resize2d_nearest_asymmetric_prim_func(
8589+
input_shape, output_hw, scales=None
8590+
)
8591+
assert _fast_path_used(fast_prim_func)
8592+
fast_out_shape = tuple(int(s) for s in fast_resized.shape)
8593+
fast_actual = _run_prim_func(fast_prim_func, input_np, fast_out_shape)
8594+
8595+
slow_resized, slow_prim_func = _build_topi_resize2d_nearest_asymmetric_prim_func(
8596+
input_shape, output_hw, scales=(2.0, 2.0)
8597+
)
8598+
assert not _fast_path_used(slow_prim_func)
8599+
slow_out_shape = tuple(int(s) for s in slow_resized.shape)
8600+
slow_actual = _run_prim_func(slow_prim_func, input_np, slow_out_shape)
8601+
8602+
np.testing.assert_array_equal(fast_actual, slow_actual)
8603+
8604+
84108605
def test_einsum():
84118606
eqn = "ij->i"
84128607
einsum_node = helper.make_node("Einsum", ["x"], ["y"], equation=eqn)

0 commit comments

Comments
 (0)