Skip to content

Commit 64c0e4e

Browse files
authored
Arm backend: Fix provenance metadata for Unsqueeze (#24664)
Fix provenance metadata for Unsqueeze cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Elena Zhelezina <elena.zhelezina@arm.com>
1 parent 724cef0 commit 64c0e4e

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

‎backends/arm/_passes/unsqueeze_before_repeat_pass.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2024-2025 Arm Limited and/or its affiliates.
1+
# Copyright 2024-2026 Arm Limited and/or its affiliates.
22
# All rights reserved.
33
#
44
# This source code is licensed under the BSD-style license found in the
@@ -56,6 +56,7 @@ def call(self, graph_module: torch.fx.GraphModule):
5656
graph_module.graph,
5757
exir_ops.edge.aten.view_copy.default,
5858
(node.all_input_nodes[0], new_shape),
59+
from_node=node,
5960
)
6061
node.replace_input_with(node.all_input_nodes[0], view_node)
6162
modified_graph = True

‎backends/arm/test/passes/test_unsqueeze_before_repeat_pass.py‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from executorch.backends.arm._passes import UnsqueezeBeforeRepeatPass
1010
from executorch.backends.arm.test import common
1111
from executorch.backends.arm.test.tester.test_pipeline import PassPipeline
12+
from executorch.exir import EdgeCompileConfig, to_edge
13+
from executorch.exir.dialects._ops import ops as exir_ops
1214

1315
pipeline_input_t = Tuple[torch.Tensor, ...]
1416
test_case_t = Tuple[
@@ -57,3 +59,80 @@ def test_unsqueeze_before_repeat_tosa_FP(test_data: test_case_t):
5759
)
5860
pipeline.pop_stage(-1) # Do not compare output
5961
pipeline.run()
62+
63+
64+
class _RepeatBlock(torch.nn.Module):
65+
"""Provide an actual named module boundary for exported FX provenance."""
66+
67+
def forward(self, x: torch.Tensor) -> torch.Tensor:
68+
return x.repeat(2, 1, 1, 1)
69+
70+
71+
class _NestedRepeat(torch.nn.Module):
72+
def __init__(self) -> None:
73+
super().__init__()
74+
self.repeat_block = _RepeatBlock()
75+
76+
def forward(self, x: torch.Tensor) -> torch.Tensor:
77+
return self.repeat_block(x)
78+
79+
80+
def test_unsqueeze_before_repeat_preserves_provenance() -> None:
81+
"""The new view retains repeat provenance, but gets its own
82+
shape/qparams.
83+
"""
84+
model = _NestedRepeat().eval()
85+
inputs = (torch.randn(2, 3, 4),)
86+
ep = to_edge(
87+
torch.export.export(model, inputs),
88+
compile_config=EdgeCompileConfig(_check_ir_validity=False),
89+
).exported_program()
90+
gm = ep.graph_module
91+
repeats = [
92+
n for n in gm.graph.nodes if n.target == exir_ops.edge.aten.repeat.default
93+
]
94+
assert len(repeats) == 1
95+
repeat = repeats[0]
96+
97+
original_stack = repeat.meta.get("nn_module_stack")
98+
assert original_stack, "Export must preserve Repeat module provenance"
99+
source_fn_stack = [("repeat_block", _RepeatBlock)]
100+
repeat.meta["source_fn_stack"] = source_fn_stack
101+
repeat.meta["stack_trace"] = "original repeat location"
102+
repeat.meta["debug_handle"] = 123456
103+
# These refer to Repeat's inputs/output, not to the new view. Inheriting
104+
# them would assign an incorrect quantization annotation to the view.
105+
repeat.meta["input_qparams"] = {0: "repeat-input-qparams"}
106+
repeat.meta["output_qparams"] = {0: "repeat-output-qparams"}
107+
108+
result = UnsqueezeBeforeRepeatPass()(gm)
109+
assert result is not None
110+
assert result.modified
111+
repeats_after = [
112+
n
113+
for n in result.graph_module.graph.nodes
114+
if n.target == exir_ops.edge.aten.repeat.default
115+
]
116+
assert len(repeats_after) == 1
117+
view = repeats_after[0].args[0]
118+
assert isinstance(view, torch.fx.Node)
119+
assert view.target == exir_ops.edge.aten.view_copy.default
120+
assert view.args[1] == [1, 2, 3, 4]
121+
122+
assert view.meta.get("nn_module_stack") == original_stack
123+
assert view.meta.get("source_fn_stack") == source_fn_stack
124+
assert view.meta.get("debug_handle") == 123456
125+
assert "original repeat location" in view.meta.get("stack_trace", "")
126+
assert not view.meta.get("input_qparams")
127+
assert not view.meta.get("output_qparams")
128+
assert tuple(view.meta["val"].shape) == (1, 2, 3, 4)
129+
assert tuple(repeats_after[0].meta["val"].shape) == (2, 2, 3, 4)
130+
131+
# Keep both semantics and idempotence unchanged.
132+
actual = result.graph_module(*inputs)
133+
if isinstance(actual, (tuple, list)):
134+
actual = actual[0]
135+
torch.testing.assert_close(actual, model(*inputs))
136+
repeated = UnsqueezeBeforeRepeatPass()(result.graph_module)
137+
assert repeated is not None
138+
assert not repeated.modified

0 commit comments

Comments
 (0)