88 1. capture the complex I/O signature before rewriting (drives the boundary
99 adapters in ``_compiler._insert_complex_io_adapters``);
1010 2. call upstream to expand every complex op into real ops on separate re/im;
11- 3. normalize the SoA seams (``aten.complex`` / ``aten.real`` / ``aten.imag``)
12- into the interleaved ``[..., 2]`` layout the rest of the TRT flow expects,
13- so the engine only ever sees real tensors (Option A in the RFC).
11+ 3. put back what the retrace undid: upstream returns a module rebuilt from
12+ scratch, so the decomposition and constant-folding stages that ran earlier
13+ find their work partly reverted;
14+ 4. normalize the seams (``aten.view_as_real`` / ``aten.complex`` /
15+ ``aten.real`` / ``aten.imag``) into the interleaved ``[..., 2]`` layout the
16+ rest of the TRT flow expects, so the engine only ever sees real tensors
17+ (Option A in the RFC).
1418
1519Gated by ``settings.use_complex_decomposition``; falls back to the legacy pass on
1620older torch (the upstream API only exists in torch>=2.14.0.dev), or if
2630from torch .fx import GraphModule
2731from torch_tensorrt ._features import has_complex_decomposition
2832from torch_tensorrt .dynamo ._settings import CompilationSettings
33+ from torch_tensorrt .dynamo .lowering .passes .constant_folding import constant_fold
34+ from torch_tensorrt .dynamo .lowering .passes .mark_constant_fold_exclusions import (
35+ mark_constant_fold_exclusions ,
36+ )
2937from torch_tensorrt .dynamo .lowering .passes .pass_utils import (
3038 clean_up_graph_after_modifications ,
3139)
@@ -91,7 +99,9 @@ def complex_decomposition_adapter(
9199 # call either fully succeeds or raises; it never partially mutates
92100 # anything, since it builds a new GraphModule rather than editing
93101 # the existing one in place).
94- decomposed_gm = decompose_complex_in_graph (gm , flat_args )
102+ decomposed_gm = decompose_complex_in_graph (
103+ gm , flat_args , decompositions = _trt_decomposition_table (settings )
104+ )
95105 except Exception as e :
96106 # decompose_complex_in_graph is upstream, experimental PyTorch code
97107 # (torch._functorch._aot_autograd.complex_decomposition) with
@@ -107,11 +117,18 @@ def complex_decomposition_adapter(
107117 return complex_graph_detection (gm , settings )
108118 gm = decomposed_gm
109119
110- # (3) Normalize SoA seams into the interleaved [..., 2] layout used by TRT.
120+ # (3) Fold constants again. The retrace builds the module from scratch, so
121+ # a constant that the constant-folding stage had already reduced to one
122+ # frozen tensor comes back as a live chain of ops reading it, and the
123+ # converters for those ops expect a TRT tensor rather than a weight.
124+ gm = mark_constant_fold_exclusions (gm , settings )
125+ gm = constant_fold (gm , settings )
126+
127+ # (4) Normalize the seams into the interleaved [..., 2] layout used by TRT.
111128 gm = _normalize_complex_boundary_for_trt (gm )
112129 gm = clean_up_graph_after_modifications (gm )
113130
114- # (4 ) Re-attach the captured I/O signature onto the RETURNED module so
131+ # (5 ) Re-attach the captured I/O signature onto the RETURNED module so
115132 # _insert_complex_io_adapters can restore the complex boundary.
116133 gm .meta ["complex_output_indices" ] = complex_output_indices
117134 gm .meta ["complex_input_names" ] = complex_input_names
@@ -123,6 +140,29 @@ def complex_decomposition_adapter(
123140 return gm
124141
125142
143+ def _trt_decomposition_table (
144+ settings : CompilationSettings ,
145+ ) -> dict [Any , Any ]:
146+ """The op set the rest of the TRT flow expects to see.
147+
148+ ``decompose_complex_in_graph`` re-traces through ``make_fx``, so the module
149+ it returns is built from whatever ``ComplexTensor`` dispatched to, not from
150+ the ops that survived the decomposition run at export time. Without a table
151+ that retrace re-introduces ops the flow has already decided to expand
152+ (``aten.stack``) or to drop (``aten.alias``), long after the stage that
153+ would have handled them, and the partitioner then cuts the graph around
154+ them. Handing the retrace the same table keeps the two in step.
155+ """
156+ from torch_tensorrt .dynamo .lowering import get_decompositions
157+
158+ return get_decompositions (
159+ settings .enable_experimental_decompositions ,
160+ settings .decompose_attention ,
161+ settings .use_distributed_mode_trace ,
162+ use_fp32_acc = settings .use_fp32_acc ,
163+ )
164+
165+
126166def _graph_has_complex (gm : GraphModule ) -> bool :
127167 from torch_tensorrt .dynamo .utils import COMPLEX_DTYPES
128168
@@ -174,20 +214,30 @@ def _fake_flat_args(gm: GraphModule) -> list[torch.Tensor]:
174214
175215
176216def _normalize_complex_boundary_for_trt (gm : GraphModule ) -> GraphModule :
177- """Fold upstream's SoA seams into the interleaved ``[..., 2]`` real layout.
217+ """Fold upstream's seams into the interleaved ``[..., 2]`` real layout.
178218
179- Upstream leaves the graph in terms of separate re/im joined by
180- ``aten.complex(re, im)`` and unpacked by ``aten.real`` / ``aten.imag``. TRT
181- has no converter for any of those. We:
219+ Every op below reinterprets a complex value as a pair of real halves, or
220+ the other way round, and TRT has no converter for any of them. Leaving one
221+ in the graph does not just fail an op check: the partitioner cuts the graph
222+ around it and the model comes back split into several engines with PyTorch
223+ blocks in between. We rewrite:
182224
225+ * ``view_as_real(z)`` -> the interleaved ``[..., 2]`` tensor it would
226+ have returned, for each producer ``z`` can have (see Pass A)
183227 * ``real(z)`` / ``imag(z)`` where ``z = complex(re, im)`` -> re / im
184228 (cancel the pack/unpack round-trip)
185- * remaining ``aten.complex(re, im)`` -> ``stack ([re, im], -1)`` tagged as
229+ * remaining ``aten.complex(re, im)`` -> ``cat ([re, im], -1)`` tagged as
186230 complex-layout, so the [..., 2] tensor flows to the boundary adapters.
187231
188- Confirmed against real decompose_complex_in_graph output: the boundary op
189- set is aten.complex/real/imag as assumed above, unpacked via real/imag
190- (not view_as_real).
232+ Which of these appear depends on where the complex region sits. A graph
233+ whose own inputs or outputs are complex meets upstream at ``aten.complex``,
234+ because that is what ``ComplexTensor`` packs with on the way out. A graph
235+ that is real on both ends but complex in the middle, as a rotary embedding
236+ is, meets it at ``view_as_real`` instead: the entry ``view_as_complex`` is
237+ left alone (its argument is a plain real tensor, so ``ComplexTensor`` never
238+ sees it), and ``ComplexTensor`` splits its halves with ``torch.real`` /
239+ ``torch.imag``, which are composite ops that expand to ``view_as_real``
240+ plus ``select`` before any subclass can intercept them.
191241 """
192242 g = gm .graph
193243 aten = torch .ops .aten
@@ -210,7 +260,43 @@ def _propagate_meta(node: torch.fx.Node) -> None:
210260 with fake_mode :
211261 node .meta ["val" ] = node .target (* arg_vals )
212262
213- # Pass A: cancel real(complex(re,im)) / imag(complex(re,im)) round-trips.
263+ def _pack_interleaved (re : Any , im : Any ) -> torch .fx .Node :
264+ """Build the ``[..., 2]`` tensor holding ``re`` and ``im`` side by side.
265+
266+ Callers set the insertion point. ``cat`` of two unsqueezed halves
267+ rather than ``stack`` because the stage that expands ``stack`` has
268+ already run by the time this pass does.
269+ """
270+ re_u = g .call_function (aten .unsqueeze .default , (re , - 1 ))
271+ _propagate_meta (re_u )
272+ im_u = g .call_function (aten .unsqueeze .default , (im , - 1 ))
273+ _propagate_meta (im_u )
274+ packed = g .call_function (aten .cat .default , ([re_u , im_u ], - 1 ))
275+ _propagate_meta (packed )
276+ return packed
277+
278+ # Pass A: rewrite view_as_real over each producer it can have.
279+ for node in list (g .nodes ):
280+ if node .op != "call_function" or node .target != aten .view_as_real .default :
281+ continue
282+ src = node .args [0 ]
283+ if not isinstance (src , torch .fx .Node ):
284+ continue
285+ if src .target == aten .view_as_complex .default :
286+ # view_as_complex consumes a trailing dimension of 2 and
287+ # view_as_real puts it back holding the same values, so the pair is
288+ # an identity on the real tensor that went in. Dropping it leaves
289+ # the halves to be picked out of that tensor directly.
290+ replacement = src .args [0 ]
291+ elif src .target == aten .complex .default :
292+ with g .inserting_before (node ):
293+ replacement = _pack_interleaved (src .args [0 ], src .args [1 ])
294+ else :
295+ continue
296+ node .replace_all_uses_with (replacement )
297+ g .erase_node (node )
298+
299+ # Pass B: cancel real(complex(re,im)) / imag(complex(re,im)) round-trips.
214300 for node in list (g .nodes ):
215301 if node .op != "call_function" or node .target not in (
216302 aten .real .default ,
@@ -233,18 +319,12 @@ def _propagate_meta(node: torch.fx.Node) -> None:
233319 node .replace_all_uses_with (src .args [idx ])
234320 g .erase_node (node )
235321
236- # Pass B : turn surviving aten.complex(re, im) into stack([re, im], -1) .
322+ # Pass C : turn surviving aten.complex(re, im) into the interleaved layout .
237323 for node in list (g .nodes ):
238324 if node .op != "call_function" or node .target != aten .complex .default :
239325 continue
240- re , im = node .args [0 ], node .args [1 ]
241326 with g .inserting_before (node ):
242- re_u = g .call_function (aten .unsqueeze .default , (re , - 1 ))
243- _propagate_meta (re_u )
244- im_u = g .call_function (aten .unsqueeze .default , (im , - 1 ))
245- _propagate_meta (im_u )
246- packed = g .call_function (aten .cat .default , ([re_u , im_u ], - 1 ))
247- _propagate_meta (packed )
327+ packed = _pack_interleaved (node .args [0 ], node .args [1 ])
248328 packed .meta ["is_complex_layout" ] = True
249329 node .replace_all_uses_with (packed )
250330 g .erase_node (node )
0 commit comments