-
Notifications
You must be signed in to change notification settings - Fork 816
Optionally pad QK/V for better kernel selection #3339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
janEbert
wants to merge
18
commits into
NVIDIA:main
Choose a base branch
from
janEbert:optional-mla-pad
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
438ecfe
Automate MLA padding-related kernel selection
janEbert 573edd8
Add padding-related tests
janEbert 50dbfef
Fix tests
janEbert d24be6d
Add memoization
janEbert bdc487e
Add test to CI
janEbert 836f919
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 9a49768
Skip padding when using fused+packed FP8 path
janEbert 0da1c2f
Fix lints
janEbert 08a8c4a
Refactor optional pad if-guard
janEbert 48108e3
Fix lints
janEbert 8b3160b
Merge output trim code paths
janEbert ac101a6
Reduce verbosity
janEbert d899f2b
Explicitly mention padding target and limits
janEbert e09f99b
Merge input pad code paths
janEbert 9f1834a
Remove standalone MLA pad tests
janEbert 0df52ca
Assert MLA pad selects a faster backend
janEbert 2184714
Remove removed test from test runner script
janEbert ebcc35d
Reduce verbosity
janEbert File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
226 changes: 226 additions & 0 deletions
226
tests/pytorch/attention/test_dpa_mla_qkv_head_dim_pad.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| # Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # See LICENSE for license information. | ||
|
|
||
| """Tests for the optional MLA head-dim pad in DotProductAttention. | ||
|
|
||
| Covers: | ||
| * `should_pad_qkv_head_dim` decides correctly (native unfused vs padded fused). | ||
| * DPA with `head_dim_v > head_dim_qk` runs and produces a V-width output. | ||
| * The pad-then-trim is an identity for both `qk > v` and `v > qk`: padding Q/K/V to the | ||
| wider head dim, running with the equal (padded) shape, and trimming back equals the | ||
| native mismatched-dim run. | ||
| """ | ||
|
|
||
| import math | ||
| import pathlib | ||
| import sys | ||
|
|
||
| import pytest | ||
| import torch | ||
|
|
||
| from transformer_engine.pytorch.attention.dot_product_attention import DotProductAttention | ||
| from transformer_engine.pytorch.attention.dot_product_attention import ( | ||
| dot_product_attention as dpa_module, | ||
| ) | ||
| import transformer_engine.pytorch.attention.dot_product_attention.utils as dpa_utils | ||
|
|
||
| _current_file = pathlib.Path(__file__).resolve() | ||
| sys.path = [str(_current_file.parent.parent)] + sys.path | ||
| from utils import reset_rng_states | ||
|
|
||
|
|
||
| def _build_dpa( | ||
| qk, v, num_heads=4, qkv_format="thd", attn_mask_type="padding_causal", softmax_scale=None | ||
| ): | ||
| return DotProductAttention( | ||
| num_attention_heads=num_heads, | ||
| kv_channels=(qk, v), | ||
| attention_type="self", | ||
| attn_mask_type=attn_mask_type, | ||
| qkv_format=qkv_format, | ||
| softmax_scale=softmax_scale, | ||
| ).to(dtype=torch.bfloat16, device="cuda") | ||
|
|
||
|
|
||
| def _thd_inputs(qk, v, t=32, h=4): | ||
| cu = torch.IntTensor([0, 6, 19, 22, t]).cuda() | ||
| q = torch.randn(t, h, qk, device="cuda", dtype=torch.bfloat16, requires_grad=True) | ||
| k = torch.randn(t, h, qk, device="cuda", dtype=torch.bfloat16) | ||
| v = torch.randn(t, h, v, device="cuda", dtype=torch.bfloat16) | ||
| return q, k, v, cu | ||
|
|
||
|
|
||
| def _run_dpa(dpa, q, k, v, cu, max_seqlen=13): | ||
| return dpa( | ||
| q, | ||
| k, | ||
| v, | ||
| cu_seqlens_q=cu, | ||
| cu_seqlens_kv=cu, | ||
| max_seqlen_q=max_seqlen, | ||
| max_seqlen_kv=max_seqlen, | ||
| attn_mask_type="padding_causal", | ||
| ) | ||
|
|
||
|
|
||
| # should_pad_qkv_head_dim | ||
| @pytest.mark.parametrize( | ||
| "native_unfused,padded_fused,expected", | ||
| [ | ||
| (False, False, False), # native already fused -> no pad | ||
| (True, False, False), # both unfused -> no upgrade -> no pad | ||
| (True, True, True), # native unfused, padded fused -> pad | ||
| ], | ||
| ) | ||
| def test_should_pad_qkv_head_dim(monkeypatch, native_unfused, padded_fused, expected): | ||
| """`should_pad_qkv_head_dim` returns True iff native is unfused and padded is fused.""" | ||
| params = dpa_utils.AttentionParams( | ||
| qkv_layout="thd_thd_thd", | ||
| num_heads=4, | ||
| num_gqa_groups=4, | ||
| max_seqlen_q=13, | ||
| max_seqlen_kv=13, | ||
| head_dim_qk=96, | ||
| head_dim_v=128, | ||
| attn_mask_type="padding_causal", | ||
| is_training=True, | ||
| qkv_dtype=torch.bfloat16, | ||
| ) | ||
|
|
||
| # get_attention_backend returns | ||
| # (use_flash, flash_backend, use_fused, fused_backend, use_unfused, available) | ||
| native = ( | ||
| False, | ||
| None, | ||
| not native_unfused, | ||
| None, | ||
| native_unfused, | ||
| [False, not native_unfused, native_unfused], | ||
| ) | ||
| padded = ( | ||
| False, | ||
| None, | ||
| padded_fused, | ||
| None, | ||
| not padded_fused, | ||
| [False, padded_fused, not padded_fused], | ||
| ) | ||
|
|
||
| def fake_backend(p): | ||
| # native probe: real (mismatched) head_dim_qk/v; padded probe: both = max(qk, v). | ||
| # Distinguish by head_dim_qk (native=96, padded=max(96,128)=128). | ||
| is_padded = p.head_dim_qk != params.head_dim_qk | ||
| return padded if is_padded else native | ||
|
|
||
| monkeypatch.setattr(dpa_utils, "get_attention_backend", fake_backend) | ||
| # The decision is memoized on `attention_params` (see | ||
| # `_should_pad_qkv_head_dim_cache`); reset the cache so each parametrization | ||
| # re-probes the freshly monkeypatched backend instead of returning a stale result. | ||
| dpa_utils._should_pad_qkv_head_dim_cache["attention_params"] = None | ||
| dpa_utils._should_pad_qkv_head_dim_cache["result"] = None | ||
| assert dpa_utils.should_pad_qkv_head_dim(params) is expected | ||
|
|
||
|
|
||
| def test_should_pad_qkv_head_dim_equal_dims(): | ||
| """No pad when head_dim_qk == head_dim_v.""" | ||
| params = dpa_utils.AttentionParams( | ||
| qkv_layout="thd_thd_thd", | ||
| num_heads=4, | ||
| num_gqa_groups=4, | ||
| max_seqlen_q=13, | ||
| max_seqlen_kv=13, | ||
| head_dim_qk=128, | ||
| head_dim_v=128, | ||
| attn_mask_type="padding_causal", | ||
| is_training=True, | ||
| qkv_dtype=torch.bfloat16, | ||
| ) | ||
| assert dpa_utils.should_pad_qkv_head_dim(params) is False | ||
|
|
||
|
|
||
| def test_should_pad_qkv_head_dim_is_memoized(monkeypatch): | ||
| """`should_pad_qkv_head_dim` memoizes on the native (pre-pad) params: a second call | ||
| with an equal config skips the `get_attention_backend` probes entirely, even if the | ||
| caller mutated the first params object in place -- as the production forward does when | ||
| it pads `head_dim_qk`/`head_dim_v` after this returns.""" | ||
| base = dict( | ||
| qkv_layout="thd_thd_thd", | ||
| num_heads=4, | ||
| num_gqa_groups=4, | ||
| max_seqlen_q=13, | ||
| max_seqlen_kv=13, | ||
| attn_mask_type="padding_causal", | ||
| is_training=True, | ||
| qkv_dtype=torch.bfloat16, | ||
| ) | ||
| calls = {"n": 0} | ||
|
|
||
| def fake_backend(p): | ||
| calls["n"] += 1 | ||
| # native probe has head_dim_qk=96; padded probe has head_dim_qk=128. | ||
| if p.head_dim_qk != 96: | ||
| return (False, None, True, None, False, [False, True, False]) # fused -> pad | ||
| return (False, None, False, None, True, [False, False, True]) # unfused native | ||
|
|
||
| monkeypatch.setattr(dpa_utils, "get_attention_backend", fake_backend) | ||
| dpa_utils._should_pad_qkv_head_dim_cache["attention_params"] = None | ||
| dpa_utils._should_pad_qkv_head_dim_cache["result"] = None | ||
|
|
||
| params = dpa_utils.AttentionParams(head_dim_qk=96, head_dim_v=128, **base) | ||
| assert dpa_utils.should_pad_qkv_head_dim(params) is True | ||
| assert calls["n"] == 2 # one native + one padded probe | ||
|
|
||
| # Simulate the production forward mutating the live params in place after the call. | ||
| params.head_dim_qk = 128 | ||
| params.head_dim_v = 128 | ||
|
|
||
| # A fresh native params with the same config must still hit the memo (the key is a | ||
| # copy, not the mutated live object) and must not re-probe. | ||
| params2 = dpa_utils.AttentionParams(head_dim_qk=96, head_dim_v=128, **base) | ||
| assert dpa_utils.should_pad_qkv_head_dim(params2) is True | ||
| assert calls["n"] == 2 # cache hit: no new probes | ||
|
|
||
|
|
||
| # v > qk end-to-end | ||
| @pytest.mark.parametrize("qk,v", [(64, 192), (96, 192)]) | ||
| def test_dpa_v_gt_qk_runs(qk, v): | ||
| """DPA with head_dim_v > head_dim_qk runs and produces a V-width output.""" | ||
| reset_rng_states() | ||
| dpa = _build_dpa(qk, v) | ||
| q, k, v_t, cu = _thd_inputs(qk, v) | ||
| out = _run_dpa(dpa, q, k, v_t, cu) | ||
| assert tuple(out.shape) == (32, 4 * v), out.shape # V-width | ||
| out.float().sum().backward() # backward must not crash | ||
|
|
||
|
|
||
| # pad-then-trim is an identity (both directions) | ||
| @pytest.mark.parametrize("qk,v", [(192, 128), (64, 192)]) | ||
| def test_dpa_mla_pad_is_identity(qk, v): | ||
| """Pad-then-trim is an identity: padding Q/K/V to the wider head dim, running with the equal | ||
| (padded) shape, and trimming back equals the native mismatched-dim run -- for both qk > v and v | ||
| > qk. Both runs use the same `softmax_scale` (`1/sqrt(qk)`) that the production forward keeps | ||
| when padding. | ||
| """ | ||
| reset_rng_states() | ||
| m = max(qk, v) | ||
| scale = 1.0 / math.sqrt(qk) | ||
| cu = torch.IntTensor([0, 6, 19, 22, 32]).cuda() | ||
|
|
||
| # Reference: native mismatched-dim run (the production forward; it pads internally | ||
| # only when should_pad_qkv_head_dim upgrades the selected backend). | ||
| dpa_ref = _build_dpa(qk, v) # softmax_scale defaults to 1/sqrt(qk) | ||
| q, k, v_t, _ = _thd_inputs(qk, v) | ||
|
greptile-apps[bot] marked this conversation as resolved.
Outdated
|
||
| out_ref = _run_dpa(dpa_ref, q, k, v_t, cu) | ||
| assert tuple(out_ref.shape) == (32, 4 * v), out_ref.shape | ||
|
|
||
| # Test: manually pad to the common width, run with the equal (padded) shape, trim. | ||
| # Same softmax_scale as the reference so pad-then-trim is a true identity. | ||
| dpa = _build_dpa(m, m, softmax_scale=scale) | ||
| q_p, k_p, v_p, _, _ = dpa_module._pad_qkv_head_dim(q, k, v_t) | ||
| assert q_p.shape[-1] == k_p.shape[-1] == v_p.shape[-1] == m | ||
| out = _run_dpa(dpa, q_p, k_p, v_p, cu) | ||
| # Trim back to the original V width. | ||
| out = dpa_module._trim_output(out, 4, m, v) | ||
| torch.testing.assert_close(out, out_ref, atol=1e-2, rtol=1e-2) | ||
| out.float().sum().backward() # padded path backward must not crash | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel the test file is a little overly verbose - some of the functionalities are pretty obvious, for example, with the caching mechanism. Do you think we can fold/reduce the testing into one test that calls
test_dot_product_attention, like with the other tests, and add it totest_attention.py? Essentially, what we want to see is that for a givenModelConfigsuch ashead_dim_qk=96andhead_dim_v=128, it can cleverly pad and take advantage of one of the faster backends? Thanks!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did it slightly differently: added a new test that reuses the
test_dpa_mlaModelConfigs and parametrizations and then checks whether theshould_padlogic works as expected. Let me know if that's fine.