Skip to content

Commit 8812915

Browse files
authored
Fix create_constant_placeholder crashing when torch.fx renames the requested placeholder (pytorch#21542)
Fixes pytorch#14055 Fixes pytorch#21541 ## Problem Found while verifying pytorch#21489: the textbook CNN used there, one top-level `nn.Sequential` of conv, batchnorm, relu and maxpool, gets past the pooling check it used to crash on and now dies one pass later, in `FuseBatchNormPass`. `create_constant_placeholder` records the name it requested, not the name torch.fx assigned: ```python node = graph.create_node(op="placeholder", name=name, target=name) ... node_names = [n.name for n in graph.nodes if n.op == "placeholder"] node_index = node_names.index(name) # ValueError after any rename ... arg_spec = TensorArgument(name) ``` torch.fx renames the node whenever the requested name is not a valid identifier or collides with an existing node. Two plain models trigger this today. A top-level `nn.Sequential` names its parameters `0.weight`, so `FuseBatchNormPass` requests `0_weight_fused_bn`, fx assigns `_0_weight_fused_bn`, and lowering to XNNPACK dies with `ValueError: '0_weight_fused_bn' is not in list`. torchvision regnet under Vulkan hits the same lines through its hyphenated module names (pytorch#14055). The identical models behind a named attribute lower fine, which is why existing coverage never reached this path. Two further defects sit behind the crash. A placeholder's `target` is emitted verbatim as a function parameter name on recompile, so fixing only the lookup produces `def forward(self, 0_weight_fused_bn, ...)`, a SyntaxError. And the shared-weight dedup from pytorch#18031 matched on `target == name`, so after a rename it misses and creates a duplicate placeholder. ## Fix One name everywhere: `node.name`, `node.target`, the state_dict key and the graph signature all follow the name fx assigned. The requested name is kept in `node.meta` and used for the dedup, so the pytorch#18031 contract is preserved: a second request for the same name returns the existing node, now also when fx renamed it. Keying the state_dict by the assigned name inherits the fx namespace uniqueness guarantee, so a colliding request can never overwrite an existing parameter. For any requested name that is a valid identifier with no collision, the assigned name equals the requested name and behavior is unchanged. ## Effect Same script on either side of the change, executed on executor_runner built from this branch. maxdiff is deviation from eager. ``` BEFORE AFTER nn.Sequential(conv, bn, relu) ValueError at utils.py:151 delegated, maxdiff 4.0e-07 two-block Sequential CNN ValueError at utils.py:151 delegated, maxdiff 4.8e-07 same models behind an attribute lower and run unchanged, maxdiff <= 5.5e-07 ``` ## Testing Two helper tests in `test_create_delete_constant_placeholder.py`: a digit-leading request checks node, signature, state_dict and a recompile round trip; a dedup test checks that requesting the same name twice returns the existing node for a clean, a digit-leading and a hyphenated name. One pass test in `test_batch_norm_fusion.py` fuses a top-level Sequential end to end. `test_conv_bn` in `runtime/test/test_runtime_xnnpack.py`, skipped on this bug, is re-enabled (linux-gated, runs in CI). Verified failing-first by reverting only `backends/transforms/utils.py`: exactly the three new tests fail, all with `ValueError` at `utils.py:151`. ``` backends/transforms/test + backends/xnnpack/test/passes + runtime/test/test_runtime_xnnpack.py main : 165 passed, 57 skipped this PR : 168 passed, 57 skipped ``` lintrunner reports no issues on all changed files. The helper is shared by the Arm, Vulkan and XNNPACK backends. Callers passing valid unique names get identical behavior; renamed cases previously crashed, so no caller can depend on the old behavior. cc @GregoryComer @digantdesai @cbilgin @JakeStevens
1 parent 4b4df96 commit 8812915

4 files changed

Lines changed: 117 additions & 20 deletions

File tree

‎backends/transforms/test/test_create_delete_constant_placeholder.py‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,90 @@ def _test_create_delete(kind: InputKind, persistent_buffer: bool = None):
106106
assert len(exported_program.constants) == 0
107107

108108

109+
def test_create_delete_sanitized_name():
110+
"""
111+
torch.fx renames a placeholder whose requested name is not a valid
112+
identifier, e.g. '0_weight_fused_bn' from a top-level nn.Sequential.
113+
Target, graph signature and state_dict must follow the assigned name.
114+
"""
115+
module = EmptyNetwork()
116+
exported_program = export(module, args=module.test_data, strict=True)
117+
exported_program = to_edge(exported_program).exported_program()
118+
graph = exported_program.graph_module.graph
119+
120+
input_node = list(graph.nodes)[0]
121+
with graph.inserting_before(input_node):
122+
const_node = create_constant_placeholder(
123+
exp_program=exported_program,
124+
graph=graph,
125+
kind=InputKind.PARAMETER,
126+
name="0_test_node",
127+
data=torch.ones(1),
128+
)
129+
130+
assert const_node.name != "0_test_node"
131+
assert const_node.name.isidentifier()
132+
assert const_node.target == const_node.name
133+
assert const_node.name in exported_program.graph_signature.inputs_to_parameters
134+
target = exported_program.graph_signature.inputs_to_parameters[const_node.name]
135+
assert target in exported_program.state_dict
136+
137+
with graph.inserting_after(input_node):
138+
add_node = graph.create_node(
139+
"call_function",
140+
exir_ops.edge.aten.add.Tensor,
141+
args=(input_node, const_node),
142+
kwargs={},
143+
)
144+
output_node = graph.output_node()
145+
output_node.replace_input_with(input_node, add_node)
146+
147+
# Recompiling emits the placeholder target as a function parameter name
148+
assert exported_program.module()(torch.zeros(1)) == 1
149+
150+
output_node.replace_input_with(add_node, input_node)
151+
graph.eliminate_dead_code()
152+
delete_constant_placeholder(exported_program, const_node)
153+
assert len(exported_program.state_dict) == 0
154+
assert len(exported_program.graph_signature.input_specs) == 1
155+
156+
157+
def test_create_same_name_returns_existing_node():
158+
"""
159+
Requesting the same name twice must return the existing node instead of
160+
creating a duplicate, including when torch.fx renamed the placeholder.
161+
"""
162+
# "block1-0_weight" mirrors torchvision regnet's hyphenated module names
163+
for requested_name in ("test_node", "0_test_node", "block1-0_weight"):
164+
module = EmptyNetwork()
165+
exported_program = export(module, args=module.test_data, strict=True)
166+
exported_program = to_edge(exported_program).exported_program()
167+
graph = exported_program.graph_module.graph
168+
169+
input_node = list(graph.nodes)[0]
170+
with graph.inserting_before(input_node):
171+
first = create_constant_placeholder(
172+
exp_program=exported_program,
173+
graph=graph,
174+
kind=InputKind.PARAMETER,
175+
name=requested_name,
176+
data=torch.ones(1),
177+
)
178+
with graph.inserting_before(input_node):
179+
second = create_constant_placeholder(
180+
exp_program=exported_program,
181+
graph=graph,
182+
kind=InputKind.PARAMETER,
183+
name=requested_name,
184+
data=torch.ones(1),
185+
)
186+
187+
assert first is second
188+
placeholders = [n for n in graph.nodes if n.op == "placeholder"]
189+
assert len(placeholders) == 2 # the created node and the user input
190+
assert len(exported_program.state_dict) == 1
191+
192+
109193
def test_create_delete_parameter():
110194
_test_create_delete(InputKind.PARAMETER)
111195

‎backends/transforms/utils.py‎

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,20 @@ def create_constant_placeholder(
109109
decide where to insert the node, at an insertion point before the first input node.
110110
"""
111111

112-
target = name
112+
# Multiple pattern replacements may request the same shared weight; return
113+
# the existing node to avoid duplicate parameter names on recompile.
114+
for n in graph.nodes:
115+
if n.op == "placeholder" and n.meta.get("requested_name") == name:
116+
return n
113117

114-
# If a placeholder with this target already exists, return it to avoid
115-
# duplicate parameter names in the generated function signature which would
116-
# cause a SyntaxError on recompile. This can happen when multiple pattern
117-
# replacements independently create placeholders for a shared weight.
118-
if name in exp_program.state_dict or name in exp_program.constants:
119-
for n in graph.nodes:
120-
if n.op == "placeholder" and n.target == name:
121-
return n
118+
fake_tensor = _get_fake_tensor_mode(graph, data)
119+
120+
# torch.fx may rename the node (invalid identifier or collision); the
121+
# target, state_dict key and graph signature must follow the assigned name.
122+
node = graph.create_node(op="placeholder", name=name, target=name)
123+
target = node.target = node.name
124+
node.meta["val"] = fake_tensor
125+
node.meta["requested_name"] = name
122126

123127
# Add data to state_dict/ constants
124128
match kind:
@@ -140,15 +144,9 @@ def create_constant_placeholder(
140144
case _:
141145
raise RuntimeError("Can only create constant input nodes.")
142146

143-
fake_tensor = _get_fake_tensor_mode(graph, data)
144-
145-
# Create node
146-
node = graph.create_node(op="placeholder", name=name, target=name)
147-
node.meta["val"] = fake_tensor
148-
149147
# Add tensor to graph_signature in the same order as nodes in the graph
150148
node_names = [n.name for n in graph.nodes if n.op == "placeholder"]
151-
node_index = node_names.index(name)
149+
node_index = node_names.index(node.name)
152150

153151
input_specs = exp_program.graph_signature.input_specs
154152
user_input_indices = [
@@ -161,7 +159,7 @@ def create_constant_placeholder(
161159
f"Failed to insert {name}; Const placeholder nodes must be inserted before user input nodes in the graph."
162160
)
163161

164-
arg_spec = TensorArgument(name)
162+
arg_spec = TensorArgument(node.name)
165163
input_spec = InputSpec(kind, arg_spec, target, persistent_buffer)
166164
input_specs.insert(node_index, input_spec)
167165

‎backends/xnnpack/test/passes/test_batch_norm_fusion.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,24 @@ def test_fp32_conv_batch_norm_fusion(self):
8989
.run_method_and_compare_outputs()
9090
)
9191

92+
def test_fp32_conv_batch_norm_fusion_top_level_sequential(self):
93+
"""
94+
A top-level nn.Sequential yields digit-leading fused placeholder names,
95+
which torch.fx renames. The pass must survive the rename.
96+
"""
97+
model = torch.nn.Sequential(
98+
torch.nn.Conv2d(2, 2, (2, 2)),
99+
torch.nn.BatchNorm2d(2),
100+
).eval()
101+
(
102+
Tester(model, (torch.randn(2, 2, 4, 4),))
103+
.export()
104+
.to_edge()
105+
.run_passes(self.PassStage)
106+
.check_not([self.bn_name])
107+
.run_method_and_compare_outputs()
108+
)
109+
92110
def test_q8_conv_batch_norm_fusion(self):
93111
for transpose in [False, True]:
94112
(

‎runtime/test/test_runtime_xnnpack.py‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,7 @@ def test_mlp(self):
116116

117117
# ------------------------------------------------------------------
118118
# BatchNorm + Conv (common in MobileNet-style models)
119-
# Skipped: FuseBatchNormPass crashes on Sequential(Conv2d, BN) export.
120-
# TODO(#11225): re-enable once the XNNPACK pass is fixed.
121119
# ------------------------------------------------------------------
122-
@unittest.skip("FuseBatchNormPass bug in XNNPACK backend")
123120
def test_conv_bn(self):
124121
model = torch.nn.Sequential(
125122
torch.nn.Conv2d(3, 16, 3, padding=1),

0 commit comments

Comments
 (0)