Skip to content

Commit 9fffe15

Browse files
authored
Fix stale dims and constants in downward permute propagation (#24739)
Summary: As PropagateViewCopyPermuteDownPass walks a permute down, it rewrites each node it crosses for the permute's input layout: the dims of a slice or reduction, and a binary op's constant input, which it permutes in place. When the fork at the end of the walk could not be split, the pass fell back to a pointwise region move planned from the permute's original position. That region stops at the first node it cannot include, such as a slice, a reduction or a backend op that is not pointwise, and left the crossed nodes from there on applying their rewritten dims or constants to the permuted tensor. For example, `permute(x, [0, 3, 1, 2]) -> clamp -> slice_copy(dim=1, 0, 4)` feeding both `permute(0, 2, 3, 1)` and `neg`, the pattern Arm's RewriteConvPass leaves around a channel split between convs, became `slice_copy(clamp(x), 2, 0, 4)`: for x of shape [1, 3, 5, 8] the outputs had shapes [1, 3, 4, 8] and [1, 8, 3, 4] instead of [1, 3, 5, 4] and [1, 4, 3, 5]. A keepdim sum in the same place made the pass raise while re-tracing, and a constant added after a crossed backend op that is not pointwise failed to broadcast or was broadcast along the wrong axis. Plan the region move only when the walk has not advanced. Otherwise the permute moves past the frontier, where every rewrite is valid, and the next iteration retries the fork from there. The test target now also depends on `//executorch/exir:lib`, for `to_edge`. Differential Revision: D130233379 Pull Request resolved: #24739
1 parent 6605290 commit 9fffe15

3 files changed

Lines changed: 102 additions & 1 deletion

File tree

‎backends/transforms/propagate_view_copy_permute_pass.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,13 @@ def _maybe_split_fork(
11981198
self._apply_fork_split(node, frontier, producer, branch_splits)
11991199
return True
12001200

1201+
if frontier is not node:
1202+
# The walk already rewrote the operations up to ``frontier`` for
1203+
# the permute's input, and a region planned from ``node`` may stop
1204+
# before some of them. Move ``node`` past ``frontier`` instead and
1205+
# retry the fork from there on the next iteration.
1206+
return False
1207+
12011208
# If the branches use each other's results, try moving the dimension
12021209
# reorder past the connected group of operations as one change.
12031210
region_move = self._plan_pointwise_region_move(node)

‎backends/transforms/targets.bzl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ def define_common_targets():
405405
deps = [
406406
"//caffe2:torch",
407407
":propagate_view_copy_permute_pass",
408+
"//executorch/exir:lib",
408409
"//executorch/exir/dialects:lib",
409410
"fbsource//third-party/pypi/pytest:pytest",
410411
],

‎backends/transforms/test/test_propagate_view_copy_permute_pass.py‎

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# This source code is licensed under the BSD-style license found in the
66
# LICENSE file in the root directory of this source tree.
77

8-
from collections.abc import Sequence
8+
from collections.abc import Callable, Sequence
99

1010
import pytest
1111
import sympy # type: ignore[import-untyped]
@@ -15,16 +15,19 @@
1515
PropagateViewCopyPermuteDownPass,
1616
PropagateViewCopyPermuteUpPass,
1717
)
18+
from executorch.exir import to_edge
1819
from executorch.exir.dialects._ops import ops as exir_ops
1920
from torch._subclasses.fake_tensor import FakeTensorMode
2021
from torch.fx.experimental.symbolic_shapes import ShapeEnv
2122

2223
PERMUTE = exir_ops.edge.aten.permute_copy.default
2324
ABS = exir_ops.edge.aten.abs.default
25+
EXP = exir_ops.edge.aten.exp.default
2426
NEG = exir_ops.edge.aten.neg.default
2527
SIGMOID = exir_ops.edge.aten.sigmoid.default
2628
ADD = exir_ops.edge.aten.add.Tensor
2729
SUM = exir_ops.edge.aten.sum.dim_IntList
30+
SLICE = exir_ops.edge.aten.slice_copy.Tensor
2831
WHERE = exir_ops.edge.aten.where.self
2932

3033

@@ -301,6 +304,96 @@ def test_downward_permute_crosses_reduction_over_all_dims() -> None:
301304
torch.testing.assert_close(result(values), expected)
302305

303306

307+
@pytest.mark.parametrize(
308+
("target", "args", "expected_dims"),
309+
[(SLICE, (1, 0, 4), 3), (SUM, ([1], True), [3])],
310+
ids=["slice", "keepdim_sum"],
311+
)
312+
def test_downward_permute_crosses_arg_update_op_before_unsplittable_fork(
313+
target: Callable[..., object],
314+
args: tuple[object, ...],
315+
expected_dims: int | list[int],
316+
) -> None:
317+
"""Keep a crossed op's dims valid when the fork after it cannot be split.
318+
319+
Here ``P`` means permute:
320+
321+
P(x) -> abs -> op --+-> P -> output
322+
+-> neg -> output
323+
324+
Crossing ``op`` rewrites its dims for the unpermuted input, so the first
325+
permute must not then move past ``abs`` alone.
326+
327+
"""
328+
values = torch.randn(1, 3, 5, 8)
329+
graph = torch.fx.Graph()
330+
x = graph.placeholder("x")
331+
x.meta["val"] = values
332+
permute = graph.call_function(PERMUTE, args=(x, [0, 3, 1, 2]))
333+
permute.meta["val"] = values.permute(0, 3, 1, 2)
334+
absolute = graph.call_function(ABS, args=(permute,))
335+
absolute.meta["val"] = permute.meta["val"].abs()
336+
crossed = graph.call_function(target, args=(absolute, *args))
337+
crossed.meta["val"] = target(absolute.meta["val"], *args)
338+
restored = graph.call_function(PERMUTE, args=(crossed, [0, 2, 3, 1]))
339+
restored.meta["val"] = crossed.meta["val"].permute(0, 2, 3, 1)
340+
neg = graph.call_function(NEG, args=(crossed,))
341+
neg.meta["val"] = -crossed.meta["val"]
342+
graph.output((restored, neg))
343+
graph_module = torch.fx.GraphModule(torch.nn.Module(), graph)
344+
expected = graph_module(values)
345+
346+
result = PropagateViewCopyPermuteDownPass().call(graph_module).graph_module
347+
moved_op = next(node for node in result.graph.nodes if node.target == target)
348+
349+
assert moved_op.args[1] == expected_dims
350+
torch.testing.assert_close(result(values), expected)
351+
352+
353+
class _BackendElementwiseExpPass(PropagateViewCopyPermuteDownPass):
354+
def is_elementwise(self, node: torch.fx.Node) -> bool:
355+
return node.target != EXP and super().is_elementwise(node)
356+
357+
def is_backend_elementwise(self, node: torch.fx.Node) -> bool:
358+
return node.target == EXP
359+
360+
361+
class _BiasAfterExp(torch.nn.Module):
362+
def __init__(self) -> None:
363+
super().__init__()
364+
self.bias = torch.nn.Parameter(torch.randn(1, 8, 1, 1))
365+
366+
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
367+
y = torch.exp(x.permute(0, 3, 1, 2).abs()) + self.bias
368+
return y.permute(0, 2, 3, 1), -y
369+
370+
371+
def test_downward_permute_crosses_constant_before_unsplittable_fork() -> None:
372+
"""Keep a crossed constant valid when the fork after it cannot be split.
373+
374+
Here ``P`` means permute and ``exp`` stands in for a backend op that the
375+
pass may cross but not include in a pointwise region:
376+
377+
P(x) -> abs -> exp -> add(bias) --+-> P -> output
378+
+-> neg -> output
379+
380+
Crossing ``add`` permutes ``bias`` for the unpermuted input, so the first
381+
permute must not then move past ``abs`` alone.
382+
383+
"""
384+
values = torch.randn(1, 3, 5, 8)
385+
model = _BiasAfterExp()
386+
expected = model(values)
387+
exported_program = to_edge(torch.export.export(model, (values,))).exported_program()
388+
389+
result = _BackendElementwiseExpPass(exported_program=exported_program).call(
390+
exported_program.graph_module
391+
)
392+
bias = exported_program.state_dict["bias"]
393+
394+
torch.testing.assert_close(result.graph_module(bias, values), expected)
395+
396+
304397
def test_upward_permute_crosses_rank_expanding_binary_after_reduction() -> None:
305398
values = torch.arange(6, dtype=torch.float32).reshape(2, 3)
306399
bias_value = torch.ones((1, 1, 1, 1), dtype=torch.float32)

0 commit comments

Comments
 (0)