Repository navigation
[EPLB] Add contention-aware expert migration batching #52641
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
Merged
robertgshaw2-redhat
merged 44 commits into
vllm-project:main
from
DOCCA0:agent/eplb-migration-batching
Oct 3, 2026
+588
−8
Merged
Changes from all commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
4f63218
Add contention-aware EPLB migration batching
DOCCA0 5da6f91
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 a3db9bd
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 32b5a50
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 e0d1dac
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 d2ef1cc
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 5909a58
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 35cdfc1
Explain EPLB migration batching invariants
DOCCA0 94d7d04
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 b5ad4d7
Precompute EPLB migration schedules per rebalance cycle
DOCCA0 ad1661e
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 009a5c0
Clarify default EPLB migration path
DOCCA0 b49207d
State EPLB batching default explicitly
DOCCA0 390544c
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 5bcab41
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 e976f0a
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 fd5dfdc
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 eee71ba
Merge upstream main into EPLB migration batching
DOCCA0 37db8fc
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 d28f087
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 a464393
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 38c2124
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 940a0c8
Keep migration batching off the sync EPLB path
DOCCA0 48dcf66
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 bf96b0d
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 235e33d
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 c181864
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 ae4211c
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 bffb84d
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 f785f41
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 41946d0
Merge branch 'main' into agent/eplb-migration-batching
robertgshaw2-redhat 2a18186
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 ece5aee
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 3b0305c
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 a2c4a90
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 219b302
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 e08b095
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 20adb20
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 aa39dca
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 cf30fc6
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 b2ad080
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 5b37c5a
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 63952b9
Merge branch 'main' into agent/eplb-migration-batching
robertgshaw2-redhat a5c4bca
Merge branch 'main' into agent/eplb-migration-batching
DOCCA0 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,278 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
| import torch | ||
|
|
||
| from vllm.config.parallel import EPLBConfig | ||
| from vllm.distributed.eplb.eplb_communicator import EplbCommunicator | ||
| from vllm.distributed.eplb.migration_scheduler import ( | ||
| MigrationFlow, | ||
| schedule_migration_batches, | ||
| schedule_migration_batches_for_layers, | ||
| ) | ||
| from vllm.distributed.eplb.rebalance_execute import move_from_buffer, move_to_buffer | ||
|
|
||
|
|
||
| class _MockEplbCommunicator(EplbCommunicator): | ||
| def __init__(self) -> None: | ||
| self.send_calls: list[tuple[list[torch.Tensor], int, int]] = [] | ||
| self.recv_calls: list[tuple[list[torch.Tensor], int, int]] = [] | ||
| self.context_calls = 0 | ||
| self.execute_count = 0 | ||
|
|
||
| def add_send( | ||
| self, tensors: list[torch.Tensor], dst_rank: int, expert_id: int | ||
| ) -> None: | ||
| self.send_calls.append((tensors, dst_rank, expert_id)) | ||
|
|
||
| def add_recv( | ||
| self, tensors: list[torch.Tensor], src_rank: int, expert_id: int | ||
| ) -> None: | ||
| self.recv_calls.append((tensors, src_rank, expert_id)) | ||
|
|
||
| def set_transfer_context(self, old_indices: np.ndarray, layer_idx: int) -> None: | ||
| self.context_calls += 1 | ||
|
|
||
| def execute(self) -> None: | ||
| self.execute_count += 1 | ||
|
|
||
|
|
||
| def test_migration_batching_is_disabled_by_default_and_can_be_enabled() -> None: | ||
| assert not EPLBConfig().enable_migration_batching | ||
| assert EPLBConfig(enable_migration_batching=True).enable_migration_batching | ||
|
|
||
|
|
||
| def test_migration_batching_only_applies_to_async_eplb() -> None: | ||
| assert not EPLBConfig(use_async=True).migration_batching_enabled | ||
| assert EPLBConfig( | ||
| use_async=True, enable_migration_batching=True | ||
| ).migration_batching_enabled | ||
| assert not EPLBConfig( | ||
| use_async=True, enable_migration_batching=False | ||
| ).migration_batching_enabled | ||
| assert not EPLBConfig( | ||
| use_async=False, enable_migration_batching=True | ||
| ).migration_batching_enabled | ||
|
|
||
|
|
||
| def test_schedule_migration_batches_is_deterministic() -> None: | ||
| transfers = [ | ||
| (1, 3, 0), | ||
| (2, 4, 1), | ||
| (0, 1, 2), | ||
| (0, 2, 3), | ||
| (0, 3, 4), | ||
| (0, 4, 5), | ||
| ] | ||
| num_local_experts, old_indices, new_indices = _placements_for_transfers( | ||
| 5, transfers | ||
| ) | ||
|
|
||
| batches = schedule_migration_batches(num_local_experts, old_indices, new_indices) | ||
|
|
||
| assert [_endpoints(batch) for batch in batches] == [ | ||
| [(1, 3), (2, 4)], | ||
| [(0, 1)], | ||
| [(0, 2)], | ||
| [(0, 3)], | ||
| [(0, 4)], | ||
| ] | ||
| for batch in batches: | ||
| _assert_no_endpoint_conflict(batch) | ||
|
|
||
|
|
||
| def test_schedule_migration_batches_coalesces_rank_pair() -> None: | ||
| transfers = [ | ||
| (0, 1, 0), | ||
| (0, 1, 1), | ||
| (2, 3, 2), | ||
| ] | ||
| num_local_experts, old_indices, new_indices = _placements_for_transfers( | ||
| 4, transfers | ||
| ) | ||
|
|
||
| assert schedule_migration_batches(num_local_experts, old_indices, new_indices) == [ | ||
| [ | ||
| MigrationFlow(0, 1, expert_ids=(0, 1)), | ||
| MigrationFlow(2, 3, expert_ids=(2,)), | ||
| ] | ||
| ] | ||
|
|
||
|
|
||
| def test_schedule_migration_batches_covers_random_instructions() -> None: | ||
| rng = np.random.default_rng(42) | ||
| transfers = [] | ||
| for expert_id in range(80): | ||
| src_rank, dst_rank = rng.integers(0, 8, size=2).tolist() | ||
| if src_rank != dst_rank: | ||
| transfers.append((src_rank, dst_rank, expert_id)) | ||
| num_local_experts, old_indices, new_indices = _placements_for_transfers( | ||
| 8, transfers | ||
| ) | ||
|
|
||
| batches = schedule_migration_batches(num_local_experts, old_indices, new_indices) | ||
|
|
||
| scheduled = _flatten_transfers(batches) | ||
| assert len(scheduled) == len(transfers) | ||
| assert set(scheduled) == set(transfers) | ||
| for batch in batches: | ||
| _assert_no_endpoint_conflict(batch) | ||
|
|
||
|
|
||
| def test_schedule_migration_batches_excludes_local_copies() -> None: | ||
| # Old: rank 0 [0, 1], rank 1 [1, 2]. New: rank 1 needs expert 0. | ||
| old_indices = np.array([0, 1, 1, 2], dtype=np.int64) | ||
| new_indices = np.array([0, 1, 0, 2], dtype=np.int64) | ||
|
|
||
| assert schedule_migration_batches(2, old_indices, new_indices) == [ | ||
| [MigrationFlow(0, 1, expert_ids=(0,))] | ||
| ] | ||
|
|
||
|
|
||
| def test_schedule_migration_batches_requires_matching_shapes() -> None: | ||
| old_indices = np.array([0, 1], dtype=np.int64) | ||
| new_indices = np.array([0], dtype=np.int64) | ||
|
|
||
| with pytest.raises(AssertionError): | ||
| schedule_migration_batches(1, old_indices, new_indices) | ||
|
|
||
|
|
||
| def test_schedule_migration_batches_balances_replicas() -> None: | ||
| # Rank 0 and rank 1 hold expert 0; ranks 2, 3, and 4 need it. | ||
| old_indices = np.array([0, 0, 1, 1, 1], dtype=np.int64) | ||
| new_indices = np.array([0, 0, 0, 0, 0], dtype=np.int64) | ||
|
|
||
| batches = schedule_migration_batches(1, old_indices, new_indices) | ||
|
|
||
| assert set(_flatten_transfers(batches)) == { | ||
| (0, 2, 0), | ||
| (0, 4, 0), | ||
| (1, 3, 0), | ||
| } | ||
|
|
||
|
|
||
| def test_schedule_migration_batches_for_layers_matches_individual_calls() -> None: | ||
| old_indices = np.array([[0, 1, 2, 3], [3, 2, 1, 0]], dtype=np.int64) | ||
| new_indices = np.array([[1, 2, 3, 0], [0, 3, 2, 1]], dtype=np.int64) | ||
|
|
||
| assert schedule_migration_batches_for_layers(1, old_indices, new_indices) == [ | ||
| schedule_migration_batches(1, old_indices[0], new_indices[0]), | ||
| schedule_migration_batches(1, old_indices[1], new_indices[1]), | ||
| ] | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("use_precomputed", [False, True]) | ||
| def test_move_to_buffer_uses_multiple_batches(use_precomputed: bool) -> None: | ||
| old_indices = np.array([0, 1, 2, 3], dtype=np.int64) | ||
| new_indices = np.array([1, 2, 3, 0], dtype=np.int64) | ||
| communicator = _MockEplbCommunicator() | ||
| migration_batches = ( | ||
| schedule_migration_batches(1, old_indices, new_indices) | ||
| if use_precomputed | ||
| else None | ||
| ) | ||
|
|
||
| move_to_buffer( | ||
| num_local_experts=1, | ||
| old_indices=old_indices, | ||
| new_indices=new_indices, | ||
| expert_weights=[torch.zeros(1, 1)], | ||
| expert_weights_buffers=[torch.zeros(1, 1)], | ||
| stream=None, | ||
| ep_rank=0, | ||
| communicator=communicator, | ||
| layer_idx=7, | ||
| enable_migration_batching=True, | ||
| migration_batches=migration_batches, | ||
| ) | ||
|
|
||
| assert communicator.context_calls == 2 | ||
| assert communicator.execute_count == 2 | ||
| assert [(dst, expert) for _, dst, expert in communicator.send_calls] == [(3, 0)] | ||
| assert [(src, expert) for _, src, expert in communicator.recv_calls] == [(1, 1)] | ||
|
|
||
|
|
||
| def test_move_to_buffer_uses_primary_duplicate_destination() -> None: | ||
| old_indices = np.array([0, 1, 2, 3], dtype=np.int64) | ||
| new_indices = np.array([2, 2, 0, 3], dtype=np.int64) | ||
| expert_weights = [torch.zeros(2, 1)] | ||
| recv_buffer = torch.zeros(2, 1) | ||
| communicator = _MockEplbCommunicator() | ||
|
|
||
| transfer_metadata = move_to_buffer( | ||
| num_local_experts=2, | ||
| old_indices=old_indices, | ||
| new_indices=new_indices, | ||
| expert_weights=expert_weights, | ||
| expert_weights_buffers=[recv_buffer], | ||
| stream=None, | ||
| ep_rank=0, | ||
| communicator=communicator, | ||
| enable_migration_batching=True, | ||
| ) | ||
|
|
||
| assert len(communicator.recv_calls) == 1 | ||
| recv_tensors, src_rank, expert_id = communicator.recv_calls[0] | ||
| assert (src_rank, expert_id) == (1, 2) | ||
| assert recv_tensors[0].data_ptr() == recv_buffer[0].data_ptr() | ||
|
|
||
| recv_buffer[0].fill_(42) | ||
| move_from_buffer( | ||
| expert_weights=expert_weights, | ||
| expert_weights_buffers=[recv_buffer], | ||
| transfer_metadata=transfer_metadata, | ||
| new_indices=new_indices, | ||
| ep_rank=0, | ||
| ) | ||
| torch.testing.assert_close(expert_weights[0], torch.full((2, 1), 42.0)) | ||
|
|
||
|
|
||
| def _endpoints( | ||
| flows: list[MigrationFlow], | ||
| ) -> list[tuple[int, int]]: | ||
| return [(item.src_rank, item.dst_rank) for item in flows] | ||
|
|
||
|
|
||
| def _assert_no_endpoint_conflict(batch: list[MigrationFlow]) -> None: | ||
| endpoints: set[int] = set() | ||
| for flow in batch: | ||
| assert flow.src_rank not in endpoints | ||
| assert flow.dst_rank not in endpoints | ||
| endpoints.update((flow.src_rank, flow.dst_rank)) | ||
|
|
||
|
|
||
| def _flatten_transfers( | ||
| batches: list[list[MigrationFlow]], | ||
| ) -> list[tuple[int, int, int]]: | ||
| return [ | ||
| (flow.src_rank, flow.dst_rank, expert_id) | ||
| for batch in batches | ||
| for flow in batch | ||
| for expert_id in flow.expert_ids | ||
| ] | ||
|
|
||
|
|
||
| def _placements_for_transfers( | ||
| num_ranks: int, | ||
| transfers: list[tuple[int, int, int]], | ||
| ) -> tuple[int, np.ndarray, np.ndarray]: | ||
| old_counts = [0] * num_ranks | ||
| new_counts = [0] * num_ranks | ||
| for src_rank, dst_rank, _ in transfers: | ||
| old_counts[src_rank] += 1 | ||
| new_counts[dst_rank] += 1 | ||
| num_local_experts = max(1, *old_counts, *new_counts) | ||
| old_indices = np.full(num_ranks * num_local_experts, -1, dtype=np.int64) | ||
| new_indices = np.full_like(old_indices, -1) | ||
| old_counts = [0] * num_ranks | ||
| new_counts = [0] * num_ranks | ||
| for src_rank, dst_rank, expert_id in transfers: | ||
| old_offset = src_rank * num_local_experts + old_counts[src_rank] | ||
| new_offset = dst_rank * num_local_experts + new_counts[dst_rank] | ||
| old_indices[old_offset] = expert_id | ||
| new_indices[new_offset] = expert_id | ||
| old_counts[src_rank] += 1 | ||
| new_counts[dst_rank] += 1 | ||
| return num_local_experts, old_indices, new_indices | ||
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
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.