Skip to content
Merged
22 changes: 22 additions & 0 deletions python/cudnn/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,28 @@ neither names the thing that breaks it most directly: a device-to-host read.
hand you — or assert in-kernel. Reading lengths back to decide whether to
raise buys nothing: the Router had to choose an engine before any buffer
existed.
- **Prove it; do not grep for it.** The list above is a reminder, not a
detector — the spellings are many (`int(cu[i])` on a CUDA tensor is a
blocking copy that a search for `.item()` will not find) and a reviewer who
greps a subset concludes "clean". Assert the property instead:

```python
torch.cuda.set_sync_debug_mode("error") # any blocking D2H now raises
try:
out.backward(grad) # or graph.execute(...)
finally:
torch.cuda.set_sync_debug_mode("default")
```

Put that in a test (see `test_varlen_backward_does_not_sync`), and check the
test is RED against the old code before trusting it — a sync test that was
never seen to fail is asserting nothing.
- **Suspect duplicated logic first.** Every violation found so far has been a
*second* copy of a conversion that was already device-side somewhere else:
the packed-to-padded LSE repad existed in both `sdpa/fwd/torch_op.py` (with
`searchsorted`, device-side) and `torch/sdpa_provider.py` (a `for i in
range(B): int(cu[i])` loop). Extract the correct one and call it from both
rather than writing the obvious loop again.

Known violations, all pre-existing and each needing a kernel-side change, so
none is precedent:
Expand Down
11 changes: 11 additions & 0 deletions python/cudnn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,17 @@ def __getattr__(name: str) -> Any:
globals()["jax"] = _jax
return _jax

if name == "torch":
# `import cudnn; cudnn.torch.install()` works like `import cudnn.torch`,
# mirroring the `jax` branch above. Deferred so `import cudnn` never
# eagerly imports torch; the submodule raises its own descriptive error
# when torch (or the 2.13+ flash-impl registry) is unavailable — which
# is why this is NOT a _LAZY_OPTIONAL_IMPORTS entry: that path would
# blame the `[cutedsl]` extra for a missing framework.
_torch_mod = importlib.import_module(".torch", __name__)
globals()["torch"] = _torch_mod
return _torch_mod

if name == "fla":
# `import cudnn; cudnn.fla.accelerate_fla()` works like `import cudnn.fla`.
# Deferred so `import cudnn` never eagerly imports torch / the FLA shim.
Expand Down
345 changes: 286 additions & 59 deletions python/cudnn/sdpa/fwd/torch_op.py

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions python/cudnn/torch/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""PyTorch integration for the cuDNN frontend Python API.

Importing this package registers the ``"CUDNN"`` provider with
``torch.nn.attention``'s flash-attention implementation registry
(PyTorch 2.13+, the same mechanism FA3/FA4 use). Registration is passive —
activation stays explicit:

import cudnn.torch
torch.nn.attention.activate_flash_attention_impl("CUDNN")

After activation, ``F.scaled_dot_product_attention`` under
``sdpa_kernel([SDPBackend.CUDNN_ATTENTION])`` and
``torch.nn.attention.varlen.varlen_attn`` run on the cuDNN *Python* API
(pygraph + engine Router: FROST OSS kernels or cuDNN-backend engines), with
hybrid fallback to the existing implementations for configurations the
python path does not serve yet. ``restore_flash_attention_impl()`` reverts.

On torch < 2.13 (no registry), ``cudnn.torch.install()`` applies the
``F.scaled_dot_product_attention`` overrides directly.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"""

from cudnn.torch.sdpa_provider import calls, install, served_plan_names # noqa: F401
310 changes: 310 additions & 0 deletions python/cudnn/torch/sdpa_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""The "CUDNN" torch.nn.attention provider: torch.sdpa on the cuDNN Python API.

Routes PyTorch's cuDNN SDPA backend through the cudnn-frontend Python API
instead of the vendored C++ frontend. Overrides the CUDA dispatch-key kernels
of

aten::_scaled_dot_product_cudnn_attention
aten::_scaled_dot_product_cudnn_attention_backward

with Python implementations that call the cudnn-frontend Python API custom ops
(``torch.ops.cudnn.sdpa_fwd`` / ``sdpa_bwd`` from ``cudnn.sdpa.fwd.torch_op``).
The native Autograd wrapper of the aten op is untouched: it saves our forward's
outputs and routes grad through the (also overridden) aten backward, so vanilla

with sdpa_kernel([SDPBackend.CUDNN_ATTENTION]):
F.scaled_dot_product_attention(q, k, v, is_causal=True)

transparently runs on the Python API after ``install()``.

Conveniently, aten's logsumexp convention for this op is (B, H, S, 1) float32
(keepdim) — bit-identical in layout to cuDNN's Stats tensor, so tensors cross
the boundary with no reshape or copy.

Hybrid fallback to the C++ worker ops (bit-exact with the shadowed native
kernel): attn_bias, dropout_p > 0, and the padded dense backward (per-batch
lengths). Dense and varlen backward both run on the python API. The forward runs on the python API
either way, so training still exercises the python fwd path.
"""

import math
from typing import Optional

import torch

# Importing this module registers torch.ops.cudnn.sdpa_fwd / sdpa_bwd.
import cudnn.sdpa.fwd.torch_op as _cudnn_ops # noqa: F401

_lib: Optional[torch.library.Library] = None

# Observability for tests: how many aten calls the bridge served on the
# python API vs fell back (cpp = C++ worker ops; fa2 = flash varlen kernels).
calls = {"fwd": 0, "bwd": 0, "fwd_cpp": 0, "bwd_cpp": 0, "fwd_fa2": 0, "bwd_fa2": 0}


def _fwd(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_bias: Optional[torch.Tensor],
compute_log_sumexp: bool,
dropout_p: float = 0.0,
is_causal: bool = False,
return_debug_mask: bool = False,
*,
scale: Optional[float] = None,
):
if attn_bias is not None or dropout_p != 0.0 or return_debug_mask:
# Not wired in the python path yet — fall back to the C++ implementation
# through the (un-shadowed) worker op. Bit-exact with the native kernel.
calls["fwd_cpp"] += 1
return torch.ops.aten._cudnn_attention_forward(
query, key, value, attn_bias, None, None,
query.size(-2), key.size(-2), compute_log_sumexp,
dropout_p, is_causal, return_debug_mask, scale=scale,
) # fmt: skip

calls["fwd"] += 1
attn_scale = scale if scale is not None else 1.0 / math.sqrt(query.size(-1))

# Below-autograd call: runs the raw CUDA impl (graph-cached cuDNN execute).
o, stats = torch.ops.cudnn.sdpa_fwd(query, key, value, attn_scale, is_causal=is_causal, return_lse=compute_log_sumexp)

# aten contract: (output, logsumexp(B,H,S,1) f32, cum_seq_q, cum_seq_k,
# max_q, max_k, philox_seed, philox_offset, debug_attn_mask)
philox_seed = torch.zeros((), dtype=torch.long, device=query.device)
philox_offset = torch.zeros((), dtype=torch.long, device=query.device)
return (o, stats, None, None, query.size(-2), key.size(-2), philox_seed, philox_offset, None)


def _bwd(
grad_out: torch.Tensor,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
out: torch.Tensor,
logsumexp: torch.Tensor,
philox_seed: torch.Tensor,
philox_offset: torch.Tensor,
attn_bias: Optional[torch.Tensor],
cum_seq_q: Optional[torch.Tensor],
cum_seq_k: Optional[torch.Tensor],
max_q: int,
max_k: int,
dropout_p: float,
is_causal: bool,
*,
scale: Optional[float] = None,
):
# Anything cudnn::sdpa_bwd does not serve goes to the C++ worker op
# (bit-exact with the shadowed native kernel): attention bias, dropout,
# and the padded dense path (per-batch lengths). Everything else runs on
# the python API, closing the last C++ hop in a dense training step.
if attn_bias is not None or dropout_p > 0.0 or cum_seq_q is not None:
calls["bwd_cpp"] += 1
return torch.ops.aten._cudnn_attention_backward(
grad_out, query, key, value, out, logsumexp,
philox_seed, philox_offset, attn_bias, cum_seq_q, cum_seq_k,
max_q, max_k, dropout_p, is_causal, scale=scale,
) # fmt: skip

calls["bwd"] += 1
attn_scale = scale if scale is not None else query.shape[-1] ** -0.5
# aten hands us logsumexp as (B, H, S) fp32 — exactly the layout the dense
# backward wants, modulo the trailing 1 the descriptor declares.
lse = logsumexp if logsumexp.dim() == 4 else logsumexp.unsqueeze(-1)
return torch.ops.cudnn.sdpa_bwd(
grad_out, query, key, value, out, lse, attn_scale,
is_causal=is_causal,
is_deterministic=torch.are_deterministic_algorithms_enabled(),
) # fmt: skip


def install() -> None:
"""Register the overrides (idempotent per process: last registration wins)."""
global _lib
if _lib is None:
_lib = torch.library.Library("aten", "IMPL")
_lib.impl("_scaled_dot_product_cudnn_attention", _fwd, "CUDA")
_lib.impl("_scaled_dot_product_cudnn_attention_backward", _bwd, "CUDA")


# ---------------------------------------------------------------------------
# varlen_attn (THD) via the cuDNN python API
#
# torch.nn.attention.varlen.varlen_attn routes to flash kernels in 2.13 (its
# in-tree cuDNN branch is dead: `_should_use_cudnn` is hardcoded False, and
# its `_cudnn_attention_backward` call predates the 2.13 schema). We hook one
# level up instead: override the `torch_attn::_varlen_attn{,_backward}`
# custom ops at the CUDA key. Their autograd wiring is untouched; unlike the
# dead branch we also serve GQA and causal sliding windows.
# ---------------------------------------------------------------------------


def _norm_window(window_size):
ws = list(window_size) if window_size is not None else [-1, -1]
if len(ws) != 2:
raise ValueError(f"window_size must have length 2, got {len(ws)}")
return ws


def _fa_window_left_to_cudnn(w: int) -> int:
"""FA2 window_size=(w, 0) attends to [i-w, i] — w tokens back PLUS self.
cuDNN's diagonal_band_left_bound=lb masks j <= i-lb, i.e. lb visible
tokens including self. So lb = w + 1."""
return w + 1 if w >= 0 else -1


def _varlen_supported(ws, seqused_k=None, block_table=None, num_splits=None) -> bool:
"""Configs the cudnn python varlen path serves today; everything else falls
back to the flash kernels (exactly what the stock op body runs)."""
if seqused_k is not None or block_table is not None: # paged KV not wired yet
return False
if num_splits is not None and num_splits != 1:
return False
# left-window + causal only; asymmetric/right bounds pending window_right in sdpa_*_ex
return ws[1] in (-1, 0) and not (ws[0] >= 0 and ws[1] != 0)


def _varlen_fwd_flash(query, key, value, cu_seq_q, cu_seq_k, max_q, max_k, is_causal, scale, ws, seqused_k, block_table, num_splits):
calls["fwd_fa2"] += 1
output, softmax_lse, _rng, _, _ = torch.ops.aten._flash_attention_forward(
query, key, value, cu_seq_q, cu_seq_k, max_q, max_k, 0.0, is_causal,
return_debug_mask=False, scale=scale,
window_size_left=ws[0], window_size_right=ws[1],
seqused_k=seqused_k, block_table=block_table, num_splits=num_splits,
) # fmt: skip
rng_state = torch.zeros((2,), dtype=torch.uint64, device=query.device)
return output, softmax_lse, rng_state


def _varlen_fwd(query, key, value, cu_seq_q, cu_seq_k, max_q, max_k, is_causal=False, scale=None, window_size=None, enable_gqa=False, seqused_k=None, block_table=None, num_splits=None,): # fmt: skip
ws = _norm_window(window_size)
if not _varlen_supported(ws, seqused_k, block_table, num_splits):
return _varlen_fwd_flash(query, key, value, cu_seq_q, cu_seq_k, max_q, max_k, is_causal, scale, ws, seqused_k, block_table, num_splits)
is_causal = is_causal or ws[1] == 0

calls["fwd"] += 1
attn_scale = scale if scale is not None else query.shape[-1] ** -0.5
o, stats = torch.ops.cudnn.sdpa_fwd(
query, key, value, attn_scale,
is_causal=is_causal, window_left=_fa_window_left_to_cudnn(ws[0]),
cu_seqlens_q=cu_seq_q, cu_seqlens_kv=cu_seq_k,
max_seqlen_q=max_q, max_seqlen_kv=max_k, return_lse=True,
) # fmt: skip
lse = stats.squeeze(-1).transpose(0, 1).contiguous() # (T,H,1) -> (H,T) flash convention
rng_state = torch.zeros((2,), dtype=torch.uint64, device=query.device)
return o, lse, rng_state


def _varlen_fwd_out(out, query, key, value, cu_seq_q, cu_seq_k, max_q, max_k, is_causal=False, scale=None, window_size=None, enable_gqa=False, seqused_k=None, block_table=None, num_splits=None,): # fmt: skip
"""torch_attn::_varlen_attn_out — same as fwd but writes into `out`; returns lse."""
ws = _norm_window(window_size)
if not _varlen_supported(ws, seqused_k, block_table, num_splits):
calls["fwd_fa2"] += 1
return torch.ops.aten._flash_attention_forward_no_dropout_inplace(
out, query, key, value, cu_seq_q, cu_seq_k, max_q, max_k, 0.0, is_causal,
False, scale=scale, window_size_left=ws[0], window_size_right=ws[1],
seqused_k=seqused_k, block_table=block_table, num_splits=num_splits,
) # fmt: skip
o, lse, _rng = _varlen_fwd(
query, key, value, cu_seq_q, cu_seq_k, max_q, max_k,
is_causal=is_causal, scale=scale, window_size=window_size, enable_gqa=enable_gqa,
seqused_k=seqused_k, block_table=block_table, num_splits=num_splits,
) # fmt: skip
out.copy_(o)
return lse


def _varlen_bwd(grad_out, query, key, value, out, lse, cu_seq_q, cu_seq_k, max_q, max_k, is_causal, rng_state, scale=None, window_size=None,): # fmt: skip
ws = _norm_window(window_size)
if not _varlen_supported(ws):
calls["bwd_fa2"] += 1 # fwd for this config ran flash too (same predicate)
unused = torch.empty(0, device=query.device)
dq, dk, dv = torch.ops.aten._flash_attention_backward(
grad_out, query, key, value, out, lse, cu_seq_q, cu_seq_k, max_q, max_k,
0.0, is_causal, rng_state, unused, scale=scale,
window_size_left=ws[0], window_size_right=ws[1],
) # fmt: skip
return dq, dk, dv
Comment on lines +222 to +232

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

_varlen_bwd can route to cuDNN after the forward pass ran on FlashAttention.

_varlen_supported accepts seqused_k, block_table, and num_splits. Line 231 calls it with ws only, so those three default to None and the predicate returns True. The comment claims the same predicate ran in the forward pass. That is not correct: _varlen_fwd at Line 193 passes all four arguments and falls back to flash when block_table or seqused_k is set.

For a paged-KV or split forward pass, the backward pass then runs the cuDNN THD kernel over key/value that hold paged blocks. The gradients are wrong, or the operator fails.

The backward op schema does not carry block_table. Detect the mismatch instead. One option: record the routing decision in rng_state, which the forward pass already returns and the backward pass already receives. Another option: raise an explicit error for the unsupported case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cudnn/torch/sdpa_provider.py` around lines 229 - 239, Update
_varlen_bwd so its backend selection matches _varlen_fwd when paged-KV or split
execution is involved; do not call _varlen_supported with only ws, since the
backward schema lacks block_table. Reuse routing state carried through rng_state
to detect that forward used FlashAttention and dispatch the corresponding
backward path, or explicitly reject unsupported mismatches instead of invoking
the cuDNN THD kernel.

is_causal = is_causal or ws[1] == 0

calls["bwd"] += 1
attn_scale = scale if scale is not None else query.shape[-1] ** -0.5
# (H, T) packed -> (B, H, max_q, 1) padded: the backend rejects ragged LSE
# for bprop THD on SM8X/SM12X, so the bwd op takes the padded layout.
# (H, T) -> (T, H) for the shared device-side repad. The naive
# `for i in range(B): int(cu_seq_q[i])` loop that used to live here was
# 2*B blocking D2H copies per backward call, before the kernel even
# launched — an async-launch API turned synchronous, and un-capturable
# (python/cudnn/AGENTS.md Rule 3).
lse_padded = _cudnn_ops.thd_lse_to_padded(lse.transpose(0, 1), cu_seq_q, max_q)
dq, dk, dv = torch.ops.cudnn.sdpa_bwd(
grad_out, query, key, value, out, lse_padded, attn_scale,
is_causal=is_causal, window_left=_fa_window_left_to_cudnn(ws[0]),
cu_seqlens_q=cu_seq_q, cu_seqlens_kv=cu_seq_k,
max_seqlen_q=max_q, max_seqlen_kv=max_k,
is_deterministic=torch.are_deterministic_algorithms_enabled(),
) # fmt: skip
return dq, dk, dv


# ---------------------------------------------------------------------------
# torch.nn.attention flash-impl registry integration (PyTorch 2.13+)
#
# The same mechanism FA3/FA4 use: activation registers python overrides of
# existing CUDA kernels; restore drops the Library handles to deregister.
#
# import cudnn.torch # registers "CUDNN" (no activation)
# torch.nn.attention.activate_flash_attention_impl("CUDNN")
# ---------------------------------------------------------------------------


class _RegistryHandle:
def __init__(self, *libs: torch.library.Library):
self._libs = list(libs)

def remove(self) -> None:
for lib in self._libs:
lib._destroy()
self._libs = []
Comment on lines +266 to +273

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

PyTorch 2.13 register_flash_attention_impl register_fn handle remove torch.library.Library _destroy

💡 Result:

In PyTorch, the register_flash_attention_impl function and its associated mechanisms are part of the Scaled Dot Product Attention (SDPA) registry system [1][2]. They are designed for SDPA backend providers to register and manage custom kernels with the PyTorch dispatcher [2][3]. Key components of this system include: 1. register_flash_attention_impl: This function registers a callable (register_fn) that performs the actual dispatcher registration for a specific flash attention implementation (e.g., "FA4") [2][3]. 2. register_fn: When invoked (via activate_flash_attention_impl), this callable performs the registration with the PyTorch dispatcher [3]. It may optionally return a FlashAttentionHandle object [2][3]. 3. handle and remove: The FlashAttentionHandle is used to maintain the state of the registered implementation [2][3]. If a handle is returned, the registry stores it [3]. When restore_flash_attention_impl is called to revert to the default implementation, the system calls the remove() method on the active handle to clean up or unregister the custom kernel [4]. Regarding torch.library.Library and _destroy: While torch.library.Library is a core PyTorch mechanism for registering custom operators and kernels with the dispatcher, it manages registrations and their lifetimes automatically via handle objects [5]. The internal _destroy method (or similar lifecycle cleanup methods) is typically managed by the internal C++ dispatch layer to ensure that when a library object goes out of scope or is explicitly removed, the associated registrations are destroyed or disabled to prevent dangling pointers or invalid states [4][6]. Users generally do not interact with _destroy directly; rather, they use the high-level Python API (like handle.remove()) or manage the scope of the torch.library.Library object [5][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target file context ---'
sed -n '220,310p' python/cudnn/torch/sdpa_provider.py
printf '%s\n' '--- related registry usage ---'
rg -n -C 4 'register_flash_attention_impl|restore_flash_attention_impl|_RegistryHandle|_destroy' python test
printf '%s\n' '--- repository references ---'
rg -n 'torch(==|>=|~=)|2\.13|PyTorch' pyproject.toml setup.cfg setup.py requirements* .github 2>/dev/null || true

Repository: NVIDIA/cudnn-frontend

Length of output: 9137


🌐 Web query:

site:github.com/pytorch/pytorch torch/nn/attention/_registry.py register_flash_attention_impl 2.13 _destroy Library

💡 Result:

In PyTorch, the function register_flash_attention_impl, located in torch/nn/attention/_registry.py, is used by Scaled Dot Product Attention (SDPA) backend providers to register their flash attention implementations with the PyTorch dispatcher [1][2]. Regarding the "_destroy" reference in your query, the current implementation of the flash attention registry in PyTorch does not include a function named _destroy [1][2]. Instead, the documentation explicitly notes that registered implementations are kept alive for the lifetime of the process, stating: "the registry keeps that handle alive for the lifetime of the process (until explicit uninstall support exists)" [1][3][4]. This implies that there is currently no public or internal "_destroy" mechanism for these library handles, consistent with the documented lack of explicit uninstall support [1][3]. If you are encountering a reference to a _destroy method, it may be part of an internal, undocumented, or experimental API, or it could be a misunderstanding of the current design where library lifecycles are managed for the duration of the process [1].

Citations:


🌐 Web query:

site:docs.pytorch.org/docs/2.13 torch.library.Library _destroy register_flash_attention_impl

💡 Result:

The terms mentioned, torch.library.Library and register_flash_attention_impl, are associated with PyTorch's extension and kernel registration mechanisms, though they do not constitute a single standard public API workflow [1][2]. torch.library.Library is a PyTorch class used in the C++ and Python APIs for defining and registering custom operators into the PyTorch dispatcher [3]. It is typically used by extension authors to integrate custom operations (like specialized kernels) so they can be dispatched through standard PyTorch functions. The _destroy method is generally used internally by the PyTorch library system to clean up or unregister these library objects when they are no longer needed, ensuring that the dispatcher state remains consistent. register_flash_attention_impl is an internal or experimental function related to the registration of Flash Attention kernels [1]. In recent PyTorch versions (such as those involving FlexAttention), Flash Attention implementations can be registered as part of the attention backend system [1]. This allows the user to select specific kernel implementations (e.g., "FLASH", "TRITON") for attention operations. In summary: 1. torch.library.Library: A core utility for registering custom operators and kernels with the PyTorch dispatcher. The _destroy method is an internal cleanup mechanism for these libraries. 2. register_flash_attention_impl: An internal function used to register or provide implementations for Flash Attention within PyTorch's attention handling framework [1]. These components are typically utilized by developers building high-performance kernels or extending PyTorch's operator dispatch system, rather than by end-users writing standard neural network models [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

for ref in v2.13.0 main; do
  printf '\n--- torch/nn/attention/_registry.py (%s) ---\n' "$ref"
  curl -fsSL "https://raw.githubusercontent.com/pytorch/pytorch/${ref}/torch/nn/attention/_registry.py" \
    | rg -n -C 8 'register_flash_attention_impl|restore_flash_attention_impl|remove\(|active|lifetime|uninstall' || true
  printf '\n--- torch/library.py Library lifecycle (%s) ---\n' "$ref"
  curl -fsSL "https://raw.githubusercontent.com/pytorch/pytorch/${ref}/torch/library.py" \
    | rg -n -C 8 'class Library|def _destroy|def __del__|def destroy' || true
done

Repository: NVIDIA/cudnn-frontend

Length of output: 10165


🌐 Web query:

"until explicit uninstall support exists" "register_flash_attention_impl"

💡 Result:

The phrase "until explicit uninstall support exists" appears in the documentation for the PyTorch function torch.nn.attention.activate_flash_attention_impl [1][2]. In the context of registering and activating custom Scaled Dot Product Attention (SDPA) backends in PyTorch, when a backend's register_flash_attention_impl callable returns a FlashAttentionHandle, the PyTorch registry keeps that handle alive for the lifetime of the process [1][3]. The note "(until explicit uninstall support exists)" is included in the documentation to clarify that, currently, there is no mechanism to explicitly uninstall or remove these registered handles once they have been activated for the duration of the process [1][4]. This design ensures that necessary state for the flash attention implementation remains available for the process's lifetime, but it also reflects a limitation in the current registration API where backend providers cannot dynamically remove their implementations [1][5].

Citations:


Guard the use of torch.library.Library._destroy(). PyTorch 2.13 defines this private method, while the registry only requires the handle to implement remove(). A future release can change or remove _destroy(), causing restore_flash_attention_impl() to fail. Add version-gated compatibility coverage or use a supported lifecycle API when available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cudnn/torch/sdpa_provider.py` around lines 273 - 280, Update
_RegistryHandle.remove to avoid unconditionally calling the private
Library._destroy method; use a supported lifecycle API when available and gate
the private fallback by the relevant PyTorch version or attribute check.
Preserve remove() as a safe no-op after clearing registrations, and ensure
restore_flash_attention_impl() remains compatible with releases where _destroy
is absent.



def _registry_register() -> _RegistryHandle:
import cudnn.sdpa.fwd.torch_op # noqa: F401 — registers cudnn::sdpa_fwd / sdpa_bwd

lib = torch.library.Library("aten", "IMPL")
lib.impl("_scaled_dot_product_cudnn_attention", _fwd, "CUDA")
lib.impl("_scaled_dot_product_cudnn_attention_backward", _bwd, "CUDA")
vlib = torch.library.Library("torch_attn", "IMPL")
from torch.nn.attention import varlen as _varlen_mod # noqa: F401 — ensure torch_attn ops are defined

vlib.impl("_varlen_attn", _varlen_fwd, "CUDA")
vlib.impl("_varlen_attn_out", _varlen_fwd_out, "CUDA")
vlib.impl("_varlen_attn_backward", _varlen_bwd, "CUDA")
return _RegistryHandle(lib, vlib)


def _register_with_torch() -> None:
try:
from torch.nn.attention import register_flash_attention_impl
except ImportError:
return # torch < 2.13: use install() directly
register_flash_attention_impl("CUDNN", register_fn=_registry_register)


_register_with_torch()


def served_plan_names() -> list:
"""Which execution plan served each cached graph (debug/reporting)."""
names = []
for graph, _ws in _cudnn_ops._graph_cache.values():
try:
names.append(graph.get_plan_name_at_index(graph._plan_index))
except Exception as e: # noqa: BLE001
names.append(f"<unavailable: {e}>")
return names
Comment on lines +302 to +310

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

served_plan_names reports only the experimental forward cache.

The function iterates _cudnn_ops._fprop_cache, which belongs to cudnn.experimental.ops.sdpa. After registry activation, the varlen forward path runs torch.ops.cudnn.sdpa_fwd from cudnn.sdpa.fwd.torch_op and populates _graph_cache in that module. Those plans never appear in the returned list. A caller that debugs a varlen run gets an empty or misleading result.

Iterate both caches, and label each entry with its source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cudnn/torch/sdpa_provider.py` around lines 309 - 317, Update
served_plan_names to inspect both _cudnn_ops._fprop_cache and the _graph_cache
used by cudnn.sdpa.fwd.torch_op, including plans from each cache in the result.
Label every reported plan with its cache/source so experimental and
registry-backed forward entries are distinguishable.

Loading
Loading