Skip to content

Commit 28a7fac

Browse files
authored
Fix IndexError when a squeeze view is reached upstream (#21620)
Differential Revision: D115002472 Pull Request resolved: #21620
1 parent 82fcf09 commit 28a7fac

2 files changed

Lines changed: 143 additions & 36 deletions

File tree

backends/transforms/remove_permutes_around_elementwise_ops.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@ def _adapt_permute_across_view(
144144
Adapts from input-rank to output-rank space (downstream direction).
145145
Returns the adjusted permutation, or None if not possible.
146146
"""
147+
inp = node.args[0]
148+
assert isinstance(inp, torch.fx.Node)
149+
inp_val = inp.meta.get("val")
150+
if inp_val is None:
151+
return None
152+
in_shape = inp_val.shape
153+
# ``permute`` must live in the view's input-rank space. It does not when
154+
# the view is reached by upstream traversal, where the permutation is
155+
# expressed at the view's output rank; bail out so the caller drops the
156+
# subgraph instead of indexing out of range or silently mis-adapting.
157+
if len(permute) != len(in_shape):
158+
return None
159+
147160
# Handle explicit unsqueeze_copy(dim)
148161
if node.target in self._UNSQUEEZE_OPS:
149162
dim = cast(int, node.args[1])
@@ -176,9 +189,6 @@ def _adapt_permute_across_view(
176189
return new_perm
177190

178191
# Handle view_copy (squeeze/unsqueeze-like reshape)
179-
inp = node.args[0]
180-
assert isinstance(inp, torch.fx.Node)
181-
in_shape = inp.meta["val"].shape
182192
out_shape = node.meta["val"].shape
183193

184194
if len(out_shape) == len(in_shape) + 1:
@@ -294,6 +304,18 @@ def visit( # noqa: C901
294304
return True
295305
if node in processed_nodes or not self.is_node_permutable(node):
296306
return False
307+
# A permutable op can still change rank via broadcasting (e.g.
308+
# [8, 1] + [1, 1, 1] -> [1, 8, 1]), which would leave the node carrying
309+
# a permutation of the wrong rank. Downstream rewrites index the
310+
# permutation by dim (update_cat / update_mean_dim / update_slice_copy),
311+
# so bail out rather than mis-permute or index out of range.
312+
# Squeeze/unsqueeze views are exempt: they intentionally carry their
313+
# input-rank permutation and are rank-checked in
314+
# _adapt_permute_across_view.
315+
if not self._is_squeeze_unsqueeze_view(node):
316+
node_shape = getattr(node.meta.get("val"), "shape", None)
317+
if node_shape is not None and len(node_shape) != len(current_start_permute):
318+
return False
297319
subgraph.nodes.add(node)
298320
subgraph.node_end_permute[node] = current_end_permute
299321
subgraph.node_start_permute[node] = current_start_permute

backends/transforms/test/test_permute_optimization_passes.py

Lines changed: 118 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,39 +1034,6 @@ def test_permute_unsqueeze_copy_neg_dim_mul_squeeze_copy_permute(self) -> None:
10341034
"permute_unsqueeze_copy_neg_dim_mul_squeeze_copy_permute",
10351035
)
10361036

1037-
def test_unsqueeze_at_moved_position(self) -> None:
1038-
"""The permutation moves the unsqueeze position (P[index] != index), so
1039-
the adapted permutation must be built from P[index], not index."""
1040-
builder = GraphBuilder()
1041-
x_data = torch.randn(1, 8, 16)
1042-
x = builder.placeholder("x", x_data)
1043-
p1 = builder.call_operator(
1044-
op=exir_ops.edge.aten.permute_copy.default, args=(x, [0, 2, 1])
1045-
)
1046-
u = builder.call_operator(
1047-
op=exir_ops.edge.aten.unsqueeze_copy.default, args=(p1, 1)
1048-
)
1049-
mul = builder.call_operator(op=exir_ops.edge.aten.mul.Tensor, args=(u, u))
1050-
sq = builder.call_operator(
1051-
op=exir_ops.edge.aten.squeeze_copy.dim, args=(mul, 1)
1052-
)
1053-
p2 = builder.call_operator(
1054-
op=exir_ops.edge.aten.permute_copy.default, args=(sq, [0, 2, 1])
1055-
)
1056-
builder.output([p2])
1057-
original = builder.get_graph_module()
1058-
gm_before = copy.deepcopy(original)
1059-
1060-
p = RemovePermutesAroundElementwiseOps()
1061-
result = cast(PassResult, p(original))
1062-
self.assertTrue(result.modified)
1063-
self.assertEqual(
1064-
count_node(result.graph_module, exir_ops.edge.aten.permute_copy.default), 0
1065-
)
1066-
validate_numerics(
1067-
gm_before, result.graph_module, [x_data], "UnsqueezeAtMovedPosition"
1068-
)
1069-
10701037
def test_upstream_view_rank_mismatch_no_crash(self) -> None:
10711038
"""Regression test for IndexError when a squeeze/unsqueeze view_copy
10721039
is reached via upstream traversal with a permutation whose rank does
@@ -1170,6 +1137,124 @@ def test_permutation_sink_view_splitting_the_non_unit_dim(self) -> None:
11701137
"permutation_sink_view_splitting_the_non_unit_dim",
11711138
)
11721139

1140+
def test_upstream_squeeze_view_rank_mismatch_no_crash(self) -> None:
1141+
"""Regression test for IndexError when a squeeze view_copy is reached
1142+
via upstream traversal with a permutation at the view's output rank.
1143+
1144+
Graph:
1145+
y [8, 4, 1] x [4, 8]
1146+
| |
1147+
view_copy (squeeze 3D→2D) permute [1, 0]
1148+
[8, 4] [8, 4]
1149+
\\ /
1150+
---- add (2D) -----------
1151+
|
1152+
permute [1, 0]
1153+
|
1154+
output
1155+
1156+
`visit` reaches the view_copy from `add` with the 2D permutation
1157+
[1, 0], but the view's input is 3D, so the squeezed position (2) is
1158+
out of range for the permutation. Before the fix,
1159+
_adapt_permute_across_view crashed with IndexError: list index out
1160+
of range."""
1161+
builder = GraphBuilder()
1162+
x_data = torch.randn(4, 8)
1163+
y_data = torch.randn(8, 4, 1)
1164+
x = builder.placeholder("x", x_data)
1165+
y = builder.placeholder("y", y_data)
1166+
# Squeeze via view_copy: [8, 4, 1] → [8, 4]
1167+
view_sq = builder.call_operator(
1168+
op=exir_ops.edge.aten.view_copy.default, args=(y, [8, 4])
1169+
)
1170+
# Start permute: [4, 8] → [8, 4]
1171+
p1 = builder.call_operator(
1172+
op=exir_ops.edge.aten.permute_copy.default, args=(x, [1, 0])
1173+
)
1174+
add = builder.call_operator(
1175+
op=exir_ops.edge.aten.add.Tensor, args=(p1, view_sq)
1176+
)
1177+
# End permute: [8, 4] → [4, 8]
1178+
p2 = builder.call_operator(
1179+
op=exir_ops.edge.aten.permute_copy.default, args=(add, [1, 0])
1180+
)
1181+
builder.output([p2])
1182+
original = builder.get_graph_module()
1183+
gm_before = copy.deepcopy(original)
1184+
1185+
# Should not crash, and should skip the subgraph due to rank mismatch
1186+
p = RemovePermutesAroundElementwiseOps()
1187+
result = cast(PassResult, p(original))
1188+
self.assertFalse(result.modified)
1189+
self.assertEqual(
1190+
count_node(result.graph_module, exir_ops.edge.aten.permute_copy.default), 2
1191+
)
1192+
validate_numerics(
1193+
gm_before,
1194+
result.graph_module,
1195+
[x_data, y_data],
1196+
"upstream_squeeze_view_rank_mismatch_no_crash",
1197+
)
1198+
1199+
def test_broadcast_rank_increase_no_crash(self) -> None:
1200+
"""Regression test for IndexError when broadcasting raises a node's
1201+
rank above the rank of the permutation it is visited with.
1202+
1203+
Graph:
1204+
x [1, 8] full([1, 1, 1])
1205+
| |
1206+
permute [1, 0] |
1207+
[8, 1] |
1208+
\\ /
1209+
------ add ------------
1210+
[1, 8, 1] (rank 3)
1211+
|
1212+
slice_copy(dim=2)
1213+
|
1214+
view_copy -> [8] (permutation sink)
1215+
1216+
`add` is visited with the rank-2 permutation [1, 0] but broadcasts to
1217+
rank 3. The numel-1 `full` input and the sink `view_copy` both let
1218+
traversal terminate without ever meeting a rank-matched permute, so the
1219+
subgraph closed with a rank-2 permutation on rank-3 nodes. Before the
1220+
fix, update_slice_copy did `start_permute[2]` and raised
1221+
IndexError: list index out of range."""
1222+
builder = GraphBuilder()
1223+
x_data = torch.randn(1, 8)
1224+
x = builder.placeholder("x", x_data)
1225+
p1 = builder.call_operator(
1226+
op=exir_ops.edge.aten.permute_copy.default, args=(x, [1, 0])
1227+
)
1228+
ones = builder.call_operator(
1229+
op=exir_ops.edge.aten.full.default, args=([1, 1, 1], 1.0)
1230+
)
1231+
# Broadcast [8, 1] + [1, 1, 1] -> [1, 8, 1]: rank 3 under a rank 2 permute
1232+
add = builder.call_operator(op=exir_ops.edge.aten.add.Tensor, args=(p1, ones))
1233+
sl = builder.call_operator(
1234+
op=exir_ops.edge.aten.slice_copy.Tensor, args=(add, 2, 0, 1)
1235+
)
1236+
# Sink view: input [1, 8, 1] has a single non-unit dim
1237+
sink = builder.call_operator(
1238+
op=exir_ops.edge.aten.view_copy.default, args=(sl, [8])
1239+
)
1240+
builder.output([sink])
1241+
original = builder.get_graph_module()
1242+
gm_before = copy.deepcopy(original)
1243+
1244+
# Should not crash, and should skip the subgraph due to rank mismatch
1245+
p = RemovePermutesAroundElementwiseOps()
1246+
result = cast(PassResult, p(original))
1247+
self.assertFalse(result.modified)
1248+
self.assertEqual(
1249+
count_node(result.graph_module, exir_ops.edge.aten.permute_copy.default), 1
1250+
)
1251+
validate_numerics(
1252+
gm_before,
1253+
result.graph_module,
1254+
[x_data],
1255+
"broadcast_rank_increase_no_crash",
1256+
)
1257+
11731258

11741259
# ──────────────────────────────────────────────────────────────────────
11751260
# Tests for RemovePermutesAroundElementwiseOps

0 commit comments

Comments
 (0)