|
32 | 32 | FlashAttentionUtils, |
33 | 33 | _get_supported_versions, |
34 | 34 | check_set_window_size, |
| 35 | + get_thd_padding_mask, |
35 | 36 | ) |
36 | 37 | from transformer_engine.pytorch.attention import RotaryPositionEmbedding |
37 | 38 | import transformer_engine.pytorch.cpp_extensions as ext |
@@ -129,6 +130,124 @@ def test_flash_attention_supported_version_message(): |
129 | 130 | ) |
130 | 131 |
|
131 | 132 |
|
| 133 | +@pytest.mark.parametrize( |
| 134 | + "cu_seqlens,cu_seqlens_padded,expected", |
| 135 | + [ |
| 136 | + ([0, 3, 8], [0, 4, 12], [False] * 3 + [True] + [False] * 5 + [True] * 3), |
| 137 | + ([0, 3, 8], [0, 4, 12], [False] * 3 + [True] + [False] * 5 + [True] * 7), |
| 138 | + ([0, 3, 8], [0, 3, 8], [False] * 8 + [True] * 4), |
| 139 | + ([0, 0, 3, 3, 5], [0, 0, 4, 4, 8], [False] * 3 + [True] + [False] * 2 + [True] * 4), |
| 140 | + ], |
| 141 | +) |
| 142 | +def test_thd_padding_mask_capacity(cu_seqlens, cu_seqlens_padded, expected): |
| 143 | + """Cover sequence gaps, reserved buffer tails, and zero-length sequences.""" |
| 144 | + actual = get_thd_padding_mask( |
| 145 | + len(expected), |
| 146 | + torch.tensor(cu_seqlens, dtype=torch.int32, device="cuda"), |
| 147 | + torch.tensor(cu_seqlens_padded, dtype=torch.int32, device="cuda"), |
| 148 | + ) |
| 149 | + torch.testing.assert_close(actual, torch.tensor(expected, dtype=torch.bool, device="cuda")) |
| 150 | + |
| 151 | + |
| 152 | +@pytest.mark.parametrize("fa_version", [3, 4]) |
| 153 | +def test_dpa_flash_thd_padding_capacity_cuda_graph(fa_version, monkeypatch): |
| 154 | + """Check padded outputs and gradients, including buffer tails, in eager and raw graphs.""" |
| 155 | + if fa_version == 3 and ( |
| 156 | + device_compute_capability != (9, 0) or not FlashAttentionUtils.v3_is_installed |
| 157 | + ): |
| 158 | + pytest.skip("FlashAttention 3 on Hopper is required.") |
| 159 | + if fa_version == 4 and ( |
| 160 | + not (10, 0) <= device_compute_capability < (12, 0) |
| 161 | + or not FlashAttentionUtils.v4_is_installed |
| 162 | + ): |
| 163 | + pytest.skip("FlashAttention 4 on SM100/SM110 is required.") |
| 164 | + for key, value in { |
| 165 | + "NVTE_FLASH_ATTN": "1", |
| 166 | + "NVTE_FUSED_ATTN": "0", |
| 167 | + "NVTE_UNFUSED_ATTN": "0", |
| 168 | + "NVTE_FLASH_ATTN_V2": "0", |
| 169 | + "NVTE_FLASH_ATTN_V3": str(int(fa_version == 3)), |
| 170 | + "NVTE_FLASH_ATTN_V4": str(int(fa_version == 4)), |
| 171 | + }.items(): |
| 172 | + monkeypatch.setenv(key, value) |
| 173 | + # Restore the complete backend cache along with the environment at test teardown. |
| 174 | + for key, value in _attention_backends.items(): |
| 175 | + monkeypatch.setitem(_attention_backends, key, value) |
| 176 | + _attention_backends["backend_selection_requires_update"] = True |
| 177 | + reset_rng_states() |
| 178 | + cu_q, cu_kv, padded_q, padded_kv = [ |
| 179 | + torch.tensor(offsets, dtype=torch.int32, device="cuda") |
| 180 | + for offsets in ([0, 61, 96], [0, 45, 118], [0, 64, 112], [0, 64, 160]) |
| 181 | + ] |
| 182 | + # Expected valid rows are independent of get_thd_padding_mask. |
| 183 | + q_rows = torch.tensor(list(range(61)) + list(range(64, 99)), device="cuda") |
| 184 | + kv_rows = torch.tensor(list(range(45)) + list(range(64, 137)), device="cuda") |
| 185 | + qkv = [ |
| 186 | + torch.randn(tokens, 4, 64, dtype=torch.bfloat16, device="cuda", requires_grad=True) |
| 187 | + for tokens in (128, 192, 192) |
| 188 | + ] |
| 189 | + grad_output = torch.randn(128, 256, dtype=torch.bfloat16, device="cuda") |
| 190 | + block = DotProductAttention( |
| 191 | + 4, |
| 192 | + 64, |
| 193 | + qkv_format="thd", |
| 194 | + attn_mask_type="padding", |
| 195 | + attention_type="cross", |
| 196 | + attention_dropout=0.0, |
| 197 | + ).cuda() |
| 198 | + |
| 199 | + def run(inputs, grad, padded): |
| 200 | + """Run the selected FlashAttention backend and differentiate Q/K/V.""" |
| 201 | + output = block( |
| 202 | + *inputs, |
| 203 | + cu_seqlens_q=cu_q, |
| 204 | + cu_seqlens_kv=cu_kv, |
| 205 | + cu_seqlens_q_padded=padded_q if padded else None, |
| 206 | + cu_seqlens_kv_padded=padded_kv if padded else None, |
| 207 | + max_seqlen_q=128, |
| 208 | + max_seqlen_kv=128, |
| 209 | + pad_between_seqs=padded, |
| 210 | + ) |
| 211 | + assert _attention_backends["use_flash_attention"] |
| 212 | + assert _attention_backends["flash_attention_backend"].major == fa_version |
| 213 | + assert not _attention_backends["use_fused_attention"] |
| 214 | + assert not _attention_backends["use_unfused_attention"] |
| 215 | + return (output, *torch.autograd.grad(output, inputs, grad)) |
| 216 | + |
| 217 | + def check(actual): |
| 218 | + """Compare valid rows with compact attention and require exact-zero padding.""" |
| 219 | + compact_qkv = [ |
| 220 | + tensor.detach()[rows].requires_grad_() |
| 221 | + for tensor, rows in zip(qkv, (q_rows, kv_rows, kv_rows)) |
| 222 | + ] |
| 223 | + expected = run(compact_qkv, grad_output[q_rows], False) |
| 224 | + for tensor, reference, rows in zip(actual, expected, (q_rows, q_rows, kv_rows, kv_rows)): |
| 225 | + assert tensor.shape[0] == (128 if rows is q_rows else 192) |
| 226 | + torch.testing.assert_close(tensor[rows], reference, atol=1.5e-2, rtol=1.5e-2) |
| 227 | + padding = torch.ones(tensor.shape[0], dtype=torch.bool, device="cuda") |
| 228 | + padding[rows] = False |
| 229 | + assert torch.count_nonzero(tensor[padding]).item() == 0 |
| 230 | + |
| 231 | + check(run(qkv, grad_output, True)) |
| 232 | + stream = torch.cuda.Stream() |
| 233 | + stream.wait_stream(torch.cuda.current_stream()) |
| 234 | + with torch.cuda.stream(stream): |
| 235 | + for _ in range(3): |
| 236 | + run(qkv, grad_output, True) |
| 237 | + torch.cuda.current_stream().wait_stream(stream) |
| 238 | + torch.cuda.synchronize() |
| 239 | + graph = torch.cuda.CUDAGraph() |
| 240 | + with torch.cuda.graph(graph, stream=stream): |
| 241 | + captured = run(qkv, grad_output, True) |
| 242 | + for _ in range(2): |
| 243 | + # Replays must consume fresh inputs and upstream gradients at the same addresses. |
| 244 | + with torch.no_grad(): |
| 245 | + for tensor in (*qkv, grad_output): |
| 246 | + tensor.normal_() |
| 247 | + graph.replay() |
| 248 | + check(captured) |
| 249 | + |
| 250 | + |
132 | 251 | # Define F16 data types to test |
133 | 252 | param_types = [torch.float16] |
134 | 253 | if is_bf16_available(): |
@@ -1560,6 +1679,22 @@ def get_dummy_cuda_rng_tracker() -> CudaRNGStatesTracker: |
1560 | 1679 |
|
1561 | 1680 | if backend in ["UnfusedDotProductAttention"]: |
1562 | 1681 | return out, max_logit, (q_grad, k_grad, v_grad, d_softmax_offset) |
| 1682 | + if backend == "FlashAttention" and qkv_format == "thd" and pad_between_seqs: |
| 1683 | + tensors_and_boundaries = [("out", out, cu_seqlens_q, cu_seqlens_q_after_pad)] |
| 1684 | + if is_training: |
| 1685 | + tensors_and_boundaries.extend( |
| 1686 | + [ |
| 1687 | + ("dq", q_grad, cu_seqlens_q, cu_seqlens_q_after_pad), |
| 1688 | + ("dk", k_grad, cu_seqlens_kv, cu_seqlens_kv_after_pad), |
| 1689 | + ("dv", v_grad, cu_seqlens_kv, cu_seqlens_kv_after_pad), |
| 1690 | + ] |
| 1691 | + ) |
| 1692 | + for tensor_name, tensor, cu_seqlens, cu_seqlens_padded in tensors_and_boundaries: |
| 1693 | + padding_mask = get_thd_padding_mask(tensor.shape[0], cu_seqlens, cu_seqlens_padded) |
| 1694 | + assert ( |
| 1695 | + torch.count_nonzero(tensor[padding_mask]).item() == 0 |
| 1696 | + ), f"{backend} left nonzero values in {tensor_name} padding" |
| 1697 | + |
1563 | 1698 | if backend in ["FusedAttention", "FlashAttention"]: |
1564 | 1699 | if qkv_format == "thd" and pad_between_seqs: |
1565 | 1700 | out_orig = torch.Tensor([]).to(device="cuda", dtype=dtype) |
|
0 commit comments