Skip to content

Commit eb0a0bf

Browse files
committed
Arm backend: block invalid bilinear RESIZE downscale at 1/16
TOSA bilinear RESIZE requires the downscale ratio to be strictly greater than 1/16. The Arm backend currently accepts the exact 1/16 boundary case and can emit RESIZE parameters that the TOSA reference model rejects. Move upsample operator gating into explicit Arm operator support checks. Keep nearest upsample explicitly supported there, and reject bilinear upsample cases whose computed TOSA RESIZE scale hits the invalid 1/16 boundary. Keep the corresponding validation in the fake TOSA RESIZE op so the dialect-level constraint is still enforced directly. Also add regressions for the dialect-level validation and the end-to-end Arm bilinear interpolate case with align_corners=False and scale_factor=1/16. Signed-off-by: Per Held <per.held@arm.com> Change-Id: I612fc7315fa4d1bd158e2f71bcaa493fcaf08c03
1 parent ad2f500 commit eb0a0bf

7 files changed

Lines changed: 146 additions & 6 deletions

File tree

backends/arm/operator_support/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@
2424
to_dim_order_copy_support,
2525
tosa_supported_operators,
2626
unfold_copy_support,
27+
upsample_support,
2728
where_support,
2829
)

backends/arm/operator_support/tosa_profile_supported_op_lists.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@
8787
exir_ops.edge.aten.select_copy.int,
8888
exir_ops.edge.aten.sub.Tensor,
8989
exir_ops.edge.aten.tanh.default,
90-
exir_ops.edge.aten.upsample_bilinear2d.vec,
91-
exir_ops.edge.aten.upsample_nearest2d.vec,
9290
exir_ops.edge.aten.view_copy.default,
9391
exir_ops.edge.aten.unsqueeze_copy.default,
9492
exir_ops.edge.aten.squeeze_copy.dims,
@@ -211,8 +209,6 @@
211209
exir_ops.edge.aten._log_softmax.default,
212210
exir_ops.edge.aten.sub.Tensor,
213211
exir_ops.edge.aten.tanh.default,
214-
exir_ops.edge.aten.upsample_bilinear2d.vec,
215-
exir_ops.edge.aten.upsample_nearest2d.vec,
216212
exir_ops.edge.aten.var.correction,
217213
exir_ops.edge.aten.var.dim,
218214
exir_ops.edge.aten.view_copy.default,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
"""Provide TOSA support checks for upsample operators."""
6+
7+
import torch.fx as fx
8+
from executorch.backends.arm._passes.arm_pass_utils import get_first_fake_tensor
9+
from executorch.backends.arm._passes.rewrite_upsample import RewriteUpsamplePass
10+
from executorch.backends.arm.common.type import ensure_type
11+
from executorch.backends.arm.operator_support.tosa_supported_operators import (
12+
register_tosa_support_check,
13+
SupportedTOSAOperatorCheck,
14+
)
15+
from executorch.backends.arm.tosa import TosaSpecification
16+
from executorch.exir.dialects._ops import ops as exir_ops
17+
18+
19+
@register_tosa_support_check
20+
class UpsampleNearest2dSupported(SupportedTOSAOperatorCheck):
21+
"""Provide the explicit TOSA support gate for nearest upsample."""
22+
23+
targets = [exir_ops.edge.aten.upsample_nearest2d.vec]
24+
25+
def is_node_tosa_supported(
26+
self, _node: fx.Node, _tosa_spec: TosaSpecification
27+
) -> bool: # type: ignore[override, misc]
28+
return True
29+
30+
31+
@register_tosa_support_check
32+
class UpsampleBilinear2dSupported(SupportedTOSAOperatorCheck):
33+
"""Reject bilinear upsample cases that cannot lower to a valid TOSA
34+
RESIZE.
35+
"""
36+
37+
targets = [exir_ops.edge.aten.upsample_bilinear2d.vec]
38+
39+
def is_node_tosa_supported(
40+
self, node: fx.Node, _tosa_spec: TosaSpecification
41+
) -> bool: # type: ignore[override, misc]
42+
input_node = ensure_type(fx.Node, node.args[0])
43+
align_corners = ensure_type(bool, node.args[2])
44+
input_size_yx = get_first_fake_tensor(input_node).shape[2:]
45+
output_size_yx = get_first_fake_tensor(node).shape[2:]
46+
47+
try:
48+
scale_y_n, scale_y_d, _, _ = RewriteUpsamplePass.get_resize_parameters_1d(
49+
input_size_yx[0], output_size_yx[0], align_corners
50+
)
51+
scale_x_n, scale_x_d, _, _ = RewriteUpsamplePass.get_resize_parameters_1d(
52+
input_size_yx[1], output_size_yx[1], align_corners
53+
)
54+
except RuntimeError as err:
55+
self.reporter.report_reject(node, str(err))
56+
return False
57+
58+
# get_resize_parameters_1d() returns the TOSA RESIZE scale fraction for
59+
# each spatial dimension. For align_corners=False, this is the effective
60+
# output_size / input_size ratio, so the 1/16 boundary is checked
61+
# directly in the same representation that RESIZE lowering will use.
62+
if scale_y_d >= 16 * scale_y_n or scale_x_d >= 16 * scale_x_n:
63+
self.reporter.report_reject(
64+
node,
65+
"Bilinear RESIZE downscale must be strictly greater than 1/16",
66+
)
67+
return False
68+
69+
return True
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
import executorch.backends.arm.tosa.dialect # noqa: F401
7+
8+
import pytest
9+
import torch
10+
11+
from executorch.backends.arm.tosa.dialect.lib import TosaValueError
12+
from executorch.backends.arm.tosa.specification import (
13+
TosaLoweringContext,
14+
TosaSpecification,
15+
)
16+
from executorch.exir.dialects._ops import ops as exir_ops
17+
from torch._subclasses.fake_tensor import FakeTensorMode
18+
19+
20+
def test_bilinear_resize_rejects_exact_one_sixteenth_downscale():
21+
with TosaLoweringContext(
22+
TosaSpecification.create_from_string("TOSA-1.0+INT")
23+
), FakeTensorMode() as mode:
24+
with pytest.raises(
25+
TosaValueError,
26+
match="Bilinear RESIZE downscale must be strictly greater than 1/16",
27+
):
28+
exir_ops.backend.tosa.RESIZE.default(
29+
mode.from_tensor(
30+
torch.randint(0, 10, (1, 3, 256, 448), dtype=torch.int8)
31+
),
32+
[2, 32, 2, 32],
33+
[15, 15],
34+
[-15, -15],
35+
resize_mode="bilinear",
36+
)

backends/arm/test/ops/test_upsample_bilinear2d.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,25 @@ def forward(self, x):
140140
return self.upsample(x)
141141

142142

143+
class InterpolateAlignCornersFalse(torch.nn.Module):
144+
def __init__(
145+
self,
146+
size: Optional[Tuple[int]],
147+
scale_factor: Optional[float | Tuple[float]],
148+
):
149+
super().__init__()
150+
self.upsample = lambda x: torch.nn.functional.interpolate(
151+
x,
152+
size=size,
153+
scale_factor=scale_factor,
154+
mode="bilinear",
155+
align_corners=False,
156+
)
157+
158+
def forward(self, x):
159+
return self.upsample(x)
160+
161+
143162
@common.parametrize(
144163
"test_data",
145164
test_data_suite_tosa | test_data_suite_tosa_bf16 | test_data_suite_tosa_fp16,
@@ -231,6 +250,17 @@ def test_upsample_bilinear2d_vec_tosa_FP_Interpolate(
231250
pipeline.run()
232251

233252

253+
def test_upsample_bilinear2d_vec_tosa_does_not_delegate_exact_one_sixteenth_downscale():
254+
pipeline = OpNotSupportedPipeline[input_t1](
255+
InterpolateAlignCornersFalse(size=None, scale_factor=1.0 / 16.0),
256+
(torch.randn(1, 3, 256, 448),),
257+
{exir_op: 1},
258+
n_expected_delegates=0,
259+
)
260+
261+
pipeline.run()
262+
263+
234264
@common.parametrize("test_data", test_data_suite_tosa)
235265
def test_upsample_bilinear2d_vec_tosa_INT_intropolate(
236266
test_data: torch.Tensor,

backends/arm/test/targets.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def define_arm_tests():
4747
"misc/test_compile_spec.py",
4848
# "misc/test_evaluate_model.py",
4949
"misc/test_pass_pipeline_config.py",
50+
"misc/test_tosa_dialect_resize.py",
5051
"misc/test_tosa_spec.py",
5152
"misc/test_bn_relu_folding_qat.py",
5253
"misc/test_custom_partition.py",

backends/arm/tosa/dialect/ops/resize.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,21 @@ def _get_output_dtype(
5050
return output_dtype
5151

5252

53-
def _validate_resize_parameters(scale, border):
53+
def _validate_resize_parameters(scale, border, resize_mode):
5454
def in_int16_range(values):
5555
return all((x >= -(2**15)) and (x <= 2**15 - 1) for x in values)
5656

5757
if not in_int16_range(scale):
5858
raise TosaValueError("scale is out of the int16 range", op="RESIZE")
5959
if not in_int16_range(border):
6060
raise TosaValueError("border is out of the int16 range", op="RESIZE")
61+
if resize_mode == "bilinear":
62+
scale_y_n, scale_y_d, scale_x_n, scale_x_d = scale
63+
if scale_y_d >= 16 * scale_y_n or scale_x_d >= 16 * scale_x_n:
64+
raise TosaValueError(
65+
"Bilinear RESIZE downscale must be strictly greater than 1/16",
66+
op="RESIZE",
67+
)
6168

6269

6370
@register_fake_tosa_op(
@@ -79,7 +86,7 @@ def RESIZE(
7986
f"Input tensor must be 4D, but got {x.dim()}D", op="RESIZE"
8087
)
8188
_validate_resize_mode(resize_mode)
82-
_validate_resize_parameters(scale, border)
89+
_validate_resize_parameters(scale, border, resize_mode)
8390
output_dtype = _get_output_dtype(x.dtype, tosa_spec, resize_mode)
8491

8592
input_shape = x.shape

0 commit comments

Comments
 (0)