Skip to content

Commit 1498f7a

Browse files
Arm backend: Relax SymbolicShapeSupport-checks (pytorch#22826)
Allow TOSA support checks to distinguish between symbolic tensor metadata and symbolic shape materialization. Without the TOSA shape extension, symbolic tensor input/output shapes can still be accepted for ops whose lowering does not need materialized shape values. Reject symbolic SymInt arguments and the known shape- dependent edge cases instead. For targets that require fully resolved tensor shapes, add a separate partitioner-level check that rejects nodes with unresolved tensor input or output shapes. cc @digantdesai @freddan80 @per @zingo @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Oscar Andersson <oscar.andersson@arm.com>
1 parent 500849b commit 1498f7a

5 files changed

Lines changed: 381 additions & 54 deletions

File tree

‎backends/arm/ethosu/partitioner.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def __init__(
3737
self.compile_spec = compile_spec
3838
self.additional_checks = additional_checks
3939
self.tosa_spec = compile_spec.tosa_spec
40+
self._requires_resolved_tensor_shapes = True
4041
self._decomposable_resize_support = DecomposableResizeSupported(self.tosa_spec)
4142
self._custom_partition_ops: set[OpOverload] = set()
4243
self.intermediate_path = compile_spec._get_intermediate_path()

‎backends/arm/operator_support/tosa_supported_operators.py‎

Lines changed: 147 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,10 @@ def tosa_support_factory(
513513
if additional_positive_checks:
514514
positive_checks.extend(additional_positive_checks)
515515
negative_checks = _negative_checks(
516-
tosa_spec, exported_program, reporter, additional_checks
516+
tosa_spec,
517+
exported_program,
518+
reporter,
519+
additional_checks,
517520
)
518521

519522
# An op must be accepted by at least one postitive check, and not rejected by any
@@ -530,8 +533,44 @@ def tosa_support_factory(
530533
return any_chain(*additional_positive_overrides or (), default_checks)
531534

532535

536+
def _has_symbolic_shape(node: fx.Node) -> bool:
537+
val = node.meta.get("val")
538+
vals = val if isinstance(val, (list, tuple)) else (val,)
539+
for node_val in vals:
540+
if isinstance(node_val, torch.SymInt):
541+
return True
542+
543+
shape = getattr(node_val, "shape", None)
544+
if shape is not None and any(isinstance(dim, torch.SymInt) for dim in shape):
545+
return True
546+
547+
return False
548+
549+
533550
class SymbolicShapeSupportCheck(OperatorSupportBase):
534-
"""Reject symbolic tensor shapes for specs without the shape extension."""
551+
"""Reject symbolic shape constructs that require the TOSA shape
552+
extension.
553+
"""
554+
555+
_SYMBOLIC_SPATIAL_DIM_TARGETS = (
556+
exir_ops.edge.aten.convolution.default,
557+
exir_ops.edge.aten.avg_pool2d.default,
558+
exir_ops.edge.aten.max_pool2d.default,
559+
exir_ops.edge.aten.max_pool2d_with_indices.default,
560+
exir_ops.edge.aten._adaptive_avg_pool2d.default,
561+
torch.ops.aten.conv_transpose2d.input,
562+
)
563+
_SYMBOLIC_MEAN_TARGETS = (
564+
exir_ops.edge.aten.mean.dim,
565+
exir_ops.edge.aten.mean.default,
566+
)
567+
_SYMBOLIC_VIEW_SHAPE_TARGETS = (
568+
exir_ops.edge.aten.squeeze_copy.dim,
569+
exir_ops.edge.aten.squeeze_copy.dims,
570+
exir_ops.edge.aten.unsqueeze_copy.default,
571+
)
572+
_SYMBOLIC_PAD_TARGETS = (exir_ops.edge.aten.constant_pad_nd.default,)
573+
_SYMBOLIC_SLICE_TARGETS = (exir_ops.edge.aten.slice_copy.Tensor,)
535574

536575
def __init__(self, reporter: WhyNoPartitionReporter):
537576
"""Initialize the check with a reporter.
@@ -543,88 +582,147 @@ def __init__(self, reporter: WhyNoPartitionReporter):
543582
self.reporter = reporter
544583

545584
@staticmethod
546-
def _has_symbolic_shape(node: fx.Node) -> bool:
547-
val = node.meta.get("val")
548-
vals = val if isinstance(val, (list, tuple)) else (val,)
549-
for node_val in vals:
550-
if isinstance(node_val, torch.SymInt):
585+
def _has_symbolic_shape_argument(arg: object) -> bool:
586+
if isinstance(arg, torch.SymInt):
587+
return True
588+
589+
if isinstance(arg, fx.Node):
590+
return SymbolicShapeSupportCheck._has_symbolic_shape_argument(
591+
arg.meta.get("val")
592+
)
593+
594+
if isinstance(arg, (list, tuple)):
595+
return any(
596+
SymbolicShapeSupportCheck._has_symbolic_shape_argument(item)
597+
for item in arg
598+
)
599+
600+
return False
601+
602+
@staticmethod
603+
def _get_mean_reduction_dims(node: fx.Node, input_rank: int) -> tuple[int, ...]:
604+
if node.target == exir_ops.edge.aten.mean.default:
605+
return tuple(range(input_rank))
606+
607+
dims = node.kwargs.get("dim", node.args[1] if len(node.args) > 1 else None)
608+
if dims is None:
609+
return tuple(range(input_rank))
610+
if isinstance(dims, int):
611+
return (dims % input_rank,)
612+
return tuple(dim % input_rank for dim in typing.cast(Sequence[int], dims))
613+
614+
def _has_unsupported_symbolic_tensor_shape(self, node: fx.Node) -> bool:
615+
if node.target not in (
616+
*self._SYMBOLIC_SPATIAL_DIM_TARGETS,
617+
*self._SYMBOLIC_MEAN_TARGETS,
618+
*self._SYMBOLIC_VIEW_SHAPE_TARGETS,
619+
*self._SYMBOLIC_PAD_TARGETS,
620+
*self._SYMBOLIC_SLICE_TARGETS,
621+
):
622+
return False
623+
if not node.all_input_nodes:
624+
return False
625+
626+
input_node = node.all_input_nodes[0]
627+
input_fake_tensor = get_first_fake_tensor(input_node)
628+
if not any(isinstance(s, torch.SymInt) for s in input_fake_tensor.shape):
629+
return False
630+
631+
if node.target in self._SYMBOLIC_SPATIAL_DIM_TARGETS:
632+
if any(isinstance(s, torch.SymInt) for s in input_fake_tensor.shape[2:]):
633+
self.reporter.report_reject(node, "Symbolic spatial dims unsupported")
551634
return True
552635

553-
shape = getattr(node_val, "shape", None)
554-
if shape is not None and any(
555-
isinstance(dim, torch.SymInt) for dim in shape
636+
if node.target in self._SYMBOLIC_MEAN_TARGETS:
637+
if any(
638+
isinstance(input_fake_tensor.shape[dim], torch.SymInt)
639+
for dim in self._get_mean_reduction_dims(
640+
node, len(input_fake_tensor.shape)
641+
)
556642
):
643+
self.reporter.report_reject(node, "Symbolic mean dims unsupported")
557644
return True
558645

646+
if node.target in self._SYMBOLIC_VIEW_SHAPE_TARGETS:
647+
self.reporter.report_reject(node, "Symbolic view dims unsupported")
648+
return True
649+
650+
if node.target in self._SYMBOLIC_PAD_TARGETS:
651+
self.reporter.report_reject(node, "Symbolic pad dims unsupported")
652+
return True
653+
654+
if node.target in self._SYMBOLIC_SLICE_TARGETS:
655+
self.reporter.report_reject(node, "Symbolic slices unsupported")
656+
return True
657+
559658
return False
560659

561-
def _partition_dynamic_upmsample_nearest2d(self, node: fx.Node) -> bool:
562-
"""Check if the node is an upsample_nearest2d with symbolic shapes.
660+
def is_node_supported(
661+
self, submodules: typing.Mapping[str, torch.nn.Module], node: fx.Node
662+
) -> bool:
663+
"""Return False for symbolic shape uses needing shape extension.
664+
665+
Without TOSA shape extension, symbolic input/output tensor dimensions
666+
are generally allowed because they are tensor metadata. Symbolic shape
667+
arguments and known shape-materialization edge cases are rejected.
563668
564669
Args:
670+
submodules (typing.Mapping[str, torch.nn.Module]): Exported modules.
565671
node (fx.Node): FX node to check.
566672
567673
Returns:
568-
bool: True if the node is an upsample_nearest2d with symbolic
569-
shapes; otherwise, False.
674+
bool: False if rejected by constraints; otherwise, True.
570675
571676
"""
572-
if node.target != exir_ops.edge.aten.upsample_nearest2d.vec:
573-
return False
677+
del submodules
678+
if node.op in ("placeholder", "output"):
679+
return True
680+
if node.op == "call_function" and node.target in (*Q_OPS, *DQ_OPS):
681+
return True
574682

575-
try:
576-
input_tensor = get_first_fake_tensor(node.all_input_nodes[0])
577-
output_tensor = get_first_fake_tensor(node)
578-
except Exception as exc:
683+
if self._has_symbolic_shape_argument(node.args):
579684
self.reporter.report_reject(
580685
node,
581-
f"upsample_nearest2d symbolic shapes need tensor metadata: {exc}",
686+
"Node has symbolic shape arguments, has the TOSA spec shape extension support?",
582687
)
583688
return False
584689

585-
input_size_xy = input_tensor.shape[2:4]
586-
output_size_xy = output_tensor.shape[2:4]
587-
if len(input_size_xy) != 2 or len(output_size_xy) != 2:
588-
self.reporter.report_reject(
589-
node, "upsample_nearest2d expects 2D spatial input/output."
590-
)
690+
if self._has_unsupported_symbolic_tensor_shape(node):
591691
return False
592692

593693
return True
594694

595-
def is_node_supported(
596-
self, submodules: typing.Mapping[str, torch.nn.Module], node: fx.Node
597-
) -> bool:
598-
"""Return False for nodes with symbolic tensor input or output shapes.
599695

600-
Dynamic shapes require the TOSA shape extension. Reject nodes with
601-
symbolic tensor dimensions before partitioning when the active spec
602-
does not enable that extension.
696+
class CheckResolvedTensorShapes(OperatorSupportBase):
697+
"""Reject nodes with unresolved tensor input or output shapes."""
603698

604-
Args:
605-
submodules (typing.Mapping[str, torch.nn.Module]): Exported modules.
606-
node (fx.Node): FX node to check.
699+
def __init__(self, reporter: WhyNoPartitionReporter):
700+
"""Initialize the check with a reporter.
607701
608-
Returns:
609-
bool: False if rejected by constraints; otherwise, True.
702+
Args:
703+
reporter (WhyNoPartitionReporter): Reporter for rejection reasons.
610704
611705
"""
706+
self.reporter = reporter
707+
708+
def is_node_supported(
709+
self, submodules: typing.Mapping[str, torch.nn.Module], node: fx.Node
710+
) -> bool:
711+
"""Return False when the node depends on unresolved tensor shapes."""
712+
del submodules
612713
if node.op in ("placeholder", "output"):
613714
return True
614715
if node.op == "call_function" and node.target in (*Q_OPS, *DQ_OPS):
615716
return True
616717

617-
if self._has_symbolic_shape(node) or any(
618-
self._has_symbolic_shape(input_node) for input_node in node.all_input_nodes
718+
if _has_symbolic_shape(node) or any(
719+
_has_symbolic_shape(input_node) for input_node in node.all_input_nodes
619720
):
620-
if node.target == exir_ops.edge.aten.upsample_nearest2d.vec:
621-
return self._partition_dynamic_upmsample_nearest2d(node)
622-
else:
623-
self.reporter.report_reject(
624-
node,
625-
"Node has symbolic shape, has the TOSA spec shape extension support?",
626-
)
627-
return False
721+
self.reporter.report_reject(
722+
node,
723+
"Node has unresolved tensor shapes, which are not supported by this target.",
724+
)
725+
return False
628726

629727
return True
630728

0 commit comments

Comments
 (0)