Skip to content

[common/PyTorch] Fused attention: pass real tensor strides to cuDNN (opt-in, v2 C API)#17

Open
pggPL wants to merge 3 commits into
mainfrom
fused_attn_real_strides
Open

[common/PyTorch] Fused attention: pass real tensor strides to cuDNN (opt-in, v2 C API)#17
pggPL wants to merge 3 commits into
mainfrom
fused_attn_real_strides

Conversation

@pggPL

@pggPL pggPL commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Problem

TE's cuDNN fused-attention integration loses tensor stride information at the pybind boundary: torch tensors are wrapped into NVTETensor (shape+ptr+dtype — no strides), and the C++ side reconstructs strides from the NVTE_QKV_Layout enum via hard-coded formulas (generateMatrixStrides). cudnn-frontend itself is stride-based (Tensor_attributes::set_stride; NVIDIA's own SDPA sample expresses bs3hd-interleaved QKV purely as strides).

Consequences of the enum channel:

  • Python must detect the layout enum by inspecting data_ptr()/storage_offset() (get_qkv_layout) — untraceable under torch.compile (gb0156) and pure per-step CPU overhead;
  • a mismatch between the declared enum and actual memory produces silently wrong results (no validation).

Change (2 commits)

1. Opt-in real-stride plumbing. The PyTorch extension reads the live at::Tensor strides of Q/K/V (and dO in backward), permutes them to cuDNN dim order [b,h,s,d], and the F16 arbitrary-seqlen backend uses them for the cuDNN graph set_stride instead of enum formulas — for dense (non-THD, non-paged) layouts. The graph/plan cache key (FADescriptor_v1) is extended with the stride values. Gated by NVTE_FUSED_ATTN_REAL_STRIDES=1 (default off → behavior unchanged).

2. Additive v2 C API. Strides travel as an explicit parameter, no hidden state:

typedef struct NVTEQKVStrides { const int64_t *q, *k, *v, *d_o; } NVTEQKVStrides;  // [b,h,s,d] each, NULL => enum-derived
void nvte_fused_attn_fwd_v2(..., NVTEQKVStrides qkv_strides, NVTETensor workspace, cudaStream_t stream);
void nvte_fused_attn_bwd_v2(..., NVTEQKVStrides qkv_strides, NVTETensor workspace, cudaStream_t stream);

nvte_fused_attn_fwd/bwd remain as thin wrappers calling _v2 with NULL strides — JAX and external callers are untouched byte-for-byte. (_v2 naming follows nvte_cublas_gemm_v2/nvte_quantize_v2.) Outputs (O, dQ/dK/dV) keep enum-derived strides. FP8/max-512 and THD/paged paths ignore the parameter (documented in the header).

Why this matters

With real strides, the enum no longer needs to encode memory geometry: q/k/v that are strided views into a packed buffer (bs3hd, sbh3d, kv-packed) work correctly even when declared as the plain separate layout. This removes the need for pointer-based layout detection in Python (torch.compile-hostile) and eliminates the silent-corruption failure mode.

Verification (RTX Ada sm89, bf16, F16 arbitrary-seqlen, fwd+bwd grads)

Case Result
flag OFF, contiguous (determinism baseline) bit-exact
flag ON, contiguous, declared separate bit-exact vs baseline
flag ON, packed [b,s,3,h,d] views declared separate bit-exact
flag OFF, packed views declared separate (status quo) wrong (max diff 4.97) — documents the hazard
flag ON, sbh3d views declared sbhd_sbhd_sbhd bit-exact
test_dot_product_attention (flag OFF and ON) 48 passed / 48 skipped
old symbols after wrapper conversion exported; flag-OFF parity across full suite

New focused test: tests/pytorch/attention/test_fused_attn_real_strides.py (5 tests).

Notes / limitations

  • THD/ragged and paged-KV intentionally keep the enum path (strides cannot express ragged memory; cu_seqlens/ragged offsets remain the channel there).
  • dQ/dK/dV allocation still follows the enum layout (pre-existing behavior).
  • Opt-in for now; flipping the default (and then removing Python-side layout detection) is follow-up work.

🤖 Generated with Claude Code

Flag lifecycle (commitment)

NVTE_FUSED_ATTN_REAL_STRIDES is a transitional rollout vehicle, scheduled for removal:

  1. now: default OFF (byte-for-byte legacy behavior; opt-in for torch.compile users),
  2. after bake time on CI/other archs: flip default to ON,
  3. remove the flag entirely and make real strides unconditional for dense F16 layouts; delete the Python-side pointer-based layout detection (get_qkv_layout) at the same time.

The code is structured so step 3 is mechanical: the flag is read in exactly two places (the pybind glue in attention.cpp and one Python gate helper in dot_product_attention.py) — removal deletes those two checks and the detection path they guard. Nothing else depends on the flag.

Rationale for not shipping always-on immediately: today's detection also normalizes odd stride patterns via a .contiguous() retry; with detection gone those patterns reach cuDNN raw (loud plan-build failure, not corruption — but a new input class that deserves bake time), and the cuDNN support surface for general strides across archs/versions is broader than the enum set we could test on sm89.

pggPL and others added 2 commits July 7, 2026 14:57
cuDNN fused attention reconstructs Q/K/V strides from the NVTE_QKV_Layout
enum (generateMatrixStrides), so the enum has to encode memory geometry and
Python must detect it by inspecting data_ptr, which is torch.compile-hostile.
cudnn-frontend itself is stride-based, so pass the real torch strides instead.

Prototype, opt-in via NVTE_FUSED_ATTN_REAL_STRIDES=1 (default off = old
behavior):

- new experimental C API nvte_fused_attn_set_strides(q, k, v, dO): a
  thread-local side channel carrying strides in cuDNN dim order [b, h, s, d];
  NULL clears it. No NVTETensor/NVTEShape ABI change.
- F16 arbitrary-seqlen fwd/bwd graph builders use the provided strides for
  the Q/K/V (and dO in bwd) set_stride instead of the enum-derived ones.
  Dense (non-THD, non-paged) layouts only; O and dQ/dK/dV outputs keep the
  enum-derived strides since TE allocates them.
- FADescriptor_v1 gains a real_strides array included in operator< so cached
  plans built for different strides are not reused.
- PyTorch extension reads q/k/v (and dO) .strides() from the incoming
  at::Tensors, permutes them from bshd/sbhd/bhsd torch order to [b, h, s, d],
  sets the side channel around the nvte_fused_attn_fwd/bwd calls and clears
  it afterwards.

Verified on sm89 (bf16, b=2 s=128 h=8 d=64, no_mask, deterministic bwd):
packed bs3hd/sbh3d views passed with the *separate* layout enum
(bshd_bshd_bshd / sbhd_sbhd_sbhd) are bit-exact vs contiguous baselines in
forward and backward with the flag on, and wrong with the flag off (enum
strides mismatch memory), demonstrating the enum no longer needs to encode
memory geometry. test_dot_product_attention passes with the flag off and on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…licit v2 API

Convert the real-stride prototype plumbing to a production shape: instead of
the thread-local nvte_fused_attn_set_strides side channel, thread the strides
through an explicit, additive C API.

- new C struct NVTEQKVStrides {q, k, v, d_o}: each member points to 4 int64
  element-strides in cuDNN dim order [b, h, s, d], or is NULL to derive the
  strides from the NVTE_QKV_Layout enum as before.
- new exported functions nvte_fused_attn_fwd_v2 / nvte_fused_attn_bwd_v2:
  identical to the v1 entry points plus an NVTEQKVStrides parameter. The old
  nvte_fused_attn_fwd/bwd become thin wrappers calling _v2 with all-NULL
  strides, so existing callers (JAX, external) are untouched and behavior is
  unchanged.
- strides-first core: the parameter is threaded explicitly through
  fused_attn_arbitrary_seqlen_fwd/bwd into the F16 arbitrary-seqlen
  fwd/bwd_impl graph builders (no globals). Provided strides are used for the
  Q/K/V (and dO in bwd) set_stride on dense (non-THD, non-paged) layouts;
  otherwise the enum-derived strides are used exactly as before. Outputs
  (O, dQ/dK/dV) keep enum-derived strides since TE allocates them. FP8 and
  max-512 backends ignore the parameter (documented in the header).
- FADescriptor_v1 keeps the real_strides cache-key extension, now populated
  from the threaded parameter; the RealStrideOverride TLS machinery and
  nvte_fused_attn_set_strides are removed entirely. Since the strides are an
  explicit argument, both phases of the two-phase (workspace-size + execute)
  call sequence trivially receive the same values.
- PyTorch extension builds NVTEQKVStrides from the incoming at::Tensor
  strides (bshd/sbhd/bhsd -> [b, h, s, d] permutation as before) and calls
  _v2 directly; still gated by NVTE_FUSED_ATTN_REAL_STRIDES=1 (default off).
- add tests/pytorch/attention/test_fused_attn_real_strides.py covering
  flag-on == flag-off for contiguous inputs, bit-exact packed bs3hd/sbh3d
  strided views declared with separate layout enums, and the unchanged
  packed-enum path.

Verified on sm89: full prototype matrix bit-exact (packed views declared
separate == contiguous baselines, fwd+bwd; negative control still wrong with
the flag off), test_dot_product_attention 48 passed/48 skipped with the flag
off and on, and old C symbols still exported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
@pggPL pggPL requested a review from cyanguwa as a code owner July 7, 2026 13:18
…op the opt-in flag

Remove NVTE_FUSED_ATTN_REAL_STRIDES: the PyTorch extension now always
passes the live torch strides of Q/K/V (and dO) to the cuDNN
fused-attention graphs for plain 4-D torch tensors; quantized tensors,
THD and non-4D inputs silently fall back to NULL strides (enum-derived).
The v2 C API contract (NVTEQKVStrides, NULL = enum-derived) is unchanged.

With real strides always on, DotProductAttention skips the pointer-based
(untyped_storage().data_ptr() / storage_offset()) qkv layout detection
for dense bshd/sbhd non-FP8 inputs and declares the format-derived
separate layout instead. This removes the torch.compile graph breaks on
UntypedStorage.data_ptr inside get_qkv_layout and the per-step CPU
overhead of the detection. The old detection's stride normalization is
preserved: tensors with stride(-1) != 1 are made contiguous in the
bypass path (plain tensor metadata, still traceable). thd, FP8 DPA and
KV-caching paths keep the existing detection since those backends do not
consume real strides.

Tests: DPA-level bit-exactness of packed-QKV strided views vs separate
contiguous tensors (fused and flash backends), stride(-1) normalization,
thd detection retention, and a torch.compile graph-break check with a
forced-detection negative control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant