@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / cc1dc7ed6d
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 14, 05:30 UTC

[Core/DBO][2/N] Dual-Batch Overlap add DeepEP High Throughput support and Prefill support (#24845)

Signed-off-by: Sage Moore <sage@neuralmagic.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com> Signed-off-by: yewentao256 <zhyanwentao@126.com> Signed-off-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com> Signed-off-by: Tyler Michael Smith <tyler@neuralmagic.com> Co-authored-by: Sage Moore <sage@neuralmagic.com> Co-authored-by: yewentao256 <zhyanwentao@126.com> Co-authored-by: Tyler Michael Smith <tyler@neuralmagic.com>

Lucas Wilkinson committed Sep 23, 2025 at 12:02 UTC cc1dc7ed6d2d901df46847724ad90147872a9d79
19 files changed +604 -238
tests/v1/attention/test_attention_splitting.py
+82 -1
@@ -5,11 +5,12 @@ import pytest
5 import torch
6
7 from tests.v1.attention.test_attention_backends import BATCH_SPECS
8 -from tests.v1.attention.utils import create_common_attn_metadata
8 +from tests.v1.attention.utils import BatchSpec, create_common_attn_metadata
9 from vllm.v1.attention.backends.utils import (UBatchSlice,
10 _make_metadata_with_slice,
11 slice_query_start_locs,
12 split_attn_metadata)
13 +from vllm.v1.worker.ubatch_utils import create_ubatch_slices
14
15
16 @pytest.fixture
@@ -155,3 +156,83 @@ def test_split_attn_metadata_decode_batch(large_decode_metadata):
156 assert results[1].num_reqs == mid_point
157 assert results[1].num_actual_tokens == mid_point
158 assert torch.equal(results[1].seq_lens, torch.tensor([2048] * mid_point))
159 +
160 +
161 +@pytest.mark.parametrize(
162 + "seq_lens,query_lens,split_point,expected_first_reqs,expected_second_reqs",
163 + [
164 + # Split in the middle of request 1
165 + ([32, 40], [8, 8], 12, 2, 1),
166 + # Split inside the first request
167 + ([32, 40], [8, 8], 4, 1, 2),
168 + ],
169 +)
170 +def test_prefill_split_across_ubatches(seq_lens, query_lens, split_point,
171 + expected_first_reqs,
172 + expected_second_reqs):
173 + """Test splitting a prefill across ubatches"""
174 + import numpy as np
175 +
176 + device = torch.device("cpu")
177 + batch_spec = BatchSpec(seq_lens=seq_lens, query_lens=query_lens)
178 + common = create_common_attn_metadata(batch_spec,
179 + block_size=16,
180 + device=device)
181 +
182 + num_scheduled_tokens = np.array(query_lens, dtype=np.int32)
183 + qsl_np = common.query_start_loc_cpu.numpy()
184 + num_tokens = common.num_actual_tokens
185 +
186 + ubatch_slices = create_ubatch_slices(num_scheduled_tokens, split_point)
187 + assert len(ubatch_slices) == 2
188 +
189 + first_meta = _make_metadata_with_slice(ubatch_slices[0], common)
190 + second_meta = _make_metadata_with_slice(ubatch_slices[1], common)
191 +
192 + # Token counts match the split
193 + assert first_meta.num_actual_tokens == split_point
194 + assert second_meta.num_actual_tokens == num_tokens - split_point
195 +
196 + # Number of requests per ubatch
197 + assert first_meta.num_reqs == expected_first_reqs
198 + assert second_meta.num_reqs == expected_second_reqs
199 +
200 + # Identify which request is split and how many tokens are in the first chunk
201 + split_req_idx = int(np.searchsorted(qsl_np, split_point, side="right") - 1)
202 + tokens_in_first_chunk = split_point - int(qsl_np[split_req_idx])
203 + orig_q_lens = (common.query_start_loc_cpu[1:] -
204 + common.query_start_loc_cpu[:-1])
205 +
206 + # Check query length continuity: first-chunk + second-chunk == original qlen
207 + # First ubatch last request query length
208 + qlen_first_last = int(first_meta.query_start_loc_cpu[-1] -
209 + first_meta.query_start_loc_cpu[-2])
210 + # Second ubatch first request query length
211 + qlen_second_first = int(second_meta.query_start_loc_cpu[1] -
212 + second_meta.query_start_loc_cpu[0])
213 + assert qlen_first_last == tokens_in_first_chunk
214 + assert qlen_first_last + qlen_second_first == int(
215 + orig_q_lens[split_req_idx])
216 +
217 + # Check seq_lens adjustments
218 + # Context lengths per original request
219 + context_lens = [s - q for s, q in zip(seq_lens, query_lens)]
220 +
221 + # First ubatch: last request's seq_len should be
222 + # context + tokens_in_first_chunk
223 + expected_seqlen = context_lens[split_req_idx] + tokens_in_first_chunk
224 + assert int(first_meta.seq_lens[-1]) == expected_seqlen
225 +
226 + # For full preceding requests in first ubatch, seq_lens should match
227 + # originals
228 + for i in range(first_meta.num_reqs - 1):
229 + assert int(first_meta.seq_lens[i]) == seq_lens[i]
230 +
231 + # Second ubatch: first request (continuation) seq_len should be full
232 + # original
233 + assert int(second_meta.seq_lens[0]) == seq_lens[split_req_idx]
234 + # Any following full requests in second ubatch should match originals
235 + for j in range(1, second_meta.num_reqs):
236 + # Map to original request index
237 + orig_idx = split_req_idx + j
238 + assert int(second_meta.seq_lens[j]) == seq_lens[orig_idx]
tests/v1/spec_decode/test_eagle.py
+2 -4
@@ -532,9 +532,8 @@ def test_propose(method, attn_backend, num_speculative_tokens, monkeypatch):
532 # Mock runner for attention metadata building
533 proposer.runner = mock.MagicMock()
534 proposer.runner.attn_groups.append([mock.MagicMock()])
535 - proposer.runner.attn_groups[0][0].metadata_builders = [
535 + proposer.runner.attn_groups[0][0].get_metadata_builder.return_value = \
536 attn_metadata_builder
537 - ]
537
538 result = proposer.propose(target_token_ids=target_token_ids,
539 target_positions=target_positions,
@@ -659,9 +658,8 @@ def test_propose_tree(spec_token_tree):
658 # Mock runner for attention metadata building.
659 proposer.runner = mock.MagicMock()
660 proposer.runner.attn_groups.append([mock.MagicMock()])
662 - proposer.runner.attn_groups[0][0].metadata_builders = [
661 + proposer.runner.attn_groups[0][0].get_metadata_builder.return_value = \
662 attn_metadata_builder
664 - ]
663
664 # Setup inputs for the proposer.
665 target_token_ids = torch.randint(0,
vllm/config/__init__.py
+7 -5
@@ -638,11 +638,13 @@ class VllmConfig:
638
639 if self.parallel_config.enable_dbo:
640 a2a_backend = envs.VLLM_ALL2ALL_BACKEND
641 - assert a2a_backend == "deepep_low_latency", \
642 - "Microbatching currently only supports the deepep_low_latency "\
643 - f"all2all backend. {a2a_backend} is not supported. To fix set "\
644 - "the VLLM_ALL2ALL_BACKEND environment variable to "\
645 - "deepep_low_latency and install the DeepEP kerenls."
641 + assert a2a_backend in \
642 + ["deepep_low_latency", "deepep_high_throughput"], \
643 + "Microbatching currently only supports the deepep_low_latency and "\
644 + f"deepep_high_throughput all2all backend. {a2a_backend} is not "\
645 + "supported. To fix set the VLLM_ALL2ALL_BACKEND environment "\
646 + "variable to deepep_low_latency or deepep_high_throughput and "\
647 + "install the DeepEP kernels."
648
649 if not self.instance_id:
650 self.instance_id = random_uuid()[:5]
vllm/config/parallel.py
+10 -4
@@ -139,12 +139,18 @@ class ParallelConfig:
139 """Disable the custom all-reduce kernel and fall back to NCCL."""
140
141 enable_dbo: bool = False
142 - """Enable microbatching for the model executor."""
142 + """Enable dual batch overlap for the model executor."""
143
144 dbo_decode_token_threshold: int = 32
145 - """The threshold for microbatching. If the number of tokens in the
146 - request is greater than this threshold, microbatching will be used.
147 - Otherwise, the request will be processed in a single batch."""
145 + """The threshold for dual batch overlap for batches only containing decodes.
146 + If the number of tokens in the request is greater than this threshold,
147 + microbatching will be used. Otherwise, the request will be processed in a
148 + single batch."""
149 + dbo_prefill_token_threshold: int = 512 # TODO(lucas): tune
150 + """The threshold for dual batch overlap for batches that contain one or more
151 + prefills. If the number of tokens in the request is greater than this
152 + threshold, microbatching will be used. Otherwise, the request will be
153 + processed in a single batch."""
154
155 ray_workers_use_nsight: bool = False
156 """Whether to profile Ray workers with nsight, see https://docs.ray.io/en/latest/ray-observability/user-guides/profiling.html#profiling-nsight-profiler."""
vllm/distributed/device_communicators/all2all.py
+19 -9
@@ -1,10 +1,11 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -from typing import Any
3 +from typing import Any, Optional
4
5 import torch
6 import torch.distributed as dist
7
8 +import vllm.envs as envs
9 from vllm.distributed import get_dp_group
10 from vllm.forward_context import get_forward_context
11 from vllm.logger import init_logger
@@ -200,12 +201,12 @@ class DeepEPHTAll2AllManager(DeepEPAll2AllManagerBase):
201
202 def _make_all2all_kwargs(self) -> dict[Any, Any]:
203 # Defaults for internode and intranode are taken from DeepEP tests.
203 - num_nvl_bytes = 1024 * 1024 * 1024
204 + num_nvl_bytes = envs.VLLM_DEEPEP_BUFFER_SIZE_MB * 1024 * 1024
205 num_rdma_bytes = None
206 num_qps_per_rank = None
207
208 if self.internode:
208 - num_rdma_bytes = 1024 * 1024 * 1024
209 + num_rdma_bytes = envs.VLLM_DEEPEP_BUFFER_SIZE_MB * 1024 * 1024
210 num_qps_per_rank = self.num_sms // 2
211 else:
212 num_rdma_bytes = 0
@@ -230,13 +231,18 @@ class DeepEPHTAll2AllManager(DeepEPAll2AllManagerBase):
231 logger.debug("DeepEP all2all args %s", buffer_kwargs)
232 handle: deep_ep.Buffer = self.handle_cache.get_or_create(
233 buffer_kwargs, deep_ep.Buffer)
233 - # It is dangerous to set num sms outside this function. num_sms is not
234 - # a part of the hash-key that identifies this object. If we are in a
235 - # situation where we make objects with different num_sms, the hash key
236 - # in get_or_create must be updated.
237 - handle.set_num_sms(self.num_sms)
234 return handle
235
236 + def set_num_sms(self, num_sms: int):
237 + import deep_ep
238 +
239 + # Right now the buffers are sized for only what the kernels were
240 + # created with. So we can only reduce the number of SMS used
241 + # but not increase it.
242 + if num_sms > self.num_sms:
243 + num_sms = self.num_sms
244 + deep_ep.Buffer.set_num_sms(num_sms)
245 +
246
247 class DeepEPLLAll2AllManager(DeepEPAll2AllManagerBase):
248 """
@@ -265,7 +271,7 @@ class DeepEPLLAll2AllManager(DeepEPAll2AllManagerBase):
271 import deep_ep
272
273 # Defaults for internode and intranode are taken from DeepEP tests.
268 - num_nvl_bytes = 1024 * 1024 * 1024
274 + num_nvl_bytes = envs.VLLM_DEEPEP_BUFFER_SIZE_MB * 1024 * 1024
275 num_qps_per_rank = num_local_experts
276 num_rdma_bytes = deep_ep.Buffer.get_low_latency_rdma_size_hint(
277 num_max_dispatch_tokens_per_rank=max_num_tokens_per_dp_rank,
@@ -291,3 +297,7 @@ class DeepEPLLAll2AllManager(DeepEPAll2AllManagerBase):
297 handle: deep_ep.Buffer = self.handle_cache.get_or_create(
298 buffer_kwargs, deep_ep.Buffer)
299 return handle
300 +
301 + # DeepEP LL uses RDMA so no SMs are used for communication
302 + def max_sms_used(self) -> Optional[int]:
303 + return 0
\ No newline at end of file
vllm/distributed/device_communicators/base_device_communicator.py
+6
@@ -60,6 +60,12 @@ class All2AllManagerBase:
60 # and reuse it for the same config.
61 raise NotImplementedError
62
63 + def set_num_sms(self, num_sms: int):
64 + pass
65 +
66 + def max_sms_used(self) -> Optional[int]:
67 + return None # None means it could use the whole GPU
68 +
69 def dispatch(self, hidden_states: torch.Tensor,
70 router_logits: torch.Tensor):
71 raise NotImplementedError
vllm/engine/arg_utils.py
+6
@@ -330,6 +330,8 @@ class EngineArgs:
330 enable_dbo: bool = ParallelConfig.enable_dbo
331 dbo_decode_token_threshold: int = \
332 ParallelConfig.dbo_decode_token_threshold
333 + dbo_prefill_token_threshold: int = \
334 + ParallelConfig.dbo_prefill_token_threshold
335 eplb_config: EPLBConfig = get_field(ParallelConfig, "eplb_config")
336 enable_eplb: bool = ParallelConfig.enable_eplb
337 expert_placement_strategy: ExpertPlacementStrategy = \
@@ -698,6 +700,9 @@ class EngineArgs:
700 parallel_group.add_argument(
701 "--dbo-decode-token-threshold",
702 **parallel_kwargs["dbo_decode_token_threshold"])
703 + parallel_group.add_argument(
704 + "--dbo-prefill-token-threshold",
705 + **parallel_kwargs["dbo_prefill_token_threshold"])
706 parallel_group.add_argument("--enable-eplb",
707 **parallel_kwargs["enable_eplb"])
708 parallel_group.add_argument("--eplb-config",
@@ -1316,6 +1321,7 @@ class EngineArgs:
1321 enable_expert_parallel=self.enable_expert_parallel,
1322 enable_dbo=self.enable_dbo,
1323 dbo_decode_token_threshold=self.dbo_decode_token_threshold,
1324 + dbo_prefill_token_threshold=self.dbo_prefill_token_threshold,
1325 enable_eplb=self.enable_eplb,
1326 eplb_config=self.eplb_config,
1327 expert_placement_strategy=self.expert_placement_strategy,
vllm/envs.py
+11
@@ -189,6 +189,8 @@ if TYPE_CHECKING:
189 VLLM_CUSTOM_SCOPES_FOR_PROFILING: bool = False
190 VLLM_KV_EVENTS_USE_INT_BLOCK_HASHES: bool = True
191 VLLM_OBJECT_STORAGE_SHM_BUFFER_NAME: str = "VLLM_OBJECT_STORAGE_SHM_BUFFER"
192 + VLLM_DEEPEP_BUFFER_SIZE_MB: int = 1024
193 + VLLM_DBO_COMM_SMS: int = 20
194 GPT_OSS_SYSTEM_TOOL_MCP_LABELS: list[str] = []
195 VLLM_PATTERN_MATCH_DEBUG: Optional[str] = None
196
@@ -1392,6 +1394,15 @@ environment_variables: dict[str, Callable[[], Any]] = {
1394 lambda: os.getenv("VLLM_OBJECT_STORAGE_SHM_BUFFER_NAME",
1395 "VLLM_OBJECT_STORAGE_SHM_BUFFER"),
1396
1397 + # The size in MB of the buffers (NVL and RDMA) used by DeepEP
1398 + "VLLM_DEEPEP_BUFFER_SIZE_MB":
1399 + lambda: int(os.getenv("VLLM_DEEPEP_BUFFER_SIZE_MB", "1024")),
1400 +
1401 + # The number of SMs to allocate for communication kernels when running DBO
1402 + # the rest of the SMs on the device will be allocated to compute
1403 + "VLLM_DBO_COMM_SMS":
1404 + lambda: int(os.getenv("VLLM_DBO_COMM_SMS", "20")),
1405 +
1406 # Valid values are container,code_interpreter,web_search_preview
1407 # ex GPT_OSS_SYSTEM_TOOL_MCP_LABELS=container,code_interpreter
1408 "GPT_OSS_SYSTEM_TOOL_MCP_LABELS":
vllm/model_executor/layers/fused_moe/deepep_ht_prepare_finalize.py
+51 -25
@@ -12,6 +12,11 @@ from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
12 from vllm.model_executor.layers.fused_moe.utils import (
13 moe_kernel_quantize_input)
14 from vllm.utils import round_up
15 +from vllm.v1.worker.ubatching import (
16 + dbo_current_ubatch_id, dbo_enabled, dbo_switch_to_comm,
17 + dbo_switch_to_compute, dbo_switch_to_compute_sync,
18 + dbo_yield_and_switch_from_comm_to_compute,
19 + dbo_yield_and_switch_from_compute_to_comm)
20
21
22 class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
@@ -46,9 +51,9 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
51 self.async_prepare = True
52
53 # The dispatch function returns a handle that the combine function
49 - # requires. We store the handle here so it is available to the
50 - # combine function.
51 - self.handle = None
54 + # requires. Under DBO microbatching we must track one handle per
55 + # micro-batch to avoid races between threads.
56 + self.handles = [None, None]
57
58 # From https://github.com/deepseek-ai/DeepEP/blob/9fe9021f29c9083cd1808ab36b740208524d9f63/deep_ep/buffer.py#L164
59 self.available_rank_configs = [2, 4, 8, 16, 24, 32, 64, 128, 144, 160]
@@ -89,6 +94,11 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
94
95 has_scales = token_scales is not None
96
97 + # We yield before launching the dispatch kernel since the dispatch
98 + # kernel will block the CPU so we want to queue up all the compute
99 + # for the other ubatch before the dispatch kernel starts.
100 + dbo_yield_and_switch_from_compute_to_comm()
101 +
102 (num_tokens_per_rank, num_tokens_per_rdma_rank,
103 dispatch_expert_num_tokens, is_token_in_rank,
104 event) = self.buffer.get_dispatch_layout(
@@ -104,7 +114,7 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
114
115 (
116 token_data, expert_topk_ids, expert_topk_weights,
107 - expert_num_tokens_per_expert_list, self.handle, event
117 + expert_num_tokens_per_expert_list, handle, event
118 ) = self.buffer.dispatch(
119 x=token_data,
120 handle=None,
@@ -119,9 +129,15 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
129 expert_alignment=1,
130 config=self._get_dispatch_config(),
131 previous_event=None,
122 - async_finish=self.async_prepare,
132 + async_finish=self.async_prepare and not dbo_enabled(),
133 allocate_on_comm_stream=False)
134
135 + # record the handle for this ubatch
136 + a2a_idx = dbo_current_ubatch_id()
137 + self.handles[a2a_idx] = handle
138 +
139 + dbo_switch_to_compute_sync()
140 +
141 return lambda: self._receiver(
142 event,
143 has_scales,
@@ -146,7 +162,7 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
162 a1_scale: Optional[torch.Tensor],
163 quant_config: FusedMoEQuantConfig,
164 ) -> mk.PrepareResultType:
149 - if self.async_prepare:
165 + if event.event is not None:
166 event.current_stream_wait()
167
168 if has_scales:
@@ -207,7 +223,7 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
223 expert_map: Optional[torch.Tensor],
224 apply_router_weight_on_input: bool,
225 quant_config: FusedMoEQuantConfig,
210 - ) -> tuple[Callable, mk.ReceiverType]:
226 + ) -> mk.ReceiverType:
227
228 if apply_router_weight_on_input:
229 topk = topk_ids.size(1)
@@ -233,14 +249,13 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
249 a1q_scale = None
250 a1_post_scale = quant_config.a1_scale
251
236 - return (lambda *args: None,
237 - self._do_dispatch(tokens=a1q,
238 - token_scales=a1q_scale,
239 - rank_topk_ids=topk_ids,
240 - rank_topk_weights=topk_weights,
241 - num_experts=num_experts,
242 - a1_scale=a1_post_scale,
243 - quant_config=quant_config))
252 + return self._do_dispatch(tokens=a1q,
253 + token_scales=a1q_scale,
254 + rank_topk_ids=topk_ids,
255 + rank_topk_weights=topk_weights,
256 + num_experts=num_experts,
257 + a1_scale=a1_post_scale,
258 + quant_config=quant_config)
259
260 def prepare(
261 self,
@@ -252,10 +267,9 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
267 apply_router_weight_on_input: bool,
268 quant_config: FusedMoEQuantConfig,
269 ) -> mk.PrepareResultType:
255 - (_, receiver) = self.prepare_async(a1, topk_weights, topk_ids,
256 - num_experts, expert_map,
257 - apply_router_weight_on_input,
258 - quant_config)
270 + receiver = self.prepare_async(a1, topk_weights, topk_ids, num_experts,
271 + expert_map, apply_router_weight_on_input,
272 + quant_config)
273 return receiver()
274
275 def _finalize(
@@ -269,7 +283,9 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
283 do_async: bool,
284 ) -> Optional[Callable]:
285
272 - assert self.handle is not None
286 + a2a_idx = dbo_current_ubatch_id()
287 + handle = self.handles[a2a_idx]
288 + assert handle is not None
289
290 # fused_expert_output can have 0 tokens - This happens when none of the
291 # tokens from the all2all reach this EP rank.
@@ -283,25 +299,35 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
299 topk_ids=topk_ids,
300 apply_router_weight_on_input=apply_router_weight_on_input,
301 )
286 -
302 + dbo_yield_and_switch_from_compute_to_comm()
303 combined_x, _, event = self.buffer.combine(
304 x=fused_expert_output,
289 - handle=self.handle,
305 + handle=handle,
306 topk_weights=None,
307 config=self._get_combine_config(),
308 previous_event=None,
293 - async_finish=do_async,
309 + async_finish=do_async and not dbo_enabled(),
310 allocate_on_comm_stream=False)
311
312 + dbo_switch_to_compute()
313 +
314 if do_async:
315
316 def _receiver():
299 - event.current_stream_wait()
317 + if event.event is not None:
318 + event.current_stream_wait()
319 + dbo_switch_to_comm()
320 # Respect inplace outputs.
321 output.copy_(combined_x, non_blocking=True)
322
303 - return lambda: _receiver()
323 + # TODO(lucas): refactor the modular kernel so this will be
324 + # handled there
325 + dbo_yield_and_switch_from_comm_to_compute()
326 +
327 + return _receiver
328 else:
329 + # TODO(lucas): support this case with the refactored modular kernel
330 + assert not dbo_enabled()
331 # Respect inplace outputs.
332 output.copy_(combined_x, non_blocking=True)
333 return None
vllm/model_executor/layers/fused_moe/deepep_ll_prepare_finalize.py
+4 -6
@@ -206,7 +206,7 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
206 apply_router_weight_on_input: bool,
207 weight_and_reduce_impl: mk.TopKWeightAndReduce,
208 do_async: bool,
209 - ) -> Optional[Callable]:
209 + ) -> tuple[Callable, Callable]:
210 assert isinstance(
211 weight_and_reduce_impl, TopKWeightAndReduceDelegate
212 ), ("Weight application and reduction happens in the combine kernel.")
@@ -233,7 +233,7 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
233 return_recv_hook=do_recv_hook,
234 out=output)
235
236 - return recv_hook
236 + return recv_hook, lambda: None
237
238 def finalize_async(
239 self,
@@ -243,8 +243,8 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
243 topk_ids: torch.Tensor,
244 apply_router_weight_on_input: bool,
245 weight_and_reduce_impl: mk.TopKWeightAndReduce,
246 - ) -> Callable:
247 - recv_hook = self._finalize(
246 + ) -> tuple[Callable, Callable]:
247 + return self._finalize(
248 output,
249 fused_expert_output,
250 topk_weights,
@@ -253,8 +253,6 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
253 weight_and_reduce_impl,
254 do_async=True,
255 )
256 - assert recv_hook is not None
257 - return recv_hook
256
257 def finalize(
258 self,
vllm/model_executor/layers/fused_moe/modular_kernel.py
+95 -36
@@ -13,7 +13,8 @@ from vllm.model_executor.layers.fused_moe.config import FusedMoEQuantConfig
13 from vllm.model_executor.layers.fused_moe.utils import ( # yapf: disable
14 _resize_cache, count_expert_num_tokens)
15 from vllm.utils import cdiv
16 -from vllm.v1.worker.ubatching import (dbo_enabled, dbo_maybe_run_recv_hook,
16 +from vllm.v1.worker.ubatching import (dbo_current_ubatch_id, dbo_enabled,
17 + dbo_maybe_run_recv_hook,
18 dbo_register_recv_hook, dbo_yield)
19
20 #
@@ -223,7 +224,7 @@ class FusedMoEPrepareAndFinalize(ABC):
224 expert_map: Optional[torch.Tensor],
225 apply_router_weight_on_input: bool,
226 quant_config: FusedMoEQuantConfig,
226 - ) -> tuple[Callable, ReceiverType]:
227 + ) -> Union[tuple[Callable, ReceiverType], ReceiverType]:
228 """
229 Perform any quantization (and/or) dispatching needed for this kernel
230 but do not wait for results from other workers.
@@ -239,10 +240,21 @@ class FusedMoEPrepareAndFinalize(ABC):
240 - apply_router_weight_on_input: When True, apply the weights to the
241 activations, before quantization + dispatching.
242
242 - Returns a callback that when invoked waits for results from other
243 - workers and has the same return signature as `prepare`, e.g.
244 -
245 - receiver = obj.prepare_async(...)
243 + Returns a callback or a hook callback pair that when invoked waits for
244 + results from other workers and has the same return signature as
245 + `prepare`, if a hook is returned this is more lightweight check that
246 + the recv is complete without doing extra work (used by DBO, will be
247 + refactored in the very near future)
248 +
249 + e.g.
250 +
251 + ret = obj.prepare_async(...)
252 +
253 + if isinstance(ret, tuple):
254 + hook, receiver = ret
255 + hook()
256 +
257 + if hook is not None:
258 a, a_scales, expert_meta, topk_ids, topk_weights = receiver()
259
260 is equivalent to:
@@ -284,7 +296,7 @@ class FusedMoEPrepareAndFinalize(ABC):
296 topk_ids: torch.Tensor,
297 apply_router_weight_on_input: bool,
298 weight_and_reduce_impl: TopKWeightAndReduce,
287 - ) -> Callable:
299 + ) -> Union[tuple[Callable, Callable], Callable]:
300 """
301 Perform any combine plus apply weights and perform a reduction on the
302 fused experts output but do not wait for results from other workers.
@@ -298,11 +310,17 @@ class FusedMoEPrepareAndFinalize(ABC):
310 - weight_and_reduce_impl: An optional TopKWeightAndReduce
311 implementation.
312
301 - Returns a callback that when invoked waits for results from other
302 - workers and has the same return signature as `finalize`, e.g.
313 + Returns a callback or a hook callback pair that when invoked waits for
314 + results from other workers and has the same return signature as
315 + `finalize`, if a hook is returned this is more lightweight check that
316 + the recv is complete without doing extra work (used by DBO, will be
317 + refactored in the very near future)
318
304 - receiver = obj.finalize_async(output, ...)
319 + ret = obj.finalize_async(output, ...)
320 ... output not valid yet ...
321 + if isinstance(ret, tuple):
322 + hook, receiver = ret
323 + hook()
324 receiver()
325 ... output valid here ...
326
@@ -600,9 +618,23 @@ class FusedMoEModularKernel(torch.nn.Module):
618 layer due to any layer specific state that may be used by the component
619 objects.
620 """
603 - fused_out_buffer = SharedResizableBuffer()
604 - workspace13_buffer = SharedResizableBuffer()
605 - workspace2_buffer = SharedResizableBuffer()
621 +
622 + class SharedBuffers:
623 +
624 + def __init__(self) -> None:
625 + self.fused_out = SharedResizableBuffer()
626 + self.workspace13 = SharedResizableBuffer()
627 + self.workspace2 = SharedResizableBuffer()
628 +
629 + # Persistent buffers that are shared across `FusedMoEModularKernel`
630 + # instances (layers), to save memory and allocattions.
631 + #
632 + # We have two sets of buffers to support dual batch overlap (DBO) where each
633 + # microbatch (ubatch) should use its own set of buffers to avoid
634 + # cross-ubatch contimination.
635 + # NOTE that memory is lazily allocated for these buffers, meaning that if
636 + # DBO isn't being used, the second SharedBuffers will be empty.
637 + shared_buffers: list[SharedBuffers] = [SharedBuffers(), SharedBuffers()]
638
639 def __init__(
640 self,
@@ -647,14 +679,18 @@ class FusedMoEModularKernel(torch.nn.Module):
679 a1, a1q, M, N, K, top_k, global_num_experts, local_num_experts,
680 expert_tokens_meta)
681
682 + # select per-ubatch buffers to avoid cross-ubatch reuse under DBO
683 + ubatch_idx = dbo_current_ubatch_id()
684 + buffers = self.shared_buffers[ubatch_idx]
685 +
686 # We can reuse the memory between cache1 and cache3 because by the
687 # time we need cache3, we're done with cache1.
652 - workspace13 = self.workspace13_buffer.get(workspace13_shape,
653 - device=a1.device,
654 - dtype=workspace_dtype)
655 - workspace2 = self.workspace2_buffer.get(workspace2_shape,
656 - device=a1.device,
657 - dtype=workspace_dtype)
688 + workspace13 = buffers.workspace13.get(workspace13_shape,
689 + device=a1.device,
690 + dtype=workspace_dtype)
691 + workspace2 = buffers.workspace2.get(workspace2_shape,
692 + device=a1.device,
693 + dtype=workspace_dtype)
694
695 assert fused_out is None or fused_out.shape == fused_out_shape, (
696 f"fused_out {fused_out.shape} but expected {fused_out_shape}")
@@ -733,9 +769,11 @@ class FusedMoEModularKernel(torch.nn.Module):
769 (_, _, fused_out_shape, _) = self.fused_experts.workspace_shapes(
770 a1, a1q, M, N, K, top_k, global_num_experts, local_num_experts,
771 expert_tokens_meta)
736 - fused_out = self.fused_out_buffer.get(fused_out_shape,
737 - device=a1q.device,
738 - dtype=a1.dtype)
772 + ubatch_idx = dbo_current_ubatch_id()
773 + buffers = self.shared_buffers[ubatch_idx]
774 + fused_out = buffers.fused_out.get(fused_out_shape,
775 + device=a1q.device,
776 + dtype=a1.dtype)
777
778 def slice_input_tensors(
779 chunk_idx: int
@@ -868,6 +906,7 @@ class FusedMoEModularKernel(torch.nn.Module):
906 if not self.prepare_finalize.supports_async():
907 # We shouldn't be running an a2a kernel that doesn't
908 # support async prepare/finalize
909 + # TODO(lucas): enable in follow-up
910 assert not dbo_enabled()
911
912 (a1q, a1q_scale, expert_tokens_meta, _expert_topk_ids,
@@ -883,7 +922,7 @@ class FusedMoEModularKernel(torch.nn.Module):
922 else:
923 # Overlap shared expert compute with all2all dispatch.
924 dbo_maybe_run_recv_hook()
886 - hook, receiver = self.prepare_finalize.prepare_async(
925 + prepare_ret = self.prepare_finalize.prepare_async(
926 a1,
927 topk_weights,
928 topk_ids,
@@ -893,13 +932,21 @@ class FusedMoEModularKernel(torch.nn.Module):
932 self.fused_experts.quant_config,
933 )
934
896 - # If DBO is being used, register the hook with the ubatch context
897 - # and call it in dbo_maybe_run_recv_hook instead of passing it to
898 - # the receiver.
899 - dbo_register_recv_hook(hook)
900 - dbo_yield()
901 - if not dbo_enabled():
902 - hook()
935 + # TODO(lucas): refactor this in the alternative schedules followup
936 + # currently unpack if we have hook + receiver pair or just
937 + # receiver (see finalize_async docstring)
938 + hook, receiver = prepare_ret \
939 + if isinstance(prepare_ret, tuple) else (None, prepare_ret)
940 +
941 + if hook is not None:
942 + if dbo_enabled():
943 + # If DBO is being used, register the hook with the ubatch
944 + # context and call it in dbo_maybe_run_recv_hook instead of
945 + # passing it to the receiver.
946 + dbo_register_recv_hook(hook)
947 + dbo_yield()
948 + else:
949 + hook()
950
951 (a1q, a1q_scale, expert_tokens_meta, _expert_topk_ids,
952 _expert_topk_weights) = receiver()
@@ -952,7 +999,7 @@ class FusedMoEModularKernel(torch.nn.Module):
999 if self.shared_experts is not None:
1000 shared_output = self.shared_experts(a1)
1001 else:
955 - recv_hook = self.prepare_finalize.finalize_async(
1002 + finalize_ret = self.prepare_finalize.finalize_async(
1003 output,
1004 fused_out,
1005 topk_weights,
@@ -964,11 +1011,23 @@ class FusedMoEModularKernel(torch.nn.Module):
1011 if self.shared_experts is not None:
1012 shared_output = self.shared_experts(a1)
1013
967 - assert recv_hook is not None
968 - dbo_register_recv_hook(recv_hook)
969 - dbo_yield()
970 - if not dbo_enabled():
971 - recv_hook()
1014 + # TODO(lucas): refactor this in the alternative schedules followup
1015 + # currently unpack if we have hook + receiver pair or just
1016 + # receiver (see finalize_async docstring)
1017 + hook, receiver = finalize_ret \
1018 + if isinstance(finalize_ret, tuple) else (None, finalize_ret)
1019 +
1020 + if hook is not None:
1021 + if dbo_enabled():
1022 + # If DBO is being used, register the hook with the ubatch
1023 + # context and call it in dbo_maybe_run_recv_hook instead of
1024 + # passing it to the receiver.
1025 + dbo_register_recv_hook(hook)
1026 + dbo_yield()
1027 + else:
1028 + hook()
1029 +
1030 + receiver()
1031
1032 if self.shared_experts is None:
1033 return output
vllm/v1/attention/backends/utils.py
+41 -3
@@ -107,19 +107,57 @@ def _make_metadata_with_slice(
107 the requests included in ubatch_slice
108 """
109
110 + assert not ubatch_slice.is_empty(), (
111 + f"Ubatch slice {ubatch_slice} is empty")
112 +
113 request_slice = ubatch_slice.request_slice
114 token_slice = ubatch_slice.token_slice
115
116 + start_locs = attn_metadata.query_start_loc_cpu
117 + first_req = request_slice.start
118 + first_tok = token_slice.start
119 + last_req = request_slice.stop - 1
120 + last_tok = token_slice.stop - 1
121 +
122 + assert start_locs[first_req] <= first_tok < start_locs[first_req + 1], \
123 + "Token slice start outside of first request"
124 + assert start_locs[last_req] <= last_tok < start_locs[last_req+1], \
125 + "Token slice end outside of last request"
126 +
127 + # If the "middle" request has tokens in both ubatches, we have to split it.
128 + # If ubatch_slice is the first ubatch then we will be splitting the last
129 + # request. If it's the second microbatch, then we will be splitting the
130 + # first request
131 + splits_first_request = first_tok > start_locs[first_req]
132 + splits_last_request = last_tok < start_locs[last_req + 1] - 1
133 +
134 + query_start_loc_cpu = slice_query_start_locs(start_locs, request_slice)
135 query_start_loc = slice_query_start_locs(attn_metadata.query_start_loc,
136 request_slice)
137 +
138 assert len(query_start_loc) >= 2, (
139 f"query_start_loc must have at least 2 elements, "
140 f"got {len(query_start_loc)}")
118 - query_start_loc_cpu = slice_query_start_locs(
119 - attn_metadata.query_start_loc_cpu, request_slice)
141
142 + if splits_first_request:
143 + tokens_skipped = first_tok - start_locs[first_req]
144 + query_start_loc[1:] -= tokens_skipped
145 + query_start_loc_cpu[1:] -= tokens_skipped
146 seq_lens = attn_metadata.seq_lens[request_slice]
147 seq_lens_cpu = attn_metadata.seq_lens_cpu[request_slice]
148 +
149 + if splits_last_request:
150 + tokens_skipped = query_start_loc_cpu[-1] - token_slice.stop
151 + query_start_loc[-1] -= tokens_skipped
152 + query_start_loc_cpu[-1] -= tokens_skipped
153 +
154 + # Make sure we don't modify the seq_lens tensors
155 + # (not cudagraph compatible)
156 + seq_lens = seq_lens.clone()
157 + seq_lens_cpu = seq_lens_cpu.clone()
158 + seq_lens[-1] -= tokens_skipped
159 + seq_lens_cpu[-1] -= tokens_skipped
160 +
161 max_seq_len = int(seq_lens_cpu.max())
162 num_computed_tokens_cpu = attn_metadata.num_computed_tokens_cpu[
163 request_slice]
@@ -167,6 +205,7 @@ def split_attn_metadata(
205 for ubatch_slice in ubatch_slices:
206 results.append(
207 _make_metadata_with_slice(ubatch_slice, common_attn_metadata))
208 +
209 return results
210
211
@@ -696,7 +735,6 @@ def split_decodes_and_prefills(
735 return num_reqs, 0, num_tokens, 0
736
737 first_prefill = is_prefill.int().argmax(dim=-1).item()
699 - assert torch.all(query_lens[first_prefill:] > decode_threshold)
738 assert torch.all(query_lens[:first_prefill] <= decode_threshold)
739 num_decodes = first_prefill
740 num_prefills = num_reqs - num_decodes
vllm/v1/spec_decode/eagle.py
+3 -6
@@ -30,7 +30,6 @@ from vllm.v1.sample.metadata import SamplingMetadata
30 from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
31 from vllm.v1.utils import CpuGpuBuffer
32 from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
33 -from vllm.v1.worker.ubatching import dbo_current_ubatch_id
33
34 logger = init_logger(__name__)
35
@@ -192,9 +191,8 @@ class EagleProposer:
191 assert self.runner is not None
192
193 # FIXME: need to consider multiple kv_cache_groups
195 - ubatch_id = dbo_current_ubatch_id()
194 attn_metadata_builder = \
197 - self.runner.attn_groups[0][0].metadata_builders[ubatch_id]
195 + self.runner.attn_groups[0][0].get_metadata_builder()
196 attn_metadata = attn_metadata_builder.build_for_drafting(
197 common_attn_metadata=common_attn_metadata, draft_index=0)
198
@@ -330,7 +328,7 @@ class EagleProposer:
328
329 # Rebuild attention metadata
330 attn_metadata_builder = \
333 - self.runner.attn_groups[0][0].metadata_builders[ubatch_id]
331 + self.runner.attn_groups[0][0].get_metadata_builder()
332 attn_metadata = attn_metadata_builder\
333 .build_for_drafting(common_attn_metadata=common_attn_metadata,
334 draft_index=token_index + 1)
@@ -538,9 +536,8 @@ class EagleProposer:
536 hidden_states: torch.Tensor,
537 common_attn_metadata: CommonAttentionMetadata,
538 ) -> list[torch.Tensor]:
541 - ubatch_id = dbo_current_ubatch_id()
539 tree_attn_metadata_builder = \
543 - self.runner.attn_groups[0][0].metadata_builders[ubatch_id]
540 + self.runner.attn_groups[0][0].get_metadata_builder()
541 assert isinstance(tree_attn_metadata_builder,
542 TreeAttentionMetadataBuilder)
543
vllm/v1/worker/gpu_model_runner.py
+76 -97
@@ -96,7 +96,8 @@ from vllm.v1.worker.gpu_ubatch_wrapper import UBatchWrapper
96 from vllm.v1.worker.kv_connector_model_runner_mixin import (
97 KVConnectorModelRunnerMixin)
98 from vllm.v1.worker.lora_model_runner_mixin import LoRAModelRunnerMixin
99 -from vllm.v1.worker.ubatch_splitting import get_dp_padding_ubatch, ubatch_split
99 +from vllm.v1.worker.ubatch_splitting import (check_ubatch_thresholds,
100 + ubatch_split)
101 from vllm.v1.worker.ubatch_utils import UBatchSlice, UBatchSlices
102 from vllm.v1.worker.utils import is_residual_scattered_for_sp
103
@@ -1032,7 +1033,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1033 num_tokens_padded = num_tokens_unpadded + self.get_local_padding(
1034 num_tokens_unpadded)
1035 ubatch_slices, num_tokens_after_padding = \
1035 - ubatch_split(max_num_scheduled_tokens,
1036 + ubatch_split(num_scheduled_tokens,
1037 num_tokens_unpadded,
1038 num_tokens_padded,
1039 self.vllm_config)
@@ -1206,7 +1207,6 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1207 ubatch_slices, common_attn_metadata)
1208 for ubid, common_attn_metadata in enumerate(
1209 common_attn_metadata_list):
1209 - assert common_attn_metadata.max_query_len == 1
1210 attn_metadata_i = (attn_group.get_metadata_builder(
1211 ubatch_id=ubid).build(
1212 common_prefix_len=common_prefix_len,
@@ -2182,9 +2182,6 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2182 ) = self._preprocess(scheduler_output, intermediate_tensors,
2183 ubatch_slices, num_tokens_after_padding)
2184
2185 - if ubatch_slices is not None:
2186 - num_input_tokens = num_input_tokens // 2
2187 -
2185 uniform_decode = (max_query_len
2186 == self.uniform_decode_query_len) and (
2187 num_scheduled_tokens
@@ -2194,6 +2191,11 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2191 cudagraph_runtime_mode, batch_descriptor = \
2192 self.cudagraph_dispatcher.dispatch(batch_descriptor)
2193
2194 + # This is currently to get around the assert in the DPMetadata
2195 + # where it wants `num_tokens_across_dp` to align with `num_tokens`
2196 + if ubatch_slices is not None:
2197 + num_input_tokens = ubatch_slices[0].num_tokens
2198 +
2199 # Run the model.
2200 # Use persistent buffers for CUDA graphs.
2201 with (set_forward_context(
@@ -2821,7 +2823,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2823 cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
2824 force_attention: bool = False,
2825 uniform_decode: bool = False,
2824 - allow_microbatching: bool = False,
2826 + allow_microbatching: bool = True,
2827 skip_eplb: bool = False,
2828 is_profile: bool = False,
2829 create_mixed_batch: bool = False,
@@ -2847,32 +2849,10 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2849 (1 token) and prefill (multiple tokens) requests.
2850 remove_lora: If False, dummy LoRAs are not destroyed after the run
2851 """
2850 - ubatch_enabled = self.parallel_config.enable_dbo
2851 - num_tokens_across_dp = None
2852 - num_pad = 0
2853 - should_ubatch = False
2854 - if ubatch_enabled:
2855 - should_ubatch = num_tokens >= \
2856 - self.parallel_config.dbo_decode_token_threshold and \
2857 - allow_microbatching
2858 -
2859 - (should_ubatch, num_tokens_across_dp) = get_dp_padding_ubatch(
2860 - num_tokens, num_tokens, should_ubatch, self.vllm_config)
2861 -
2862 - # Currently the dummy run should only be ubatching during
2863 - # cuda graph capture, meaning all DP ranks should already
2864 - # have the same batch size
2865 - if num_tokens_across_dp is not None:
2866 - assert int(num_tokens_across_dp[0]) == num_tokens // 2
2867 -
2852 assert cudagraph_runtime_mode in {
2853 CUDAGraphMode.NONE, CUDAGraphMode.PIECEWISE, CUDAGraphMode.FULL
2854 }
2855
2872 - if not should_ubatch:
2873 - num_pad, num_tokens_across_dp = self.get_dp_padding(num_tokens)
2874 - num_tokens += num_pad
2875 -
2856 # If cudagraph_mode.decode_mode() == FULL and
2857 # cudagraph_mode.separate_routine(). This means that we are using
2858 # different graphs and/or modes for mixed prefill-decode batches vs.
@@ -2888,10 +2868,6 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2868 # for GQA/MQA.
2869 max_query_len = self.uniform_decode_query_len if uniform_decode else \
2870 num_tokens
2891 - if allow_microbatching:
2892 - assert self.uniform_decode_query_len == 1
2893 - assert uniform_decode is True
2894 - assert max_query_len == 1
2871
2872 # Set num_scheduled_tokens based on num_tokens and max_num_seqs
2873 # for dummy run with LoRA so that the num_reqs collectively
@@ -2930,20 +2906,31 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2906 assert len(num_scheduled_tokens_list) == num_reqs
2907 num_scheduled_tokens = np.array(num_scheduled_tokens_list,
2908 dtype=np.int32)
2909 + total_num_scheduled_tokens = int(num_scheduled_tokens.sum())
2910
2911 ubatch_slices = None
2912 + num_tokens_after_padding = None
2913 +
2914 # We currently only microbatch if the number of tokens is
2915 # over a certain threshold.
2937 - if should_ubatch:
2938 - # We only support decode-only cudagraphs
2939 - assert num_reqs == num_tokens
2940 - assert num_tokens % 2 == 0
2941 - ubatch_slices = [
2942 - UBatchSlice(slice(0, num_reqs // 2), slice(0,
2943 - num_tokens // 2)),
2944 - UBatchSlice(slice(num_reqs // 2, num_reqs),
2945 - slice(num_tokens // 2, num_tokens))
2946 - ]
2916 + if self.parallel_config.enable_dbo and allow_microbatching:
2917 + ubatch_slices, num_tokens_after_padding = ubatch_split(
2918 + num_scheduled_tokens,
2919 + total_num_scheduled_tokens,
2920 + total_num_scheduled_tokens,
2921 + self.vllm_config,
2922 + )
2923 +
2924 + # If we failed to microbatch, currently need to resynchronize
2925 + # TODO(lucas,sage): we should be able to avoid this second sync by
2926 + # refactoring `get_dp_padding_ubatch` and `get_dp_padding` into
2927 + # a single `coordinate_batch_across_dp` function.
2928 + if num_tokens_after_padding is None:
2929 + num_pad, num_tokens_across_dp = self.get_dp_padding(num_tokens)
2930 + num_tokens_after_padding = num_tokens + num_pad
2931 + else:
2932 + num_tokens_across_dp = num_tokens_after_padding
2933 + num_tokens_after_padding = int(num_tokens_after_padding[0].item())
2934
2935 attn_metadata: Optional[PerLayerAttnMetadata] = None
2936
@@ -2966,6 +2953,11 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2953 self.seq_lens.np[num_reqs:] = 0
2954 self.seq_lens.copy_to_gpu()
2955
2956 + cum_num_tokens, _ = self._get_cumsum_and_arange(
2957 + num_scheduled_tokens)
2958 + self.query_start_loc.np[1:num_reqs + 1] = cum_num_tokens
2959 + self.query_start_loc.copy_to_gpu()
2960 +
2961 for kv_cache_group_id, kv_cache_group_spec in enumerate(
2962 self.kv_cache_config.kv_cache_groups):
2963 common_attn_metadata = CommonAttentionMetadata(
@@ -3060,7 +3052,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3052 with self.maybe_randomize_inputs(input_ids), set_forward_context(
3053 attn_metadata,
3054 self.vllm_config,
3063 - num_tokens=num_tokens,
3055 + num_tokens=num_tokens_after_padding,
3056 num_tokens_across_dp=num_tokens_across_dp,
3057 cudagraph_runtime_mode=cudagraph_runtime_mode,
3058 batch_descriptor=batch_descriptor,
@@ -3395,56 +3387,51 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3387 desc="Capturing CUDA graphs ({}, {})".format(
3388 "decode" if uniform_decode else "mixed prefill-decode",
3389 cudagraph_runtime_mode.name))
3398 - enable_dbo = self.parallel_config.enable_dbo
3399 - # DBO Only supports running Full cudagraphs with uniform
3400 - # decode lengths
3401 - if enable_dbo and uniform_decode:
3402 - for num_tokens in compilation_cases:
3403 - # If the number of tokens is greater than the microbatching
3404 - # threshold, don't generate a microbatched cudagraph
3405 - if (num_tokens
3406 - < self.parallel_config.dbo_decode_token_threshold):
3407 - continue
3390
3409 - # Warmup
3391 + # We skip EPLB here since we don't want to record dummy metrics
3392 + for num_tokens in compilation_cases:
3393 + # We currently only capture ubatched graphs when its a FULL
3394 + # cudagraph and for uniform decode batches.
3395 + capture_ubatched_graph = self.parallel_config.enable_dbo \
3396 + and cudagraph_runtime_mode == CUDAGraphMode.FULL \
3397 + and uniform_decode \
3398 + and check_ubatch_thresholds(
3399 + config=self.vllm_config.parallel_config,
3400 + num_tokens=num_tokens,
3401 + uniform_decode=uniform_decode,
3402 + )
3403 +
3404 + # Currently we capture both microbatched and non-microbatched
3405 + # graphs when capture_ubatched_graph is True, this is because
3406 + # occasionally we will be forced out of microbatching due to other
3407 + # DP ranks not microbatching (usually caused by an empty second
3408 + # microbatch; once we resolve this, we can remove the
3409 + # non-microbatched graph capture).
3410 + allow_microbatching_options = [True, False] if \
3411 + capture_ubatched_graph else [False]
3412 + for allow_microbatching in allow_microbatching_options:
3413 for _ in range(
3414 self.compilation_config.cudagraph_num_of_warmups):
3415 + # Use CUDAGraphRuntimeStyle.NONE (default) for warmup.
3416 + # But be careful, warm up with `NONE`is orthogonal to
3417 + # if we want to warm up attention or not. This is
3418 + # different from the case where `FULL` implies capture
3419 + # attention while `PIECEWISE` implies no attention.
3420 force_attention = (
3421 cudagraph_runtime_mode == CUDAGraphMode.FULL)
3422 self._dummy_run(num_tokens,
3423 cudagraph_runtime_mode=CUDAGraphMode.NONE,
3424 force_attention=force_attention,
3417 - uniform_decode=True,
3418 - allow_microbatching=True,
3419 - skip_eplb=True)
3420 -
3421 - # Graph Capture
3425 + uniform_decode=uniform_decode,
3426 + allow_microbatching=allow_microbatching,
3427 + skip_eplb=True,
3428 + remove_lora=False)
3429 self._dummy_run(num_tokens,
3423 - cudagraph_runtime_mode=CUDAGraphMode.FULL,
3424 - uniform_decode=True,
3425 - allow_microbatching=True,
3426 - skip_eplb=True)
3427 - # We skip EPLB here since we don't want to record dummy metrics
3428 - for num_tokens in compilation_cases:
3429 - for _ in range(self.compilation_config.cudagraph_num_of_warmups):
3430 - # Use CUDAGraphRuntimeStyle.NONE (default) for warmup.
3431 - # But be careful, warm up with `NONE`is orthogonal to
3432 - # if we want to warm up attention or not. This is
3433 - # different from the case where `FULL` implies capture
3434 - # attention while `PIECEWISE` implies no attention.
3435 - force_attention = (
3436 - cudagraph_runtime_mode == CUDAGraphMode.FULL)
3437 - self._dummy_run(num_tokens,
3438 - cudagraph_runtime_mode=CUDAGraphMode.NONE,
3439 - force_attention=force_attention,
3430 + cudagraph_runtime_mode=cudagraph_runtime_mode,
3431 uniform_decode=uniform_decode,
3432 + allow_microbatching=allow_microbatching,
3433 skip_eplb=True,
3434 remove_lora=False)
3443 - self._dummy_run(num_tokens,
3444 - cudagraph_runtime_mode=cudagraph_runtime_mode,
3445 - uniform_decode=uniform_decode,
3446 - skip_eplb=True,
3447 - remove_lora=False)
3435 self.maybe_remove_all_loras(self.lora_config)
3436
3437 def initialize_attn_backend(self, kv_cache_config: KVCacheConfig) -> None:
@@ -3500,24 +3487,16 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3487 attn_groups: list[AttentionGroup] = []
3488 for (attn_backend,
3489 kv_cache_spec), layer_names in attn_backends_map.items():
3503 - attn_metadata_builders = []
3504 - attn_metadata_builders.append(attn_backend.get_builder_cls()(
3505 - kv_cache_spec,
3490 + attn_group = AttentionGroup.create_with_metadata_builders(
3491 + attn_backend,
3492 layer_names,
3493 + kv_cache_spec,
3494 self.vllm_config,
3495 self.device,
3509 - ))
3510 - if self.parallel_config.enable_dbo:
3511 - attn_metadata_builders.append(
3512 - attn_backend.get_builder_cls()(
3513 - kv_cache_spec,
3514 - layer_names,
3515 - self.vllm_config,
3516 - self.device,
3517 - ))
3518 - attn_group = AttentionGroup(attn_backend,
3519 - attn_metadata_builders,
3520 - layer_names, kv_cache_spec)
3496 + num_metadata_builders=1
3497 + if not self.parallel_config.enable_dbo else 2,
3498 + )
3499 +
3500 attn_groups.append(attn_group)
3501 return attn_groups
3502
vllm/v1/worker/gpu_ubatch_wrapper.py
+81 -6
@@ -1,25 +1,28 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -import dataclasses
4 import threading
5 +from dataclasses import dataclass
6 from typing import Any, Callable, Optional
7
8 import torch
9
10 +import vllm.envs as envs
11 from vllm.compilation.cuda_graph import CUDAGraphWrapper
12 from vllm.config import CUDAGraphMode, VllmConfig
13 +from vllm.distributed import get_ep_group
14 from vllm.forward_context import (create_forward_context, get_forward_context,
15 override_forward_context)
16 from vllm.logger import init_logger
17 from vllm.platforms import current_platform
18 from vllm.sequence import IntermediateTensors
19 +from vllm.utils import has_deep_gemm
20 from vllm.v1.worker.ubatching import UBatchContext, make_ubatch_contexts
21
22 logger = init_logger(__name__)
23
24
22 -@dataclasses.dataclass
25 +@dataclass
26 class UbatchMetadata:
27 context: UBatchContext
28 input_ids: torch.Tensor
@@ -29,13 +32,55 @@ class UbatchMetadata:
32 num_tokens: int
33
34
32 -@dataclasses.dataclass
35 +@dataclass
36 class CUDAGraphMetaData:
37 cudagraph: torch.cuda.CUDAGraph
38 ubatch_metadata: UbatchMetadata
39 outputs: Optional[Any] = None
40
41
42 +class SMControlContextManager:
43 +
44 + def __init__(self, comm_sms: int, set_comm_sms: Callable[[int], None],
45 + set_compute_sms: Callable[[int], None]):
46 + """
47 + Context manager for controlling SM (Streaming Multiprocessor)
48 + allocation. Upon entering the context, it sets the number of SMs
49 + allocated for communication and computation to comm_sms and
50 + total_sms - comm_sms respectively. Upon exiting, it restores the
51 + allocation to use all available SMs (i.e. total_sms).
52 +
53 + Args:
54 + comm_sms (int): The number of SMs to allocate for communication.
55 + (The remainder will be used for computation.)
56 + set_comm_sms (Callable[[int], None]):
57 + A function that sets the number of SMs for communication.
58 + set_compute_sms (Callable[[int], None]):
59 + A function that sets the number of SMs for computation.
60 + """
61 +
62 + assert current_platform.is_cuda(), \
63 + "SM control is currently only supported on CUDA"
64 +
65 + props = torch.cuda.get_device_properties(torch.cuda.current_device())
66 + total_sms = props.multi_processor_count
67 +
68 + assert comm_sms < total_sms
69 + self.total_sms = total_sms
70 + self.compute_sms = total_sms - comm_sms
71 + self.comm_sms = comm_sms
72 + self.set_comm_sms = set_comm_sms
73 + self.set_compute_sms = set_compute_sms
74 +
75 + def __enter__(self):
76 + self.set_comm_sms(self.comm_sms)
77 + self.set_compute_sms(self.compute_sms)
78 +
79 + def __exit__(self, exc_type, exc_value, traceback):
80 + self.set_comm_sms(self.total_sms)
81 + self.set_compute_sms(self.total_sms)
82 +
83 +
84 class UBatchWrapper:
85
86 def __init__(self, runnable: Callable, vllm_config: VllmConfig,
@@ -56,6 +101,35 @@ class UBatchWrapper:
101 runnable, vllm_config, runtime_mode=runtime_mode)
102 self.graph_pool = current_platform.get_global_graph_pool()
103
104 + self.sm_control = self._create_sm_control_context(vllm_config)
105 +
106 + @staticmethod
107 + def _create_sm_control_context(vllm_config: VllmConfig):
108 + comm_sms = envs.VLLM_DBO_COMM_SMS
109 +
110 + set_comm_sms = lambda sms: None
111 + if vllm_config.parallel_config.enable_expert_parallel:
112 + # Currently only DeepEP highthroughput supports SM control so this
113 + # only affects that case.
114 + all2all_manager = get_ep_group(
115 + ).device_communicator.all2all_manager
116 +
117 + if all2all_manager.max_sms_used() is not None:
118 + comm_sms = min(comm_sms, all2all_manager.max_sms_used())
119 +
120 + if comm_sms > 0:
121 + set_comm_sms = lambda sms: all2all_manager.set_num_sms(sms)
122 +
123 + # TODO(lucas): support other kernels besides DeepGEMM
124 + set_compute_sms = lambda sms: None
125 + if has_deep_gemm() and comm_sms > 0:
126 + import deep_gemm as dg
127 + set_compute_sms = lambda sms: dg.set_num_sms(sms)
128 +
129 + return SMControlContextManager(comm_sms=comm_sms,
130 + set_comm_sms=set_comm_sms,
131 + set_compute_sms=set_compute_sms)
132 +
133 def __getattr__(self, key: str):
134 # allow accessing the attributes of the runnable.
135 if hasattr(self.runnable, key):
@@ -282,8 +356,8 @@ class UBatchWrapper:
356 dp_metadata=dp_metadata,
357 batch_descriptor=batch_descriptor,
358 cudagraph_runtime_mode=CUDAGraphMode.NONE)
285 -
286 - return self._capture_ubatches(ubatch_metadata, self.model)
359 + with self.sm_control:
360 + return self._capture_ubatches(ubatch_metadata, self.model)
361 elif num_tokens in self.cudagraphs:
362 cudagraph_metadata = self.cudagraphs[num_tokens]
363 cudagraph_metadata.cudagraph.replay()
@@ -300,4 +374,5 @@ class UBatchWrapper:
374 dp_metadata=dp_metadata,
375 batch_descriptor=batch_descriptor,
376 cudagraph_runtime_mode=CUDAGraphMode.NONE)
303 - return self._run_ubatches(ubatch_metadata, self.model)
377 + with self.sm_control:
378 + return self._run_ubatches(ubatch_metadata, self.model)
vllm/v1/worker/ubatch_splitting.py
+57 -21
@@ -3,9 +3,10 @@
3
4 from typing import Optional
5
6 +import numpy as np
7 import torch
8
8 -from vllm.config import VllmConfig
9 +from vllm.config import ParallelConfig, VllmConfig
10 from vllm.forward_context import DPMetadata
11 from vllm.logger import init_logger
12 from vllm.utils import round_up
@@ -29,6 +30,16 @@ def should_ubatch_with_num_tokens(
30 dp_size, dp_rank)
31
32
33 +def check_ubatch_thresholds(config: ParallelConfig, num_tokens: int,
34 + uniform_decode: bool) -> bool:
35 + if not config.enable_dbo:
36 + return False
37 + if uniform_decode:
38 + return num_tokens >= config.dbo_decode_token_threshold
39 + else:
40 + return num_tokens >= config.dbo_prefill_token_threshold
41 +
42 +
43 def get_dp_padding_ubatch(
44 num_tokens_unpadded: int, num_tokens_padded: int,
45 should_attempt_ubatching: bool,
@@ -95,9 +106,37 @@ def get_dp_padding_ubatch(
106 dtype=torch.int32)
107 return should_ubatch, num_tokens_after_padding
108
109 +def create_ubatch_slices(num_scheduled_tokens: np.ndarray, split_point: int) \
110 + -> UBatchSlices:
111 + # TODO(lucas): Refactor the gpu_model_runner.py so we can pass
112 + # in cu_num_tokens directly (i.e. query_start_loc)
113 + cu_num_tokens = np.zeros(len(num_scheduled_tokens) + 1, dtype=np.int32)
114 + np.cumsum(num_scheduled_tokens, dtype=np.int32, out=cu_num_tokens[1:])
115 +
116 + first_ubatch_token_slice = slice(0, split_point)
117 + second_ubatch_token_slice = slice(split_point, cu_num_tokens[-1])
118 +
119 + # Determine request slices using exclusive stop semantics
120 + # First ubatch includes requests whose tokens overlap [0, split_point)
121 + first_ubatch_req_stop = int(
122 + np.searchsorted(cu_num_tokens, split_point, side="left"))
123 + first_ubatch_req_slice = slice(0, first_ubatch_req_stop)
124 +
125 + # Second ubatch starts at the request that contains the split_point
126 + # or the request starting exactly at split_point (if on boundary)
127 + second_ubatch_req_start = int(
128 + np.searchsorted(cu_num_tokens, split_point, side="right") - 1)
129 + second_ubatch_req_slice = slice(second_ubatch_req_start,
130 + len(cu_num_tokens) - 1)
131 +
132 + return [
133 + UBatchSlice(first_ubatch_req_slice, first_ubatch_token_slice),
134 + UBatchSlice(second_ubatch_req_slice, second_ubatch_token_slice)
135 + ]
136 +
137
138 def ubatch_split(
100 - max_num_scheduled_tokens: int,
139 + num_scheduled_tokens_per_request: np.ndarray,
140 num_tokens_unpadded: int,
141 num_tokens_padded: int,
142 vllm_config: VllmConfig,
@@ -122,17 +161,20 @@ def ubatch_split(
161 return (None, None)
162
163 # Check preconditions for microbatching
125 - should_attempt_ubatching = \
126 - parallel_config.enable_dbo and \
127 - num_tokens_unpadded >= \
128 - parallel_config.dbo_decode_token_threshold \
129 - and max_num_scheduled_tokens == 1
164 + should_attempt_ubatching = check_ubatch_thresholds(
165 + parallel_config,
166 + num_tokens_unpadded,
167 + vllm_config,
168 + )
169
170 # Don't microbatch unless every other DP worker is also microbatching
132 - num_tokens_after_padding = None
133 - (should_ubatch, num_tokens_after_padding) = get_dp_padding_ubatch(
134 - num_tokens_unpadded, num_tokens_padded, should_attempt_ubatching,
135 - vllm_config)
171 + should_ubatch, num_tokens_after_padding = get_dp_padding_ubatch(
172 + num_tokens_unpadded,
173 + num_tokens_padded,
174 + should_attempt_ubatching,
175 + vllm_config,
176 + )
177 +
178 if not should_ubatch:
179 return (None, None)
180
@@ -141,15 +183,9 @@ def ubatch_split(
183 # to the second ubatch in pad_out_ubatch_slice after attention
184 # metadata creation
185 assert num_tokens_after_padding is not None
144 - total_num_tokens_per_ubatch = int(num_tokens_after_padding[0].item())
145 - padded_first_ubatch_slice = slice(0, total_num_tokens_per_ubatch)
146 - padded_second_ubatch_slice = slice(total_num_tokens_per_ubatch,
147 - num_tokens_unpadded)
148 -
149 - # Note there's an assumption here that there's 1 token per request
150 - ubatch_slices = [
151 - UBatchSlice(padded_first_ubatch_slice, padded_first_ubatch_slice),
152 - UBatchSlice(padded_second_ubatch_slice, padded_second_ubatch_slice)
153 - ]
186 + token_split_point = int(num_tokens_after_padding[0].item())
187 +
188 + ubatch_slices = create_ubatch_slices(num_scheduled_tokens_per_request,
189 + token_split_point)
190
191 return (ubatch_slices, num_tokens_after_padding)
vllm/v1/worker/ubatch_utils.py
+8
@@ -10,6 +10,14 @@ class UBatchSlice:
10 request_slice: slice
11 token_slice: slice
12
13 + def is_empty(self) -> bool:
14 + return self.request_slice.start == self.request_slice.stop \
15 + or self.token_slice.start == self.token_slice.stop
16 +
17 + @property
18 + def num_tokens(self) -> int:
19 + return self.token_slice.stop - self.token_slice.start
20 +
21
22 UBatchSlices: TypeAlias = list[UBatchSlice]
23
vllm/v1/worker/ubatching.py
+24 -11
@@ -51,8 +51,8 @@ class UBatchContext:
51 self.cpu_wait_event.wait()
52 self.cpu_wait_event.clear()
53 self._restore_context()
54 - # Assume we start on the compute stream
55 - assert current_stream() == self.compute_stream
54 + # Assume we want to start on the compute stream
55 + self.update_stream(self.compute_stream)
56 return self
57
58 def __exit__(self, exc_type, exc_val, exc_tb):
@@ -62,17 +62,15 @@ class UBatchContext:
62 self.maybe_run_recv_hook()
63 self.cpu_signal_event.set()
64 self.cpu_wait_event.clear()
65 - self.current_stream = self.compute_stream
66 - torch.cuda.set_stream(self.current_stream)
65 return False
66
67 def _restore_context(self):
68 forward_context._forward_context = self.forward_context
71 - torch.cuda.set_stream(self.current_stream)
69
70 def update_stream(self, stream):
71 self.current_stream = stream
75 - torch.cuda.set_stream(self.current_stream)
72 + if current_stream() != self.current_stream:
73 + torch.cuda.set_stream(self.current_stream)
74
75 def _signal_comm_done(self):
76 self.gpu_comm_done_event.record(self.comm_stream)
@@ -99,9 +97,20 @@ class UBatchContext:
97 self.cpu_wait_event.clear()
98 self._restore_context()
99
100 + def switch_to_comm(self):
101 + self.update_stream(self.comm_stream)
102 +
103 + def switch_to_compute(self):
104 + self.update_stream(self.compute_stream)
105 +
106 def switch_to_comm_sync(self):
107 self._signal_compute_done()
108 self.update_stream(self.comm_stream)
109 + self._wait_compute_done()
110 +
111 + def switch_to_compute_sync(self):
112 + self._signal_comm_done()
113 + self.update_stream(self.compute_stream)
114 self._wait_comm_done()
115
116 def maybe_run_recv_hook(self):
@@ -112,8 +121,7 @@ class UBatchContext:
121 def yield_(self):
122 self.current_stream = current_stream()
123 self._cpu_yield()
115 - if self.current_stream != current_stream():
116 - self.update_stream(self.current_stream)
124 + self.update_stream(self.current_stream)
125
126 def yield_and_switch_from_compute_to_comm(self):
127 assert current_stream() == self.compute_stream
@@ -153,15 +161,20 @@ def _register_ubatch_function(func):
161 return wrapper
162
163
164 +dbo_maybe_run_recv_hook = _register_ubatch_function(
165 + UBatchContext.maybe_run_recv_hook)
166 +dbo_yield = _register_ubatch_function(UBatchContext.yield_)
167 dbo_yield_and_switch_from_compute_to_comm = _register_ubatch_function(
168 UBatchContext.yield_and_switch_from_compute_to_comm)
169 dbo_yield_and_switch_from_comm_to_compute = _register_ubatch_function(
170 UBatchContext.yield_and_switch_from_comm_to_compute)
160 -dbo_yield = _register_ubatch_function(UBatchContext.yield_)
161 -dbo_maybe_run_recv_hook = _register_ubatch_function(
162 - UBatchContext.maybe_run_recv_hook)
171 +dbo_switch_to_comm = _register_ubatch_function(UBatchContext.switch_to_comm)
172 +dbo_switch_to_compute = _register_ubatch_function(
173 + UBatchContext.switch_to_compute)
174 dbo_switch_to_comm_sync = _register_ubatch_function(
175 UBatchContext.switch_to_comm_sync)
176 +dbo_switch_to_compute_sync = _register_ubatch_function(
177 + UBatchContext.switch_to_compute_sync)
178
179
180 def dbo_register_recv_hook(recv_hook):
vllm/v1/worker/utils.py
+21 -4
@@ -130,15 +130,32 @@ class MultiModalBudget:
130 @dataclass
131 class AttentionGroup:
132 backend: type[AttentionBackend]
133 + # When ubatching is enabled we will have a metadata builder for each ubatch
134 + # so that if they use internal persistant buffers for cudagraphs, and they
135 + # won't have to worry about conflicting with the other ubatches.
136 metadata_builders: list[AttentionMetadataBuilder]
137 layer_names: list[str]
138 kv_cache_spec: KVCacheSpec
139
140 + @staticmethod
141 + def create_with_metadata_builders(
142 + backend: type[AttentionBackend],
143 + layer_names: list[str],
144 + kv_cache_spec: KVCacheSpec,
145 + vllm_config: VllmConfig,
146 + device: torch.device,
147 + num_metadata_builders: int = 1,
148 + ) -> 'AttentionGroup':
149 + metadata_builders = [
150 + backend.get_builder_cls()(kv_cache_spec, layer_names, vllm_config,
151 + device)
152 + for _ in range(num_metadata_builders)
153 + ]
154 + return AttentionGroup(backend, metadata_builders, layer_names,
155 + kv_cache_spec)
156 +
157 def get_metadata_builder(self,
138 - ubatch_id: Optional[int] = None
139 - ) -> AttentionMetadataBuilder:
140 - if ubatch_id is None:
141 - return self.metadata_builders[0]
158 + ubatch_id: int = 0) -> AttentionMetadataBuilder:
159 assert len(self.metadata_builders) > ubatch_id
160 return self.metadata_builders[ubatch_id]
161