|
33 | 33 | _attention_backends, |
34 | 34 | ) |
35 | 35 | from transformer_engine.pytorch.attention.dot_product_attention.utils import ( |
| 36 | + AttentionParams, |
36 | 37 | FlashAttentionUtils, |
37 | 38 | FusedAttentionParams, |
38 | 39 | _get_supported_versions, |
39 | 40 | check_set_window_size, |
| 41 | + get_attention_backend, |
40 | 42 | get_fused_attn_spec, |
| 43 | + get_thd_padding_mask, |
41 | 44 | ) |
42 | 45 | from transformer_engine.pytorch.attention import RotaryPositionEmbedding |
43 | 46 | import transformer_engine.pytorch.cpp_extensions as ext |
@@ -212,6 +215,147 @@ def test_fused_attn_backend_message(): |
212 | 215 | assert message != "" |
213 | 216 |
|
214 | 217 |
|
| 218 | +@pytest.mark.parametrize( |
| 219 | + "cu_seqlens,cu_seqlens_padded,expected", |
| 220 | + [ |
| 221 | + ([0, 3, 8], [0, 4, 12], [False] * 3 + [True] + [False] * 5 + [True] * 3), |
| 222 | + ([0, 3, 8], [0, 4, 12], [False] * 3 + [True] + [False] * 5 + [True] * 7), |
| 223 | + ([0, 3, 8], [0, 3, 8], [False] * 8 + [True] * 4), |
| 224 | + ([0, 0, 3, 3, 5], [0, 0, 4, 4, 8], [False] * 3 + [True] + [False] * 2 + [True] * 4), |
| 225 | + ], |
| 226 | +) |
| 227 | +def test_thd_padding_mask_capacity(cu_seqlens, cu_seqlens_padded, expected): |
| 228 | + """Cover sequence gaps, reserved buffer tails, and zero-length sequences.""" |
| 229 | + actual = get_thd_padding_mask( |
| 230 | + len(expected), |
| 231 | + torch.tensor(cu_seqlens, dtype=torch.int32, device="cuda"), |
| 232 | + torch.tensor(cu_seqlens_padded, dtype=torch.int32, device="cuda"), |
| 233 | + ) |
| 234 | + torch.testing.assert_close(actual, torch.tensor(expected, dtype=torch.bool, device="cuda")) |
| 235 | + |
| 236 | + |
| 237 | +@pytest.mark.parametrize("fa_version", [3, 4]) |
| 238 | +def test_dpa_flash_thd_padding_capacity_cuda_graph(fa_version, monkeypatch): |
| 239 | + """Check padded outputs and gradients, including buffer tails, without CP. |
| 240 | +
|
| 241 | + This regression exercises eager execution and raw CUDA Graph capture. CP graph |
| 242 | + capture uses TE's make_graphed_callables and is outside this test's scope. |
| 243 | + """ |
| 244 | + for key, value in { |
| 245 | + "NVTE_FLASH_ATTN": "1", |
| 246 | + "NVTE_FUSED_ATTN": "0", |
| 247 | + "NVTE_UNFUSED_ATTN": "0", |
| 248 | + "NVTE_FLASH_ATTN_V2": "0", |
| 249 | + "NVTE_FLASH_ATTN_V3": str(int(fa_version == 3)), |
| 250 | + "NVTE_FLASH_ATTN_V4": str(int(fa_version == 4)), |
| 251 | + }.items(): |
| 252 | + monkeypatch.setenv(key, value) |
| 253 | + use_flash_attention, flash_attention_backend, *_ = get_attention_backend( |
| 254 | + AttentionParams( |
| 255 | + qkv_dtype=torch.bfloat16, |
| 256 | + qkv_layout="thd_thd_thd", |
| 257 | + batch_size=2, |
| 258 | + num_heads=4, |
| 259 | + num_gqa_groups=4, |
| 260 | + max_seqlen_q=128, |
| 261 | + max_seqlen_kv=128, |
| 262 | + num_tokens_q=128, |
| 263 | + num_tokens_kv=192, |
| 264 | + head_dim_qk=64, |
| 265 | + head_dim_v=64, |
| 266 | + attn_mask_type="padding", |
| 267 | + window_size=(-1, -1), |
| 268 | + core_attention_bias_requires_grad=False, |
| 269 | + pad_between_seqs=True, |
| 270 | + attention_dropout=0.0, |
| 271 | + context_parallel=False, |
| 272 | + deterministic=_deterministic, |
| 273 | + is_training=True, |
| 274 | + cuda_graph=True, |
| 275 | + softmax_scale=0.125, |
| 276 | + ) |
| 277 | + ) |
| 278 | + if not use_flash_attention: |
| 279 | + pytest.skip(f"FlashAttention {fa_version} does not support this CUDA Graph configuration.") |
| 280 | + assert flash_attention_backend.major == fa_version |
| 281 | + # Restore the complete backend cache along with the environment at test teardown. |
| 282 | + for key, value in _attention_backends.items(): |
| 283 | + monkeypatch.setitem(_attention_backends, key, value) |
| 284 | + _attention_backends["backend_selection_requires_update"] = True |
| 285 | + reset_rng_states() |
| 286 | + cu_q, cu_kv, padded_q, padded_kv = [ |
| 287 | + torch.tensor(offsets, dtype=torch.int32, device="cuda") |
| 288 | + for offsets in ([0, 61, 96], [0, 45, 118], [0, 64, 112], [0, 64, 160]) |
| 289 | + ] |
| 290 | + # Expected valid token positions are independent of get_thd_padding_mask. |
| 291 | + q_rows = torch.tensor(list(range(61)) + list(range(64, 99)), device="cuda") |
| 292 | + kv_rows = torch.tensor(list(range(45)) + list(range(64, 137)), device="cuda") |
| 293 | + qkv = [ |
| 294 | + torch.randn(tokens, 4, 64, dtype=torch.bfloat16, device="cuda", requires_grad=True) |
| 295 | + for tokens in (128, 192, 192) |
| 296 | + ] |
| 297 | + grad_output = torch.randn(128, 256, dtype=torch.bfloat16, device="cuda") |
| 298 | + block = DotProductAttention( |
| 299 | + 4, |
| 300 | + 64, |
| 301 | + qkv_format="thd", |
| 302 | + attn_mask_type="padding", |
| 303 | + attention_type="cross", |
| 304 | + attention_dropout=0.0, |
| 305 | + ).cuda() |
| 306 | + |
| 307 | + def run(inputs, grad, padded): |
| 308 | + """Run the selected FlashAttention backend and differentiate Q/K/V.""" |
| 309 | + output = block( |
| 310 | + *inputs, |
| 311 | + cu_seqlens_q=cu_q, |
| 312 | + cu_seqlens_kv=cu_kv, |
| 313 | + cu_seqlens_q_padded=padded_q if padded else None, |
| 314 | + cu_seqlens_kv_padded=padded_kv if padded else None, |
| 315 | + max_seqlen_q=128, |
| 316 | + max_seqlen_kv=128, |
| 317 | + pad_between_seqs=padded, |
| 318 | + ) |
| 319 | + assert _attention_backends["use_flash_attention"] |
| 320 | + assert _attention_backends["flash_attention_backend"].major == fa_version |
| 321 | + assert not _attention_backends["use_fused_attention"] |
| 322 | + assert not _attention_backends["use_unfused_attention"] |
| 323 | + return (output, *torch.autograd.grad(output, inputs, grad)) |
| 324 | + |
| 325 | + def check(actual): |
| 326 | + """Compare valid token positions with compact attention and require exact-zero padding.""" |
| 327 | + compact_qkv = [ |
| 328 | + tensor.detach()[rows].requires_grad_() |
| 329 | + for tensor, rows in zip(qkv, (q_rows, kv_rows, kv_rows)) |
| 330 | + ] |
| 331 | + expected = run(compact_qkv, grad_output[q_rows], False) |
| 332 | + for tensor, reference, rows in zip(actual, expected, (q_rows, q_rows, kv_rows, kv_rows)): |
| 333 | + assert tensor.shape[0] == (128 if rows is q_rows else 192) |
| 334 | + torch.testing.assert_close(tensor[rows], reference, atol=1.5e-2, rtol=1.5e-2) |
| 335 | + padding = torch.ones(tensor.shape[0], dtype=torch.bool, device="cuda") |
| 336 | + padding[rows] = False |
| 337 | + assert torch.count_nonzero(tensor[padding]).item() == 0 |
| 338 | + |
| 339 | + check(run(qkv, grad_output, True)) |
| 340 | + stream = torch.cuda.Stream() |
| 341 | + stream.wait_stream(torch.cuda.current_stream()) |
| 342 | + with torch.cuda.stream(stream): |
| 343 | + for _ in range(3): |
| 344 | + run(qkv, grad_output, True) |
| 345 | + torch.cuda.current_stream().wait_stream(stream) |
| 346 | + torch.cuda.synchronize() |
| 347 | + graph = torch.cuda.CUDAGraph() |
| 348 | + with torch.cuda.graph(graph, stream=stream): |
| 349 | + captured = run(qkv, grad_output, True) |
| 350 | + for _ in range(2): |
| 351 | + # Replays must consume fresh inputs and upstream gradients at the same addresses. |
| 352 | + with torch.no_grad(): |
| 353 | + for tensor in (*qkv, grad_output): |
| 354 | + tensor.normal_() |
| 355 | + graph.replay() |
| 356 | + check(captured) |
| 357 | + |
| 358 | + |
215 | 359 | # Define F16 data types to test |
216 | 360 | param_types = [torch.float16] |
217 | 361 | if is_bf16_available(): |
@@ -2166,6 +2310,22 @@ def get_dummy_cuda_rng_tracker() -> CudaRNGStatesTracker: |
2166 | 2310 |
|
2167 | 2311 | if backend in ["UnfusedDotProductAttention"]: |
2168 | 2312 | return out, max_logit, (q_grad, k_grad, v_grad, d_softmax_offset) |
| 2313 | + if backend == "FlashAttention" and qkv_format == "thd" and pad_between_seqs: |
| 2314 | + tensors_and_boundaries = [("out", out, cu_seqlens_q, cu_seqlens_q_after_pad)] |
| 2315 | + if is_training: |
| 2316 | + tensors_and_boundaries.extend( |
| 2317 | + [ |
| 2318 | + ("dq", q_grad, cu_seqlens_q, cu_seqlens_q_after_pad), |
| 2319 | + ("dk", k_grad, cu_seqlens_kv, cu_seqlens_kv_after_pad), |
| 2320 | + ("dv", v_grad, cu_seqlens_kv, cu_seqlens_kv_after_pad), |
| 2321 | + ] |
| 2322 | + ) |
| 2323 | + for tensor_name, tensor, cu_seqlens, cu_seqlens_padded in tensors_and_boundaries: |
| 2324 | + padding_mask = get_thd_padding_mask(tensor.shape[0], cu_seqlens, cu_seqlens_padded) |
| 2325 | + assert ( |
| 2326 | + torch.count_nonzero(tensor[padding_mask]).item() == 0 |
| 2327 | + ), f"{backend} left nonzero values in {tensor_name} padding" |
| 2328 | + |
2169 | 2329 | if backend in ["FusedAttention", "FlashAttention"]: |
2170 | 2330 | if qkv_format == "thd" and pad_between_seqs: |
2171 | 2331 | out_orig = torch.Tensor([]).to(device="cuda", dtype=dtype) |
|
0 commit comments