Skip to content

Commit e410635

Browse files
authored
Merge branch 'main' into sym-ite-prim-op
2 parents bba0931 + 769b0cf commit e410635

41 files changed

Lines changed: 3854 additions & 803 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/qualcomm/new_op_development.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class MyOpVisitor(NodeVisitor):
6666
- Static params: `weight = get_parameter(self.get_node(node.args[1]), self.edge_program)`
6767
- Scalar params → `AddScalarParam`; Array params → `AddTensorParam`
6868
- Data types: axis/dims=`UINT_32`, epsilon=`FLOAT_32`, booleans=`BOOL_8`
69-
- Int64 index tensors: use `.to(torch.int32)` in builder + add op to `I64_IN_OPS` in `_passes/i64_to_i32.py` for CPU fallback safety (see `op_gather.py` pattern)
69+
- Int64 index tensors: If the op **requires** int64 for PyTorch tracing validation (like `gather`, `scatter`), add to `I64_IN_OPS` in `i64_to_i32.py` + `.to(torch.int32)` in builder. If the op **accepts** int32 (like `index_select`), produce int32 directly via `dtype=torch.int32` — no `I64_IN_OPS` entry needed.
7070

7171
## Step 5: Register Builder (`builders/__init__.py`)
7272

@@ -237,7 +237,7 @@ class DecomposeMyOp(ExportPass):
237237
- **Multi-output ops**: Use `wrapper_idx=i` + `getitem` skip op
238238
- **Negative dims**: QNN needs positive → `dim = dim % len(shape)`
239239
- **QCOM_AXIS_ORDER**: `LayoutTransform` permutes NCHW→NHWC; remap axis with `.index(dim)`. `get_tensor()` auto-permutes data.
240-
- **Int64 indices**: Add to `I64_IN_OPS` in `i64_to_i32.py` + `.to(torch.int32)` in builder (see `op_gather.py`)
240+
- **Int64 indices**: Only add to `I64_IN_OPS` if the op **requires** int64 at tracing time (e.g., `gather`, `scatter`). If the op accepts int32 (e.g., `index_select`), produce int32 directly in the decomposition pass. Check PyTorch docs for actual dtype requirements.
241241
- **Recompose passes**: Detect primitive sequences and replace with single native op. Ref: `recompose_pixel_unshuffle.py`
242242
- **`partition/common_defs.py`**: Remove op from `to_be_implemented_operator` when adding support
243243
- **HTP doc bugs**: If runtime fails but docs say supported → test on-device always.

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)