Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions backends/arm/_passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@
)
from .decompose_tril_pass import DecomposeTrilPass # noqa
from .decompose_unfold_to_gather_pass import DecomposeUnfoldToGatherPass # noqa
from .decompose_unsupported_bilinear_resize_pass import ( # noqa
DecomposeUnsupportedBilinearResizePass,
)
from .decompose_var_pass import DecomposeVarPass # noqa
from .decompose_where_scalar_other_pass import DecomposeWhereScalarOtherPass # noqa
from .decorate_fp32_to_int32_casting_pass import DecorateFp32toInt32CastingPass # noqa
Expand Down
2 changes: 2 additions & 0 deletions backends/arm/_passes/arm_pass_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
DecomposeTOSAUnsupportedClampPass,
DecomposeTrilPass,
DecomposeUnfoldToGatherPass,
DecomposeUnsupportedBilinearResizePass,
DecomposeVarPass,
DecomposeWhereScalarOtherPass,
DecorateFp32toInt32CastingPass,
Expand Down Expand Up @@ -600,6 +601,7 @@ def _tosa_pipeline(
DecomposeAsStridedCopyPass(),
DecomposeMaxPool2dPass(),
SizeAdjustInputPass(),
DecomposeUnsupportedBilinearResizePass(self.tosa_spec),
RewriteAdaptiveAvgPool2dPass(),
RewriteAvgPool2dPass(),
ComputeConstantOpsAOTPass(exported_program),
Expand Down
159 changes: 159 additions & 0 deletions backends/arm/_passes/decompose_unsupported_bilinear_resize_pass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Copyright 2026 Arm Limited and/or its affiliates.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

from typing import Set, Type

import torch
from executorch.backends.arm._passes.arm_pass import ArmPass
from executorch.backends.arm._passes.arm_pass_utils import (
create_node,
get_first_fake_tensor,
)
from executorch.backends.arm._passes.rewrite_avg_pool2d_pass import RewriteAvgPool2dPass
from executorch.backends.arm._passes.rewrite_upsample import RewriteUpsamplePass
from executorch.backends.arm.common.type import ensure_type
from executorch.backends.arm.tosa.resize_utils import (
is_exact_tosa_resize_boundary_downscale_case,
)
from executorch.backends.arm.tosa.specification import TosaSpecification
from executorch.exir.dialects._ops import ops as exir_ops
from executorch.exir.pass_base import ExportPass, PassResult
from torch.fx.passes.shape_prop import _extract_tensor_metadata

_EXACT_BOUNDARY_DOWNSCALE = 16
_EQUIVALENT_AVG_POOL_KERNEL = 2
_EQUIVALENT_AVG_POOL_OFFSET = _EXACT_BOUNDARY_DOWNSCALE // 2 - 1


def is_exact_tosa_boundary_bilinear_downscale(
node: torch.fx.Node,
tosa_spec: TosaSpecification,
) -> bool:
if (
node.op != "call_function"
or node.target != exir_ops.edge.aten.upsample_bilinear2d.vec
):
return False

input_node = ensure_type(torch.fx.Node, node.args[0])
align_corners = ensure_type(bool, node.args[2])
if align_corners:
return False
input_size_yx = get_first_fake_tensor(input_node).shape[2:]
output_size_yx = get_first_fake_tensor(node).shape[2:]

scale_y_n, scale_y_d, offset_y, border_y = (
RewriteUpsamplePass.get_resize_parameters_1d(
input_size_yx[0], output_size_yx[0], align_corners
)
)
scale_x_n, scale_x_d, offset_x, border_x = (
RewriteUpsamplePass.get_resize_parameters_1d(
input_size_yx[1], output_size_yx[1], align_corners
)
)
return is_exact_tosa_resize_boundary_downscale_case(
input_hw=input_size_yx,
output_hw=output_size_yx,
scale=[scale_y_n, scale_y_d, scale_x_n, scale_x_d],
offset=[offset_y, offset_x],
border=[border_y, border_x],
tosa_spec=tosa_spec,
)


class DecomposeUnsupportedBilinearResizePass(ArmPass):
targeted_ops = (exir_ops.edge.aten.upsample_bilinear2d.vec,)
_passes_required_after: Set[Type[ExportPass]] = {RewriteAvgPool2dPass}

def __init__(self, tosa_spec: TosaSpecification) -> None:
super().__init__()
self.tosa_spec = tosa_spec

@staticmethod
def _set_fake_tensor_meta(node: torch.fx.Node, value: torch.Tensor) -> None:
node.meta["val"] = value
node.meta["tensor_meta"] = _extract_tensor_metadata(value)

def call(self, graph_module):
graph = graph_module.graph
modified = False
for node in list(graph.nodes):
if not is_exact_tosa_boundary_bilinear_downscale(node, self.tosa_spec):
continue

input_node = ensure_type(torch.fx.Node, node.args[0])
input_val = input_node.meta["val"]
input_hw = [int(dim) for dim in get_first_fake_tensor(input_node).shape[2:]]

with graph.inserting_before(node):
cropped_h = create_node(
graph,
op_target=exir_ops.edge.aten.slice_copy.Tensor,
args=(
input_node,
2,
_EQUIVALENT_AVG_POOL_OFFSET,
input_hw[0] - _EQUIVALENT_AVG_POOL_OFFSET,
1,
),
from_node=node,
)
cropped_h_val = exir_ops.edge.aten.slice_copy.Tensor(
input_val,
2,
_EQUIVALENT_AVG_POOL_OFFSET,
input_hw[0] - _EQUIVALENT_AVG_POOL_OFFSET,
1,
)
self._set_fake_tensor_meta(cropped_h, cropped_h_val)

cropped_hw = create_node(
graph,
op_target=exir_ops.edge.aten.slice_copy.Tensor,
args=(
cropped_h,
3,
_EQUIVALENT_AVG_POOL_OFFSET,
input_hw[1] - _EQUIVALENT_AVG_POOL_OFFSET,
1,
),
from_node=node,
)
cropped_hw_val = exir_ops.edge.aten.slice_copy.Tensor(
cropped_h_val,
3,
_EQUIVALENT_AVG_POOL_OFFSET,
input_hw[1] - _EQUIVALENT_AVG_POOL_OFFSET,
1,
)
self._set_fake_tensor_meta(cropped_hw, cropped_hw_val)

pooled = create_node(
graph,
op_target=exir_ops.edge.aten.avg_pool2d.default,
args=(
cropped_hw,
[_EQUIVALENT_AVG_POOL_KERNEL, _EQUIVALENT_AVG_POOL_KERNEL],
[_EXACT_BOUNDARY_DOWNSCALE, _EXACT_BOUNDARY_DOWNSCALE],
),
from_node=node,
)
pooled_val = torch.nn.functional.avg_pool2d(
cropped_hw_val,
kernel_size=_EQUIVALENT_AVG_POOL_KERNEL,
stride=_EXACT_BOUNDARY_DOWNSCALE,
)
self._set_fake_tensor_meta(pooled, pooled_val)

node.replace_all_uses_with(pooled)
graph.erase_node(node)
modified = True

if modified:
graph.lint()
graph_module.recompile()
graph_module = super().call(graph_module).graph_module
return PassResult(graph_module, modified)
6 changes: 5 additions & 1 deletion backends/arm/ethosu/partitioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
from typing import final, Optional, Sequence

from executorch.backends.arm.ethosu import EthosUBackend, EthosUCompileSpec
from executorch.backends.arm.tosa.partitioner import TOSAPartitioner
from executorch.backends.arm.tosa.partitioner import (
DecomposableResizeSupported,
TOSAPartitioner,
)
from executorch.exir.backend.partitioner import DelegationSpec
from torch._ops import OpOverload
from torch.fx.passes.operator_support import OperatorSupportBase
Expand All @@ -33,5 +36,6 @@ def __init__(
)
self.additional_checks = additional_checks
self.tosa_spec = compile_spec.tosa_spec
self._decomposable_resize_support = DecomposableResizeSupported(self.tosa_spec)
self._custom_partition_ops: set[OpOverload] = set()
self.intermediate_path = compile_spec._get_intermediate_path()
5 changes: 5 additions & 0 deletions backends/arm/operator_support/tosa_supported_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ def tosa_support_factory(
exported_program: ExportedProgram,
reporter: WhyNoPartitionReporter,
additional_checks: Optional[Sequence[OperatorSupportBase]] = None,
additional_positive_checks: Optional[Sequence[OperatorSupportBase]] = None,
) -> OperatorSupportBase:
"""Create an OperatorSupport composite for a TOSA spec.

Expand All @@ -435,12 +436,16 @@ def tosa_support_factory(
reporter (WhyNoPartitionReporter): Reporter for rejections.
additional_checks (Optional[Sequence[OperatorSupportBase]]): Extra
negative checks to apply.
additional_positive_checks (Optional[Sequence[OperatorSupportBase]]):
Extra positive checks to add to the support list.

Returns:
OperatorSupportBase: Composite checker for the given spec.

"""
positive_checks = _positive_checks(tosa_spec, exported_program, reporter)
if additional_positive_checks:
positive_checks.extend(additional_positive_checks)
negative_checks = _negative_checks(
tosa_spec, exported_program, reporter, additional_checks
)
Expand Down
31 changes: 31 additions & 0 deletions backends/arm/test/misc/tosa_dialect/test_tosa_resize.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import torch

from executorch.backends.arm.tosa.dialect.lib import TosaValueError
from executorch.backends.arm.tosa.resize_utils import (
is_exact_tosa_resize_boundary_downscale_case,
)
from executorch.backends.arm.tosa.specification import (
TosaLoweringContext,
TosaSpecification,
Expand Down Expand Up @@ -53,6 +56,34 @@ def test_resize_rejects_exact_one_sixteenth_downscale(resize_mode: str):
)


def test_exact_boundary_resize_case_helper_only_matches_boundary_rule():
spec = TosaSpecification.create_from_string("TOSA-1.0+INT")
assert is_exact_tosa_resize_boundary_downscale_case(
input_hw=(256, 448),
output_hw=(16, 28),
scale=[2, 32, 2, 32],
offset=[15, 15],
border=[-15, -15],
tosa_spec=spec,
)
assert not is_exact_tosa_resize_boundary_downscale_case(
input_hw=(20000, 448),
output_hw=(16, 28),
scale=[2, 32, 2, 32],
offset=[15, 15],
border=[-15, -15],
tosa_spec=spec,
)
assert not is_exact_tosa_resize_boundary_downscale_case(
input_hw=(256, 448),
output_hw=(15, 28),
scale=[2, 32, 2, 32],
offset=[15, 15],
border=[-15, -15],
tosa_spec=spec,
)


def test_resize_rejects_scale_numerator_over_tosa_limit():
with TosaLoweringContext(
TosaSpecification.create_from_string("TOSA-1.0+INT")
Expand Down
21 changes: 17 additions & 4 deletions backends/arm/test/ops/test_upsample_bilinear2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,27 @@ def test_upsample_bilinear2d_vec_tosa_FP_Interpolate(
pipeline.run()


def test_upsample_bilinear2d_vec_tosa_does_not_delegate_exact_one_sixteenth_downscale():
pipeline = OpNotSupportedPipeline[input_t1](
def test_upsample_bilinear2d_vec_tosa_delegates_exact_one_sixteenth_downscale():
pipeline = TosaPipelineFP[input_t1](
InterpolateAlignCornersFalse(size=None, scale_factor=1.0 / 16.0),
(torch.randn(1, 3, 256, 448),),
{exir_op: 1},
n_expected_delegates=0,
aten_op,
exir_op=[],
)
pipeline.pop_stage(-1)
pipeline.run()


@common.SkipIfNoModelConverter
def test_upsample_bilinear2d_vec_vgf_delegates_exact_one_sixteenth_downscale():
pipeline = VgfPipeline[input_t1](
InterpolateAlignCornersFalse(size=None, scale_factor=1.0 / 16.0),
(torch.randn(1, 3, 256, 448),),
aten_op,
exir_op,
quantize=False,
)
pipeline.pop_stage(-1)
pipeline.run()


Expand Down
Loading
Loading