Skip to content

Fix ragged SDPA backward workspace under-allocation for non-token-major stats layouts - #462

Merged
YangXu1990uiuc merged 2 commits into
NVIDIA:developfrom
YangXu1990uiuc:yanxu/fix-ragged-workspace-size-pr
Aug 7, 2026
Merged

Fix ragged SDPA backward workspace under-allocation for non-token-major stats layouts#462
YangXu1990uiuc merged 2 commits into
NVIDIA:developfrom
YangXu1990uiuc:yanxu/fix-ragged-workspace-size-pr

Conversation

@YangXu1990uiuc

@YangXu1990uiuc YangXu1990uiuc commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

What

A ragged (THD) sdpa_backward graph that sets max_total_seq_len_q silently returns all-zero dQ/dK/dV when the Stats tensor is head-major. The forward O and stats are correct and no error is raised, so this corrupts training without ever surfacing. Present unchanged in 9.22, 9.23, 9.24, 9.25 and 9.26, on both sm90 and sm100.

Head-major [h, total_q] is FlashAttention's and PyTorch varlen's softmax_lse layout, so this is reachable from exactly the integrations that would reach for max_total_seq_len in the first place.

Root cause

max_total_seq_len_* is documented as a workspace-sizing hint. It is not — setting it makes CompositeSDPABackwardNode re-lay-out its non-virtual intermediates onto the corresponding user tensor's stride and ragged offset, and then size them from a single stride element:

softmax_sum->set_stride(Stats->get_stride());
softmax_sum->set_ragged_offset(Stats->get_ragged_offset());
softmax_sum_size = max_total_seq_len_q.value() * (Stats->get_stride())[2] * sizeof(float);

stride[2] is the sequence stride, so the formula assumes the per-token footprint is stride[2]. That is true only for token-major [t, h, 1] stats, where stride[2] == h_q. For head-major [h, t] stats stride[2] == 1, and the buffer is under-allocated by exactly h_q.

softmax_sum then writes (h_q - 1) * t_q * 4 bytes past its slot. The frontend places the backend engine's own workspace immediately after the FE region (graph_interface.h: engine_workspace = workspace + fe_workspace_size), and the first word of that region is the persistent scheduler's tile counter. Every CTA claims work with tile_id = atomicAdd(tile_id_counter, 1) and retires unless tile_id < num_tiles; tile_id is uint32_t, so any nonzero float bit pattern is astronomically out of range. Every CTA therefore retires without storing anything — no dQ accumulation, no dK/dV TMA store — and the gradients are simply never written. No fault, no error, no hang.

Measured directly, comparing the exact int32 bits of the clobbered word against the true softmax_sum value at the same element from a clean run at the same geometry:

observed at engine workspace[0] = -1067984384   (f32 -3.3726807)
true softmax_sum(b=0, h=1, s=0) = -1067984512   (f32 -3.3726501)
difference                      = +128

A float bit pattern plus exactly 128 integer increments — one atomicAdd per CTA, every one of which then failed the tile bound and retired.

The mask plays no part in the trigger: TOP_LEFT and the completely unmasked case fail identically. It only decides whether the clobbering bytes happen to be zero — a fully-masked leading query row has row-sum 0, which is what the memset already put there. That is why a handful of geometries appear to work, and why one intermediate case produces |dQ| ≈ 6e36 garbage rather than zeros.

The fix (commit 1)

Size the workspace from every axis instead of from stride[2] alone:

static int64_t
ragged_workspace_size(int64_t max_total_seq_len,
                      std::vector<int64_t> const& dim,
                      std::vector<int64_t> const& stride,
                      int64_t elem_size) {
    return ((max_total_seq_len - 1) * stride[2] + (dim[1] - 1) * stride[1] + (dim[3] - 1) * stride[3] + 1) *
           elem_size;
}

applied to softmax_sum, dQ_accum, dK_fullhead and dV_fullhead.

For token-major inputs the new expression is bit-identical to the old one — softmax_sum gives (maxT-1)·h_q + (h_q-1)·1 + 0 + 1 = maxT·h_q, dQ_accum gives (maxT-1)·h_q·d + (h_q-1)·d + (d-1) + 1 = maxT·h_q·d. Allocations are unchanged for every layout that works today; only non-token-major layouts move.

Why this was never caught (commit 2)

test/python/sdpa/fp16.py only ever allocated token-major ragged stats, the one layout for which the old formula happens to be exactly right. There was no coverage of any other packed Stats layout, so the suite was structurally blind to this class of defect.

Commit 2 adds a ragged_stats_layout axis to ExecConfig, randomized in the ragged fwd/bwd tests, choosing between token-major and head-major. The stats allocation and the stats ragged offset now both derive from cfg.stride_stats rather than hardcoding the token-major assumption.

Against the unpatched frontend the new axis reproduces the defect immediately — 11 tests fail with Backward workspace overwritten outside its boundaries (the suite's existing guard-band check) before the run aborts. The first failing config has stride_stats = (58880, 7360, 1, 1), i.e. sequence stride 1.

Verification

unpatched with fix
-k ragged (fwd + bwd), sm100 11 failures, then abort 370 passed, 78 skipped
head-major direct probe, sm100 3/4 all-zero 4/4 correct
head-major direct probe, sm90 3/4 all-zero 4/4 correct
token-major workspace sizes byte-identical to unpatched

Sanity check on the numbers rather than just pass/fail: with the fix, a TOP_LEFT 2048/512 case with max_total_seq_len set gives |dQ| = 5.53e5, exactly matching the same case with max_total_seq_len unset; a BOTTOM_RIGHT 512/512 case gives 1.976e5 in both.

Not addressed here

  • docs/operations/Attention.md still describes max_total_seq_len_* as a workspace allocation hint. It changes intermediate tensor layout and can change results, and should say so.
  • The formula still assumes ragged_offset == cum_token × stride[2]. Offsets carrying inter-sequence padding would still under-allocate, and the frontend cannot detect this since the offsets are device data. This wants either a documented contract or for max_total_seq_len to move into the backend attention descriptor.

Summary by CodeRabbit

  • Bug Fixes

    • Improved workspace sizing for ragged attention operations, accounting for full tensor strides and dimensions.
    • Corrected ragged statistics offsets and memory capacities for packed token layouts.
    • Added support for both token-major and head-major ragged statistics layouts.
    • Improved handling of ragged training and statistics tensors across varied sequence configurations.
  • Tests

    • Expanded randomized attention testing to cover multiple ragged statistics layouts and packed token capacities.

YangXu1990uiuc and others added 2 commits July 31, 2026 00:44
max_total_seq_len_* switches softmax_sum / dQ_accum / dK_fullhead / dV_fullhead to
a THD-ragged layout and sized them as max_total_seq_len * stride[2] * sizeof(float),
which assumes the per-token footprint is stride[2]. That holds only for token-major
tensors. For head-major stats ([h, t], so stride[2] == 1 -- FlashAttention's and
PyTorch varlen's softmax_lse layout) the buffer is under-allocated by h_q and
softmax_sum overruns into the backend engine's own workspace, which the frontend
places immediately after the FE region. The first word there is the persistent
scheduler's tile_id_counter; once it holds a float bit pattern the unsigned
tile_id < num_tiles test fails in every CTA, so all CTAs retire without storing and
dQ/dK/dV come back exactly zero with no error and a correct forward.

Size from every axis instead. For token-major inputs the new expression is
bit-identical to the old one, so allocations are unchanged for every layout that
works today.

Verified on sm90 and sm100 against cuDNN 9.26.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ragged SDPA tests only ever generated token-major stats ([t, h, 1], sequence
stride h_q), so nothing exercised a packed Stats tensor whose per-token footprint
is not stride[2]. That blind spot is what let the max_total_seq_len workspace
under-allocation fixed in the previous commit survive from 9.22 to 9.26.

Adds ragged_stats_layout to ExecConfig, randomized in the ragged fwd/bwd tests,
selecting between token-major and head-major ([h, t], sequence stride 1 -- the
FlashAttention / PyTorch varlen softmax_lse layout). The stats allocation and the
stats ragged offset now both derive from cfg.stride_stats instead of hardcoding
the token-major assumption.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Ragged SDPA configuration now supports token-major and head-major statistics layouts. Test allocations preserve configured strides and packed capacities. Flash attention workspace sizing now accounts for complete strided tensor footprints.

Changes

Ragged SDPA sizing and layouts

Layer / File(s) Summary
Ragged capacity and statistics layout contract
test/python/sdpa/random_config.py
Adds packed token capacity and configurable token-major or head-major ragged statistics layouts.
Strided ragged workspace sizing
include/cudnn_frontend/node/scaled_dot_product_flash_attention.h
Calculates workspace sizes from complete tensor dimensions and strides for softmax and gradient buffers.
Ragged allocation and offset validation
test/python/sdpa/fp16.py, test/python/test_mhas_v2.py
Preserves configured statistics strides and offsets, uses packed capacities, and randomizes both layouts in forward and backward tests.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: anerudhan

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main fix: preventing ragged SDPA backward workspace under-allocation for non-token-major statistics layouts.
Description check ✅ Passed The description thoroughly explains the defect, root cause, fix, testing, compatibility impact, and known limitations, despite not following every template heading.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@include/cudnn_frontend/node/scaled_dot_product_flash_attention.h`:
- Around line 1094-1105: The ragged_workspace_size helper currently sizes only
max_total_seq_len and can under-allocate when offsets contain inter-batch gaps.
Update the backward workspace sizing flow around ragged_workspace_size to use
the physical ragged span through the final batch boundary, or explicitly reject
unsupported partially packed offsets with documentation; preserve correct
stride-based sizing for all axes. Add a regression test covering inter-batch
padding and verifying the workspace does not overlap subsequent allocations.

In `@test/python/test_mhas_v2.py`:
- Line 368: The ragged_stats_layout randomization in the forward test
configuration (around line 368) does not provide meaningful coverage because the
forward test runs with is_infer=True (default), which means TensorUid.stats and
related output tensors are not allocated or consumed, so the stats_ragged_offset
input tensor is never actually used. Remove the RandomChoice ragged_stats_layout
configuration from the forward test and relocate it to the backward test
configuration (around line 469), where is_infer=False ensures both layout
options are properly exercised. Alternatively, enable with_score_max or
with_score_sum_exp in the forward test to activate the stats outputs and allow
the randomization to provide real coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8d86a621-cd9a-41a0-bc93-12213b4ea74d

📥 Commits

Reviewing files that changed from the base of the PR and between 968f1ef and f0d2dc4.

📒 Files selected for processing (4)
  • include/cudnn_frontend/node/scaled_dot_product_flash_attention.h
  • test/python/sdpa/fp16.py
  • test/python/sdpa/random_config.py
  • test/python/test_mhas_v2.py

Comment on lines +1094 to +1105
// Byte span of a workspace tensor packed (ragged) over the token axis: the sequence axis is
// bounded by max_total_seq_len instead of dim[2], every other axis contributes its own extent.
// Sizing from stride[2] alone under-allocates any layout whose per-token footprint is not
// stride[2] -- e.g. head-major [h, t] stats, where stride[2] == 1.
static int64_t
ragged_workspace_size(int64_t max_total_seq_len,
std::vector<int64_t> const& dim,
std::vector<int64_t> const& stride,
int64_t elem_size) {
return ((max_total_seq_len - 1) * stride[2] + (dim[1] - 1) * stride[1] + (dim[3] - 1) * stride[3] + 1) *
elem_size;
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="$(fd --type f 'scaled_dot_product_flash_attention.h' | head -n 1)"
test -n "$file"

ast-grep outline "$file" --items all --match 'CompositeSDPABackwardNode'
rg -n -C 8 'ragged_workspace_size|set_ragged_offset|max_total_seq_len' "$file"
rg -n -C 5 'partially.?packed|ragged.?offset|max_total_seq_len|packed_token_capacity' .

Repository: NVIDIA/cudnn-frontend

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="include/cudnn_frontend/node/scaled_dot_product_flash_attention.h"

printf '%s\n' '--- helper and call sites ---'
sed -n '1088,1110p;1718,1734p;1924,1943p;2036,2056p;2080,2098p' "$file"

printf '%s\n' '--- ragged offset definitions and workspace construction ---'
rg -n -C 6 'get_ragged_offset|set_ragged_offset|ragged_offset_multiplier|workspace_size' include/cudnn_frontend | head -n 500

printf '%s\n' '--- offset construction in tests and examples ---'
rg -n -C 8 'cum_q|cum_kv|ragged_offset|set_ragged_offset' test python samples include | \
    rg -v '(__pycache__|\.pyc)' | head -n 500

Repository: NVIDIA/cudnn-frontend

Length of output: 50377


🌐 Web query:

cuDNN frontend ragged offset partially packed tensor max_total_seq_len semantics

💡 Result:

In the NVIDIA cuDNN frontend library, when using attention operations (such as SDPA) with packed or partially packed tensor layouts, specific semantics apply to ragged offsets and maximum sequence length parameters [1][2]. Ragged Offset Semantics: When employing a packed layout (such as THD) or a partially packed layout—where variable-length sequences are stored contiguously without padding—you must provide a ragged offset tensor to define the boundaries of individual sequences within the batch [2][3]. 1. Tensor Shape: The ragged offset tensor must have a shape of (B+1, 1, 1, 1) [1][2]. 2. Functionality: It is an integer tensor containing the cumulative token offsets (in terms of number of elements, not bytes) [1][2]. The value at index i represents the starting position of batch i, and the last value (at index B) specifies the offset of the past-the-end element of the entire ragged structure [1][2]. 3. Partially Packed Layouts: This layout allows tokens within each batch to be contiguous, even if the batches are not globally packed (e.g., gaps may exist between batches) [2][3]. The ragged offset tensor is still required to map these sequences correctly [1][2]. Max Total Seq Len Semantics: The parameter often used in conjunction with these layouts is related to workspace allocation and memory planning [1][2]. 1. Purpose: When using packed layouts with backward propagation (bprop), it is recommended to use an API (e.g., set_max_total_seq_len_q) to specify the maximum number of tokens [1][2]. 2. Default Behavior: If this parameter is not explicitly set, the library typically assumes the maximum sequence length is B x S (where B is the batch size and S is the maximum sequence length per batch), which may result in a larger-than-necessary workspace allocation [1][2]. These semantics ensure that the cuDNN kernels correctly identify the valid data segments within the memory buffers, facilitating efficient execution of attention operations on variable-length sequence data [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="include/cudnn_frontend/node/scaled_dot_product_flash_attention.h"

printf '%s\n' '--- helper and call sites ---'
sed -n '1088,1110p;1718,1734p;1924,1943p;2036,2056p;2080,2098p' "$file"

printf '%s\n' '--- ragged offset definitions and workspace construction ---'
rg -n -C 6 'get_ragged_offset|set_ragged_offset|ragged_offset_multiplier|workspace_size' include/cudnn_frontend | head -n 500

printf '%s\n' '--- offset construction in tests and examples ---'
rg -n -C 8 'cum_q|cum_kv|ragged_offset|set_ragged_offset' test python samples include/ | \
    rg -v '(__pycache__|\.pyc)' | head -n 500

Repository: NVIDIA/cudnn-frontend

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact ragged-offset and workspace tensor handling ---'
rg -n -C 12 'collect_tensors_in_workspace_node|get_fe_workspace_size_node|softmax_sum_size|dV_fullhead_size|dK_fullhead_size|dQ_accum_size' \
    include/cudnn_frontend/node/scaled_dot_product_flash_attention.h

printf '%s\n' '--- documentation and tests for partially packed layouts ---'
rg -n -i -C 10 'partially[ -]?packed|partially.?pack|ragged.?offset|cum(ulative)?[ _-]?seq|offset.*gap|gap.*offset' \
    docs test python samples include | head -n 800

printf '%s\n' '--- current branch metadata ---'
git remote -v
git rev-parse --short HEAD

Repository: NVIDIA/cudnn-frontend

Length of output: 50379


🌐 Web query:

site:docs.nvidia.com/deeplearning/cudnn/frontend/latest/operations/Attention.html "partially packed" "ragged offset"

💡 Result:

The term partially packed in the context of ragged offsets refers to a memory layout optimization for handling variable-length sequences in deep learning frameworks, specifically within NVIDIA cuDNN [1]. In this layout, sequences within a batch are stored contiguously but are not required to be globally packed across the entire memory buffer [1]. This contrasts with standard packed (or THD) layouts, where all valid tokens are concatenated into a single contiguous block ($T = \sum(\text{seq_len})$) to eliminate padding [1]. Key aspects of the partially packed layout include: 1. Ragged Offsets: The layout uses a ragged offset tensor—an array of size (B+1)—to define the start and end positions of each sequence within the buffer [1]. By tracking these offsets, the system can identify and process variable-length data without needing global contiguity [1]. 2. Contiguity Requirements: While individual batches can be partially packed, tokens within each batch must remain contiguous to be valid [1]. Interleaved or non-contiguous tokens within a single batch are not supported under this representation [1]. 3. Memory Efficiency: This approach provides a balance between the efficiency of packed layouts (which avoid padding) and the flexibility needed for specific memory management patterns, such as those used in attention mechanisms or paged attention systems where frameworks may prefer to track necessary block indices via ragged offsets [1]. Essentially, the ragged offset tensor acts as a map, allowing the compute engine to navigate the memory buffer and correctly identify the bounds of each variable-length sequence even when the sequences are not stored in one continuous global sequence [1].

Citations:


🌐 Web query:

site:docs.nvidia.com/deeplearning/cudnn "CUDNN_ATTR_TENSOR_RAGGED_OFFSET" "partially packed"

💡 Result:

In the NVIDIA cuDNN library, CUDNN_ATTR_TENSOR_RAGGED_OFFSET_DESC is an attribute used in the cuDNN backend API to define the descriptor for a ragged offset tensor [1][2]. This attribute is explicitly associated with ragged tensors, which are tensors containing nested variable-length lists, commonly utilized in operations like attention [3][4]. There is no concept of a "partially packed" ragged offset; instead, "partially-packed" is a distinct technical term in cuDNN referring to a tensor's memory layout [5][6]. 1. CUDNN_ATTR_TENSOR_RAGGED_OFFSET_DESC: This attribute is set on a tensor descriptor to provide an integer tensor (typically of size B+1) that specifies the offsets or starting positions for variable-length segments within a ragged tensor [3][7]. It is required when handling packed layouts (such as THD) where multiple batches are concatenated together, allowing the library to identify where each batch begins and ends [3][4]. 2. Partially-Packed Tensors: This terminology refers to the packing status of a tensor's dimensions [5][8]. A tensor with a given dimension format (e.g., WXYZ) is "XYZ-packed" if specific striding conditions are met for the dimensions referenced in the suffix [5][9]. This definition is independent of ragged tensor functionality [5][6]. In summary, CUDNN_ATTR_TENSOR_RAGGED_OFFSET_DESC is used to manage the boundaries of ragged data, while "partially-packed" describes specific constraints on how a tensor's strides map its dimensions in memory [5][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact ragged-offset and workspace tensor handling ---'
rg -n -C 12 'collect_tensors_in_workspace_node|get_fe_workspace_size_node|softmax_sum_size|dV_fullhead_size|dK_fullhead_size|dQ_accum_size' \
    include/cudnn_frontend/node/scaled_dot_product_flash_attention.h

printf '%s\n' '--- documentation and tests for partially packed layouts ---'
rg -n -i -C 10 'partially[ -]?packed|partially.?pack|ragged.?offset|cum(ulative)?[ _-]?seq|offset.*gap|gap.*offset' \
    docs test python samples include/ | head -n 800

printf '%s\n' '--- current branch metadata ---'
git remote -v
git rev-parse --short HEAD

Repository: NVIDIA/cudnn-frontend

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- max_total_seq_len documentation and validation ---'
rg -n -i -C 12 'max_total_seq_len(_q|_kv)?|maximum total sequence|total sequence length' \
    docs include python test | head -n 1000

printf '%s\n' '--- ragged workspace output layouts and stride assumptions ---'
rg -n -C 10 'set_dim\(\{b, h_q|set_stride\(.*outputs\[output_names::d[QKV]\]|dQ_accum|dK_fullhead|dV_fullhead' \
    include/cudnn_frontend/node/scaled_dot_product_flash_attention.h | head -n 700

printf '%s\n' '--- exact documentation block ---'
sed -n '61,110p' docs/operations/Attention.md

Repository: NVIDIA/cudnn-frontend

Length of output: 50379


🌐 Web query:

site:docs.nvidia.com/deeplearning/cudnn/frontend/latest/operations/Attention.html "max_total_seq_len"

💡 Result:

In the context of machine learning and high-performance computing, particularly within NVIDIA's cuDNN framework, max_total_seq_len (often split into max_total_seq_len_q and max_total_seq_len_kv) is a parameter used to define the maximum total number of tokens across a batch when processing ragged tensors [1]. Ragged tensors occur when sequences within a single batch have varying lengths. Because memory needs to be pre-allocated for operations like Scaled Dot-Product Attention (SDPA), this parameter informs the system of the maximum aggregate sequence length (the sum of the lengths of all sequences in the batch) [1]. Providing this value allows the framework to correctly allocate the workspace memory required for backward passes and other operations, preventing overflow or inefficient memory usage [1]. If not explicitly provided, it often defaults to the batch size multiplied by the maximum sequence length per item (B x S) [1].

Citations:


🌐 Web query:

site:docs.nvidia.com/deeplearning/cudnn "max_total_seq_len_q" packed layout

💡 Result:

In the context of NVIDIA cuDNN and its attention operations, max_total_seq_len_q is an optional configuration parameter used when working with ragged (packed) tensors [1][2][3]. When processing variable-length sequences packed together into a single continuous memory block (often referred to as the THD layout, where T = sum(sequence_lengths)) to avoid padding, this parameter specifies the maximum total number of tokens for all queries in a batch [1][3]. Key details regarding max_total_seq_len_q: Purpose: It is primarily used for efficient workspace memory allocation [1][2]. By providing this value, the library can allocate the necessary memory buffer for intermediate computations without needing to over-allocate based on a worst-case scenario [1][4]. Default Behavior: If max_total_seq_len_q is not explicitly provided, it defaults to B * S_q (where B is the batch size and S_q is the maximum sequence length per query), which may result in unnecessary memory consumption [1][3]. Usage: It should be set to the maximum sum of sequence lengths expected for the queries in your input data [1][2]. A corresponding parameter, max_total_seq_len_kv, is also typically used for keys and values [1][3]. In the THD (Total-length, Head-count, Dimension) layout, sequences are concatenated, and ragged offsets (or cumulative sequence length tensors) are used to track the boundaries of individual sequences within that packed structure [1][5][6]. This approach is widely used in high-performance Transformer implementations to optimize memory bandwidth and compute utilization by eliminating the overhead of padding tokens [1][2].

Citations:


Size workspace for the physical ragged span.

max_total_seq_len is the sum of valid tokens, but ragged offsets can include inter-batch gaps. For offsets [0, 4*H*D, 7*H*D] and five valid tokens, this helper sizes only the five-token span even though the final batch ends at ten tokens. The resulting write can overlap the next frontend workspace allocation.

Use a physical-capacity bound that includes gaps, or reject and document partially packed offsets for this backward path. Add a regression test with inter-batch padding.

🤖 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 `@include/cudnn_frontend/node/scaled_dot_product_flash_attention.h` around
lines 1094 - 1105, The ragged_workspace_size helper currently sizes only
max_total_seq_len and can under-allocate when offsets contain inter-batch gaps.
Update the backward workspace sizing flow around ragged_workspace_size to use
the physical ragged span through the final batch boundary, or explicitly reject
unsupported partially packed offsets with documentation; preserve correct
stride-based sizing for all axes. Add a regression test covering inter-batch
padding and verifying the workspace does not overlap subsequent allocations.

diag_align=RandomChoice({cudnn.diagonal_alignment.TOP_LEFT : 1, cudnn.diagonal_alignment.BOTTOM_RIGHT : 1}),
is_ragged_or_padded_or_full=RandomChoice({"ragged" : 1, "padded" : 0, "full" : 0}),
with_sink_token=RandomChoice({True : 1, False : 3}),
ragged_stats_layout=RandomChoice({"token_major" : 1, "head_major" : 1}),

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -u

rg -n -C 8 'TensorUid\.stats|with_score_max|with_score_sum_exp|stride_stats|exec_sdpa' \
  test/python/sdpa test/python/test_mhas_v2.py || true

Repository: NVIDIA/cudnn-frontend

Length of output: 50377


🏁 Script executed:

# Check if stride_stats is used in graph construction or only when allocating stats
rg -A 5 -B 5 'stride_stats' test/python/sdpa/fp16.py | head -100

Repository: NVIDIA/cudnn-frontend

Length of output: 6398


🏁 Script executed:

# Trace through exec_sdpa to see if it reads stride_stats for forward inference
ast-grep outline test/python/sdpa/fp16.py --pattern 'def exec_sdpa'

Repository: NVIDIA/cudnn-frontend

Length of output: 336


🏁 Script executed:

# Check if there's graph construction that references stats stride even when stats=None
rg 'set_stride.*stats|stride.*stats' test/python/sdpa/ -A 2 -B 2 | head -80

Repository: NVIDIA/cudnn-frontend

Length of output: 7625


🏁 Script executed:

# Check what happens with stats_ragged_offset in forward path when stats is None
sed -n '220,260p' test/python/sdpa/fp16.py

Repository: NVIDIA/cudnn-frontend

Length of output: 2382


🏁 Script executed:

# Verify that backward test enables stats allocation
sed -n '449,478p' test/python/test_mhas_v2.py

Repository: NVIDIA/cudnn-frontend

Length of output: 1902


🏁 Script executed:

# Check if graph building code conditionally uses stats_ragged_offset
sed -n '230,250p' test/python/sdpa/fp16.py

Repository: NVIDIA/cudnn-frontend

Length of output: 1361


🏁 Script executed:

# Find the actual graph building code that uses stats_ragged_offset
sed -n '250,310p' test/python/sdpa/fp16.py

Repository: NVIDIA/cudnn-frontend

Length of output: 5006


🏁 Script executed:

# Check the complete graph building logic for stats
sed -n '295,330p' test/python/sdpa/fp16.py

Repository: NVIDIA/cudnn-frontend

Length of output: 2855


🏁 Script executed:

# Find where the stats tensor is created and where stats_ragged_offset is used
sed -n '325,360p' test/python/sdpa/fp16.py

Repository: NVIDIA/cudnn-frontend

Length of output: 1741


🏁 Script executed:

# Check if stats_ragged_offset is used only when stats exists
rg -A 3 'stats.set_ragged_offset' test/python/sdpa/fp16.py

Repository: NVIDIA/cudnn-frontend

Length of output: 445


🏁 Script executed:

# Get broader context around stats.set_ragged_offset usage in forward graph
sed -n '360,385p' test/python/sdpa/fp16.py

Repository: NVIDIA/cudnn-frontend

Length of output: 1187


Move the forward test's ragged-stats-layout randomization to the backward test or enable score outputs in forward.

The forward test sets is_infer = True (default) and does not enable with_score_max or with_score_sum_exp. At lines 149–151 of test/python/sdpa/fp16.py, TensorUid.stats is allocated only when cfg.is_train. The stats_ragged_offset tensor is still created as a graph input (line 301) when ragged, but none of the output tensors (stats, score_max, score_sum_exp) consume it in forward inference mode. The randomization at line 368 does not provide real coverage of the stride configuration. The backward test at line 469 already exercises both layouts because it sets is_infer = False.

🤖 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 `@test/python/test_mhas_v2.py` at line 368, The ragged_stats_layout
randomization in the forward test configuration (around line 368) does not
provide meaningful coverage because the forward test runs with is_infer=True
(default), which means TensorUid.stats and related output tensors are not
allocated or consumed, so the stats_ragged_offset input tensor is never actually
used. Remove the RandomChoice ragged_stats_layout configuration from the forward
test and relocate it to the backward test configuration (around line 469), where
is_infer=False ensures both layout options are properly exercised.
Alternatively, enable with_score_max or with_score_sum_exp in the forward test
to activate the stats outputs and allow the randomization to provide real
coverage.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes a correctness-critical workspace under-allocation in ragged (THD) sdpa_backward when max_total_seq_len_* is set and the Stats tensor uses a non-token-major (e.g., head-major) layout—preventing silent all-zero gradients due to downstream workspace corruption in the backend engine workspace region.

Changes:

  • Update ragged workspace sizing in CompositeSDPABackwardNode to account for full tensor extents/strides rather than only stride[2].
  • Expand Python ragged SDPA randomized tests to cover both token-major and head-major packed Stats layouts, including correct packed capacity and ragged offset handling.
  • Deduplicate packed token-capacity computation into a shared helper (packed_token_capacity) and use it consistently.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
include/cudnn_frontend/node/scaled_dot_product_flash_attention.h Fixes ragged backward intermediate workspace sizing via a stride/dim-aware span calculation when max_total_seq_len_* is provided.
test/python/sdpa/random_config.py Adds packed_token_capacity() and introduces ragged_stats_layout to randomize token-major vs head-major packed stats strides for ragged configs.
test/python/sdpa/fp16.py Uses packed_token_capacity(); allocates ragged Stats/score_* tensors and their ragged offsets based on cfg.stride_stats (supports head-major).
test/python/test_mhas_v2.py Extends ragged fwd/bwd random tests to include ragged_stats_layout randomization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vedaanta

vedaanta commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

@cudnn-ci-bot run

@cudnn-ci-bot

Copy link
Copy Markdown
usage: @cudnn-ci-bot run <targets>

targets (comma-separated):
  backend         downstream backend CI
  frost           FROST engine tests
  multi_gpu       multi-GPU smoke test
  oss             open-source kernel tests
  pycudnn         Python binding tests
  python_samples  Python samples
  python_tests    Python test suite
  none            nothing optional, just the standard pipeline

examples:
  @cudnn-ci-bot run python_tests
  @cudnn-ci-bot run python_samples,oss
  @cudnn-ci-bot run none

Only allowlisted maintainers can use @cudnn-ci-bot run.

@vedaanta

vedaanta commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

@cudnn-ci-bot run backend,frost

@cudnn-ci-bot

Copy link
Copy Markdown

🚀 Running mirror pipeline

Branch: cudnn-gh/pr-462-f0d2dc4
Pipeline: 61543162
Targets: backend, frost

@YangXu1990uiuc

Copy link
Copy Markdown
Collaborator Author

Thanks @vedaanta. Triaging the two CodeRabbit comments — I checked both against the code, and I'd like to land this as-is and carry them separately.

On the test coverage comment (test_mhas_v2.py:368) — it's correct. is_train is not is_infer, the forward ragged test never sets is_infer = False, and with_score_max/with_score_sum_exp default to False and aren't randomized there. So none of the three set_ragged_offset(stats_ragged_offset) call sites in sdpa/fp16.py execute and stride_stats reaches no tensor under test — the randomization only perturbs an unconsumed graph input. Harmless but useless. The backward test, which does set is_infer = False, is where the real coverage is, and that half is exercising both layouts as intended. I'll drop the forward-side randomization in a follow-up rather than respin an approved PR for a no-op test line.

On the partially-packed workspace comment (scaled_dot_product_flash_attention.h:1105) — the concern is real, but it's orthogonal to this PR and not introduced by it. The token-axis term is max_total_seq_len in both the old and the new expression, so the exposure is identical before and after. Concretely, at b=4 h=8 t=512:

stats layout old new
token-major 4096 4096 identical — no regression
head-major 512 4096 old under-allocates by h_q×, which is what this PR fixes

The underlying issue is that max_total_seq_len is documented as the sum of sequence lengths, and that quantity cannot bound a partially-packed buffer — for the layout in our own docs ([0, 4·H·D, 7·H·D], Q = aa00bbb0) there are five valid tokens but data reaches token index 6.

The suggested remedy of computing a physical-capacity bound isn't available to the frontend at build time: the gaps live in the ragged-offset device tensor and aren't known until execute. The only lever here is the host-side max_total_seq_len. So the fix is a contract change — document it as the packed capacity (the last ragged offset expressed in tokens) rather than the sum of sequence lengths, or add a separate capacity attribute — which deserves its own PR and its own regression test with inter-batch padding, as CodeRabbit suggests. Filing that separately.

@YangXu1990uiuc
YangXu1990uiuc merged commit 96548b4 into NVIDIA:develop Aug 7, 2026
1 check passed
@YangXu1990uiuc

YangXu1990uiuc commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

note to self: claude::88367f80-c82c-4c8f-93a2-e66ed5a89e07 — "审计PyTorch中CuDNN Attention相关问题"

cwd /home/scratch.yanxu_gpu/cudnn · workspace /home/scratch.yanxu_gpu/cudnn_pt191576 (ROOTCAUSE_maxtot_zero_grad.md, probe_maxtot2/)

vedaanta added a commit to vedaanta/cudnn-frontend that referenced this pull request Aug 8, 2026
Ragged Q/K/V/O were always generated with canonical packed strides
(token stride == h*d): the gaps stride machinery was dense-only, and
PR NVIDIA#462 varies only the STATS layout. So no sweep could ever produce a
tensor like a K/V view of a kv-interleaved [T, 2, H, D] buffer (token
stride 2*h*d) — the layout torch.nn.attention.varlen users get by
slicing a fused KV projection.

New with_ragged_token_gap knob in both ragged random sweeps: each of
Q/K/V/O independently draws token stride h*d + gap, gap in
{0, 8, 64, roundup8(h*d)} seeded from rng_geom_seed (deterministic
through serialize/deserialize repro; explicit strides still win). Gaps
stay multiples of 8 elements so ragged base addresses keep the packed
layout's alignment class (the graph API requires 16-byte-aligned
pointers). Harness: ragged buffers (incl. gradients) are allocated with
the configured strides, and ragged offsets scale by each tensor's actual
stride[2] — the same generalization NVIDIA#462 made for stats offsets.

KNOWN FAILURES this exposes (intentionally not masked): the FROST THD
forward engines (sdpa_fwd_prefill_sm120, sdpa_fwd_prefill_sm100_d128)
claim non-packed-stride THD graphs and silently mis-address them (100%
of O wrong; the stride ORDER is still BSHD, so the order-only layout
gate passes). Backend engines serve every gapped combination correctly,
fwd and bwd. The failing configs under
CUDNN_FRONTEND_ENABLE_FROST_ENGINES=1 are the repro set for fixing the
THD lowerings to honor declared strides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vedaanta added a commit to vedaanta/cudnn-frontend that referenced this pull request Aug 10, 2026
Ragged Q/K/V/O were always generated with canonical packed strides
(token stride == h*d): the gaps stride machinery was dense-only, and
PR NVIDIA#462 varies only the STATS layout. So no sweep could ever produce a
tensor like a K/V view of a kv-interleaved [T, 2, H, D] buffer (token
stride 2*h*d) — the layout torch.nn.attention.varlen users get by
slicing a fused KV projection.

New with_ragged_token_gap knob in both ragged random sweeps: each of
Q/K/V/O independently draws token stride h*d + gap, gap in
{0, 8, 64, roundup8(h*d)} seeded from rng_geom_seed (deterministic
through serialize/deserialize repro; explicit strides still win). Gaps
stay multiples of 8 elements so ragged base addresses keep the packed
layout's alignment class (the graph API requires 16-byte-aligned
pointers). Harness: ragged buffers (incl. gradients) are allocated with
the configured strides, and ragged offsets scale by each tensor's actual
stride[2] — the same generalization NVIDIA#462 made for stats offsets.

KNOWN FAILURES this exposes (intentionally not masked): the FROST THD
forward engines (sdpa_fwd_prefill_sm120, sdpa_fwd_prefill_sm100_d128)
claim non-packed-stride THD graphs and silently mis-address them (100%
of O wrong; the stride ORDER is still BSHD, so the order-only layout
gate passes). Backend engines serve every gapped combination correctly,
fwd and bwd. The failing configs under
CUDNN_FRONTEND_ENABLE_FROST_ENGINES=1 are the repro set for fixing the
THD lowerings to honor declared strides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vedaanta added a commit to vedaanta/cudnn-frontend that referenced this pull request Aug 10, 2026
Ragged Q/K/V/O were always generated with canonical packed strides
(token stride == h*d): the gaps stride machinery was dense-only, and
PR NVIDIA#462 varies only the STATS layout. So no sweep could ever produce a
tensor like a K/V view of a kv-interleaved [T, 2, H, D] buffer (token
stride 2*h*d) — the layout torch.nn.attention.varlen users get by
slicing a fused KV projection.

New with_ragged_token_gap knob in both ragged random sweeps: each of
Q/K/V/O independently draws token stride h*d + gap, gap in
{0, 8, 64, roundup8(h*d)} seeded from rng_geom_seed (deterministic
through serialize/deserialize repro; explicit strides still win). Gaps
stay multiples of 8 elements so ragged base addresses keep the packed
layout's alignment class (the graph API requires 16-byte-aligned
pointers). Harness: ragged buffers (incl. gradients) are allocated with
the configured strides, and ragged offsets scale by each tensor's actual
stride[2] — the same generalization NVIDIA#462 made for stats offsets.

KNOWN FAILURES this exposes (intentionally not masked): the FROST THD
forward engines (sdpa_fwd_prefill_sm120, sdpa_fwd_prefill_sm100_d128)
claim non-packed-stride THD graphs and silently mis-address them (100%
of O wrong; the stride ORDER is still BSHD, so the order-only layout
gate passes). Backend engines serve every gapped combination correctly,
fwd and bwd. The failing configs under
CUDNN_FRONTEND_ENABLE_FROST_ENGINES=1 are the repro set for fixing the
THD lowerings to honor declared strides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vedaanta added a commit to vedaanta/cudnn-frontend that referenced this pull request Aug 11, 2026
Ragged Q/K/V/O were always generated with canonical packed strides
(token stride == h*d): the gaps stride machinery was dense-only, and
PR NVIDIA#462 varies only the STATS layout. So no sweep could ever produce a
tensor like a K/V view of a kv-interleaved [T, 2, H, D] buffer (token
stride 2*h*d) — the layout torch.nn.attention.varlen users get by
slicing a fused KV projection.

New with_ragged_token_gap knob in both ragged random sweeps: each of
Q/K/V/O independently draws token stride h*d + gap, gap in
{0, 8, 64, roundup8(h*d)} seeded from rng_geom_seed (deterministic
through serialize/deserialize repro; explicit strides still win). Gaps
stay multiples of 8 elements so ragged base addresses keep the packed
layout's alignment class (the graph API requires 16-byte-aligned
pointers). Harness: ragged buffers (incl. gradients) are allocated with
the configured strides, and ragged offsets scale by each tensor's actual
stride[2] — the same generalization NVIDIA#462 made for stats offsets.

KNOWN FAILURES this exposes (intentionally not masked): the FROST THD
forward engines (sdpa_fwd_prefill_sm120, sdpa_fwd_prefill_sm100_d128)
claim non-packed-stride THD graphs and silently mis-address them (100%
of O wrong; the stride ORDER is still BSHD, so the order-only layout
gate passes). Backend engines serve every gapped combination correctly,
fwd and bwd. The failing configs under
CUDNN_FRONTEND_ENABLE_FROST_ENGINES=1 are the repro set for fixing the
THD lowerings to honor declared strides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vedaanta added a commit that referenced this pull request Aug 11, 2026
…516)

* test(sdpa): fuzz per-tensor ragged token-stride gaps in test_mhas_v2

Ragged Q/K/V/O were always generated with canonical packed strides
(token stride == h*d): the gaps stride machinery was dense-only, and
PR #462 varies only the STATS layout. So no sweep could ever produce a
tensor like a K/V view of a kv-interleaved [T, 2, H, D] buffer (token
stride 2*h*d) — the layout torch.nn.attention.varlen users get by
slicing a fused KV projection.

New with_ragged_token_gap knob in both ragged random sweeps: each of
Q/K/V/O independently draws token stride h*d + gap, gap in
{0, 8, 64, roundup8(h*d)} seeded from rng_geom_seed (deterministic
through serialize/deserialize repro; explicit strides still win). Gaps
stay multiples of 8 elements so ragged base addresses keep the packed
layout's alignment class (the graph API requires 16-byte-aligned
pointers). Harness: ragged buffers (incl. gradients) are allocated with
the configured strides, and ragged offsets scale by each tensor's actual
stride[2] — the same generalization #462 made for stats offsets.

KNOWN FAILURES this exposes (intentionally not masked): the FROST THD
forward engines (sdpa_fwd_prefill_sm120, sdpa_fwd_prefill_sm100_d128)
claim non-packed-stride THD graphs and silently mis-address them (100%
of O wrong; the stride ORDER is still BSHD, so the order-only layout
gate passes). Backend engines serve every gapped combination correctly,
fwd and bwd. The failing configs under
CUDNN_FRONTEND_ENABLE_FROST_ENGINES=1 are the repro set for fixing the
THD lowerings to honor declared strides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(sdpa): simplify ragged token gaps to whole tokens; always-on in the fp16 ragged sweeps

Per review: drop the True/False dice — the fp16 ragged sweeps enable the
knob unconditionally, and the packed case comes from the gap draw itself.
Each of Q/K/V/O independently draws gap = n*h*d tokens, n in 0..3 (n=0
packed, n=1 a kv-interleaved [T, 2, H, D] view, n=2 a [T, 3, H, D]
QKV-interleave). Whole-token gaps keep every ragged base address in the
packed layout's alignment class by construction, replacing the previous
multiples-of-8-elements rule. The config field stays default-False:
fixed configs and the fp8/mxfp8 harnesses still assume packed
allocations, and the cu_ragged form derives offsets internally.

Verified: backend engines pass 5/5 seeded repros, a bf16 training
config, and a 128-test slice of the fwd ragged L0 sweep; FROST THD fwd
engines keep failing gapped draws (the intended standing repro set).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(sdpa): draw all four ragged token gaps up front; drop unused binding

The gap RNG was consumed lazily, one draw per stride left unspecified — so
explicitly pinning e.g. stride_q shifted the gaps K/V/O derive from the
same rng_geom_seed. Draw all four values up front in fixed Q/K/V/O order
and apply each only where the stride is missing: per-tensor layouts are
now a function of the seed alone. The all-defaults path (the sweeps)
consumes draws in the same order as before, so existing seeded repros
reproduce identical strides.

Adds a GPU-free regression test pinning stride_q and asserting the K/V/O
strides match the all-defaults derivation (seed chosen so the old lazy
behavior visibly shifts two of the three gaps).

Also renames the unused batch binding in compute_packed_strides (RUF059).

Addresses CodeRabbit review feedback on #516.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(sdpa): ragged token gaps on by default; auto-packed where not yet expressible

Drop the per-sweep with_ragged_token_gap opt-in: the field now defaults
True, so EVERY ragged config fuzzes its per-tensor token strides.
fill_derived_fields falls back to packed automatically where a gap is not
yet expressible or handled — cu / offset-multiplier forms bind offsets as
cu (x multiplier) and cannot declare a token gap (#538), and the
fp8/mxfp8 harnesses (1-byte data types) allocate assuming packed (#537) —
so mixed sweeps (ragged + cu_ragged in one RandomChoice) gap exactly the
draws that support it. Explicit strides are never touched (the gap only
fills strides left None), so recorded repro dicts reproduce exactly.

The regression test also locks in the new semantics: default-on for plain
ragged, packed for the three fallback forms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.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.

4 participants