Repository navigation
[ROCm][Perf] Kimi-K3: token-sharded residual stream for long prefills #59693
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
vanshbhatia-amd
wants to merge
25
commits into
vllm-project:main
Choose a base branch
from
vanshbhatia-amd:k3/prefill-sp
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.
+830
−10
Open
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
8156003
[ROCm][Perf] Kimi-K3: token-sharded residual stream for long prefills
vanshbhatia-amd 09db6bc
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd 58096d0
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd b4e214f
[ROCm] Kimi-K3 latent-MoE tail: avoid hipBLASLt addmm_ fault
vanshbhatia-amd 14e30a4
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd 239cee6
[ROCm][Kimi-K3] Address review on token-sharded prefill
vanshbhatia-amd da9186c
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd adb2392
[ROCm][Kimi-K3] Check shard routing on a real MoE; target unreduced p…
vanshbhatia-amd f0c6a4b
[ROCm][Kimi-K3] Keep prefill SP off when routing is observed; test th…
vanshbhatia-amd 3acec0a
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd a09d2aa
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd 694b504
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd 8ddfef0
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd c7ff67b
[ROCm][Kimi-K3] Narrow the optional latent norm in the prefill SP test
vanshbhatia-amd a38bc49
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd 9e09d08
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd 6214ff8
[ROCm][Kimi-K3] Keep monolithic MoE kernels off the shard-routed path
vanshbhatia-amd 9b485ac
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd f8bbf36
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd 5b6bf0b
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd e939aac
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd 1e29d8f
[ROCm][Kimi-K3] Project the routed input locally on the token shard
vanshbhatia-amd 768634a
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd a651dc2
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd 4a121d5
Merge branch 'main' into k3/prefill-sp
vanshbhatia-amd 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,285 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """Token-sharded Kimi-K3 prefill on ROCm must equal the replicated path. | ||
|
|
||
| Each rank ends up holding only its own token rows, so the multi-GPU checks | ||
| compare every rank's shard with the matching rows of the all-reduce result. | ||
| """ | ||
|
|
||
| from types import SimpleNamespace | ||
|
|
||
| import pytest | ||
| import torch | ||
| import torch.distributed as dist | ||
| import torch.nn.functional as F | ||
| from torch.multiprocessing import spawn | ||
|
|
||
| from tests.utils import ( | ||
| ensure_current_vllm_config, | ||
| init_test_distributed_environment, | ||
| multi_gpu_test, | ||
| ) | ||
| from vllm.distributed import get_tp_group | ||
| from vllm.model_executor.layers.fused_moe.runner import moe_runner | ||
| from vllm.model_executor.layers.layernorm import RMSNorm | ||
| from vllm.model_executor.layers.linear import ReplicatedLinear | ||
| from vllm.models.kimi_k3.amd import latent_moe_runner, sp | ||
| from vllm.models.kimi_k3.amd.latent_moe_runner import ROCmLatentMoERunner | ||
| from vllm.models.kimi_k3.amd.linear import KimiRoutedOutputTransform | ||
| from vllm.platforms import current_platform | ||
| from vllm.utils.network_utils import get_open_port | ||
|
|
||
| pytestmark = pytest.mark.skipif( | ||
| not current_platform.is_rocm(), | ||
| reason="Token-sharded Kimi-K3 prefill is only wired up on ROCm", | ||
| ) | ||
|
|
||
| HIDDEN_SIZE = 7168 | ||
| LATENT_SIZE = 3584 | ||
| TOP_K = 16 | ||
| NUM_EXPERTS = 896 | ||
| EPS = 1e-5 | ||
| DTYPE = torch.bfloat16 | ||
|
|
||
|
|
||
| def _build_transform(device: torch.device) -> KimiRoutedOutputTransform: | ||
| norm = RMSNorm(LATENT_SIZE, eps=EPS).to(device=device, dtype=DTYPE) | ||
| up_proj = ReplicatedLinear( | ||
| LATENT_SIZE, | ||
| HIDDEN_SIZE, | ||
| bias=False, | ||
| params_dtype=DTYPE, | ||
| prefix="routed_expert_up_proj", | ||
| ).to(device=device) | ||
|
|
||
| torch.manual_seed(0) | ||
| norm.weight.data.copy_(1 + 0.1 * torch.randn_like(norm.weight)) | ||
| up_proj.weight.data.copy_(torch.randn_like(up_proj.weight) / LATENT_SIZE**0.5) | ||
| return KimiRoutedOutputTransform(norm, up_proj) | ||
|
|
||
|
|
||
| def _tail_runner( | ||
| transform: KimiRoutedOutputTransform, tp_world: int | ||
| ) -> ROCmLatentMoERunner: | ||
| runner = object.__new__(ROCmLatentMoERunner) | ||
| attrs = { | ||
| "routed_output_transform": transform, | ||
| "_up_proj_shard_size": HIDDEN_SIZE // tp_world, | ||
| "_logged_sharded_tail": False, | ||
| "moe_config": SimpleNamespace( | ||
| tp_size=tp_world, | ||
| ep_size=1, | ||
| is_sequence_parallel=False, | ||
| skip_final_all_reduce=False, | ||
| ), | ||
| } | ||
| for name, value in attrs.items(): | ||
| object.__setattr__(runner, name, value) | ||
| return runner | ||
|
|
||
|
|
||
| def _all_reduced(tensor: torch.Tensor, group) -> torch.Tensor: | ||
| reduced = tensor.clone() | ||
| dist.all_reduce(reduced, group=group) | ||
| return reduced | ||
|
|
||
|
|
||
| def _check_sharded_tail(device: torch.device, tp_world: int, rank: int) -> None: | ||
| """The sharded tail returns this rank's rows of the replicated tail.""" | ||
| transform = _build_transform(device) | ||
| runner = _tail_runner(transform, tp_world) | ||
| group = get_tp_group().device_group | ||
|
|
||
| for iteration, num_tokens in enumerate((tp_world, 2 * tp_world, 64)): | ||
| torch.manual_seed(100 * iteration + rank + 1) | ||
| routed = 0.01 * torch.randn(num_tokens, LATENT_SIZE, device=device, dtype=DTYPE) | ||
| shared = torch.randn(num_tokens, HIDDEN_SIZE, device=device, dtype=DTYPE) | ||
|
|
||
| expected = F.linear( | ||
| F.rms_norm( | ||
| _all_reduced(routed, group), | ||
| (LATENT_SIZE,), | ||
| transform.norm.weight, | ||
| EPS, | ||
| ), | ||
| transform.up_proj.weight, | ||
| ) | ||
| expected.add_(_all_reduced(shared, group)) | ||
| rows = num_tokens // tp_world | ||
|
|
||
| sp.ACTIVE = True | ||
| try: | ||
| actual = runner._shard_up_proj_tail(routed, shared, None) | ||
| finally: | ||
| sp.ACTIVE = False | ||
|
|
||
| assert actual.shape == (rows, HIDDEN_SIZE) | ||
| torch.testing.assert_close( | ||
| actual, expected[rank * rows : (rank + 1) * rows], atol=8e-2, rtol=3e-2 | ||
| ) | ||
|
|
||
|
|
||
| def _check_routing_gather(device: torch.device, tp_world: int, rank: int) -> None: | ||
| """Packed routing survives the gather bit-exactly and in rank order.""" | ||
| rows = 5 | ||
| gen = torch.Generator(device=device).manual_seed(7) | ||
| weights = torch.rand(tp_world * rows, TOP_K, device=device, generator=gen) | ||
| ids = torch.randint( | ||
| NUM_EXPERTS, (tp_world * rows, TOP_K), device=device, generator=gen | ||
| ).to(torch.int32) | ||
| shard = slice(rank * rows, (rank + 1) * rows) | ||
|
|
||
| got_weights, got_ids = sp.all_gather_routing(weights[shard], ids[shard]) | ||
|
|
||
| assert got_weights.dtype == weights.dtype and got_ids.dtype == ids.dtype | ||
| torch.testing.assert_close(got_weights, weights, atol=0, rtol=0) | ||
| torch.testing.assert_close(got_ids, ids, atol=0, rtol=0) | ||
|
|
||
|
|
||
| def _check_padded_gather_scatter( | ||
| device: torch.device, tp_world: int, rank: int | ||
| ) -> None: | ||
| """Token counts that do not divide TP round-trip through the padded shards.""" | ||
| group = get_tp_group().device_group | ||
| for num_tokens in (tp_world * 8 - 3, tp_world * 8 + 1, tp_world * 8): | ||
| padded = -(-num_tokens // tp_world) * tp_world | ||
| rows = padded // tp_world | ||
| gen = torch.Generator(device=device).manual_seed(num_tokens) | ||
| full = torch.randn(padded, 64, device=device, generator=gen) | ||
| full[num_tokens:] = 0 | ||
|
|
||
| gathered = sp.gather_tokens(full[rank * rows : (rank + 1) * rows], num_tokens) | ||
| torch.manual_seed(rank + 1) | ||
| partial = torch.randn(num_tokens, 64, device=device) | ||
| scattered = sp.scatter_tokens(partial) | ||
|
|
||
| torch.testing.assert_close(gathered, full[:num_tokens], atol=0, rtol=0) | ||
| expected = F.pad(_all_reduced(partial, group), (0, 0, 0, padded - num_tokens)) | ||
| torch.testing.assert_close( | ||
| scattered, expected[rank * rows : (rank + 1) * rows], atol=1e-5, rtol=1e-5 | ||
| ) | ||
|
|
||
|
|
||
| _CHECKS = { | ||
| "sharded_tail": _check_sharded_tail, | ||
| "routing_gather": _check_routing_gather, | ||
| "padded_gather_scatter": _check_padded_gather_scatter, | ||
| } | ||
|
|
||
|
|
||
| def _worker(local_rank: int, world_size: int, port: str, check: str) -> None: | ||
| device = torch.device(f"cuda:{local_rank}") | ||
| torch.accelerator.set_device_index(device) | ||
| with ensure_current_vllm_config(): | ||
| init_test_distributed_environment( | ||
| world_size, 1, local_rank, port, local_rank=local_rank | ||
| ) | ||
| _CHECKS[check](device, world_size, local_rank) | ||
|
|
||
|
|
||
| def _run_ranks(check: str, tp_size: int) -> None: | ||
| spawn( | ||
| _worker, | ||
| args=(tp_size, str(get_open_port()), check), | ||
| nprocs=tp_size, | ||
| join=True, | ||
| ) | ||
|
|
||
|
|
||
| @multi_gpu_test(num_gpus=4) | ||
| def test_sharded_tail_tp4_matches_replicated_rows() -> None: | ||
| _run_ranks("sharded_tail", 4) | ||
|
|
||
|
|
||
| @multi_gpu_test(num_gpus=8) | ||
| def test_sharded_tail_tp8_matches_replicated_rows() -> None: | ||
| _run_ranks("sharded_tail", 8) | ||
|
|
||
|
|
||
| @multi_gpu_test(num_gpus=4) | ||
| def test_routing_gather_tp4_is_exact() -> None: | ||
| _run_ranks("routing_gather", 4) | ||
|
|
||
|
|
||
| @multi_gpu_test(num_gpus=8) | ||
|
vanshbhatia-amd marked this conversation as resolved.
Resolved
|
||
| def test_padded_gather_scatter_tp8() -> None: | ||
| _run_ranks("padded_gather_scatter", 8) | ||
|
|
||
|
|
||
| class _Router(torch.nn.Module): | ||
| def select_experts(self, hidden_states, router_logits, **kwargs): | ||
| return "routed" | ||
|
|
||
|
|
||
| def test_pre_routed_overrides_then_restores() -> None: | ||
| router = _Router() | ||
| topk = (torch.ones(2, TOP_K), torch.zeros(2, TOP_K, dtype=torch.int32)) | ||
|
|
||
| with sp.pre_routed(router, topk): | ||
| assert router.select_experts(hidden_states=None, router_logits=None) is topk | ||
| assert router.select_experts(None, None) == "routed" | ||
| assert "select_experts" not in router.__dict__ | ||
|
|
||
| with pytest.raises(RuntimeError), sp.pre_routed(router, topk): | ||
| raise RuntimeError | ||
| assert router.select_experts(None, None) == "routed" | ||
|
|
||
|
|
||
| def test_pre_routed_keeps_an_instance_override() -> None: | ||
| router = _Router() | ||
| router.__dict__["select_experts"] = lambda *args, **kwargs: "patched" | ||
|
|
||
| with sp.pre_routed(router, ("w", "i")): | ||
| assert router.select_experts(None, None) == ("w", "i") | ||
| assert router.select_experts(None, None) == "patched" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "min_tokens,tp_size,num_tokens,has_residual,has_aux,last_rank,expected", | ||
| [ | ||
| (1024, 8, 16384, False, False, True, True), | ||
| (1024, 8, 1024, False, False, True, True), | ||
| (0, 8, 16384, False, False, True, False), | ||
| (1024, 1, 16384, False, False, True, False), | ||
| (1024, 8, 1016, False, False, True, False), | ||
| (1024, 8, 16381, False, False, True, True), | ||
| (1024, 8, 16384, True, False, True, False), | ||
| (1024, 8, 16384, False, True, True, False), | ||
| (1024, 8, 16384, False, False, False, False), | ||
| ], | ||
| ) | ||
| def test_should_shard( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| min_tokens: int, | ||
| tp_size: int, | ||
| num_tokens: int, | ||
| has_residual: bool, | ||
| has_aux: bool, | ||
| last_rank: bool, | ||
| expected: bool, | ||
| ) -> None: | ||
| monkeypatch.setenv("VLLM_KIMI_K3_AMD_PREFILL_SP_MIN_TOKENS", str(min_tokens)) | ||
| monkeypatch.setattr(sp, "get_tensor_model_parallel_world_size", lambda: tp_size) | ||
| monkeypatch.setattr( | ||
| sp, "get_pp_group", lambda: SimpleNamespace(is_last_rank=last_rank) | ||
| ) | ||
| assert sp.should_shard(num_tokens, has_residual, has_aux) is expected | ||
|
|
||
|
|
||
| def test_unshardable_tail_narrows_to_own_rows(monkeypatch: pytest.MonkeyPatch) -> None: | ||
| """Without the sharded tail the runner still returns only this rank's rows.""" | ||
| full = torch.arange(8 * 3, dtype=torch.float32).view(8, 3) | ||
| monkeypatch.setattr(moe_runner.MoERunner, "forward", lambda *a, **k: full) | ||
| monkeypatch.setattr( | ||
| latent_moe_runner, "get_tensor_model_parallel_world_size", lambda: 4 | ||
| ) | ||
| monkeypatch.setattr(latent_moe_runner, "get_tensor_model_parallel_rank", lambda: 2) | ||
| runner = object.__new__(ROCmLatentMoERunner) | ||
| object.__setattr__(runner, "_tail_shardable", False) | ||
|
|
||
| monkeypatch.setattr(sp, "ACTIVE", True) | ||
| out = runner.forward(torch.empty(8, 3), torch.empty(8, 2)) | ||
| torch.testing.assert_close(out, full[4:6]) | ||
|
|
||
| monkeypatch.setattr(sp, "ACTIVE", False) | ||
| assert runner.forward(torch.empty(8, 3), torch.empty(8, 2)) is full | ||
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
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.
Uh oh!
There was an error while loading. Please reload this page.