Skip to content

Commit dc8bd28

Browse files
committed
Arm backend: Add transpose propagation pass
Adds abstract pass PropagateViewCopyPermutePass and two implementations PropagateViewCopyPermuteUpPass and PropagateViewCopyPermutePass Adds a helper function to ViewMap to update the view-shape after a view-op swap + additional validity checks. Adds new InsertDataLayoutCastsPass under the propagation passes to not move permutes/views outside of their intended dtype cast The move exposed an issue where tosa ops require the same dtype on both inputs which is not guaranteed at that stage in the pipeline. This is fixed by adding CastInt64BuffersToInt32Pass before TOSA dialect rewrite. Signed-off-by: Adrian Lundell <adrian.lundell@arm.com> Change-Id: I31bc29540cebce9eb0ee3d5a3b2c016c19b15809
1 parent ae64df0 commit dc8bd28

9 files changed

Lines changed: 2500 additions & 28 deletions

backends/arm/_passes/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@
148148
)
149149
from .normalize_while_initial_args_pass import NormalizeWhileInitialArgsPass # noqa
150150
from .promote_bool_operands_pass import PromoteBoolOperandsPass # noqa
151+
from .propagate_view_copy_permute_pass import ( # noqa
152+
PropagateViewCopyPermuteDownPass,
153+
PropagateViewCopyPermuteUpPass,
154+
)
151155
from .remove_getitem_pass import RemoveGetItemPass # noqa
152156
from .remove_graph_asserts_pass import RemoveGraphAssertsPass # noqa
153157
from .remove_noop_pass import RemoveNoopPass # noqa

backends/arm/_passes/arm_pass_manager.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
AccumulateIndexPutPass,
1616
BroadcastArgsPass,
1717
CanonicalizeGatherPass,
18-
CanonicalizeViewCopyPermutePass,
1918
CastInt64BuffersToInt32Pass,
2019
CastToInt32Pass,
2120
ComputeConstantOpsAOTPass,
@@ -130,11 +129,12 @@
130129
NormalizeIndexPutNoneIndicesPass,
131130
NormalizeWhileInitialArgsPass,
132131
PromoteBoolOperandsPass,
132+
PropagateViewCopyPermuteDownPass,
133+
PropagateViewCopyPermuteUpPass,
133134
QuantizeClampArgumentsPass,
134135
RemoveGetItemPass,
135136
RemoveGraphAssertsPass,
136137
RemoveNoopPass,
137-
RemovePermutesAroundElementwiseTosaOps,
138138
ReplaceInfAndLimitValuesPass,
139139
ReplaceScalarWithTensorByProfilePass,
140140
RewriteAdaptiveAvgPool2dPass,
@@ -167,9 +167,6 @@
167167
TosaLoweringContext,
168168
TosaSpecification,
169169
)
170-
from executorch.backends.transforms.fuse_cascaded_transpose_or_permute_ops import (
171-
FuseCascadedTransposeOrPermuteOps,
172-
)
173170

174171
from executorch.exir import ExportedProgram
175172
from executorch.exir._program_utils import _get_updated_graph_signature
@@ -597,6 +594,7 @@ def _tosa_pipeline(
597594
RewriteAvgPool2dPass(),
598595
ComputeConstantOpsAOTPass(exported_program),
599596
FuseConstantArgsPass(exported_program),
597+
CastInt64BuffersToInt32Pass(exported_program),
600598
DecomposeSelectPass(),
601599
ConvertSqueezesToViewPass(),
602600
CastToInt32Pass(),
@@ -620,13 +618,13 @@ def _tosa_pipeline(
620618
RewriteMatmulPass(),
621619
RewritePadPass(),
622620
FuseViewCopyTransformPass(),
623-
RemovePermutesAroundElementwiseTosaOps(exported_program),
624-
CanonicalizeViewCopyPermutePass(),
625-
FuseCascadedTransposeOrPermuteOps(),
621+
PropagateViewCopyPermuteDownPass(self.compile_spec, exported_program),
622+
PropagateViewCopyPermuteUpPass(self.compile_spec, exported_program),
626623
RewriteHighRankSingletonPermutePass(),
627624
DecomposePermuteForU55Pass(),
628625
RewriteSlicePass(),
629626
InsertConstShapesPass(),
627+
InsertDataLayoutCastsPass(),
630628
]
631629
)
632630

backends/arm/_passes/dim_maps.py

Lines changed: 263 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,15 +306,28 @@ def map_dim(
306306
return None
307307

308308
groups = self._valid_groups()
309-
if not self._is_valid_reduction(normalized_dims, groups.source_axis_to_groups):
309+
if not self._is_valid_reduction_or_singleton(
310+
normalized_dims, groups.source_axis_to_groups
311+
):
310312
return None
311313

312-
target_dims = self._map_dims(
313-
normalized_dims,
314-
groups.source_axis_to_groups,
315-
groups.group_to_target_axes,
314+
source_to_target_axes = self.source_to_target_axes()
315+
target_dims = sorted(
316+
_dedupe(
317+
target_axis
318+
for source_dim in normalized_dims
319+
for target_axis in source_to_target_axes[source_dim]
320+
)
316321
)
317-
if not target_dims or not self._is_valid_reduction(
322+
if not target_dims or any(
323+
source_axis not in normalized_dims
324+
for target_axis in target_dims
325+
for source_axis in self.source_axes_for_target_axis(
326+
target_axis, source_to_target_axes
327+
)
328+
):
329+
return None
330+
if not self._is_valid_reduction_or_singleton(
318331
target_dims, groups.target_axis_to_groups
319332
):
320333
return None
@@ -432,6 +445,226 @@ def map_permutation_inverse(
432445
else None
433446
)
434447

448+
def remap_target_shape(self, source_shape: Sequence[_Dim]) -> list[_Dim] | None:
449+
if len(source_shape) != self.source_rank:
450+
return None
451+
452+
source_to_target_axes = self.source_to_target_axes()
453+
target_to_source_axes = [
454+
self.source_axes_for_target_axis(target_axis, source_to_target_axes)
455+
for target_axis in range(self.target_rank)
456+
]
457+
target_shape: list[_Dim] = [1] * self.target_rank
458+
459+
for source_axis, target_axes in enumerate(source_to_target_axes):
460+
updates = self._target_axis_updates_for_source_axis(
461+
source_shape,
462+
source_axis,
463+
target_axes,
464+
target_to_source_axes,
465+
)
466+
if updates is None:
467+
return None
468+
for target_axis, target_dim in updates:
469+
target_shape[target_axis] = target_dim
470+
471+
if not same_numel(source_shape, target_shape):
472+
return None
473+
if not self._preserves_source_axis_order(source_shape, source_to_target_axes):
474+
return None
475+
return target_shape
476+
477+
def _target_axis_updates_for_source_axis(
478+
self,
479+
source_shape: Sequence[_Dim],
480+
source_axis: int,
481+
target_axes: Sequence[int],
482+
target_to_source_axes: Sequence[Sequence[int]],
483+
) -> list[tuple[int, _Dim]] | None:
484+
if not target_axes:
485+
return []
486+
487+
if len(target_axes) == 1:
488+
target_axis = target_axes[0]
489+
source_axes = target_to_source_axes[target_axis]
490+
if source_axis != source_axes[0]:
491+
return []
492+
target_dim = numel(source_shape[source_axis] for source_axis in source_axes)
493+
return [(target_axis, target_dim)]
494+
495+
if any(
496+
len(target_to_source_axes[target_axis]) > 1 for target_axis in target_axes
497+
):
498+
return []
499+
500+
target_dims = [self.target_shape[target_axis] for target_axis in target_axes]
501+
if _dim_equals(source_shape[source_axis], self.source_shape[source_axis]):
502+
return list(zip(target_axes, target_dims))
503+
if _dim_equals(numel(target_dims), 1):
504+
return [(target_axes[0], source_shape[source_axis])]
505+
if _dim_equals(numel(target_dims), self.source_shape[source_axis]):
506+
return list(zip(target_axes, target_dims))
507+
return None
508+
509+
def remap_unit_slice(
510+
self,
511+
producer_shape: Sequence[_Dim],
512+
slice_dim: int,
513+
start: _Dim,
514+
end: _Dim,
515+
step: _Dim = 1,
516+
) -> tuple[list[_Dim], int, _Dim, _Dim] | None:
517+
"""Move a view before a unit slice.
518+
519+
Returns the new view shape and slice interval for:
520+
521+
view(slice(x, dim, start, end), self.target_shape)
522+
== slice(view(x, new_shape), new_dim, new_start, new_end)
523+
524+
This handles the case where a unit slice produces a singleton source
525+
axis that the view removes, so normal source-to-target dim mapping has
526+
no target axis for the slice dim.
527+
528+
"""
529+
if (
530+
len(producer_shape) != self.source_rank
531+
or not isinstance(slice_dim, int)
532+
or not isinstance(start, (int, torch.SymInt))
533+
or not isinstance(end, (int, torch.SymInt))
534+
or not isinstance(step, (int, torch.SymInt))
535+
):
536+
return None
537+
if not _dim_equals(step, 1) or not _dim_equals(end - start, 1):
538+
return None
539+
540+
try:
541+
slice_dim = _normalize_dim(slice_dim, self.source_rank)
542+
except AssertionError:
543+
return None
544+
545+
source_to_target_axes = self.source_to_target_axes()
546+
if source_to_target_axes[slice_dim]:
547+
return None
548+
549+
prev_target_axes = [
550+
target_axis
551+
for target_axes in source_to_target_axes[:slice_dim]
552+
for target_axis in target_axes
553+
]
554+
next_target_axes = [
555+
target_axis
556+
for target_axes in source_to_target_axes[slice_dim + 1 :]
557+
for target_axis in target_axes
558+
]
559+
fold_axes = [
560+
target_axes[0]
561+
for target_axes in source_to_target_axes[slice_dim + 1 :]
562+
if target_axes
563+
]
564+
fold_axes = [
565+
target_axis
566+
for target_axis in fold_axes
567+
if all(
568+
prev_target_axis <= target_axis for prev_target_axis in prev_target_axes
569+
)
570+
and all(
571+
target_axis <= next_target_axis for next_target_axis in next_target_axes
572+
)
573+
]
574+
if not fold_axes:
575+
return None
576+
577+
fold_axis = fold_axes[0]
578+
target_shape = list(self.target_shape)
579+
chunk = target_shape[fold_axis]
580+
target_shape[fold_axis] = chunk * producer_shape[slice_dim]
581+
return target_shape, fold_axis, start * chunk, end * chunk
582+
583+
def source_to_target_axes(self) -> list[list[int]]:
584+
groups = self._valid_groups()
585+
source_to_target_axes = [
586+
self._map_dims(
587+
[source_axis],
588+
groups.source_axis_to_groups,
589+
groups.group_to_target_axes,
590+
)
591+
for source_axis in range(self.source_rank)
592+
]
593+
594+
self._add_singleton_axes(source_to_target_axes)
595+
return source_to_target_axes
596+
597+
def map_source_dims_to_target_axes(
598+
self, source_dims: int | Sequence[int]
599+
) -> list[int] | None:
600+
try:
601+
normalized_dims = _normalize_dims(source_dims, self.source_rank)
602+
except AssertionError:
603+
return None
604+
source_to_target_axes = self.source_to_target_axes()
605+
return _dedupe(
606+
target_axis
607+
for source_dim in normalized_dims
608+
for target_axis in source_to_target_axes[source_dim]
609+
)
610+
611+
@staticmethod
612+
def source_axes_for_target_axis(
613+
target_axis: int, source_to_target_axes: Sequence[Sequence[int]]
614+
) -> list[int]:
615+
return [
616+
source_axis
617+
for source_axis, target_axes in enumerate(source_to_target_axes)
618+
if target_axis in target_axes
619+
]
620+
621+
def _add_singleton_axes(self, source_to_target_axes: list[list[int]]) -> None:
622+
mapped_source_axes = {
623+
source_axis
624+
for source_axis, target_axes in enumerate(source_to_target_axes)
625+
if target_axes
626+
}
627+
mapped_target_axes = {
628+
target_axis
629+
for target_axes in source_to_target_axes
630+
for target_axis in target_axes
631+
}
632+
source_singletons = [
633+
axis
634+
for axis, dim in enumerate(self.source_shape)
635+
if axis not in mapped_source_axes and _dim_equals(dim, 1)
636+
]
637+
target_singletons = [
638+
axis
639+
for axis, dim in enumerate(self.target_shape)
640+
if axis not in mapped_target_axes and _dim_equals(dim, 1)
641+
]
642+
643+
if len(source_singletons) == len(target_singletons):
644+
pairs = zip(source_singletons, target_singletons)
645+
elif len(source_singletons) == 1:
646+
pairs = zip(source_singletons * len(target_singletons), target_singletons)
647+
elif len(target_singletons) == 1:
648+
pairs = zip(source_singletons, target_singletons * len(source_singletons))
649+
else:
650+
pairs = zip(source_singletons, target_singletons)
651+
652+
for source_axis, target_axis in pairs:
653+
source_to_target_axes[source_axis].append(target_axis)
654+
655+
@staticmethod
656+
def _preserves_source_axis_order(
657+
source_shape: Sequence[_Dim],
658+
source_to_target_axes: Sequence[Sequence[int]],
659+
) -> bool:
660+
target_axes = [
661+
target_axis
662+
for source_axis, axes in enumerate(source_to_target_axes)
663+
if not _dim_equals(source_shape[source_axis], 1)
664+
for target_axis in axes
665+
]
666+
return target_axes == sorted(target_axes)
667+
435668
@staticmethod
436669
def _map_dims(
437670
source_dims: Iterable[int],
@@ -553,6 +786,30 @@ def _is_valid_reduction(
553786
group_to_axes[group].issubset(normalized_dims) for group in selected_groups
554787
)
555788

789+
@staticmethod
790+
def _is_valid_reduction_or_singleton(
791+
normalized_dims: Iterable[int],
792+
axis_to_groups: Sequence[Sequence[int]],
793+
) -> bool:
794+
"""Return whether dims cover complete groups, allowing singleton
795+
axes.
796+
"""
797+
normalized_dims = set(normalized_dims)
798+
if not normalized_dims:
799+
return False
800+
801+
group_to_axes: dict[int, set[int]] = defaultdict(set)
802+
selected_groups: set[int] = set()
803+
for axis, groups in enumerate(axis_to_groups):
804+
for group in groups:
805+
group_to_axes[group].add(axis)
806+
if axis in normalized_dims:
807+
selected_groups.add(group)
808+
809+
return all(
810+
group_to_axes[group].issubset(normalized_dims) for group in selected_groups
811+
)
812+
556813
@classmethod
557814
def _build_groups(
558815
cls, source_shape: Sequence[_Dim], target_shape: Sequence[_Dim]

backends/arm/_passes/insert_data_layout_casts_pass.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class InsertDataLayoutCastsPass(ArmOpTargetedPass):
3636
_concat_ops = {
3737
exir_ops.edge.aten.cat.default,
3838
exir_ops.edge.aten.concatenate.default,
39+
exir_ops.backend.tosa.CONCAT.default,
3940
}
4041
_single_input_ops = {
4142
exir_ops.edge.aten.constant_pad_nd.default,
@@ -44,6 +45,12 @@ class InsertDataLayoutCastsPass(ArmOpTargetedPass):
4445
exir_ops.edge.aten.permute_copy.default,
4546
exir_ops.edge.aten.slice_copy.Tensor,
4647
exir_ops.edge.aten.flip.default,
48+
exir_ops.backend.tosa.PAD.default,
49+
exir_ops.backend.tosa.RESHAPE.default,
50+
exir_ops.backend.tosa.TILE.default,
51+
exir_ops.backend.tosa.TRANSPOSE.default,
52+
exir_ops.backend.tosa.SLICE.default,
53+
exir_ops.backend.tosa.REVERSE.default,
4754
}
4855
target_ops = _concat_ops | _single_input_ops
4956

0 commit comments

Comments
 (0)