[PyTorch][torch.compile] Support for UnfusedDotProductAttention#3201
[PyTorch][torch.compile] Support for UnfusedDotProductAttention#3201pggPL wants to merge 12 commits into
Conversation
…le + CUDA graphs
Refactor TE custom kernels used by the unfused attention path so that
`torch.compile(fullgraph=True, mode="reduce-overhead")` can trace the
forward and backward and capture them into CUDA graphs without graph
breaks.
- softmax.py / softmax.cpp: register all `scaled_*_softmax_{forward,backward}`
kernels as `torch.library.custom_op`s with fake impls and an autograd
binding that mirrors the previous `torch.autograd.Function`s. The C++
backward kernels now allocate a fresh output buffer instead of writing
in-place into `output_grad`, so the ops no longer alias their inputs
(required by `torch.library.custom_op` and inductor cudagraph trees).
- utils.py: convert `ConvertTHDtoBSHD` / `ConvertBSHDtoTHD` to
`torch.library.custom_op`s, with thin wrapper classes that keep the
existing `.apply(...)` callsite syntax. Drop the
`int(cu_seqlens[-1].item())` from the hot path of `ConvertBSHDtoTHD.apply`
-- under `torch.compile` it created an unbacked SymInt, which made the
Inductor partitioner emit `None` placeholders for output buffers and
caused `cudagraph_trees` to assert. `num_tokens` is now passed in by
the caller as a regular (Sym)Int.
- backends.py: in the THD branch of unfused DPA, capture
`total_tokens_q = query_layer.shape[0]` before overwriting
`query_layer` with the BSHD form, and thread it back into
`ConvertBSHDtoTHD.apply` at the end of the forward.
- test_torch_compile.py: add `test_unfused_dpa_torch_compile`,
parametrized over qkv layouts (`bshd_bshd_bshd`, `sbhd_sbhd_sbhd`,
`thd_thd_thd`, `bs3hd`, `sbh3d`), that compiles
`UnfusedDotProductAttention.forward` directly with `fullgraph=True,
mode="reduce-overhead"` and runs forward+backward several times so the
CUDA graphs are recorded and replayed.
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Made-with: Cursor
…to unfused_attention_torch_compile Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com> # Conflicts: # tests/pytorch/test_torch_compile.py
…DotProductAttention
Make the FP8-emulation path (NVTE_UnfusedDPA_Emulate_FP8=1) of
UnfusedDotProductAttention traceable by torch.compile(fullgraph=True).
- backends.py: register the quantize+dequantize roundtrips used by
FP8EmulationFunc as torch.library custom ops
(te_fp8_emu::roundtrip_<QuantizerClass> and
te_fp8_emu::roundtrip_qkv_<QuantizerClass>) taking the quantizer as a
value-opaque argument, with fake impls for tracing. Ops are registered
only for the value-opaque quantizer classes
(Float8CurrentScalingQuantizer, MXFP8Quantizer); Float8Quantizer
(delayed scaling) carries scale/amax tensor state, is not
value-opaque, and deliberately keeps the plain eager path -- FP8
emulation with delayed scaling is not supported under torch.compile.
- backends.py: dispatch helpers `_fp8_emu_roundtrip{,_qkv}` key on
`type(quantizer).__qualname__` so they stay traceable for opaque
quantizer arguments; FP8EmulationFunc forward/backward now call them
(onnx_forward unchanged).
- backends.py: the joint q/k/v roundtrip clones any output whose
storage is shared with an input or another output, checking storage
identity directly -- the dequantized q/k/v can be views into one
combined buffer, and view metadata (`_base`) is not populated under
the torch-dispatch mode AOTAutograd runs custom ops with, so a
`_base`-guarded clone triggered the custom-op aliasing deprecation
warning under torch.compile.
- UnfusedDotProductAttention.forward: only query
FP8GlobalStateManager.get_fp8_recipe() when
fp8_meta["local_recipes"] is absent.
- test_torch_compile.py: add test_unfused_dpa_fp8_emulation_torch_compile
(current scaling + mxfp8, sbhd/bshd layouts; compiled fullgraph
forward+backward must match eager) and
test_unfused_dpa_fp8_emulation_delayed_scaling_eager guarding the
eager delayed-scaling path after the refactor.
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…output=True With fp8_output=True the backend returns a Float8Tensor -- a tensor subclass that cannot cross a torch.compile graph boundary -- so the forward dispatches to a torch._dynamo.disable'd wrapper, the same mechanism DotProductAttention and FusedAttention use module-wide. With fp8_output=False the dispatcher is resolved at trace time and adds no graph break. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…cudagraphs) Parametrize test_unfused_dpa_fp8_emulation_torch_compile over compile mode (default, reduce-overhead), run 3 iterations so the CUDA graphs are recorded and replayed. The te_fp8_emu roundtrip ops for current scaling are pure (no mutated args), so inductor cudagraphs capture them; verified no cudagraph skips with TORCH_LOGS=cudagraphs. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…n; run FP8 as an eager island FP8 in the unfused backend (emulation and Float8Tensor output) is not supported under torch.compile: the forward dispatcher routes fp8=True and/or fp8_output=True to a torch._dynamo.disable'd wrapper, same as DotProductAttention does module-wide. Remove the FP8-emulation compile tests. The te_fp8_emu::* custom ops taking value-opaque quantizers stay as the eager implementation of FP8EmulationFunc. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…Func The ops existed solely to make the FP8-emulation path traceable by torch.compile; since FP8 in the unfused backend now always runs as an eager island, they are dead machinery (plus import-time registration and output clones the plain eager path never needed). Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
The tex softmax kernels take 'float scale_factor' directly. The 0-D tensor wrapping was a leftover of the old autograd.Function idiom, where the float had to be a tensor only to fit save_for_backward; the custom ops keep the scale on ctx as a plain attribute. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…the callsite) Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…cate dict, silence W0613 - run black over the four changed files (earlier commits skipped pre-commit) - drop unused 'import os' in test_torch_compile.py - drop duplicated module-level _default_causal_mask dict in softmax.py - del unused 'output' arg in the conversion setup_context helpers Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
for more information, see https://pre-commit.ci
Greptile SummaryThis PR makes
Confidence Score: 3/5The training path through the new custom ops is well-structured, but the refactoring introduced a latent regression that crashes inference calls with THD query and non-THD KV tensors. total_tokens_q is only set in the qkv_format == thd branch but consumed by q_format == thd, a condition also true for the thd_2bshd inference layout where total_tokens_q is never assigned, reverting a path that worked before this PR. backends.py — specifically the if q_format == thd output block and its interaction with the thd_2bshd inference format. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["UnfusedDotProductAttention.forward(fp8, fp8_output, ...)"] --> B{fp8 or fp8_output?}
B -- Yes --> C["_forward_eager()\n@no_torch_dynamo — eager island"]
B -- No --> D["_forward() — fully compiled"]
C --> E["_forward() inside eager context"]
D --> F{qkv_format?}
F -- thd --> G["ConvertTHDtoBSHD.apply()\ncustom_op with fake + autograd"]
F -- bshd/sbhd --> H["transpose / no-op"]
G --> I["BSHD attention core"]
H --> I
I --> J["FusedScaleMaskSoftmax\ncustom_op with fake + autograd"]
J --> K["bmm + context layer"]
K --> L{q_format?}
L -- thd --> M["ConvertBSHDtoTHD.apply(total_tokens_q)"]
L -- sbhd/bshd --> N["reshape / transpose"]
M --> O["Output tensor"]
N --> O
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["UnfusedDotProductAttention.forward(fp8, fp8_output, ...)"] --> B{fp8 or fp8_output?}
B -- Yes --> C["_forward_eager()\n@no_torch_dynamo — eager island"]
B -- No --> D["_forward() — fully compiled"]
C --> E["_forward() inside eager context"]
D --> F{qkv_format?}
F -- thd --> G["ConvertTHDtoBSHD.apply()\ncustom_op with fake + autograd"]
F -- bshd/sbhd --> H["transpose / no-op"]
G --> I["BSHD attention core"]
H --> I
I --> J["FusedScaleMaskSoftmax\ncustom_op with fake + autograd"]
J --> K["bmm + context layer"]
K --> L{q_format?}
L -- thd --> M["ConvertBSHDtoTHD.apply(total_tokens_q)"]
L -- sbhd/bshd --> N["reshape / transpose"]
M --> O["Output tensor"]
N --> O
|
| class ConvertBSHDtoTHD: | ||
| """ | ||
| Convert a tensor from qkv_format = bshd to qkv_format = thd. | ||
| """ | ||
|
|
||
| @staticmethod | ||
| def forward(ctx, bshd_tensor, cu_seqlens): | ||
| # pylint: disable=missing-function-docstring | ||
| num_tokens = cu_seqlens[-1] | ||
| max_seqlen = bshd_tensor.shape[1] | ||
| if not bshd_tensor.is_contiguous(): | ||
| bshd_tensor = bshd_tensor.contiguous() | ||
| thd_tensor = tex.convert_bshd_to_thd( | ||
| bshd_tensor, | ||
| cu_seqlens, | ||
| num_tokens, | ||
| ) | ||
| ctx.save_for_backward(cu_seqlens) | ||
| ctx.max_seqlen = max_seqlen | ||
| return thd_tensor | ||
| Thin wrapper around the `te_attention::convert_bshd_to_thd` custom op, | ||
| kept so that callsites can continue to use the `.apply(...)` syntax. | ||
| """ | ||
|
|
||
| @staticmethod | ||
| def backward(ctx, thd_tensor): | ||
| def apply(bshd_tensor, cu_seqlens, num_tokens): | ||
| # pylint: disable=missing-function-docstring | ||
| (cu_seqlens,) = ctx.saved_tensors | ||
| batch_size = cu_seqlens.shape[0] - 1 | ||
| if not thd_tensor.is_contiguous(): | ||
| thd_tensor = thd_tensor.contiguous() | ||
| bshd_tensor = tex.convert_thd_to_bshd( | ||
| thd_tensor, | ||
| cu_seqlens, | ||
| batch_size, | ||
| ctx.max_seqlen, | ||
| ) | ||
| return bshd_tensor, None | ||
| return torch.ops.te_attention.convert_bshd_to_thd(bshd_tensor, cu_seqlens, num_tokens) |
There was a problem hiding this comment.
API arity change not reflected in the class docstring
The docstring says "kept so that callsites can continue to use the .apply(...) syntax", but the signature changed from the old apply(bshd_tensor, cu_seqlens) (2 args) to the new apply(bshd_tensor, cu_seqlens, num_tokens) (3 args). Any caller not yet updated to pass num_tokens will silently get a TypeError at runtime. Since this class is not exported publicly this is contained, but the misleading comment increases maintenance risk.
Description
Make the
UnfusedDotProductAttentionbackend traceable bytorch.compile(fullgraph=True, mode="reduce-overhead"), so the forward and backward can be captured into CUDA graphs without graph breaks.Scope:
torch.library.custom_ops with fake impls and autograd bindings; remove an unbacked-SymInt.item()from the hot path ofConvertBSHDtoTHD.fp8=True(emulation) and/orfp8_output=True(Float8Tensor output, a tensor subclass that cannot cross a graph boundary) the backend runs as an eager island — the forward dispatches to atorch._dynamo.disable'd wrapper, the same mechanismDotProductAttentionandFusedAttentionuse module-wide. FP8 attention always involves delayed scaling regardless of the recipe: S and dP are produced inside the kernel, so their amax cannot be known before quantization and they use delayed-scaling quantizers even under Float8CurrentScaling (seeDPA.init_fp8_metadata) — and delayed scaling (Float8Quantizer, tensor scale/amax state) is not supported under torch.compile.Type of change
Changes
softmax.py/softmax.cpp:scaled_*_softmax_{forward,backward}as custom ops; C++ backward kernels allocate a fresh output buffer instead of writing in-place intooutput_grad(custom ops and cudagraph trees forbid input aliasing).utils.py:ConvertTHDtoBSHD/ConvertBSHDtoTHDas custom ops;num_tokenspassed by the caller instead ofcu_seqlens[-1].item().backends.py:UnfusedDotProductAttention.forwarddispatches FP8 calls to an eager (dynamo-disabled) wrapper; the non-FP8 path is traced with no graph breaks.tests/pytorch/test_torch_compile.py:test_unfused_dpa_torch_compile(5 qkv layouts, fullgraph + reduce-overhead, fwd+bwd captured into CUDA graphs and replayed).Checklist:
🤖 Generated with Claude Code