Skip to content

Commit 2a435a2

Browse files
desh2608pre-commit-ci[bot]KshitijLakhani
authored
[PyT] Add per-sequence attention policies to packed THD attention (#3274)
* Add per-sequence causal policy to THD attention Signed-off-by: Desh Raj <r.desh26@gmail.com> * Fix mixed THD test CI coverage Signed-off-by: Desh Raj <r.desh26@gmail.com> * Use inter-sequence padding for mixed THD masks Replace the per-policy Q/K/V gather and output scatter dispatcher with the review-suggested inter-sequence-padding design. Keep the original packed Q/K/V storage, derive logical and physical cu-seqlens for each mask policy, run the ordinary scalar-mask DPA path with pad_between_seqs, and sum its disjoint zero-padded outputs. Expose a generic attn_mask_type_per_seq mapping plus window_size_per_mask_type so callers can combine full-context offline attention with bounded causal attention. Preserve the scalar fast path for uniform batches, avoid policy-dependent host synchronization, and cover forward/backward parity, existing physical padding, cross-attention, per-policy windows, bottom-right masks, invalid inputs, empty batches, and CUDA graph replay. NRT H100 validation passed all 16 TransformerEngine cases and 3 focused Megatron integration tests. In an 8-node/64-H100 matched-auto comparison over iterations 7-100, grouped averaged 1802.289 ms and pad-between averaged 1779.596 ms (-1.259%); the paired 95% interval crossed zero, indicating practical parity when the backend is held constant. In the deployable-backend comparison, grouped/Flash averaged 1596.129 ms while pad-between/auto-cuDNN averaged 1766.769 ms: pad-between was 10.691% slower by mean and 11.482% slower by 5%-trimmed mean. The paired penalty was +170.640 ms with a 95% interval of +89.728 to +251.552 ms, with all 16 fully logged workload counters matching across 100 steps. The implementation removes the original data-movement contention, but the current image cannot run full-context inter-sequence padding through FlashAttention 2 and has no FA3, so the path falls back to cuDNN fused attention. Retain grouped/Flash for production until a competitive padding-capable backend is available; this commit keeps the cleaner generic API for review and future backend support. Signed-off-by: Desh Raj <r.desh26@gmail.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Mask inactive mixed-THD lanes for FA3 Mixed-mask dispatch represents sequences owned by other policies as inter-sequence padding. FA3 preserves that layout contract, but its padding lanes are unspecified: focused Hopper testing observed NaNs in inactive forward outputs and tile-spill garbage in inactive dQ/dK/dV lanes. Build a sync-free mask from the physical cu_seqlens, sanitize each policy output with torch.where, and wrap Q/K/V in an identity-forward autograd guard that zeros only inactive gradient lanes. This keeps the pad_between_seqs design, avoids regrouping or copying tokens, and lets disjoint policy results be summed safely across FA2 and FA3. Validation: NRT H100 source-overlay run passed all 16 focused mixed-THD tests for FP16/BF16 forward and backward, pre-existing padding, cross attention, per-policy windows, CUDA graphs, and validation errors. Signed-off-by: Desh Raj <r.desh26@gmail.com> * Select mixed THD implementation by GPU backend Keep attn_mask_type_per_seq and window_size_per_mask_type invariant at the caller boundary while selecting the physical implementation inside DotProductAttention. Use inter-sequence padding only on SM90 when FlashAttention 3 is installed and enabled. Fall back to per-policy token compaction, scalar THD attention, and output restoration for FlashAttention 2 and other configurations. Preserve the uniform-policy scalar fast path and restrict CUDA graph coverage to the sync-free FA3 padding path. Add a runtime dispatch matrix covering architecture, FA3 availability, and backend environment controls. Validation: - NRT H100/FA3: 21 mixed-THD tests and 4 Megatron policy/API tests passed - IAD A100/FA2: 20 mixed-THD tests passed, 1 Hopper-only graph test skipped, and 4 Megatron policy/API tests passed - Installed-image five-step training smokes were bit-identical within FA3 across NRT/HEL and within FA2 across IAD/ORD; maximum cross-backend loss delta was 1.56e-5 * Centralize mixed THD architecture dispatch * Fix mixed THD window normalization order * Refine mixed THD policy API and backend dispatch * Add grouped override for mixed THD attention Signed-off-by: Desh Raj <r.desh26@gmail.com> * Validate mixed THD sequence policies * Cache mixed THD backend selection * Expose THD attention policies through transformer layers * Support per-policy THD diagonal alignment * Honor grouped THD dispatch through transformer layers * Fix FlashAttention cross-attention backend selection Signed-off-by: Desh Raj <r.desh26@gmail.com> --------- Signed-off-by: Desh Raj <r.desh26@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Kshitij Lakhani <33047503+KshitijLakhani@users.noreply.github.com>
1 parent 20f4cec commit 2a435a2

7 files changed

Lines changed: 1746 additions & 108 deletions

File tree

‎qa/L0_pytorch_unittest/test.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_linear_mxfp8_att
6969
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_fused_mla_q_uproj.xml $TE_PATH/tests/pytorch/attention/test_fused_mla_q_uproj.py || test_fail "test_fused_mla_q_uproj.py"
7070
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_kv_cache.xml $TE_PATH/tests/pytorch/attention/test_kv_cache.py || test_fail "test_kv_cache.py"
7171
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_cu_seqlens_cache.xml $TE_PATH/tests/pytorch/attention/test_cu_seqlens_cache.py || test_fail "test_cu_seqlens_cache.py"
72+
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_mixed_thd_attention.xml $TE_PATH/tests/pytorch/attention/test_mixed_thd_attention.py || test_fail "test_mixed_thd_attention.py"
7273
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_hf_integration.xml $TE_PATH/tests/pytorch/test_hf_integration.py || test_fail "test_hf_integration.py"
7374
export NVTE_TEST_CHECKPOINT_ARTIFACT_PATH=$TE_PATH/artifacts/tests/pytorch/test_checkpoint
7475
if [ ! -d "$NVTE_TEST_CHECKPOINT_ARTIFACT_PATH" ]; then

‎tests/pytorch/attention/test_attention.py‎

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def test_dot_product_attention(
240240

241241
# UnfusedDotProductAttention backend
242242
if unfused_attn_supported:
243-
unfused_attn_fwd, unfused_max_logit, unfused_attn_bwd = _run_dot_product_attention(
243+
unfused_attn_fwd, unfused_max_logit, unfused_attn_bwd = run_dot_product_attention(
244244
dtype,
245245
config,
246246
"UnfusedDotProductAttention",
@@ -252,7 +252,7 @@ def test_dot_product_attention(
252252

253253
# FusedAttention backend
254254
if fused_attn_supported:
255-
fused_attn_fwd, fused_max_logit, fused_attn_bwd = _run_dot_product_attention(
255+
fused_attn_fwd, fused_max_logit, fused_attn_bwd = run_dot_product_attention(
256256
dtype,
257257
config,
258258
"FusedAttention",
@@ -265,7 +265,7 @@ def test_dot_product_attention(
265265

266266
# FlashAttention backend
267267
if flash_attn_supported:
268-
flash_attn_fwd, _, flash_attn_bwd = _run_dot_product_attention(
268+
flash_attn_fwd, _, flash_attn_bwd = run_dot_product_attention(
269269
dtype,
270270
config,
271271
"FlashAttention",
@@ -1149,7 +1149,47 @@ def test_dpa_qkv_layout_thd_declarative(dtype, model_configs, model, qkv_layout)
11491149
test_dpa_qkv_layout_thd(dtype, model_configs, model, qkv_layout, declarative_packed=True)
11501150

11511151

1152-
def _run_dot_product_attention(
1152+
def make_cu_seqlens(seqlens: torch.Tensor) -> torch.Tensor:
1153+
"""Construct CUDA cumulative sequence lengths from per-sequence lengths."""
1154+
cu_seqlens = torch.zeros(seqlens.numel() + 1, dtype=torch.int32, device=seqlens.device)
1155+
cu_seqlens[1:] = torch.cumsum(seqlens, dim=0)
1156+
return cu_seqlens
1157+
1158+
1159+
def make_dot_product_attention(
1160+
dtype: torch.dtype,
1161+
config: ModelConfig,
1162+
qkv_format: str,
1163+
*,
1164+
get_rng_state_tracker=None,
1165+
is_training: bool = True,
1166+
attention_type: str = None,
1167+
) -> DotProductAttention:
1168+
"""Construct a configured DotProductAttention test module."""
1169+
block = DotProductAttention(
1170+
config.num_heads,
1171+
(config.head_dim_qk, config.head_dim_v),
1172+
num_gqa_groups=config.num_gqa_groups,
1173+
attention_dropout=config.dropout_p,
1174+
qkv_format=qkv_format,
1175+
attn_mask_type=config.attn_mask_type,
1176+
sequence_parallel=False,
1177+
tp_size=1,
1178+
get_rng_state_tracker=get_rng_state_tracker,
1179+
tp_group=None,
1180+
layer_number=1,
1181+
attention_type=config.attn_type if attention_type is None else attention_type,
1182+
softmax_type=config.softmax_type,
1183+
return_max_logit=config.return_max_logit,
1184+
).to(dtype=dtype, device="cuda")
1185+
if not is_training:
1186+
block = block.eval()
1187+
if is_training and config.softmax_type != "vanilla":
1188+
block.softmax_offset.requires_grad = True
1189+
return block
1190+
1191+
1192+
def run_dot_product_attention(
11531193
dtype: torch.dtype,
11541194
config: ModelConfig,
11551195
backend: str,
@@ -1158,6 +1198,7 @@ def _run_dot_product_attention(
11581198
pad_between_seqs: bool,
11591199
is_training: bool,
11601200
declarative_packed: bool = False,
1201+
forward_kwargs: Dict[str, Any] = None,
11611202
) -> Tuple[torch.Tensor, Tuple[torch.Tensor, torch.Tensor, torch.Tensor]]:
11621203
"""Run DotProductAttention module with one forward pass and one backward pass.
11631204
@@ -1203,10 +1244,8 @@ def _run_dot_product_attention(
12031244
seqlens_kv = torch.full(
12041245
[config.batch_size], config.max_seqlen_kv, dtype=torch.int32, device="cuda"
12051246
)
1206-
cu_seqlens_q = torch.zeros(config.batch_size + 1, dtype=torch.int32, device="cuda")
1207-
cu_seqlens_kv = torch.zeros(config.batch_size + 1, dtype=torch.int32, device="cuda")
1208-
cu_seqlens_q[1:] = torch.cumsum(seqlens_q, dim=0)
1209-
cu_seqlens_kv[1:] = torch.cumsum(seqlens_kv, dim=0)
1247+
cu_seqlens_q = make_cu_seqlens(seqlens_q)
1248+
cu_seqlens_kv = make_cu_seqlens(seqlens_kv)
12101249

12111250
seqlens_q_after_pad = seqlens_q.clone()
12121251
seqlens_kv_after_pad = seqlens_kv.clone()
@@ -1432,26 +1471,13 @@ def get_dummy_cuda_rng_tracker() -> CudaRNGStatesTracker:
14321471
return _DUMMY_CUDA_RNG_STATE_TRACKER
14331472

14341473
# Set up model
1435-
block = DotProductAttention(
1436-
config.num_heads,
1437-
(config.head_dim_qk, config.head_dim_v),
1438-
num_gqa_groups=config.num_gqa_groups,
1439-
attention_dropout=config.dropout_p,
1440-
qkv_format=qkv_format,
1441-
attn_mask_type=config.attn_mask_type,
1442-
sequence_parallel=False,
1443-
tp_size=1,
1474+
block = make_dot_product_attention(
1475+
dtype,
1476+
config,
1477+
qkv_format,
14441478
get_rng_state_tracker=get_dummy_cuda_rng_tracker,
1445-
tp_group=None,
1446-
layer_number=1,
1447-
attention_type=config.attn_type,
1448-
softmax_type=config.softmax_type,
1449-
return_max_logit=config.return_max_logit,
1450-
).to(dtype=dtype, device="cuda")
1451-
if not is_training:
1452-
block = block.eval()
1453-
if is_training and config.softmax_type != "vanilla":
1454-
block.softmax_offset.requires_grad = True
1479+
is_training=is_training,
1480+
)
14551481

14561482
# Run a forward and backward pass
14571483
if backend in ["UnfusedDotProductAttention"]:
@@ -1474,11 +1500,7 @@ def get_dummy_cuda_rng_tracker() -> CudaRNGStatesTracker:
14741500
else:
14751501
packed_kwargs["kv_layer"] = packed_tensor
14761502
k, v = None, None
1477-
out = block(
1478-
q,
1479-
k,
1480-
v,
1481-
**packed_kwargs,
1503+
dpa_forward_kwargs = dict(
14821504
window_size=config.window_size,
14831505
attention_mask=attention_mask,
14841506
qkv_format=qkv_format,
@@ -1502,6 +1524,9 @@ def get_dummy_cuda_rng_tracker() -> CudaRNGStatesTracker:
15021524
# Only pass num_splits when exercising the FlashAttention path
15031525
num_splits=config.num_splits if backend == "FlashAttention" else 1,
15041526
)
1527+
if forward_kwargs is not None:
1528+
dpa_forward_kwargs.update(forward_kwargs)
1529+
out = block(q, k, v, **packed_kwargs, **dpa_forward_kwargs)
15051530
max_logit = None
15061531
if config.return_max_logit:
15071532
out, max_logit = out

0 commit comments

Comments
 (0)