Skip to content

Commit 8c79ad6

Browse files
authored
Revert "[Core] Replace routing replay with device cache and async D2H pipeline" (vllm-project#39917) (vllm-project#42434)
Signed-off-by: aoshen02 <aoshen@inferact.ai>
1 parent 0d2732d commit 8c79ad6

15 files changed

Lines changed: 614 additions & 1377 deletions

File tree

docs/training/routed_experts_replay.md

Lines changed: 0 additions & 289 deletions
This file was deleted.
Lines changed: 199 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,244 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
import types
4+
from types import SimpleNamespace
5+
from unittest.mock import patch
36

47
import pytest
58
import torch
69

10+
from vllm.distributed.eplb.eplb_state import EplbLayerState
11+
from vllm.model_executor.layers.fused_moe.config import RoutingMethodType
12+
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
13+
RoutedExpertsCapturer,
14+
)
15+
from vllm.model_executor.layers.fused_moe.router.base_router import BaseRouter
16+
717
pytestmark = pytest.mark.cpu_test
818

19+
_REC_MODULE = "vllm.model_executor.layers.fused_moe.routed_experts_capturer"
920

10-
def test_bind_routing_capture_to_model_sets_layer_view(monkeypatch):
11-
import vllm.model_executor.layers.fused_moe.layer as fused_moe_layer
12-
import vllm.model_executor.layers.fused_moe.routed_experts_capturer as rec_mod
1321

14-
class _DummyMoEConfig:
15-
is_sequence_parallel = False
16-
dp_size = 1
22+
def _capturer_with_buffer(
23+
*,
24+
max_tokens: int = 8,
25+
num_layers: int = 4,
26+
num_experts_per_tok: int = 2,
27+
dp_rank: int = 0,
28+
) -> RoutedExpertsCapturer:
29+
c = RoutedExpertsCapturer()
30+
c.dp_rank = dp_rank
31+
c._device_buffer = torch.full(
32+
(max_tokens, num_layers, num_experts_per_tok),
33+
-1,
34+
dtype=torch.int32,
35+
)
36+
return c
1737

18-
class _DummyQuantMethod:
19-
supports_internal_mk = True
2038

21-
class DummyFusedMoE:
22-
_routing_replay_out: torch.Tensor
39+
class DummyRouter(BaseRouter):
40+
@property
41+
def routing_method_type(self) -> RoutingMethodType:
42+
return RoutingMethodType.FUSED_TOPK
2343

24-
def __init__(self, moe_layer_id):
25-
self.moe_layer_id = moe_layer_id
26-
self.moe_config = _DummyMoEConfig()
27-
self.quant_method = _DummyQuantMethod()
44+
def _compute_routing(
45+
self, hidden_states, router_logits, indices_type, *, input_ids=None
46+
):
47+
topk_ids = torch.tensor([[1, 2], [3, 4]], dtype=torch.int64)
48+
topk_weights = torch.ones_like(topk_ids, dtype=torch.float32)
49+
return topk_weights, topk_ids
2850

29-
monkeypatch.setattr(fused_moe_layer, "FusedMoE", DummyFusedMoE)
51+
def _apply_eplb_mapping(self, topk_ids: torch.Tensor) -> torch.Tensor:
52+
# Make mapping observable without requiring CUDA EPLB path.
53+
return topk_ids + 10
3054

31-
num_layers, num_tokens, top_k = 4, 8, 2
32-
buffer = torch.zeros((num_layers, num_tokens, top_k), dtype=torch.int16)
3355

34-
class DummyDeviceCache:
35-
def __init__(self, buf):
36-
self.buffer = buf
56+
def _make_router(eplb_state: EplbLayerState | None = None) -> DummyRouter:
57+
return DummyRouter(
58+
top_k=2,
59+
global_num_experts=16,
60+
eplb_state=eplb_state,
61+
indices_type_getter=None,
62+
)
3763

38-
class DummyCapturer:
39-
def get_device_cache(self):
40-
return DummyDeviceCache(buffer)
4164

42-
monkeypatch.setattr(rec_mod, "get_global_experts_capturer", lambda: DummyCapturer())
65+
def test_base_router_capture_pre_eplb_mapping():
66+
router = _make_router()
67+
captured = []
4368

44-
m0 = DummyFusedMoE(moe_layer_id=0)
45-
m2 = DummyFusedMoE(moe_layer_id=2)
69+
def capture_fn(ids):
70+
captured.append(ids.clone())
4671

47-
class DummyModel:
48-
def modules(self):
49-
return iter([m0, m2])
72+
router.set_capture_fn(capture_fn)
73+
topk_weights, topk_ids = router.select_experts(
74+
hidden_states=torch.empty(1),
75+
router_logits=torch.empty(1),
76+
)
5077

51-
rec_mod.bind_routing_capture_to_model(DummyModel())
78+
assert topk_weights.shape == topk_ids.shape
79+
assert len(captured) == 1
80+
assert torch.equal(captured[0], torch.tensor([[1, 2], [3, 4]]))
81+
assert torch.equal(topk_ids, torch.tensor([[11, 12], [13, 14]]))
5282

53-
assert torch.equal(m0._routing_replay_out, buffer[0])
54-
assert torch.equal(m2._routing_replay_out, buffer[2])
5583

84+
def test_base_router_capture_with_eplb_enabled():
85+
eplb_state = EplbLayerState()
86+
eplb_state.expert_load_view = torch.zeros(32, dtype=torch.int64)
87+
eplb_state.logical_to_physical_map = torch.arange(32).view(32, 1)
88+
eplb_state.logical_replica_count = torch.ones(32, dtype=torch.int64)
89+
eplb_state.should_record_tensor = torch.ones((), dtype=torch.bool)
90+
router = _make_router(eplb_state=eplb_state)
5691

57-
def test_bind_routing_capture_to_model_noop_when_disabled(monkeypatch):
58-
import vllm.model_executor.layers.fused_moe.routed_experts_capturer as rec_mod
92+
captured = []
5993

60-
class DummyCapturer:
61-
def get_device_cache(self):
62-
return None
94+
def capture_fn(ids):
95+
captured.append(ids.clone())
6396

64-
monkeypatch.setattr(rec_mod, "get_global_experts_capturer", lambda: DummyCapturer())
97+
router.set_capture_fn(capture_fn)
98+
_, topk_ids = router.select_experts(
99+
hidden_states=torch.empty(1),
100+
router_logits=torch.empty(1),
101+
)
65102

66-
class DummyModel:
67-
def modules(self):
68-
return iter([])
103+
assert len(captured) == 1
104+
# Capture should see logical ids pre-EPLB mapping.
105+
assert torch.equal(captured[0], torch.tensor([[1, 2], [3, 4]]))
106+
# Our DummyRouter mapping adds +10.
107+
assert torch.equal(topk_ids, torch.tensor([[11, 12], [13, 14]]))
69108

70-
rec_mod.bind_routing_capture_to_model(DummyModel())
71109

110+
def test_gpu_model_runner_binds_router_capture(monkeypatch):
111+
from vllm.v1.worker import gpu_model_runner as gmr
72112

73-
# =========================================================================
74-
# Tests for device-cache routing replay architecture
75-
# =========================================================================
113+
class DummyFusedMoE:
114+
def __init__(self):
115+
self.layer_id = 7
116+
self.router = _make_router()
76117

118+
class DummyCapturer:
119+
def __init__(self):
120+
self.calls = []
77121

78-
class TestRoutedExpertsDeviceCache:
79-
"""Tests for _RoutedExpertsDeviceCache (GPU buffer for routing data)."""
122+
def capture(self, layer_id, topk_ids):
123+
self.calls.append((layer_id, topk_ids))
80124

81-
def test_allocation_shape_and_dtype(self):
82-
"""Device cache allocates (L, N, K) int16 buffer."""
83-
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
84-
_RoutedExpertsDeviceCache,
85-
)
125+
dummy_module = DummyFusedMoE()
86126

87-
cache = _RoutedExpertsDeviceCache(
88-
num_hidden_layers=40,
89-
max_num_batched_tokens=8192,
90-
num_experts_per_tok=8,
91-
device="cpu",
92-
)
93-
assert cache.buffer.shape == (40, 8192, 8)
94-
assert cache.buffer.dtype == torch.int16
127+
# Patch the runtime import inside _bind_routed_experts_capturer.
128+
import vllm.model_executor.layers.fused_moe.layer as fused_moe_layer
95129

96-
def test_per_layer_view_is_contiguous(self):
97-
"""buffer[layer_id] gives contiguous (N, K) view for FlashInfer."""
98-
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
99-
_RoutedExpertsDeviceCache,
100-
)
130+
monkeypatch.setattr(fused_moe_layer, "FusedMoE", DummyFusedMoE)
101131

102-
cache = _RoutedExpertsDeviceCache(
103-
num_hidden_layers=40,
104-
max_num_batched_tokens=8192,
105-
num_experts_per_tok=8,
106-
device="cpu",
132+
dummy_self = types.SimpleNamespace(
133+
compilation_config=types.SimpleNamespace(
134+
static_forward_context={"dummy": dummy_module}
107135
)
108-
layer_view = cache.buffer[0]
109-
assert layer_view.is_contiguous()
110-
assert layer_view.shape == (8192, 8)
136+
)
111137

138+
capturer = DummyCapturer()
139+
gmr.GPUModelRunner._bind_routed_experts_capturer(dummy_self, capturer)
112140

113-
class TestRoutedExpertsHostCache:
114-
"""Tests for _RoutedExpertsHostCache (per-request numpy buffer)."""
141+
assert dummy_module.router.capture_fn is not None
142+
dummy_module.router.capture_fn(torch.tensor([[5, 6]]))
115143

116-
def test_sentinel_initialization(self):
117-
"""Host cache initializes with zeros by default."""
118-
import numpy as np
144+
assert len(capturer.calls) == 1
145+
layer_id, topk_ids = capturer.calls[0]
146+
assert layer_id == 7
147+
assert torch.equal(topk_ids, torch.tensor([[5, 6]]))
119148

120-
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
121-
_RoutedExpertsHostCache,
122-
)
123149

124-
cache = _RoutedExpertsHostCache(
125-
num_hidden_layers=40,
126-
num_experts_per_tok=8,
127-
max_model_len=1024,
128-
)
129-
buf = cache.get_or_grow_buffer("req1", max_pos=100)
130-
assert buf.dtype == np.int16
131-
assert (buf == 0).all(), "Host cache must initialize with zeros"
132-
133-
def test_grow_preserves_existing_data(self):
134-
"""Growing the buffer preserves previously written data."""
135-
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
136-
_RoutedExpertsHostCache,
137-
)
150+
def test_gpu_model_runner_binding_stage(monkeypatch):
151+
from vllm.v1.worker import gpu_model_runner as gmr
138152

139-
cache = _RoutedExpertsHostCache(
140-
num_hidden_layers=40,
141-
num_experts_per_tok=8,
142-
max_model_len=1024,
143-
)
144-
buf = cache.get_or_grow_buffer("req1", max_pos=50)
145-
buf[0, 0, 0] = 42
146-
buf2 = cache.get_or_grow_buffer("req1", max_pos=200)
147-
assert buf2[0, 0, 0] == 42, "Data lost during buffer grow"
148-
149-
def test_free_request_removes_buffer(self):
150-
"""Freeing a request removes its buffer."""
151-
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
152-
_RoutedExpertsHostCache,
153-
)
153+
class DummyFusedMoE:
154+
def __init__(self):
155+
self.layer_id = 11
156+
self.router = _make_router()
157+
158+
class DummyCapturer:
159+
def __init__(self):
160+
self.calls = []
161+
162+
def capture(self, layer_id, topk_ids):
163+
self.calls.append((layer_id, topk_ids))
164+
165+
dummy_module = DummyFusedMoE()
166+
167+
import vllm.model_executor.layers.fused_moe.layer as fused_moe_layer
168+
169+
monkeypatch.setattr(fused_moe_layer, "FusedMoE", DummyFusedMoE)
154170

155-
cache = _RoutedExpertsHostCache(
156-
num_hidden_layers=40,
157-
num_experts_per_tok=8,
158-
max_model_len=1024,
171+
dummy_self = types.SimpleNamespace(
172+
compilation_config=types.SimpleNamespace(
173+
static_forward_context={"dummy": dummy_module}
159174
)
160-
cache.get_or_grow_buffer("req1", max_pos=50)
161-
cache.free_request("req1")
162-
assert cache.get_buffer("req1") is None
175+
)
176+
177+
# Before binding, no capture hook.
178+
assert dummy_module.router.capture_fn is None
179+
180+
capturer = DummyCapturer()
181+
gmr.GPUModelRunner._bind_routed_experts_capturer(dummy_self, capturer)
182+
183+
# After binding, hook should exist and be callable.
184+
assert callable(dummy_module.router.capture_fn)
185+
dummy_module.router.capture_fn(torch.tensor([[9, 10]]))
186+
assert len(capturer.calls) == 1
187+
188+
189+
def test_routed_experts_capturer_single_dp_no_metadata():
190+
"""dp_metadata is None: capture writes the full topk_ids rows."""
191+
capturer = _capturer_with_buffer(dp_rank=0)
192+
topk = torch.tensor([[1, 2], [3, 4], [5, 6]], dtype=torch.int32)
193+
ctx = SimpleNamespace(dp_metadata=None)
194+
with patch(f"{_REC_MODULE}.get_forward_context", return_value=ctx):
195+
capturer.capture(layer_id=0, topk_ids=topk)
196+
assert torch.equal(capturer._device_buffer[:3, 0, :], topk)
197+
assert capturer._device_buffer[3, 0, 0].item() == -1
198+
199+
200+
def test_routed_experts_capturer_dp_naive_concatenated_all_ranks():
201+
"""n == sum(num_tokens_dp): slice this rank's segment from concatenated topk."""
202+
capturer = _capturer_with_buffer(dp_rank=1)
203+
num_tokens_dp = torch.tensor([2, 3], dtype=torch.int32)
204+
ctx = SimpleNamespace(
205+
dp_metadata=SimpleNamespace(num_tokens_across_dp_cpu=num_tokens_dp)
206+
)
207+
# Concatenated order: rank0 rows then rank1 rows.
208+
topk = torch.tensor(
209+
[[0, 1], [2, 3], [10, 11], [12, 13], [14, 15]], dtype=torch.int32
210+
)
211+
with patch(f"{_REC_MODULE}.get_forward_context", return_value=ctx):
212+
capturer.capture(layer_id=0, topk_ids=topk)
213+
want = topk[2:5]
214+
assert torch.equal(capturer._device_buffer[:3, 0, :], want)
215+
216+
217+
def test_routed_experts_capturer_dp_modular_local_tokens():
218+
"""n == token_num_per_dp: topk is already local to this DP rank."""
219+
capturer = _capturer_with_buffer(dp_rank=1)
220+
num_tokens_dp = torch.tensor([2, 3], dtype=torch.int32)
221+
ctx = SimpleNamespace(
222+
dp_metadata=SimpleNamespace(num_tokens_across_dp_cpu=num_tokens_dp)
223+
)
224+
topk = torch.tensor([[10, 11], [12, 13], [14, 15]], dtype=torch.int32)
225+
with patch(f"{_REC_MODULE}.get_forward_context", return_value=ctx):
226+
capturer.capture(layer_id=0, topk_ids=topk)
227+
assert torch.equal(capturer._device_buffer[:3, 0, :], topk)
228+
229+
230+
def test_routed_experts_capturer_dp_unexpected_batch_raises():
231+
"""Mismatch between topk batch dim and DP layout: fail fast."""
232+
capturer = _capturer_with_buffer(dp_rank=0)
233+
num_tokens_dp = torch.tensor([2, 3], dtype=torch.int32)
234+
ctx = SimpleNamespace(
235+
dp_metadata=SimpleNamespace(num_tokens_across_dp_cpu=num_tokens_dp)
236+
)
237+
# total=5, local=2: n=1 matches neither naive (5) nor modular (2).
238+
topk = torch.tensor([[1, 2]], dtype=torch.int32)
239+
with (
240+
patch(f"{_REC_MODULE}.get_forward_context", return_value=ctx),
241+
pytest.raises(AssertionError, match="unexpected topk_ids batch dim"),
242+
):
243+
capturer.capture(layer_id=0, topk_ids=topk)
244+
assert capturer._device_buffer[0, 0, 0].item() == -1

0 commit comments

Comments
 (0)