Skip to content

Commit 526e555

Browse files
committed
update
1 parent b79fd5f commit 526e555

11 files changed

Lines changed: 515 additions & 296 deletions

examples/dynamo/aot_plugin.py

Lines changed: 27 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,8 @@
5353

5454
@triton.jit
5555
def add_one_kernel(x_ptr, y_ptr, n_elements, BLOCK_SIZE: tl.constexpr):
56-
# Arg order matters for the AOT path: TRT launches the embedded PTX with
57-
# arguments in (input_ptrs, output_ptrs, extra_args) order — inputs first,
58-
# then outputs, then anything from ``extra_args`` in ``@trtp.aot_impl``.
59-
# If this kernel declared ``(x_ptr, n_elements, y_ptr, ...)`` then TRT
60-
# would feed the output pointer into ``n_elements`` and ``n_elements``
61-
# into ``y_ptr`` at launch, which is a wild pointer dereference (engine
62-
# builds fine, ``enqueueV3`` returns -1 and the process segfaults).
56+
# AOT path requires (inputs, outputs, extra_args) order — swapping any
57+
# two slots feeds the wrong value into the kernel and segfaults.
6358
pid = tl.program_id(0)
6459
block_start = pid * BLOCK_SIZE
6560
offsets = block_start + tl.arange(0, BLOCK_SIZE)
@@ -71,13 +66,8 @@ def add_one_kernel(x_ptr, y_ptr, n_elements, BLOCK_SIZE: tl.constexpr):
7166

7267
@triton.jit
7368
def add_one_inplace_kernel(x_ptr, n_elements, BLOCK_SIZE: tl.constexpr):
74-
# Distinct kernel for the aliased-I/O variant. The plugin descriptor
75-
# declares its output as ``X.aliased()`` — at runtime TRT passes a
76-
# *single* pointer for the aliased I/O pair (the shared buffer), not two.
77-
# If we re-used ``add_one_kernel`` here, TRT would supply: pointer,
78-
# n_elements, padding... and the kernel's ``y_ptr`` slot would absorb
79-
# ``n_elements`` while ``n_elements`` would read the padding zero — the
80-
# mask would be all-false and the kernel would do nothing.
69+
# Aliased-I/O variant: TRT routes a single pointer for the shared
70+
# input/output buffer, so the kernel takes one pointer, not two.
8171
pid = tl.program_id(0)
8272
block_start = pid * BLOCK_SIZE
8373
offsets = block_start + tl.arange(0, BLOCK_SIZE)
@@ -191,29 +181,13 @@ def add_plugin_aot_impl(
191181
launch_params.block_x = compiled_kernel.metadata.num_warps * 32 # threads per block
192182
launch_params.shared_mem = compiled_kernel.metadata.shared # bytes of shared mem
193183

194-
# ``extra_args`` are scalar arguments appended to the kernel's argument list at
195-
# launch. ``n_elements`` is passed as a 32-bit symbolic integer so TRT
196-
# evaluates it from the actual tensor size at runtime.
197-
#
198-
# Triton >= 3.x always emits two trailing ``.param .u64 .ptr`` slots in
199-
# the compiled PTX for ``global_scratch`` and ``profile_scratch`` — even
200-
# when their sizes (``compiled_kernel.metadata.global_scratch_size`` /
201-
# ``profile_scratch_size``) are 0. Triton's own runtime allocates
202-
# zero-sized scratch buffers and passes those pointers at launch; TRT's
203-
# AOT plugin path doesn't know about them and would otherwise leave the
204-
# two trailing slots filled with stale register state — symptom:
205-
# ``Failed to enqueue status -1`` and a segfault on the first call.
206-
# We pad ``extra_args`` with four ``SymInt32(0)`` (two per u64 slot) so
207-
# the kernel sees null pointers for both scratch params; since their
208-
# sizes are 0 the kernel never dereferences them.
209-
extra_args = trtp.SymIntExprs(1 + 4)
210-
extra_args[0] = trtp.SymInt32(N)
211-
for _i in range(1, 5):
212-
extra_args[_i] = trtp.SymInt32(0)
184+
extra_args = torch_tensorrt.dynamo.conversion.plugins.make_aot_extra_args(
185+
[trtp.SymInt32(N)], compiled_kernel=compiled_kernel
186+
)
213187

214188
return (
215-
compiled_kernel.metadata.name, # kernel function name in PTX
216-
compiled_kernel.asm["ptx"], # PTX source — embedded in TRT engine
189+
compiled_kernel.metadata.name,
190+
compiled_kernel.asm["ptx"],
217191
launch_params,
218192
extra_args,
219193
)
@@ -244,35 +218,15 @@ def add_plugin_aot_impl(
244218
# In-place variant: aliased plugin I/O
245219
# -----------------------------------------
246220
#
247-
# This second registration shows the same kernel exposed as an *in-place* plugin —
248-
# the engine mutates the input buffer directly instead of allocating a separate
249-
# output. Useful for KV-cache updates and any pattern where only a small slice of
250-
# a large state changes per call.
251-
#
252-
# Three things change vs. ``my::add_one`` above:
253-
#
254-
# 1. ``mutates_args=("X",)`` on the torch op. This is the load-bearing signal —
255-
# it tells the QDP descriptor in ``_generate_plugin.py`` that input ``X`` is
256-
# a candidate for aliasing, and it also tells PyTorch's autograd and
257-
# functionalization machinery that the op has side effects on ``X``.
258-
#
259-
# 2. The registered fake returns ``X`` by identity (``return X``). Combined with
260-
# the schema's ``mutates_args``, this is what makes the descriptor emit
261-
# ``X.aliased()`` (see ``_generate_plugin._generic_plugin_desc``) instead of
262-
# building a fresh output ``TensorDesc``.
221+
# Same kernel exposed as an in-place plugin: the engine mutates the input
222+
# buffer directly via QDP ``TensorDesc.aliased()`` instead of allocating a
223+
# separate output. Useful for KV-cache updates and similar patterns.
263224
#
264-
# 3. The descriptor itself uses ``X.aliased()``. ``aliased()`` returns a
265-
# ``TensorDesc`` that shares its data buffer with ``X`` — TRT will route the
266-
# same pointer to both the input and output binding at runtime.
267-
#
268-
# The eager torch impl has to mutate ``X`` itself (so the semantics match what
269-
# the engine will do) and return ``X.clone()``. ``torch.library`` forbids
270-
# returning an input by identity from a custom op, hence the clone.
271-
#
272-
# Note on the AOT kernel: we re-use ``add_one_kernel`` unchanged. Its signature
273-
# takes two pointers (``x_ptr``, ``y_ptr``). With aliased I/O declared, TRT
274-
# passes the same buffer for both — the kernel reads from ``x_ptr`` and writes
275-
# to ``y_ptr``, which is the same memory, so the effect is in-place.
225+
# Two signals together declare aliasing to the framework:
226+
# * ``mutates_args=("X",)`` on the torch op
227+
# * the registered fake returns ``X`` by identity
228+
# The eager impl must mutate ``X`` itself and return a clone — ``torch.library``
229+
# rejects returning an input by identity.
276230

277231

278232
@torch.library.custom_op("my::add_one_inplace", mutates_args=("X",)) # type: ignore[misc]
@@ -281,26 +235,16 @@ def add_one_inplace(X: torch.Tensor) -> torch.Tensor:
281235
BLOCK_SIZE = 256
282236
grid = lambda meta: (triton.cdiv(X.numel(), meta["BLOCK_SIZE"]),)
283237
add_one_inplace_kernel[grid](X, X.numel(), BLOCK_SIZE=BLOCK_SIZE)
284-
# Must not return X by identity — torch.library's no-alias constraint
285-
# rejects that. The TRT path doesn't observe this clone (aliasing is
286-
# declared at the descriptor level), it's purely for the eager impl.
287238
return X.clone()
288239

289240

290241
@torch.library.register_fake("my::add_one_inplace")
291242
def _(X: torch.Tensor) -> torch.Tensor:
292-
# Identity return is the secondary signal the descriptor uses to detect
293-
# aliasing. Combined with ``mutates_args=("X",)`` above, this is what
294-
# makes ``_generic_plugin_desc`` emit ``X.aliased()`` for the output.
295243
return X
296244

297245

298246
@trtp.register("my::add_one_inplace")
299247
def add_one_inplace_desc(X: trtp.TensorDesc) -> Tuple[trtp.TensorDesc]:
300-
# ``aliased()`` is the QDP API that declares output-shares-storage-with-input.
301-
# Engine build will fail with "PreviewFeature::kALIASED_PLUGIN_IO_10_03 not
302-
# enabled" unless the build config enables that preview feature; the
303-
# converter wires this on for you when it sees a non-empty aliased_map.
304248
return X.aliased()
305249

306250

@@ -313,7 +257,6 @@ def add_one_inplace_aot_impl(
313257
type_str = "fp32" if X.dtype == trt.float32 else "fp16"
314258

315259
block_size = 256
316-
# Single-pointer signature — see the comment on ``add_one_inplace_kernel``.
317260
src = triton.compiler.ASTSource(
318261
fn=add_one_inplace_kernel,
319262
signature={
@@ -332,14 +275,9 @@ def add_one_inplace_aot_impl(
332275
launch_params.block_x = compiled_kernel.metadata.num_warps * 32
333276
launch_params.shared_mem = compiled_kernel.metadata.shared
334277

335-
# See the matching note on the non-in-place ``add_plugin_aot_impl``:
336-
# Triton 3.x emits two trailing ``.param .u64 .ptr`` slots for the
337-
# global/profile scratch buffers, and TRT's AOT path needs them zeroed
338-
# explicitly via ``extra_args`` so the kernel doesn't read stale state.
339-
extra_args = trtp.SymIntExprs(1 + 4)
340-
extra_args[0] = trtp.SymInt32(N)
341-
for _i in range(1, 5):
342-
extra_args[_i] = trtp.SymInt32(0)
278+
extra_args = torch_tensorrt.dynamo.conversion.plugins.make_aot_extra_args(
279+
[trtp.SymInt32(N)], compiled_kernel=compiled_kernel
280+
)
343281

344282
return (
345283
compiled_kernel.metadata.name,
@@ -376,10 +314,6 @@ def forward(self, X: torch.Tensor) -> torch.Tensor:
376314

377315

378316
class MyInplaceModel(torch.nn.Module):
379-
"""Drives the in-place plugin. The op mutates ``X`` in place; the returned
380-
tensor carries the post-mutation value (a clone, only to satisfy
381-
torch.library's no-alias rule)."""
382-
383317
def forward(self, X: torch.Tensor) -> torch.Tensor:
384318
return torch.ops.my.add_one_inplace.default(X)
385319

@@ -416,18 +350,8 @@ def forward(self, X: torch.Tensor) -> torch.Tensor:
416350
# In-place plugin demo
417351
# ---------------------
418352
#
419-
# The standard "compile once, run many times" comparison pattern doesn't
420-
# work for an in-place op because each call mutates the input — running
421-
# eager and TRT on the same buffer double-applies the mutation. We work
422-
# off a base tensor and clone for each call instead.
423-
#
424-
# Three things to verify, beyond "it ran":
425-
# 1. The compiled module contains a TRT engine (not a PyTorch fallback —
426-
# a regression here would silently pass on value because the eager
427-
# kernel mutates the input the same way).
428-
# 2. The engine's return value matches the expected post-mutation tensor.
429-
# 3. The user's input buffer was mutated in place by the engine — the
430-
# actual reason to use aliased plugin I/O in the first place.
353+
# In-place ops mutate their input, so eager and TRT must run on separate
354+
# cloned buffers; otherwise each comparison double-applies the mutation.
431355

432356
print("\nIn-place plugin demo:")
433357
inplace_model = MyInplaceModel().to("cuda").eval()
@@ -452,22 +376,18 @@ def forward(self, X: torch.Tensor) -> torch.Tensor:
452376
if isinstance(m, (PythonTorchTensorRTModule, TorchTensorRTModule))
453377
]
454378
assert engine_submodules, (
455-
"Expected a TRT engine submodule for the in-place plugin path, but the"
456-
" compiled module is pure PyTorch — check that the un-functionalize"
457-
" lowering pass restored the mutating op before partitioning. Graph:\n"
458-
f"{model_trt_inplace.graph}"
379+
"Expected a TRT engine submodule for the in-place plugin path; got a"
380+
f" pure-PyTorch fallback. Graph:\n{model_trt_inplace.graph}"
459381
)
460382
print(f" TRT engine submodule(s) present: {len(engine_submodules)}")
461383

462384
with torch.no_grad():
463385
trt_input = base.clone()
464386
trt_out = model_trt_inplace(trt_input)
465387
assert torch.allclose(trt_out, expected_post), "TRT output mismatch"
466-
assert torch.allclose(trt_input, expected_post), (
467-
"Engine did not mutate the input buffer — aliased plugin I/O is"
468-
" not active. Check that PreviewFeature.ALIASED_PLUGIN_IO_10_03"
469-
" was enabled and that the descriptor emitted X.aliased()."
470-
)
388+
assert torch.allclose(
389+
trt_input, expected_post
390+
), "Engine did not mutate the input buffer — aliased plugin I/O is not active."
471391

472392
print(" Output matches expected post-mutation value.")
473393
print(" Input buffer was mutated in place by the TRT engine.")

py/torch_tensorrt/dynamo/conversion/plugins/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from torch_tensorrt.dynamo.conversion.plugins._aot_utils import make_aot_extra_args
12
from torch_tensorrt.dynamo.conversion.plugins._custom_op import custom_op
23
from torch_tensorrt.dynamo.conversion.plugins._generate_plugin import generate_plugin
34
from torch_tensorrt.dynamo.conversion.plugins._generate_plugin_converter import (
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Helpers for writing AOT QDP plugins backed by Triton kernels."""
2+
3+
from typing import Any, Sequence
4+
5+
6+
def _has_triton_scratch_params(compiled_kernel: Any) -> bool:
7+
md = getattr(compiled_kernel, "metadata", None)
8+
if md is None:
9+
return False
10+
return hasattr(md, "global_scratch_size") and hasattr(md, "profile_scratch_size")
11+
12+
13+
def make_aot_extra_args(
14+
user_args: Sequence[Any],
15+
*,
16+
compiled_kernel: Any = None,
17+
) -> Any:
18+
"""Build a ``trtp.SymIntExprs`` for an AOT plugin's ``extra_args`` return.
19+
20+
When ``compiled_kernel`` is a Triton-compiled kernel, four trailing
21+
``SymInt32(0)`` are appended to cover the two ``.param .u64 .ptr`` slots
22+
(``global_scratch``, ``profile_scratch``) that Triton >= 3.x always emits
23+
in PTX even when their sizes are zero. TRT's AOT plugin path does not
24+
plumb those slots through, so without padding ``enqueueV3`` reads stale
25+
register state for them and segfaults on the first call.
26+
"""
27+
import tensorrt.plugin as trtp
28+
29+
pad = 4 if _has_triton_scratch_params(compiled_kernel) else 0
30+
total = len(user_args) + pad
31+
out = trtp.SymIntExprs(total)
32+
for i, arg in enumerate(user_args):
33+
out[i] = arg
34+
zero = trtp.SymInt32(0)
35+
for i in range(len(user_args), total):
36+
out[i] = zero
37+
return out

py/torch_tensorrt/dynamo/conversion/plugins/_generate_plugin.py

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -71,31 +71,34 @@ def _generate_plugin(plugin_name: str) -> None:
7171

7272
# retrieve the corresponding torch operation using the passed in string
7373
torch_op = getattr(getattr(torch.ops, namespace), name)
74+
default_schema = torch_op.default._schema
7475

7576
# Positional indices of tensor inputs marked as mutated in the schema
76-
# (Tensor(aN!) ... -> ...). These are candidates for in-place QDP outputs
77-
# via `TensorDesc.aliased()` — the actual alias map (output_idx -> input_idx)
78-
# is determined at fake-run time by checking output-to-input identity, since
79-
# torch.library does not stamp aliasing onto the return alias_info.
80-
_schema = torch_op._schemas[""]
77+
# (Tensor(aN!) ... -> ...) — candidates for in-place QDP outputs via
78+
# `TensorDesc.aliased()`. The actual alias map (output_idx -> input_idx)
79+
# is decided at fake-run time by checking output-to-input identity.
8180
_tensor_arg_positions = [
8281
i
83-
for i, a in enumerate(_schema.arguments)
82+
for i, a in enumerate(default_schema.arguments)
8483
if a.type.isSubtypeOf(torch._C.TensorType.get())
8584
]
8685
_mutated_tensor_arg_positions = {
8786
_tensor_arg_positions.index(i)
88-
for i, a in enumerate(_schema.arguments)
87+
for i, a in enumerate(default_schema.arguments)
8988
if a.type.isSubtypeOf(torch._C.TensorType.get())
9089
and a.alias_info is not None
9190
and a.alias_info.is_write
9291
}
9392

93+
# Cached frozenset of aliased output indices, populated by the descriptor
94+
# on first build-time call so the runtime impl skips the per-call probe.
95+
_aliased_indices_cache: list[Any] = [None]
96+
9497
# helper function that generates the required signature based on the torch operation
9598
def generate_signature(
9699
torch_op: Callable[[Any], Any],
97100
) -> Tuple[str, str, str, dict[str, Any], dict[str, Any]]:
98-
schema = torch_op._schemas[""]
101+
schema = torch_op.default._schema
99102

100103
arg_list = []
101104

@@ -202,21 +205,19 @@ def _generic_plugin_desc(*args: Any, **kwargs: Any) -> Tuple[trtp.TensorDesc, ..
202205

203206
output = torch_op(*fake_args, *non_tensor_args, **torch_kwargs)
204207

205-
# Normalize to a list of fake outputs. Multi-output torch ops return
206-
# a tuple; single-output ops return a bare Tensor.
207208
outputs_list = list(output) if isinstance(output, (tuple, list)) else [output]
208209

209-
# Build output_idx -> input_idx alias map. An output is aliased to a
210-
# mutated input when both (a) the input is declared mutates_args in the
211-
# schema and (b) the fake kernel returned that exact tensor by identity.
212-
# The schema gate prevents accidental aliasing when a fake kernel
213-
# incidentally returns an input (e.g. an identity op without mutation).
210+
# output_idx -> input_idx alias map: an output aliases a mutated input
211+
# iff the schema marks the input as mutated AND the fake returns that
212+
# tensor by identity. The schema gate prevents accidental aliasing on
213+
# incidental identity returns from non-mutating ops.
214214
alias_map: dict[int, int] = {}
215215
for out_idx, fake_out in enumerate(outputs_list):
216216
for in_idx in _mutated_tensor_arg_positions:
217217
if in_idx < len(fake_args) and fake_out is fake_args[in_idx]:
218218
alias_map[out_idx] = in_idx
219219
break
220+
_aliased_indices_cache[0] = frozenset(alias_map.keys())
220221

221222
input_node_expr = list(
222223
itertools.chain.from_iterable(
@@ -239,10 +240,14 @@ def _generic_plugin_desc(*args: Any, **kwargs: Any) -> Tuple[trtp.TensorDesc, ..
239240

240241
out_descs = []
241242
for out_idx, fake_out in enumerate(outputs_list):
242-
# In-place output: tell TRT this output shares its buffer with the
243-
# mutated input. `aliased()` carries the input's shape/dtype so we
244-
# don't rebuild the descriptor from the symbolic expressions.
245243
if out_idx in alias_map:
244+
# Aliased output shares its buffer with a mutated input;
245+
# `aliased()` carries the input's shape/dtype.
246+
# Limitation: TRT preview-feature ALIASED_PLUGIN_IO_10_03
247+
# inserts a defensive copy that breaks aliasing when a
248+
# multi-output plugin's aliased output is consumed by
249+
# another TRT layer in the same engine. Single-output
250+
# plugins (the common KV-cache pattern) work end-to-end.
246251
out_descs.append(tensor_args[alias_map[out_idx]].aliased())
247252
continue
248253

@@ -312,13 +317,15 @@ def _generic_plugin_impl(
312317

313318
dest_tensors = [torch.as_tensor(o, device="cuda") for o in outputs]
314319

315-
# Outputs declared `.aliased()` share storage with their input — the
316-
# mutation already lands in the destination buffer, so copying onto it
317-
# would either be a no-op self-copy or, worse, clobber the in-place
318-
# result if the eager kernel returned a fresh tensor.
319-
aliased_outputs = {
320-
i for i, o in enumerate(outputs) if o.get_aliased() is not None
321-
}
320+
# Skip copy_ for aliased outputs: storage is shared with the input
321+
# the eager op already mutated, so copying would either no-op against
322+
# itself or clobber the in-place result when the eager op returns a
323+
# fresh tensor (as torch.library forces it to do).
324+
aliased_outputs = _aliased_indices_cache[0]
325+
if aliased_outputs is None:
326+
aliased_outputs = frozenset(
327+
i for i, o in enumerate(outputs) if o.get_aliased() is not None
328+
)
322329

323330
stream = torch.cuda.ExternalStream(stream)
324331
with torch.cuda.stream(stream):

0 commit comments

Comments
 (0)