|
| 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