Skip to content

Commit 75e91e3

Browse files
committed
Copy returned views out of AOTI methods as contiguous tensors
ReplaceViewCopyWithViewPass turns a method's slice_copy, select_copy and split_copy into views so that AOTInductor compiles them natively. When the view is what the method returns, AOTInductor returns it with its base's strides, and the CUDA backend fails to copy it into the output tensor ("SlimTensor must be contiguous"); a policy that returns sample[..., :action_dim] cannot run. Clone a returned view with contiguous memory format. A strided view is then copied out by a kernel. Inductor still drops the clone of a view that is already contiguous, so a returned slice that starts at an offset, such as x[1:], still fails the runtime's storage offset check.
1 parent e6cfbf8 commit 75e91e3

2 files changed

Lines changed: 88 additions & 2 deletions

File tree

‎backends/aoti/passes/replace_view_copy_with_view.py‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
# because this should only be used in the AOTInductor backend, as it
1010
# has less restrictions on whether the tensor memory is densely packed,
1111

12-
from typing import Dict, Iterable
12+
import operator
13+
from typing import Dict, Iterable, List
1314

1415
import torch
1516
from executorch.exir.dialects._ops import ops
@@ -40,22 +41,47 @@ class ReplaceViewCopyWithViewPass(ExportPass):
4041
def call(self, graph_module: fx.GraphModule) -> PassResult:
4142
graph_changed = False
4243

43-
for node in graph_module.graph.nodes:
44+
for node in list(graph_module.graph.nodes):
4445
if node.op != "call_function" or node.target not in _VIEW_TARGETS:
4546
continue
4647

4748
if self._has_blocking_user(node, node.users.keys()):
4849
continue
4950

51+
edge = isinstance(node.target, EdgeOpOverload)
5052
node.target = _VIEW_TARGETS[node.target]
5153
graph_changed = True
54+
# Method outputs are contiguous tensors, so a returned view is
55+
# copied out as one.
56+
for returned in self._returned_values(node):
57+
self._clone_contiguous(graph_module.graph, returned, edge)
5258

5359
if graph_changed:
5460
graph_module.graph.lint()
5561
graph_module.recompile()
5662

5763
return PassResult(graph_module, graph_changed)
5864

65+
def _returned_values(self, node: fx.Node) -> List[fx.Node]:
66+
values = [node] if any(user.op == "output" for user in node.users) else []
67+
for user in node.users:
68+
if user.target is operator.getitem and any(
69+
getitem_user.op == "output" for getitem_user in user.users
70+
):
71+
values.append(user)
72+
return values
73+
74+
def _clone_contiguous(self, graph: fx.Graph, node: fx.Node, edge: bool) -> None:
75+
# The returned node becomes the clone, so the outputs keep their names.
76+
with graph.inserting_before(node):
77+
view = graph.call_function(node.target, node.args, node.kwargs)
78+
view.meta = dict(node.meta)
79+
node.target = (
80+
ops.edge.aten.clone.default if edge else torch.ops.aten.clone.default
81+
)
82+
node.args = (view,)
83+
node.kwargs = {"memory_format": torch.contiguous_format}
84+
5985
def _has_blocking_user(self, node: fx.Node, users: Iterable[fx.Node]) -> bool:
6086
for user in users:
6187
if self._is_mutating_user(node, user) or self._is_view_user(node, user):
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import unittest
8+
9+
import torch
10+
from executorch.backends.aoti.passes.replace_view_copy_with_view import (
11+
ReplaceViewCopyWithViewPass,
12+
)
13+
from executorch.exir import to_edge
14+
from executorch.exir.dialects._ops import ops as exir_ops
15+
from torch.export import export
16+
17+
18+
class TestReplaceViewCopyWithViewPass(unittest.TestCase):
19+
def _targets(self, module, inputs):
20+
graph_module = (
21+
to_edge(export(module, inputs, strict=True)).exported_program().graph_module
22+
)
23+
ReplaceViewCopyWithViewPass()(graph_module)
24+
return [
25+
node.target
26+
for node in graph_module.graph.nodes
27+
if node.op == "call_function"
28+
]
29+
30+
def test_intermediate_slice_becomes_view(self):
31+
class M(torch.nn.Module):
32+
def forward(self, x):
33+
return x[:, :6] * 2
34+
35+
targets = self._targets(M(), (torch.randn(4, 32),))
36+
self.assertIn(exir_ops.edge.aten.slice.Tensor, targets)
37+
self.assertNotIn(exir_ops.edge.aten.slice_copy.Tensor, targets)
38+
self.assertNotIn(exir_ops.edge.aten.clone.default, targets)
39+
40+
def test_returned_slice_is_cloned_contiguous(self):
41+
class M(torch.nn.Module):
42+
def forward(self, x):
43+
return (x * 2)[:, :6]
44+
45+
targets = self._targets(M(), (torch.randn(4, 32),))
46+
self.assertIn(exir_ops.edge.aten.slice.Tensor, targets)
47+
self.assertEqual(targets[-1], exir_ops.edge.aten.clone.default)
48+
49+
def test_returned_select_is_cloned_contiguous(self):
50+
class M(torch.nn.Module):
51+
def forward(self, x):
52+
return (x * 2)[:, 2]
53+
54+
targets = self._targets(M(), (torch.randn(4, 32),))
55+
self.assertIn(exir_ops.edge.aten.select.int, targets)
56+
self.assertEqual(targets[-1], exir_ops.edge.aten.clone.default)
57+
58+
59+
if __name__ == "__main__":
60+
unittest.main()

0 commit comments

Comments
 (0)