Skip to content

Commit cc580bc

Browse files
authored
Arm backend: Decompose exact 1/16 bilinear downscale (#20960)
Arm backend: Decompose exact 1/16 bilinear downscale This is a decomposition of a bilinear resize corner case which needs a non-standard mapping to TOSA, just to capture more graphs fully. Signed-off-by: Rob Elliott [Robert.Elliott@arm.com](mailto:Robert.Elliott@arm.com) Change-Id: Ib620350be2cf92ff8f4e5406a5ae934f2dc9f2e8 cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @rascani Signed-off-by: Rob Elliott <Robert.Elliott@arm.com>
1 parent 27bd1bb commit cc580bc

11 files changed

Lines changed: 585 additions & 11 deletions

backends/arm/_passes/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@
101101
)
102102
from .decompose_tril_pass import DecomposeTrilPass # noqa
103103
from .decompose_unfold_to_gather_pass import DecomposeUnfoldToGatherPass # noqa
104+
from .decompose_unsupported_bilinear_resize_pass import ( # noqa
105+
DecomposeUnsupportedBilinearResizePass,
106+
)
104107
from .decompose_var_pass import DecomposeVarPass # noqa
105108
from .decompose_where_scalar_other_pass import DecomposeWhereScalarOtherPass # noqa
106109
from .decorate_fp32_to_int32_casting_pass import DecorateFp32toInt32CastingPass # noqa

backends/arm/_passes/arm_pass_manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
DecomposeTOSAUnsupportedClampPass,
100100
DecomposeTrilPass,
101101
DecomposeUnfoldToGatherPass,
102+
DecomposeUnsupportedBilinearResizePass,
102103
DecomposeVarPass,
103104
DecomposeWhereScalarOtherPass,
104105
DecorateFp32toInt32CastingPass,
@@ -600,6 +601,7 @@ def _tosa_pipeline(
600601
DecomposeAsStridedCopyPass(),
601602
DecomposeMaxPool2dPass(),
602603
SizeAdjustInputPass(),
604+
DecomposeUnsupportedBilinearResizePass(self.tosa_spec),
603605
RewriteAdaptiveAvgPool2dPass(),
604606
RewriteAvgPool2dPass(),
605607
ComputeConstantOpsAOTPass(exported_program),
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+
from typing import Set, Type
7+
8+
import torch
9+
from executorch.backends.arm._passes.arm_pass import ArmPass
10+
from executorch.backends.arm._passes.arm_pass_utils import (
11+
create_node,
12+
get_first_fake_tensor,
13+
)
14+
from executorch.backends.arm._passes.rewrite_avg_pool2d_pass import RewriteAvgPool2dPass
15+
from executorch.backends.arm._passes.rewrite_upsample import RewriteUpsamplePass
16+
from executorch.backends.arm.common.type import ensure_type
17+
from executorch.backends.arm.tosa.resize_utils import (
18+
is_exact_tosa_resize_boundary_downscale_case,
19+
)
20+
from executorch.backends.arm.tosa.specification import TosaSpecification
21+
from executorch.exir.dialects._ops import ops as exir_ops
22+
from executorch.exir.pass_base import ExportPass, PassResult
23+
from torch.fx.passes.shape_prop import _extract_tensor_metadata
24+
25+
_EXACT_BOUNDARY_DOWNSCALE = 16
26+
_EQUIVALENT_AVG_POOL_KERNEL = 2
27+
_EQUIVALENT_AVG_POOL_OFFSET = _EXACT_BOUNDARY_DOWNSCALE // 2 - 1
28+
29+
30+
def is_exact_tosa_boundary_bilinear_downscale(
31+
node: torch.fx.Node,
32+
tosa_spec: TosaSpecification,
33+
) -> bool:
34+
if (
35+
node.op != "call_function"
36+
or node.target != exir_ops.edge.aten.upsample_bilinear2d.vec
37+
):
38+
return False
39+
40+
input_node = ensure_type(torch.fx.Node, node.args[0])
41+
align_corners = ensure_type(bool, node.args[2])
42+
if align_corners:
43+
return False
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+
scale_y_n, scale_y_d, offset_y, border_y = (
48+
RewriteUpsamplePass.get_resize_parameters_1d(
49+
input_size_yx[0], output_size_yx[0], align_corners
50+
)
51+
)
52+
scale_x_n, scale_x_d, offset_x, border_x = (
53+
RewriteUpsamplePass.get_resize_parameters_1d(
54+
input_size_yx[1], output_size_yx[1], align_corners
55+
)
56+
)
57+
return is_exact_tosa_resize_boundary_downscale_case(
58+
input_hw=input_size_yx,
59+
output_hw=output_size_yx,
60+
scale=[scale_y_n, scale_y_d, scale_x_n, scale_x_d],
61+
offset=[offset_y, offset_x],
62+
border=[border_y, border_x],
63+
tosa_spec=tosa_spec,
64+
)
65+
66+
67+
class DecomposeUnsupportedBilinearResizePass(ArmPass):
68+
targeted_ops = (exir_ops.edge.aten.upsample_bilinear2d.vec,)
69+
_passes_required_after: Set[Type[ExportPass]] = {RewriteAvgPool2dPass}
70+
71+
def __init__(self, tosa_spec: TosaSpecification) -> None:
72+
super().__init__()
73+
self.tosa_spec = tosa_spec
74+
75+
@staticmethod
76+
def _set_fake_tensor_meta(node: torch.fx.Node, value: torch.Tensor) -> None:
77+
node.meta["val"] = value
78+
node.meta["tensor_meta"] = _extract_tensor_metadata(value)
79+
80+
def call(self, graph_module):
81+
graph = graph_module.graph
82+
modified = False
83+
for node in list(graph.nodes):
84+
if not is_exact_tosa_boundary_bilinear_downscale(node, self.tosa_spec):
85+
continue
86+
87+
input_node = ensure_type(torch.fx.Node, node.args[0])
88+
input_val = input_node.meta["val"]
89+
input_hw = [int(dim) for dim in get_first_fake_tensor(input_node).shape[2:]]
90+
91+
with graph.inserting_before(node):
92+
cropped_h = create_node(
93+
graph,
94+
op_target=exir_ops.edge.aten.slice_copy.Tensor,
95+
args=(
96+
input_node,
97+
2,
98+
_EQUIVALENT_AVG_POOL_OFFSET,
99+
input_hw[0] - _EQUIVALENT_AVG_POOL_OFFSET,
100+
1,
101+
),
102+
from_node=node,
103+
)
104+
cropped_h_val = exir_ops.edge.aten.slice_copy.Tensor(
105+
input_val,
106+
2,
107+
_EQUIVALENT_AVG_POOL_OFFSET,
108+
input_hw[0] - _EQUIVALENT_AVG_POOL_OFFSET,
109+
1,
110+
)
111+
self._set_fake_tensor_meta(cropped_h, cropped_h_val)
112+
113+
cropped_hw = create_node(
114+
graph,
115+
op_target=exir_ops.edge.aten.slice_copy.Tensor,
116+
args=(
117+
cropped_h,
118+
3,
119+
_EQUIVALENT_AVG_POOL_OFFSET,
120+
input_hw[1] - _EQUIVALENT_AVG_POOL_OFFSET,
121+
1,
122+
),
123+
from_node=node,
124+
)
125+
cropped_hw_val = exir_ops.edge.aten.slice_copy.Tensor(
126+
cropped_h_val,
127+
3,
128+
_EQUIVALENT_AVG_POOL_OFFSET,
129+
input_hw[1] - _EQUIVALENT_AVG_POOL_OFFSET,
130+
1,
131+
)
132+
self._set_fake_tensor_meta(cropped_hw, cropped_hw_val)
133+
134+
pooled = create_node(
135+
graph,
136+
op_target=exir_ops.edge.aten.avg_pool2d.default,
137+
args=(
138+
cropped_hw,
139+
[_EQUIVALENT_AVG_POOL_KERNEL, _EQUIVALENT_AVG_POOL_KERNEL],
140+
[_EXACT_BOUNDARY_DOWNSCALE, _EXACT_BOUNDARY_DOWNSCALE],
141+
),
142+
from_node=node,
143+
)
144+
pooled_val = torch.nn.functional.avg_pool2d(
145+
cropped_hw_val,
146+
kernel_size=_EQUIVALENT_AVG_POOL_KERNEL,
147+
stride=_EXACT_BOUNDARY_DOWNSCALE,
148+
)
149+
self._set_fake_tensor_meta(pooled, pooled_val)
150+
151+
node.replace_all_uses_with(pooled)
152+
graph.erase_node(node)
153+
modified = True
154+
155+
if modified:
156+
graph.lint()
157+
graph_module.recompile()
158+
graph_module = super().call(graph_module).graph_module
159+
return PassResult(graph_module, modified)

backends/arm/ethosu/partitioner.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
from typing import final, Optional, Sequence
77

88
from executorch.backends.arm.ethosu import EthosUBackend, EthosUCompileSpec
9-
from executorch.backends.arm.tosa.partitioner import TOSAPartitioner
9+
from executorch.backends.arm.tosa.partitioner import (
10+
DecomposableResizeSupported,
11+
TOSAPartitioner,
12+
)
1013
from executorch.exir.backend.partitioner import DelegationSpec
1114
from torch._ops import OpOverload
1215
from torch.fx.passes.operator_support import OperatorSupportBase
@@ -33,5 +36,6 @@ def __init__(
3336
)
3437
self.additional_checks = additional_checks
3538
self.tosa_spec = compile_spec.tosa_spec
39+
self._decomposable_resize_support = DecomposableResizeSupported(self.tosa_spec)
3640
self._custom_partition_ops: set[OpOverload] = set()
3741
self.intermediate_path = compile_spec._get_intermediate_path()

backends/arm/operator_support/tosa_supported_operators.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ def tosa_support_factory(
423423
exported_program: ExportedProgram,
424424
reporter: WhyNoPartitionReporter,
425425
additional_checks: Optional[Sequence[OperatorSupportBase]] = None,
426+
additional_positive_checks: Optional[Sequence[OperatorSupportBase]] = None,
426427
) -> OperatorSupportBase:
427428
"""Create an OperatorSupport composite for a TOSA spec.
428429
@@ -435,12 +436,16 @@ def tosa_support_factory(
435436
reporter (WhyNoPartitionReporter): Reporter for rejections.
436437
additional_checks (Optional[Sequence[OperatorSupportBase]]): Extra
437438
negative checks to apply.
439+
additional_positive_checks (Optional[Sequence[OperatorSupportBase]]):
440+
Extra positive checks to add to the support list.
438441
439442
Returns:
440443
OperatorSupportBase: Composite checker for the given spec.
441444
442445
"""
443446
positive_checks = _positive_checks(tosa_spec, exported_program, reporter)
447+
if additional_positive_checks:
448+
positive_checks.extend(additional_positive_checks)
444449
negative_checks = _negative_checks(
445450
tosa_spec, exported_program, reporter, additional_checks
446451
)

backends/arm/test/misc/tosa_dialect/test_tosa_resize.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
import torch
1010

1111
from executorch.backends.arm.tosa.dialect.lib import TosaValueError
12+
from executorch.backends.arm.tosa.resize_utils import (
13+
is_exact_tosa_resize_boundary_downscale_case,
14+
)
1215
from executorch.backends.arm.tosa.specification import (
1316
TosaLoweringContext,
1417
TosaSpecification,
@@ -53,6 +56,34 @@ def test_resize_rejects_exact_one_sixteenth_downscale(resize_mode: str):
5356
)
5457

5558

59+
def test_exact_boundary_resize_case_helper_only_matches_boundary_rule():
60+
spec = TosaSpecification.create_from_string("TOSA-1.0+INT")
61+
assert is_exact_tosa_resize_boundary_downscale_case(
62+
input_hw=(256, 448),
63+
output_hw=(16, 28),
64+
scale=[2, 32, 2, 32],
65+
offset=[15, 15],
66+
border=[-15, -15],
67+
tosa_spec=spec,
68+
)
69+
assert not is_exact_tosa_resize_boundary_downscale_case(
70+
input_hw=(20000, 448),
71+
output_hw=(16, 28),
72+
scale=[2, 32, 2, 32],
73+
offset=[15, 15],
74+
border=[-15, -15],
75+
tosa_spec=spec,
76+
)
77+
assert not is_exact_tosa_resize_boundary_downscale_case(
78+
input_hw=(256, 448),
79+
output_hw=(15, 28),
80+
scale=[2, 32, 2, 32],
81+
offset=[15, 15],
82+
border=[-15, -15],
83+
tosa_spec=spec,
84+
)
85+
86+
5687
def test_resize_rejects_scale_numerator_over_tosa_limit():
5788
with TosaLoweringContext(
5889
TosaSpecification.create_from_string("TOSA-1.0+INT")

backends/arm/test/ops/test_upsample_bilinear2d.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,14 +305,27 @@ def test_upsample_bilinear2d_vec_tosa_FP_Interpolate(
305305
pipeline.run()
306306

307307

308-
def test_upsample_bilinear2d_vec_tosa_does_not_delegate_exact_one_sixteenth_downscale():
309-
pipeline = OpNotSupportedPipeline[input_t1](
308+
def test_upsample_bilinear2d_vec_tosa_delegates_exact_one_sixteenth_downscale():
309+
pipeline = TosaPipelineFP[input_t1](
310310
InterpolateAlignCornersFalse(size=None, scale_factor=1.0 / 16.0),
311311
(torch.randn(1, 3, 256, 448),),
312-
{exir_op: 1},
313-
n_expected_delegates=0,
312+
aten_op,
313+
exir_op=[],
314314
)
315+
pipeline.pop_stage(-1)
316+
pipeline.run()
317+
315318

319+
@common.SkipIfNoModelConverter
320+
def test_upsample_bilinear2d_vec_vgf_delegates_exact_one_sixteenth_downscale():
321+
pipeline = VgfPipeline[input_t1](
322+
InterpolateAlignCornersFalse(size=None, scale_factor=1.0 / 16.0),
323+
(torch.randn(1, 3, 256, 448),),
324+
aten_op,
325+
exir_op,
326+
quantize=False,
327+
)
328+
pipeline.pop_stage(-1)
316329
pipeline.run()
317330

318331

0 commit comments

Comments
 (0)