Repository navigation
[Perf][ROCm][Kimi-K3] Fuse all-reduce with RMSNorm in latent-MoE #57920
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
base: main
Are you sure you want to change the base?
Changes from all commits
2981333
419b75c
6e4b736
58a2ac5
8b4a7a3
116e320
5511129
422e1fb
1d03372
1501e62
8d58461
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """AITER fused AR+RMSNorm must equal RMSNorm(all_reduce(partial)). | ||
|
|
||
| One TP2 spawn covers the fused kernel (4 tokens) and the unfused fallback | ||
| (256 tokens). Hidden dim 3584 is outside AITER's old static-template set. | ||
| """ | ||
|
|
||
| import pytest | ||
| import torch | ||
| from torch.multiprocessing import spawn | ||
|
|
||
| from tests.utils import ensure_current_vllm_config, init_test_distributed_environment | ||
| from vllm.distributed import cleanup_dist_env_and_memory | ||
| from vllm.distributed.communication_op import tensor_model_parallel_all_reduce | ||
| from vllm.model_executor.layers.layernorm import RMSNorm | ||
| from vllm.models.common.amd.ops.fused_allreduce_rms_norm import ( | ||
| can_fuse_allreduce_rms_norm, | ||
| fused_allreduce_rms_norm_out, | ||
| ) | ||
| from vllm.platforms import current_platform | ||
| from vllm.utils.network_utils import get_open_port | ||
| from vllm.utils.torch_utils import set_random_seed | ||
|
|
||
| pytestmark = pytest.mark.skipif( | ||
| not current_platform.is_rocm(), | ||
| reason="AITER fused AR+RMSNorm is only wired up on ROCm", | ||
| ) | ||
|
|
||
| HIDDEN_SIZE = 3584 | ||
| EPS = 1e-5 | ||
| DTYPE = torch.bfloat16 | ||
| WORLD_SIZE = 2 | ||
|
|
||
|
|
||
| def _check_matches_unfused(norm: RMSNorm, partial: torch.Tensor) -> None: | ||
| ref = norm(tensor_model_parallel_all_reduce(partial.clone())) | ||
| out = fused_allreduce_rms_norm_out(partial.clone(), norm) | ||
| torch.accelerator.synchronize() | ||
| torch.testing.assert_close(out, ref, atol=2e-2, rtol=2e-2) | ||
|
|
||
|
|
||
| @ensure_current_vllm_config() | ||
| def _worker_fused_ar_rms(local_rank, world_size, port, seed): | ||
| device = torch.device(f"cuda:{local_rank}") | ||
| torch.accelerator.set_device_index(device) | ||
| init_test_distributed_environment( | ||
| world_size, 1, local_rank, port, local_rank=local_rank | ||
| ) | ||
|
|
||
| set_random_seed(seed) | ||
| norm = RMSNorm(HIDDEN_SIZE, eps=EPS).to(device=device, dtype=DTYPE) | ||
| with torch.no_grad(): | ||
| norm.weight.normal_(mean=1.0, std=0.1) | ||
|
|
||
| torch.manual_seed(seed + local_rank) | ||
| fused = torch.randn(4, HIDDEN_SIZE, dtype=DTYPE, device=device) | ||
| fallback = torch.randn(256, HIDDEN_SIZE, dtype=DTYPE, device=device) | ||
|
|
||
| _check_matches_unfused(norm, fused) | ||
| assert not can_fuse_allreduce_rms_norm(fallback) | ||
| _check_matches_unfused(norm, fallback) | ||
|
|
||
| cleanup_dist_env_and_memory() | ||
|
|
||
|
|
||
| def test_fused_allreduce_rms_norm_out(): | ||
| if current_platform.device_count() < WORLD_SIZE: | ||
| pytest.skip(f"Need >= {WORLD_SIZE} GPUs") | ||
| spawn( | ||
| _worker_fused_ar_rms, | ||
| args=(WORLD_SIZE, str(get_open_port()), 42), | ||
| nprocs=WORLD_SIZE, | ||
| join=True, | ||
| ) | ||
|
|
||
|
|
||
| def test_gate_is_closed_without_aiter_custom_ar(monkeypatch): | ||
| from vllm import _aiter_ops | ||
|
|
||
| monkeypatch.setattr( | ||
| _aiter_ops.rocm_aiter_ops, "is_custom_all_reduce_enabled", lambda: False | ||
| ) | ||
| hidden = torch.empty(4, HIDDEN_SIZE, dtype=DTYPE) | ||
| assert not can_fuse_allreduce_rms_norm(hidden) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """AITER fused all-reduce + RMSNorm (no residual).""" | ||
|
|
||
| import torch | ||
|
|
||
| from vllm._aiter_ops import rocm_aiter_ops | ||
| from vllm.distributed import ( | ||
| get_tensor_model_parallel_world_size, | ||
| tensor_model_parallel_all_reduce, | ||
| ) | ||
| from vllm.model_executor.layers.layernorm import RMSNorm | ||
|
|
||
| # Zero residual, cached per shape so CUDA-graph captures keep a stable pointer. | ||
| _zero_residuals: dict[tuple[torch.Size, torch.dtype, torch.device], torch.Tensor] = {} | ||
|
|
||
|
|
||
| def _zero_residual(hidden_states: torch.Tensor) -> torch.Tensor: | ||
| key = (hidden_states.shape, hidden_states.dtype, hidden_states.device) | ||
| buf = _zero_residuals.get(key) | ||
| if buf is None: | ||
| buf = torch.zeros_like(hidden_states) | ||
| _zero_residuals[key] = buf | ||
| return buf | ||
|
|
||
|
|
||
| def can_fuse_allreduce_rms_norm(hidden_states: torch.Tensor) -> bool: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: Can we re-use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is also a valid point, if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we are talking about this: If I understand correctly, the issue is that in the current design we want to keep the code cleaner by keeping flashinfer/trtllm and aiter separate and independent from each other. but that implementation still keeps them closely related. Perhaps we need a separate PR for this. |
||
| """True when AITER's one-stage fused AR+RMSNorm covers this tensor.""" | ||
| if not rocm_aiter_ops.is_custom_all_reduce_enabled(): | ||
| return False | ||
| # gfx1250 custom AR has no fused_allreduce_rmsnorm: | ||
| # https://github.com/ROCm/aiter/blob/v0.1.22/csrc/include/custom_all_reduce_gfx1250.h | ||
| # https://github.com/ROCm/aiter/blob/v0.1.22/csrc/include/custom_all_reduce.h#L69 | ||
| from vllm.platforms.rocm import on_gfx1250 | ||
|
|
||
| if on_gfx1250(): | ||
| return False | ||
| if hidden_states.dim() != 2 or not hidden_states.is_contiguous(): | ||
| return False | ||
| if hidden_states.dtype not in (torch.bfloat16, torch.float16): | ||
| return False | ||
|
|
||
| aiter_ar = rocm_aiter_ops.get_aiter_allreduce() | ||
| if aiter_ar is None or aiter_ar.disabled: | ||
| return False | ||
| if getattr(aiter_ar.aiter_ca, "_is_gfx1250", False): | ||
| return False | ||
| if not aiter_ar.supports_dynamic_hidden_dim: | ||
| return False | ||
| if not aiter_ar.should_custom_ar(hidden_states): | ||
| return False | ||
| return aiter_ar.use_1stage_fused_ar_rms(hidden_states) | ||
|
|
||
|
|
||
| def fused_allreduce_rms_norm_out( | ||
|
amd-mghanimi marked this conversation as resolved.
|
||
| hidden_states: torch.Tensor, | ||
| norm: RMSNorm, | ||
| ) -> torch.Tensor: | ||
| """Equivalent to ``norm(all_reduce(hidden_states))``.""" | ||
| if get_tensor_model_parallel_world_size() == 1: | ||
| return norm(hidden_states) | ||
|
|
||
| if not can_fuse_allreduce_rms_norm(hidden_states): | ||
| return norm(tensor_model_parallel_all_reduce(hidden_states)) | ||
|
|
||
| norm_out, _ = rocm_aiter_ops.get_fused_allreduce_rmsnorm_op()( | ||
| input_=hidden_states, | ||
| residual=_zero_residual(hidden_states), | ||
| weight=norm.weight, | ||
| epsilon=norm.variance_epsilon, | ||
| gemma_norm=False, | ||
| ) | ||
| return norm_out | ||
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.
Question: Would the WorkspaceManager be safer here instead of manual allocation and caching?
Uh oh!
There was an error while loading. Please reload this page.
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 think the term
zero_residualis related to difference betweenRMSNormandAddRMSNorm, so when there is nothing to add, we still need a dummy zero tensor. it takes a few kilo bytes of memory.The old implementation has this pattern:
vllm/vllm/compilation/passes/fusion/allreduce_rms_fusion.py
Lines 1184 to 1191 in 606d124
My understanding is that this way we make sure that the dummy cache is allocated once for all other calls since they are stored like key/value dict. I guess we could also use
get_persistent/get_persistent_resourcethen we have to make dependency on v1 workers? since we don't want to change this tensor maybe it is an overkill?