-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Fix config.softmax_scale not being considered
#3698
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
2
commits into
NVIDIA:main
Choose a base branch
from
janEbert:fix-softmax-scale
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 all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
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
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
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,201 @@ | ||
| # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. | ||
|
|
||
| """Tests for softmax_scale config propagation. | ||
|
|
||
| Verifies that TransformerConfig.softmax_scale is properly considered | ||
| in both Attention.flash_decode_and_prefill and MLASelfAttention.__init__. | ||
| """ | ||
|
|
||
| import math | ||
| from unittest.mock import patch | ||
|
|
||
| import pytest | ||
| import torch | ||
|
|
||
| from megatron.core.extensions.transformer_engine import TEDotProductAttention | ||
| from megatron.core.models.common.embeddings import _yarn_get_mscale | ||
| from megatron.core.models.gpt.gpt_layer_specs import ( | ||
| get_gpt_layer_with_transformer_engine_submodules, | ||
| ) | ||
| from megatron.core.tensor_parallel.random import model_parallel_cuda_manual_seed | ||
| from megatron.core.transformer.attention import SelfAttention | ||
| from megatron.core.transformer.enums import AttnMaskType | ||
| from megatron.core.transformer.multi_latent_attention import ( | ||
| MLASelfAttention, | ||
| MLASelfAttentionSubmodules, | ||
| ) | ||
| from megatron.core.transformer.transformer_config import MLATransformerConfig, TransformerConfig | ||
| from tests.unit_tests.test_utilities import Utils | ||
|
|
||
|
|
||
| def _get_mla_submodules(): | ||
| submodules = get_gpt_layer_with_transformer_engine_submodules( | ||
| multi_latent_attention=True | ||
| ).self_attention.submodules | ||
| assert isinstance(submodules, MLASelfAttentionSubmodules) | ||
| return submodules | ||
|
|
||
|
|
||
| class TestAttentionSoftmaxScale: | ||
| """Tests that flash_decode_and_prefill respects config.softmax_scale.""" | ||
|
|
||
| @pytest.fixture(scope='function', autouse=True) | ||
| def setup_and_teardown(self): | ||
| if not torch.cuda.is_available(): | ||
| pytest.skip("GPU required") | ||
| Utils.initialize_model_parallel(1, 1) | ||
| model_parallel_cuda_manual_seed(123) | ||
| yield | ||
| Utils.destroy_model_parallel() | ||
|
|
||
| def _make_attention(self, softmax_scale=None): | ||
| config = TransformerConfig( | ||
| num_layers=2, | ||
| hidden_size=128, | ||
| num_attention_heads=4, | ||
| use_cpu_initialization=True, | ||
| softmax_scale=softmax_scale, | ||
| ) | ||
| submodules = get_gpt_layer_with_transformer_engine_submodules().self_attention.submodules | ||
| attn = SelfAttention(config, submodules, layer_number=1) | ||
| attn.eval() | ||
| attn.cuda() | ||
| return attn | ||
|
|
||
| def _call_and_capture_softmax_scale(self, attn): | ||
| """Call flash_decode_and_prefill with HAVE_FA3=False, capture softmax_scale from FA2.""" | ||
| head_dim = attn.config.kv_channels | ||
| num_heads = attn.config.num_attention_heads | ||
| # q shape: [total_tokens, num_heads, 1, head_dim] — squeezed to [total, heads, head_dim] | ||
| q = torch.randn(2, num_heads, 1, head_dim, device='cuda', dtype=torch.float16) | ||
| k = torch.randn(2, num_heads, head_dim, device='cuda', dtype=torch.float16) | ||
| v = torch.randn(2, num_heads, head_dim, device='cuda', dtype=torch.float16) | ||
| cu_seqlens_q = torch.tensor([0, 1, 2], device='cuda', dtype=torch.int32) | ||
| cu_seqlens_k = torch.tensor([0, 1, 2], device='cuda', dtype=torch.int32) | ||
| seqlens_k = torch.tensor([1, 1], device='cuda', dtype=torch.int32) | ||
| block_table = torch.zeros(2, 1, device='cuda', dtype=torch.int32) | ||
|
|
||
| captured = {} | ||
|
|
||
| def fake_fa_varlen(*args, **kwargs): | ||
| captured['softmax_scale'] = kwargs.get('softmax_scale') | ||
| return torch.randn(2, num_heads, head_dim, device='cuda', dtype=torch.float16) | ||
|
|
||
| with ( | ||
| patch('megatron.core.transformer.attention.HAVE_FA3', False), | ||
| patch('megatron.core.transformer.attention.flash_attn_varlen_func', fake_fa_varlen), | ||
| ): | ||
| attn.batch_invariant_mode = False | ||
| attn.flash_decode_and_prefill( | ||
| q, | ||
| k, | ||
| v, | ||
| max_seqlen_q=1, | ||
| max_seqlen_k=1, | ||
| cu_seqlens_q=cu_seqlens_q, | ||
| cu_seqlens_k=cu_seqlens_k, | ||
| seqlens_k=seqlens_k, | ||
| block_table=block_table, | ||
| is_decode_only=False, | ||
| ) | ||
|
|
||
| return captured['softmax_scale'] | ||
|
|
||
| def test_config_softmax_scale_used(self): | ||
| """When config.softmax_scale is set, flash_decode_and_prefill should use it.""" | ||
| attn = self._make_attention(softmax_scale=0.042) | ||
| assert not hasattr(attn, 'softmax_scale') | ||
| scale = self._call_and_capture_softmax_scale(attn) | ||
| assert scale == 0.042 | ||
|
|
||
| def test_default_softmax_scale(self): | ||
| """When config.softmax_scale is None and no instance attr, use 1/sqrt(head_dim).""" | ||
| attn = self._make_attention(softmax_scale=None) | ||
| assert not hasattr(attn, 'softmax_scale') | ||
| scale = self._call_and_capture_softmax_scale(attn) | ||
| head_dim = attn.config.kv_channels | ||
| assert scale == pytest.approx(head_dim**-0.5) | ||
|
|
||
| def test_instance_attr_takes_precedence(self): | ||
| """When self.softmax_scale is set, it takes precedence over config.softmax_scale.""" | ||
| attn = self._make_attention(softmax_scale=0.042) | ||
| attn.softmax_scale = 0.123 | ||
| scale = self._call_and_capture_softmax_scale(attn) | ||
| assert scale == 0.123 | ||
|
|
||
|
|
||
| class TestMLASoftmaxScale: | ||
| """Tests that MLASelfAttention.__init__ respects config.softmax_scale.""" | ||
|
|
||
| @pytest.fixture(scope='function', autouse=True) | ||
| def setup_and_teardown(self): | ||
| if not torch.cuda.is_available(): | ||
| pytest.skip("GPU required") | ||
| Utils.initialize_model_parallel(1, 1) | ||
| model_parallel_cuda_manual_seed(123) | ||
| yield | ||
| Utils.destroy_model_parallel() | ||
|
|
||
| def _make_mla(self, softmax_scale=None, rotary_scaling_factor=40.0, mscale_all_dim=0.0): | ||
| config = MLATransformerConfig( | ||
| num_layers=2, | ||
| hidden_size=12, | ||
| num_attention_heads=4, | ||
| use_cpu_initialization=True, | ||
| q_lora_rank=32, | ||
| kv_lora_rank=32, | ||
| qk_head_dim=128, | ||
| v_head_dim=128, | ||
| qk_pos_emb_head_dim=64, | ||
| rope_type='yarn', | ||
| rotary_base=10000, | ||
| original_max_position_embeddings=32, | ||
| softmax_scale=softmax_scale, | ||
| rotary_scaling_factor=rotary_scaling_factor, | ||
| mscale_all_dim=mscale_all_dim, | ||
| ) | ||
| attention = MLASelfAttention( | ||
| config, _get_mla_submodules(), layer_number=1, attn_mask_type=AttnMaskType.causal | ||
| ) | ||
| return config, attention | ||
|
|
||
| def test_config_softmax_scale_applied(self): | ||
| """When config.softmax_scale is set, MLA uses mscale^2 * config.softmax_scale.""" | ||
| config, attention = self._make_mla(softmax_scale=0.042) | ||
| mscale = _yarn_get_mscale(config.rotary_scaling_factor, config.mscale_all_dim) | ||
| expected = mscale * mscale * 0.042 | ||
| assert attention.softmax_scale == pytest.approx(expected) | ||
|
|
||
| def test_default_softmax_scale(self): | ||
| """When config.softmax_scale is None, MLA uses mscale^2 / sqrt(q_head_dim).""" | ||
| config, attention = self._make_mla(softmax_scale=None) | ||
| q_head_dim = config.qk_head_dim + config.qk_pos_emb_head_dim | ||
| mscale = _yarn_get_mscale(config.rotary_scaling_factor, config.mscale_all_dim) | ||
| expected = mscale * mscale * (1 / math.sqrt(q_head_dim)) | ||
| assert attention.softmax_scale == pytest.approx(expected) | ||
|
|
||
| def test_config_softmax_scale_with_yarn_scaling(self): | ||
| """With rotary_scaling_factor > 1, mscale != 1, and config.softmax_scale is applied (not None).""" | ||
| config, attention = self._make_mla(softmax_scale=0.042, rotary_scaling_factor=4.0, mscale_all_dim=1.0) | ||
| mscale = _yarn_get_mscale(config.rotary_scaling_factor, config.mscale_all_dim) | ||
| assert mscale != 1.0, "mscale should be nontrivial with scaling_factor > 1 and mscale_all_dim != 0" | ||
| expected = mscale * mscale * 0.042 | ||
| assert attention.softmax_scale == pytest.approx(expected) | ||
|
|
||
| def test_config_softmax_scale_with_trivial_scaling(self): | ||
| """With rotary_scaling_factor and mscale = 0.""" | ||
| config, attention = self._make_mla(softmax_scale=0.042, rotary_scaling_factor=4.0) | ||
| mscale = _yarn_get_mscale(config.rotary_scaling_factor, config.mscale_all_dim) | ||
| assert mscale == 1.0, "mscale should be trivial with mscale_all_dim == 0" | ||
| expected = mscale * mscale * 0.042 | ||
| assert attention.softmax_scale == pytest.approx(expected) | ||
|
|
||
| def test_softmax_scale_propagated_to_core_attention(self): | ||
| """The computed softmax_scale should be passed to core_attention.""" | ||
| _, attention = self._make_mla(softmax_scale=0.042) | ||
| if isinstance(attention.core_attention, TEDotProductAttention): | ||
| assert attention.core_attention.flash_attention.softmax_scale == attention.softmax_scale | ||
| assert attention.core_attention.fused_attention.softmax_scale == attention.softmax_scale | ||
| assert attention.core_attention.unfused_attention.softmax_scale == attention.softmax_scale | ||
| else: | ||
| assert attention.core_attention.softmax_scale == attention.softmax_scale |
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'm not super familiar with the details of MLA... does it even make sense to have a configured softmax_scale for MLA? I'm wondering if we should instead fail in this case?
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.
We could ask the same about standard Attention, right? :)
One argument that objectively speaks for allowing this would be a hypothetical μP implementation for MLA, which would likely make use of
softmax_scalesimilar to how it's done for standard Attention.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.
For comparing this to standard attention, you can basically ignore the
mscalestuff, that's just relevant for YaRN.MLA defines its softmax_scale as
(qk_head_dim + qk_pos_emb_head_dim)^-0.5. The first term in the sum is the same as in standard attention; the second term is the per-head dimensionality of the decoupled queries, which are part of the modified, decoupled RoPE for MLA.