Skip to content

Commit e13975c

Browse files
shoumikhinAnthony Shoumikhin
andauthored
Lift each constant once when exporting a partitioned graph (#4642)
Co-authored-by: Anthony Shoumikhin <shoumikhin@gmail.com>
1 parent 9bee63b commit e13975c

2 files changed

Lines changed: 219 additions & 2 deletions

File tree

py/torch_tensorrt/dynamo/_exporter.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,25 @@ def lift(
174174
# At first the user_inputs are only present in the graph_signature.input_specs and hence non_user_input_idx=0
175175
# The input_specs should be of the form [params, buffers, constant_tensors, custom_obj, user_inputs]
176176
non_user_input_idx = 0
177+
# Inlining the partitions left in PyTorch copies each one's own get_attr back
178+
# into the graph, so one constant can arrive as several get_attr nodes sharing a
179+
# target. fx uniquifies a placeholder's name but not its target, and placeholder
180+
# codegen emits the target, so lifting that target twice yields a forward() with
181+
# a duplicated argument that fails to compile.
182+
lifted_placeholders: Dict[str, torch.fx.Node] = {}
177183
for node in gm.graph.nodes:
178184
if node.op == "get_attr":
185+
existing = lifted_placeholders.get(node.target)
186+
if existing is not None:
187+
# This read may itself be a graph output, and the rename below only
188+
# runs for the first read, so point the output at the placeholder
189+
# being reused before the node goes away.
190+
if node.name in output_names:
191+
output_names[node.name] = existing.name
192+
node.replace_all_uses_with(existing)
193+
gm.graph.erase_node(node)
194+
continue
195+
179196
lift_val = None
180197
input_kind = None
181198

@@ -220,14 +237,26 @@ def lift(
220237
lift_val, static_shapes=True
221238
)
222239

240+
# Two attributes whose names differ only by a dot sanitize to one
241+
# argument name, so take the node's own name, which fx has already
242+
# made unique against everything else in the graph.
243+
const_placeholder_node.target = const_placeholder_node.name
244+
223245
node.replace_all_uses_with(const_placeholder_node)
246+
lifted_placeholders[node.target] = const_placeholder_node
224247
gm.graph.erase_node(node)
225248

226249
# Verify if the const_placeholder being added is one of the output nodes
227250
# This happens if there is just a single static arange op in the graph
228251
# https://github.com/pytorch/TensorRT/issues/3189
229-
if const_placeholder_name in output_names:
230-
output_names[const_placeholder_name] = const_placeholder_node.name
252+
# Keyed on the get_attr node's own name, which is what the output spec
253+
# holds. The sanitised target only matches it when fx did not have to
254+
# rename the node, so an attribute like W or myBuf or 0.weight left the
255+
# spec pointing at a node that was just erased. The dedup branch above
256+
# keys on node.name for the same reason, and so does upstream's own
257+
# lifting pass.
258+
if node.name in output_names:
259+
output_names[node.name] = const_placeholder_node.name
231260

232261
# Add these parameters/buffers/constants to the existing graph signature
233262
# before user inputs. These specs are looked up in the state_dict during ExportedProgram creation.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""Lifting a constant read by several get_attr nodes must produce one placeholder.
2+
3+
Lives here rather than with the exporter's other tests because this directory is
4+
collected by the lane that runs on every pull request, and the tests for the rest
5+
of the exporter are not.
6+
"""
7+
8+
import pytest
9+
import torch
10+
from torch.export.graph_signature import (
11+
ExportGraphSignature,
12+
InputKind,
13+
InputSpec,
14+
OutputKind,
15+
OutputSpec,
16+
TensorArgument,
17+
)
18+
from torch_tensorrt.dynamo._exporter import lift
19+
20+
21+
@pytest.mark.unit
22+
def test_lift_reuses_one_placeholder_per_constant():
23+
"""A constant read by several nodes is lifted once.
24+
25+
fx uniquifies a placeholder's name but not its target, and placeholder codegen
26+
emits the target, so lifting one constant twice yields a forward() with a
27+
duplicated argument that fails to compile with "SyntaxError: duplicate
28+
argument". One get_attr node with several users is fine; what breaks is several
29+
get_attr nodes carrying the same target, which is what the partitions left in
30+
PyTorch produce once the inliner copies them back into one graph.
31+
"""
32+
from torch._subclasses.fake_tensor import FakeTensorMode
33+
34+
fake_mode = FakeTensorMode()
35+
graph = torch.fx.Graph()
36+
x = graph.placeholder("x")
37+
with fake_mode:
38+
x.meta["val"] = torch.empty(3, 4)
39+
# Two constants, each read by several get_attr nodes, which is what inlining
40+
# the partitions left in PyTorch produces. Two rather than one, so that reusing
41+
# whichever placeholder was lifted first is distinguishable from reusing the one
42+
# for this target.
43+
acc = x
44+
for target in ("a", "b", "a", "b"):
45+
acc = graph.call_function(torch.add, (acc, graph.get_attr(target)))
46+
last = acc
47+
graph.output((last,))
48+
49+
root = torch.nn.Module()
50+
# Different values per constant, so reusing the placeholder for the wrong target
51+
# changes the result rather than producing the same answer by luck.
52+
root.register_buffer("a", torch.full((3, 4), 2.0))
53+
root.register_buffer("b", torch.full((3, 4), 5.0))
54+
gm = torch.fx.GraphModule(root, graph)
55+
56+
graph_signature = ExportGraphSignature(
57+
input_specs=[
58+
InputSpec(InputKind.USER_INPUT, TensorArgument(name="x"), target=None)
59+
],
60+
output_specs=[
61+
OutputSpec(
62+
OutputKind.USER_OUTPUT, TensorArgument(name=last.name), target=None
63+
)
64+
],
65+
)
66+
67+
lifted_gm, lifted_signature, _, _ = lift(gm, graph_signature)
68+
69+
placeholders = [n for n in lifted_gm.graph.nodes if n.op == "placeholder"]
70+
targets = [str(n.target) for n in placeholders]
71+
assert len(targets) == len(set(targets)), f"duplicate placeholders: {targets}"
72+
73+
# The user input plus one placeholder per constant, not one per read.
74+
assert len(placeholders) == 3, f"expected 3 placeholders, got {targets}"
75+
buffer_specs = [
76+
spec for spec in lifted_signature.input_specs if spec.kind == InputKind.BUFFER
77+
]
78+
assert len(buffer_specs) == 2
79+
# Each spec must describe the placeholder in the same position, or the signature
80+
# and the arguments disagree even when the counts match.
81+
for spec, placeholder in zip(lifted_signature.input_specs, placeholders):
82+
assert (
83+
spec.arg.name == placeholder.name
84+
), f"spec {spec.arg.name} does not match placeholder {placeholder.name}"
85+
86+
# The duplicate argument surfaces when the graph is turned into python.
87+
lifted_gm.recompile()
88+
89+
# Reusing the wrong placeholder passes every check above, so compare the value.
90+
# Reading a twice and b twice gives x + 2*(2 + 5).
91+
x_input = torch.ones(3, 4)
92+
a = root.get_buffer("a")
93+
b = root.get_buffer("b")
94+
torch.testing.assert_close(lifted_gm(a, b, x_input), (x_input + 2 * (a + b),))
95+
96+
97+
@pytest.mark.unit
98+
def test_inlined_partitions_produce_the_duplicate_reads_this_lifts():
99+
"""The duplicate get_attr nodes the dedup handles come from real inlining.
100+
101+
The test above builds them by hand, so on its own it would keep passing if the
102+
inliner stopped producing them and the dedup became dead code. This drives the
103+
partitioner and the inliner instead, so the shape under test stays tied to the
104+
thing that creates it. No engine is built and nothing is saved.
105+
"""
106+
from torch_tensorrt.dynamo._exporter import inline_torch_modules
107+
from torch_tensorrt.dynamo.partitioning._adjacency_partitioner import partition
108+
109+
class SharedBuffer(torch.nn.Module):
110+
def __init__(self):
111+
super().__init__()
112+
self.register_buffer("w", torch.full((4, 4), 2.0))
113+
114+
def forward(self, x):
115+
# Interleaved, so the buffer is read from several partitions that stay in
116+
# PyTorch. One partition reading it twice does not produce duplicates.
117+
a = torch.sub(x, self.w)
118+
b = torch.mul(a, a)
119+
c = torch.sub(b, self.w)
120+
d = torch.mul(c, c)
121+
return torch.sub(d, self.w)
122+
123+
exported = torch.export.export(SharedBuffer().eval(), (torch.randn(4, 4),))
124+
partitioned, _ = partition(
125+
exported.module(),
126+
min_block_size=1,
127+
torch_executed_ops={"torch.ops.aten.sub.Tensor"},
128+
)
129+
inline_torch_modules(partitioned)
130+
131+
targets = [str(n.target) for n in partitioned.graph.nodes if n.op == "get_attr"]
132+
assert len(targets) > len(
133+
set(targets)
134+
), f"inlining no longer produces duplicate get_attr targets: {targets}"
135+
136+
# Lifting this graph is left to the test above, which builds the same shape
137+
# directly. ep.module() also emits a guards node that lift cannot resolve, so
138+
# carrying this graph further would fail for a reason unrelated to the dedup.
139+
140+
141+
def test_lift_renames_an_output_whose_node_fx_had_to_rename():
142+
"""A lifted read that is also a graph output must keep its output spec pointing at
143+
the placeholder, including when fx renamed the get_attr node.
144+
145+
The output spec holds the get_attr node's name. Keying the rename on the sanitised
146+
target instead only agrees when fx did not rename anything, so an attribute like
147+
``W`` or ``myBuf`` left the spec naming a node that had just been erased and
148+
building the program raised SpecViolationError.
149+
"""
150+
from torch._subclasses.fake_tensor import FakeTensorMode
151+
152+
for attr in ("W", "myBuf", "lower"):
153+
fake_mode = FakeTensorMode()
154+
graph = torch.fx.Graph()
155+
x = graph.placeholder("x")
156+
read = graph.get_attr(attr)
157+
with fake_mode:
158+
x.meta["val"] = torch.empty(3, 4)
159+
read.meta["val"] = torch.empty(3, 4)
160+
graph.output((read,))
161+
root = torch.nn.Module()
162+
setattr(root, attr, torch.ones(3, 4))
163+
gm = torch.fx.GraphModule(root, graph)
164+
165+
signature = ExportGraphSignature(
166+
input_specs=[
167+
InputSpec(
168+
kind=InputKind.USER_INPUT, arg=TensorArgument(name="x"), target=None
169+
)
170+
],
171+
output_specs=[
172+
OutputSpec(
173+
kind=OutputKind.USER_OUTPUT,
174+
arg=TensorArgument(name=read.name),
175+
target=None,
176+
)
177+
],
178+
)
179+
lifted_gm, lifted_signature, _, _ = lift(gm, signature)
180+
181+
placeholders = [n for n in lifted_gm.graph.nodes if n.op == "placeholder"]
182+
lifted_names = {n.name for n in placeholders}
183+
spec_name = lifted_signature.output_specs[0].arg.name
184+
assert spec_name in lifted_names, (
185+
f"attribute {attr!r}: the output spec names {spec_name!r}, which is not a "
186+
f"placeholder in the lifted graph {sorted(lifted_names)}. The spec still "
187+
"points at the erased get_attr node."
188+
)

0 commit comments

Comments
 (0)