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][1/N] Add Dual-Batch Overlap mechanism to VLLM (#23693)
Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com> Signed-off-by: Sage Moore <sage@neuralmagic.com> Signed-off-by: Lucas Wilkinson <lwilkinson@neuralmagic.com> Signed-off-by: yewentao256 <zhyanwentao@126.com> Co-authored-by: Lucas Wilkinson <lwilkins@redhat.com> Co-authored-by: Lucas Wilkinson <lwilkinson@neuralmagic.com> Co-authored-by: yewentao256 <zhyanwentao@126.com> Co-authored-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com> Co-authored-by: Robert Shaw <114415538+robertgshaw2-redhat@users.noreply.github.com>
Sage Moore committed
Sep 16, 2025 at 09:21 UTC
567939953b7a9cb0ded6bf0bb21a76917b8fed97
22 files changed
+1255
-170
examples/offline_inference/data_parallel.py
+8
@@ -87,6 +87,11 @@ def parse_args():
87
default=0.8,
88
help=("Fraction of GPU memory vLLM is allowed to allocate (0.0, 1.0]."),
89
)
90
+ parser.add_argument(
91
+ "--enable-dbo",
92
+ action="store_true",
93
+ help=("Enable microbatched execution"),
94
+ )
95
parser.add_argument(
96
"--compilation-config",
97
type=int,
@@ -113,6 +118,7 @@ def main(
118
max_model_len,
119
compilation_config,
120
gpu_memory_utilization,
121
+ enable_dbo,
122
quantization,
123
):
124
os.environ["VLLM_DP_RANK"] = str(global_dp_rank)
@@ -167,6 +173,7 @@ def main(
173
max_num_seqs=max_num_seqs,
174
max_model_len=max_model_len,
175
gpu_memory_utilization=gpu_memory_utilization,
176
+ enable_dbo=enable_dbo,
177
quantization=quantization,
178
compilation_config=compilation_config,
179
)
@@ -227,6 +234,7 @@ if __name__ == "__main__":
234
args.max_model_len,
235
args.compilation_config,
236
args.gpu_memory_utilization,
237
+ args.enable_dbo,
238
args.quantization,
239
),
240
)
tests/v1/attention/test_attention_splitting.py
+5
-5
@@ -6,7 +6,7 @@ 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
9
-from vllm.v1.attention.backends.utils import (UbatchSlice,
9
+from vllm.v1.attention.backends.utils import (UBatchSlice,
10
_make_metadata_with_slice,
11
slice_query_start_locs,
12
split_attn_metadata)
@@ -106,7 +106,7 @@ def mixed_small_metadata():
106
def test_make_metadata_with_slice_decode_batch(small_decode_metadata):
107
"""Test slicing decode batch metadata"""
108
# Split first request only
109
- ubatch_slice = UbatchSlice(slice(0, 1), slice(0, 1))
109
+ ubatch_slice = UBatchSlice(slice(0, 1), slice(0, 1))
110
111
result = _make_metadata_with_slice(ubatch_slice, small_decode_metadata)
112
@@ -120,7 +120,7 @@ def test_make_metadata_with_slice_decode_batch(small_decode_metadata):
120
121
def test_make_metadata_with_slice_mixed_batch(mixed_small_metadata):
122
"""Test slicing mixed batch metadata"""
123
- ubatch_slice = UbatchSlice(slice(1, 3),
123
+ ubatch_slice = UBatchSlice(slice(1, 3),
124
slice(1, 7)) # Requests 1-3, tokens 1-7
125
126
result = _make_metadata_with_slice(ubatch_slice, mixed_small_metadata)
@@ -137,8 +137,8 @@ def test_split_attn_metadata_decode_batch(large_decode_metadata):
137
num_tokens = large_decode_metadata.num_reqs
138
mid_point = num_tokens // 2
139
ubatch_slices = [
140
- UbatchSlice(slice(0, mid_point), slice(0, mid_point)),
141
- UbatchSlice(slice(mid_point, num_tokens), slice(mid_point,
140
+ UBatchSlice(slice(0, mid_point), slice(0, mid_point)),
141
+ UBatchSlice(slice(mid_point, num_tokens), slice(mid_point,
142
num_tokens)),
143
]
144
tests/v1/spec_decode/test_eagle.py
+6
-2
@@ -365,7 +365,9 @@ def test_propose(method, attn_backend, num_speculative_tokens, monkeypatch):
365
# Mock runner for attention metadata building
366
proposer.runner = mock.MagicMock()
367
proposer.runner.attn_groups.append([mock.MagicMock()])
368
- proposer.runner.attn_groups[0][0].metadata_builder = attn_metadata_builder
368
+ proposer.runner.attn_groups[0][0].metadata_builders = [
369
+ attn_metadata_builder
370
+ ]
371
372
result = proposer.propose(target_token_ids=target_token_ids,
373
target_positions=target_positions,
@@ -489,7 +491,9 @@ def test_propose_tree(spec_token_tree):
491
# Mock runner for attention metadata building.
492
proposer.runner = mock.MagicMock()
493
proposer.runner.attn_groups.append([mock.MagicMock()])
492
- proposer.runner.attn_groups[0][0].metadata_builder = attn_metadata_builder
494
+ proposer.runner.attn_groups[0][0].metadata_builders = [
495
+ attn_metadata_builder
496
+ ]
497
498
# Setup inputs for the proposer.
499
target_token_ids = torch.randint(0,
vllm/config/__init__.py
+8
@@ -2848,6 +2848,14 @@ class VllmConfig:
2848
"when cudagraph_mode piecewise cudagraphs is used, "\
2849
f"cudagraph_mode={self.compilation_config.cudagraph_mode}"
2850
2851
+ if self.parallel_config.enable_dbo:
2852
+ a2a_backend = envs.VLLM_ALL2ALL_BACKEND
2853
+ assert a2a_backend == "deepep_low_latency", \
2854
+ "Microbatching currently only supports the deepep_low_latency "\
2855
+ f"all2all backend. {a2a_backend} is not supported. To fix set "\
2856
+ "the VLLM_ALL2ALL_BACKEND environment variable to "\
2857
+ "deepep_low_latency and install the DeepEP kerenls."
2858
+
2859
if not self.instance_id:
2860
self.instance_id = random_uuid()[:5]
2861
vllm/config/parallel.py
+8
@@ -137,6 +137,14 @@ class ParallelConfig:
137
disable_custom_all_reduce: bool = False
138
"""Disable the custom all-reduce kernel and fall back to NCCL."""
139
140
+ enable_dbo: bool = False
141
+ """Enable microbatching for the model executor."""
142
+
143
+ dbo_decode_token_threshold: int = 32
144
+ """The threshold for microbatching. If the number of tokens in the
145
+ request is greater than this threshold, microbatching will be used.
146
+ Otherwise, the request will be processed in a single batch."""
147
+
148
ray_workers_use_nsight: bool = False
149
"""Whether to profile Ray workers with nsight, see https://docs.ray.io/en/latest/ray-observability/user-guides/profiling.html#profiling-nsight-profiler."""
150
vllm/distributed/device_communicators/all2all.py
-5
@@ -251,9 +251,4 @@ class DeepEPLLAll2AllManager(DeepEPAll2AllManagerBase):
251
logger.debug("DeepEP all2all args %s", buffer_kwargs)
252
handle: deep_ep.Buffer = self.handle_cache.get_or_create(
253
buffer_kwargs, deep_ep.Buffer)
254
- # It is dangerous to set num sms outside this function. num_sms is not
255
- # a part of the hash-key that identifies this object. If we are in a
256
- # situation where we make objects with different num_sms, the hash key
257
- # in get_or_create must be updated.
258
- handle.set_num_sms(self.num_sms)
254
return handle
vllm/engine/arg_utils.py
+10
@@ -327,6 +327,9 @@ class EngineArgs:
327
data_parallel_hybrid_lb: bool = False
328
data_parallel_backend: str = ParallelConfig.data_parallel_backend
329
enable_expert_parallel: bool = ParallelConfig.enable_expert_parallel
330
+ enable_dbo: bool = ParallelConfig.enable_dbo
331
+ dbo_decode_token_threshold: int = \
332
+ ParallelConfig.dbo_decode_token_threshold
333
eplb_config: EPLBConfig = get_field(ParallelConfig, "eplb_config")
334
enable_eplb: bool = ParallelConfig.enable_eplb
335
expert_placement_strategy: ExpertPlacementStrategy = \
@@ -695,6 +698,11 @@ class EngineArgs:
698
parallel_group.add_argument(
699
"--enable-expert-parallel",
700
**parallel_kwargs["enable_expert_parallel"])
701
+ parallel_group.add_argument("--enable-dbo",
702
+ **parallel_kwargs["enable_dbo"])
703
+ parallel_group.add_argument(
704
+ "--dbo-decode-token-threshold",
705
+ **parallel_kwargs["dbo_decode_token_threshold"])
706
parallel_group.add_argument("--enable-eplb",
707
**parallel_kwargs["enable_eplb"])
708
parallel_group.add_argument("--eplb-config",
@@ -1339,6 +1347,8 @@ class EngineArgs:
1347
data_parallel_backend=self.data_parallel_backend,
1348
data_parallel_hybrid_lb=self.data_parallel_hybrid_lb,
1349
enable_expert_parallel=self.enable_expert_parallel,
1350
+ enable_dbo=self.enable_dbo,
1351
+ dbo_decode_token_threshold=self.dbo_decode_token_threshold,
1352
enable_eplb=self.enable_eplb,
1353
eplb_config=self.eplb_config,
1354
expert_placement_strategy=self.expert_placement_strategy,
vllm/forward_context.py
+101
-20
@@ -14,6 +14,7 @@ import vllm.envs as envs
14
from vllm.config import CUDAGraphMode, ParallelConfig, VllmConfig
15
from vllm.logger import init_logger
16
from vllm.platforms import current_platform
17
+from vllm.v1.worker.ubatch_utils import UBatchSlices, is_second_ubatch_empty
18
19
if TYPE_CHECKING:
20
from vllm.attention.backends.abstract import AttentionMetadata
@@ -97,6 +98,53 @@ class DPMetadata:
98
dist.all_reduce(num_tokens_tensor, group=group)
99
return num_tokens_tensor.cpu()
100
101
+ @staticmethod
102
+ def should_ubatch_across_dp(
103
+ should_ubatch: bool, orig_num_tokens_per_ubatch: int,
104
+ padded_num_tokens_per_ubatch: int, dp_size: int,
105
+ dp_rank: int) -> tuple[bool, Optional[torch.Tensor]]:
106
+ """
107
+ 1. Decides if each DP rank is going to microbatch. Either all ranks
108
+ run with microbatching or none of them do. If this function decides
109
+ not to run with microbatching. It will "abort" meaning that no padding
110
+ information will be returned to the caller. It will return (False, None)
111
+
112
+ 2. Determines the total number of tokens that each rank will run.
113
+ All ranks will be padded out so that the run with the same number
114
+ of tokens
115
+
116
+ Returns: tuple[
117
+ should_ubatch: Are all DP ranks going to microbatch
118
+ num_tokens_after_padding: A tensor containing the total number of
119
+ tokens per-microbatch for each DP rank including padding. Will be
120
+ None if should_ubatch if False
121
+ ]
122
+ """
123
+
124
+ device = current_platform.device_type
125
+ tensor = torch.zeros(3, dp_size, device=device, dtype=torch.int32)
126
+ tensor[0][dp_rank] = orig_num_tokens_per_ubatch
127
+ tensor[1][dp_rank] = padded_num_tokens_per_ubatch
128
+ tensor[2][dp_rank] = 1 if should_ubatch else 0
129
+
130
+ from vllm.distributed.parallel_state import get_dp_group
131
+ dist.all_reduce(tensor, group=get_dp_group().device_group)
132
+
133
+ result: bool = bool(torch.all(tensor[2] == 1).item())
134
+ if not result:
135
+ return result, None
136
+
137
+ orig_num_tokens_tensor = tensor[0, :]
138
+ padded_num_tokens_tensor = tensor[1, :]
139
+
140
+ orig_min_num_tokens = int(orig_num_tokens_tensor.min().item())
141
+ padded_max_num_tokens = int(padded_num_tokens_tensor.max().item())
142
+ if is_second_ubatch_empty(orig_min_num_tokens, padded_max_num_tokens):
143
+ logger.debug("Aborting ubatching %s %s", orig_min_num_tokens,
144
+ padded_max_num_tokens)
145
+ return False, None
146
+ return result, padded_num_tokens_tensor.cpu()
147
+
148
@staticmethod
149
def make(
150
parallel_config: ParallelConfig,
@@ -119,14 +167,15 @@ class DPMetadata:
167
168
# If num_tokens_across_dp is None, it will be computed by all_reduce
169
# Otherwise, num_tokens_across_dp[dp_rank] should be equal to batchsize
122
- assert (num_tokens_across_dp is None
123
- or num_tokens_across_dp[dp_rank] == batchsize)
170
+ assert (num_tokens_across_dp is None or num_tokens_across_dp[dp_rank]
171
+ == batchsize), f"{num_tokens_across_dp[dp_rank]} {batchsize}"
172
if num_tokens_across_dp is None:
173
num_tokens_across_dp = DPMetadata.num_tokens_across_dp(
174
batchsize, dp_size, dp_rank)
175
max_tokens_across_dp_cpu = torch.max(num_tokens_across_dp)
176
cu_tokens_across_dp_cpu = torch.cumsum(num_tokens_across_dp, dim=0)
129
- return DPMetadata(max_tokens_across_dp_cpu, cu_tokens_across_dp_cpu)
177
+ return DPMetadata(max_tokens_across_dp_cpu, cu_tokens_across_dp_cpu,
178
+ num_tokens_across_dp)
179
180
@contextmanager
181
def chunked_sizes(self, max_chunk_size_per_rank: int, chunk_idx: int):
@@ -179,9 +228,12 @@ class ForwardContext:
228
Type AttentionMetadata for v0,
229
Type Dict[str, AttentionMetadata] for v1, map from layer_name of each
230
attention layer to its attention metadata
182
- set dynamically for each forward pass
231
+ Type List[Dict[str, AttentionMetadata]] for DBO. List of size two, one
232
+ for each microbatch.
233
+ Set dynamically for each forward pass
234
"""
184
- attn_metadata: Union["AttentionMetadata", dict[str, "AttentionMetadata"]]
235
+ attn_metadata: Union["AttentionMetadata", dict[str, "AttentionMetadata"],
236
+ list[dict[str, "AttentionMetadata"]]]
237
# TODO: remove after making all virtual_engines share the same kv cache
238
virtual_engine: int # set dynamically for each forward pass
239
# set dynamically for each forward pass
@@ -191,6 +243,8 @@ class ForwardContext:
243
cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE
244
batch_descriptor: Optional[BatchDescriptor] = None
245
246
+ ubatch_slices: Optional[UBatchSlices] = None
247
+
248
def __post_init__(self):
249
assert self.cudagraph_runtime_mode in [
250
CUDAGraphMode.NONE, CUDAGraphMode.PIECEWISE, CUDAGraphMode.FULL], \
@@ -208,6 +262,39 @@ def get_forward_context() -> ForwardContext:
262
return _forward_context
263
264
265
+def create_forward_context(
266
+ attn_metadata: Any,
267
+ vllm_config: VllmConfig,
268
+ virtual_engine: int = 0,
269
+ dp_metadata: Optional[DPMetadata] = None,
270
+ cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
271
+ batch_descriptor: Optional[BatchDescriptor] = None,
272
+ ubatch_slices: Optional[UBatchSlices] = None):
273
+ return ForwardContext(no_compile_layers=vllm_config.compilation_config.
274
+ static_forward_context,
275
+ virtual_engine=virtual_engine,
276
+ attn_metadata=attn_metadata,
277
+ dp_metadata=dp_metadata,
278
+ cudagraph_runtime_mode=cudagraph_runtime_mode,
279
+ batch_descriptor=batch_descriptor,
280
+ ubatch_slices=ubatch_slices)
281
+
282
+
283
+@contextmanager
284
+def override_forward_context(forward_context: Optional[ForwardContext]):
285
+ """A context manager that overrides the current forward context.
286
+ This is used to override the forward context for a specific
287
+ forward pass.
288
+ """
289
+ global _forward_context
290
+ prev_context = _forward_context
291
+ _forward_context = forward_context
292
+ try:
293
+ yield
294
+ finally:
295
+ _forward_context = prev_context
296
+
297
+
298
@contextmanager
299
def set_forward_context(
300
attn_metadata: Any,
@@ -216,7 +303,8 @@ def set_forward_context(
303
num_tokens: Optional[int] = None,
304
num_tokens_across_dp: Optional[torch.Tensor] = None,
305
cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
219
- batch_descriptor: Optional[BatchDescriptor] = None):
306
+ batch_descriptor: Optional[BatchDescriptor] = None,
307
+ ubatch_slices: Optional[UBatchSlices] = None):
308
"""A context manager that stores the current forward context,
309
can be attention metadata, etc.
310
Here we can inject common logic for every model forward pass.
@@ -225,6 +313,7 @@ def set_forward_context(
313
need_to_track_batchsize = track_batchsize and attn_metadata is not None
314
if need_to_track_batchsize:
315
forward_start_time = time.perf_counter()
316
+
317
dp_metadata: Optional[DPMetadata] = None
318
if vllm_config.parallel_config.data_parallel_size > 1 and (
319
attn_metadata is not None or num_tokens is not None):
@@ -232,20 +321,14 @@ def set_forward_context(
321
attn_metadata, num_tokens or 0,
322
num_tokens_across_dp)
323
235
- global _forward_context
236
- prev_context = _forward_context
237
- _forward_context = ForwardContext(
238
- no_compile_layers=vllm_config.compilation_config.
239
- static_forward_context,
240
- virtual_engine=virtual_engine,
241
- attn_metadata=attn_metadata,
242
- dp_metadata=dp_metadata,
243
- cudagraph_runtime_mode=cudagraph_runtime_mode,
244
- batch_descriptor=batch_descriptor,
245
- )
324
+ forward_context = create_forward_context(attn_metadata, vllm_config,
325
+ virtual_engine, dp_metadata,
326
+ cudagraph_runtime_mode,
327
+ batch_descriptor, ubatch_slices)
328
329
try:
248
- yield
330
+ with override_forward_context(forward_context):
331
+ yield
332
finally:
333
global last_logging_time, batchsize_logging_interval
334
if need_to_track_batchsize:
@@ -282,5 +365,3 @@ def set_forward_context(
365
logger.info(("Batchsize forward time stats "
366
"(batchsize, count, median_time(ms)): %s"),
367
forward_stats)
285
-
286
- _forward_context = prev_context
vllm/model_executor/layers/fused_moe/deepep_ht_prepare_finalize.py
+14
-12
@@ -191,7 +191,7 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
191
expert_map: Optional[torch.Tensor],
192
apply_router_weight_on_input: bool,
193
quant_config: FusedMoEQuantConfig,
194
- ) -> Callable:
194
+ ) -> tuple[Callable, mk.ReceiverType]:
195
196
if apply_router_weight_on_input:
197
topk = topk_ids.size(1)
@@ -217,13 +217,14 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
217
a1q_scale = None
218
a1_post_scale = a1_scale
219
220
- return self._do_dispatch(tokens=a1q,
221
- token_scales=a1q_scale,
222
- rank_topk_ids=topk_ids,
223
- rank_topk_weights=topk_weights,
224
- num_experts=num_experts,
225
- a1_scale=a1_post_scale,
226
- quant_config=quant_config)
220
+ return (lambda *args: None,
221
+ self._do_dispatch(tokens=a1q,
222
+ token_scales=a1q_scale,
223
+ rank_topk_ids=topk_ids,
224
+ rank_topk_weights=topk_weights,
225
+ num_experts=num_experts,
226
+ a1_scale=a1_post_scale,
227
+ quant_config=quant_config))
228
229
def prepare(
230
self,
@@ -237,10 +238,11 @@ class DeepEPHTPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
238
apply_router_weight_on_input: bool,
239
quant_config: FusedMoEQuantConfig,
240
) -> mk.PrepareResultType:
240
- receiver = self.prepare_async(a1, a1_scale, a2_scale, topk_weights,
241
- topk_ids, num_experts, expert_map,
242
- apply_router_weight_on_input,
243
- quant_config)
241
+ (_, receiver) = self.prepare_async(a1, a1_scale, a2_scale,
242
+ topk_weights, topk_ids, num_experts,
243
+ expert_map,
244
+ apply_router_weight_on_input,
245
+ quant_config)
246
return receiver()
247
248
def finalize(
vllm/model_executor/layers/fused_moe/deepep_ll_prepare_finalize.py
+29
-16
@@ -11,6 +11,9 @@ from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
11
TopKWeightAndReduceDelegate)
12
from vllm.model_executor.layers.fused_moe.utils import (
13
moe_kernel_quantize_input, normalize_batched_scales_shape)
14
+from vllm.v1.worker.ubatching import (dbo_current_ubatch_id, dbo_enabled,
15
+ dbo_maybe_run_recv_hook,
16
+ dbo_register_recv_hook, dbo_yield)
17
18
# DeepEP kernels quantize dispatch inputs in 128 element chunks.
19
DEEPEP_QUANT_BLOCK_SIZE = 128
@@ -55,7 +58,7 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
58
# The dispatch function returns a handle that the combine function
59
# requires. We store the handle here so it is available to the
60
# combine function.
58
- self.handle = None
61
+ self.handles: list[Optional[tuple]] = [None, None]
62
self.num_dispatchers_ = num_dispatchers
63
64
def num_dispatchers(self) -> int:
@@ -123,13 +126,15 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
126
expert_map: Optional[torch.Tensor],
127
apply_router_weight_on_input: bool,
128
quant_config: FusedMoEQuantConfig,
126
- ) -> mk.ReceiverType:
129
+ ) -> tuple[Callable, mk.ReceiverType]:
130
131
hidden_size = a1.size(1)
132
assert hidden_size in self.SUPPORTED_HIDDEN_SIZES, \
133
(f"Hidden Size {hidden_size} not in supported list of hidden sizes"
134
f"{self.SUPPORTED_HIDDEN_SIZES}")
135
136
+ a2a_idx = dbo_current_ubatch_id()
137
+
138
if self.use_fp8_dispatch:
139
assert hidden_size % 128 == 0, \
140
"DeepEP kernels quantize the inputs in blocks of shape 128"
@@ -148,7 +153,7 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
153
a1 = a1 * topk_weights.to(a1.dtype)
154
155
# Dispatch
151
- expert_x, expert_num_tokens, self.handle, event, hook = \
156
+ expert_x, expert_num_tokens, handle, _, hook= \
157
self.buffer.low_latency_dispatch(a1,
158
topk_ids,
159
self.max_tokens_per_rank,
@@ -156,21 +161,19 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
161
use_fp8=self.use_fp8_dispatch,
162
async_finish=False,
163
return_recv_hook=True)
164
+ self.handles[a2a_idx] = handle
165
160
- return lambda: self._receiver(hook, expert_x, expert_num_tokens,
161
- a1_scale, a1.dtype, quant_config)
166
+ return (hook, lambda: self._receiver(expert_x, expert_num_tokens,
167
+ a1_scale, a1.dtype, quant_config))
168
169
def _receiver(
170
self,
165
- hook: Callable,
171
expert_x: Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]],
172
expert_num_tokens: torch.Tensor,
173
a1_scale,
174
a1_dtype,
175
quant_config: FusedMoEQuantConfig,
176
) -> mk.PrepareResultType:
172
- hook()
173
-
177
expert_x, expert_x_scale = self._do_quant(
178
expert_x, a1_scale, a1_dtype, quant_config.quant_dtype,
179
quant_config.per_act_token_quant, quant_config.block_shape)
@@ -192,10 +195,12 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
195
apply_router_weight_on_input: bool,
196
quant_config: FusedMoEQuantConfig,
197
) -> mk.PrepareResultType:
195
- receiver = self.prepare_async(a1, a1_scale, a2_scale, topk_weights,
196
- topk_ids, num_experts, expert_map,
197
- apply_router_weight_on_input,
198
- quant_config)
198
+ hook, receiver = self.prepare_async(a1, a1_scale, a2_scale,
199
+ topk_weights, topk_ids,
200
+ num_experts, expert_map,
201
+ apply_router_weight_on_input,
202
+ quant_config)
203
+ hook()
204
return receiver()
205
206
def finalize(
@@ -210,7 +215,11 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
215
assert isinstance(
216
weight_and_reduce_impl, TopKWeightAndReduceDelegate
217
), ("Weight application and reduction happens in the combine kernel.")
213
- assert self.handle is not None
218
+
219
+ a2a_idx = dbo_current_ubatch_id()
220
+ do_recv_hook = dbo_enabled()
221
+ handle = self.handles[a2a_idx]
222
+ assert handle is not None
223
224
combine_topk_weights = topk_weights
225
if apply_router_weight_on_input:
@@ -218,12 +227,16 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
227
combine_topk_weights = torch.ones_like(topk_weights)
228
229
# TODO (varun) : Enable zero copy mode
221
- _, event, hook = self.buffer.low_latency_combine(
230
+ dbo_maybe_run_recv_hook()
231
+ _, _, recv_hook = self.buffer.low_latency_combine(
232
fused_expert_output,
233
topk_ids,
234
combine_topk_weights,
225
- self.handle,
235
+ handle,
236
async_finish=False,
237
zero_copy=False,
228
- return_recv_hook=False,
238
+ return_recv_hook=do_recv_hook,
239
out=output)
240
+ if recv_hook is not None:
241
+ dbo_register_recv_hook(recv_hook)
242
+ dbo_yield()
vllm/model_executor/layers/fused_moe/layer.py
+43
-15
@@ -38,6 +38,7 @@ from vllm.platforms import current_platform
38
from vllm.platforms.interface import CpuArchEnum
39
from vllm.utils import (cdiv, direct_register_custom_op, has_deep_ep, has_pplx,
40
round_up)
41
+from vllm.v1.worker.ubatching import dbo_current_ubatch_id
42
43
if current_platform.is_cuda_alike():
44
from .fused_batched_moe import BatchedTritonExperts
@@ -992,16 +993,28 @@ class FusedMoE(CustomOp):
993
if (self.moe_parallel_config.use_pplx_kernels
994
or self.moe_parallel_config.use_deepep_ll_kernels
995
or self.moe_config.use_flashinfer_cutlass_kernels):
995
- self.batched_hidden_states = torch.zeros(
996
- (moe.max_num_tokens, self.hidden_size),
997
- dtype=moe.in_dtype,
998
- device=torch.cuda.current_device())
996
+ if vllm_config.parallel_config.enable_dbo:
997
+ self.batched_hidden_states = torch.zeros(
998
+ (2, moe.max_num_tokens, self.hidden_size),
999
+ dtype=moe.in_dtype,
1000
+ device=torch.cuda.current_device())
1001
+
1002
+ # Note here we use `num_experts` which is logical expert count
1003
+ self.batched_router_logits = torch.zeros(
1004
+ (2, moe.max_num_tokens, num_experts),
1005
+ dtype=moe.in_dtype,
1006
+ device=torch.cuda.current_device())
1007
+ else:
1008
+ self.batched_hidden_states = torch.zeros(
1009
+ (moe.max_num_tokens, self.hidden_size),
1010
+ dtype=moe.in_dtype,
1011
+ device=torch.cuda.current_device())
1012
1000
- # Note here we use `num_experts` which is logical expert count
1001
- self.batched_router_logits = torch.zeros(
1002
- (moe.max_num_tokens, num_experts),
1003
- dtype=moe.in_dtype,
1004
- device=torch.cuda.current_device())
1013
+ # Note here we use `num_experts` which is logical expert count
1014
+ self.batched_router_logits = torch.zeros(
1015
+ (moe.max_num_tokens, num_experts),
1016
+ dtype=moe.in_dtype,
1017
+ device=torch.cuda.current_device())
1018
1019
@property
1020
def shared_experts(self) -> Optional[torch.nn.Module]:
@@ -1708,14 +1721,29 @@ class FusedMoE(CustomOp):
1721
hidden_states = full_hidden_states[chunk_start:chunk_end, :]
1722
router_logits = full_router_logits[chunk_start:chunk_end, :]
1723
1711
- assert (self.batched_hidden_states.size(0) # type: ignore
1724
+ assert self.batched_hidden_states is not None
1725
+ assert self.batched_router_logits is not None
1726
+ # This is only true when DBO has been enabled in the config.
1727
+ # Both tensors will have an outer dimension for the ubatch id
1728
+ if self.batched_hidden_states.dim() == 3:
1729
+ assert self.batched_router_logits.dim() == 3
1730
+ batch_buffer_idx = dbo_current_ubatch_id()
1731
+ batched_hidden_states = self.batched_hidden_states[
1732
+ batch_buffer_idx, :]
1733
+ batched_router_logits = self.batched_router_logits[
1734
+ batch_buffer_idx, :]
1735
+ else:
1736
+ batched_hidden_states = self.batched_hidden_states
1737
+ batched_router_logits = self.batched_router_logits
1738
+
1739
+ assert (batched_hidden_states.size(0) # type: ignore
1740
>= chunk_size)
1713
- assert (self.batched_router_logits.size(0) # type: ignore
1741
+ assert (batched_router_logits.size(0) # type: ignore
1742
>= chunk_size)
1715
- staged_hidden_states = self.batched_hidden_states[:
1716
- chunk_size, :] # type: ignore
1717
- staged_router_logits = self.batched_router_logits[:
1718
- chunk_size, :] # type: ignore
1743
+ staged_hidden_states = batched_hidden_states[:
1744
+ chunk_size, :] # type: ignore
1745
+ staged_router_logits = batched_router_logits[:
1746
+ chunk_size, :] # type: ignore
1747
staged_hidden_states.copy_(hidden_states, non_blocking=True)
1748
staged_router_logits.copy_(router_logits, non_blocking=True)
1749
vllm/model_executor/layers/fused_moe/modular_kernel.py
+48
-15
@@ -13,6 +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,
17
+ dbo_register_recv_hook, dbo_yield)
18
19
#
20
# This file defines a set of base classes used to make MoE kernels more modular.
@@ -226,7 +228,7 @@ class FusedMoEPrepareAndFinalize(ABC):
228
expert_map: Optional[torch.Tensor],
229
apply_router_weight_on_input: bool,
230
quant_config: FusedMoEQuantConfig,
229
- ) -> ReceiverType:
231
+ ) -> tuple[Callable, ReceiverType]:
232
"""
233
Perform any quantization (and/or) dispatching needed for this kernel
234
but do not wait for results from other workers.
@@ -496,6 +498,23 @@ def _chunk_scales(scales: Optional[torch.Tensor], start: int,
498
return None
499
500
501
+class SharedResizableBuffer:
502
+
503
+ def __init__(self):
504
+ self.buffer = None
505
+
506
+ def get(self, shape: tuple[int, ...], device: torch.device,
507
+ dtype: torch.dtype):
508
+ shape_numel = prod(shape)
509
+ if self.buffer is None or self.buffer.numel() < shape_numel:
510
+ self.buffer = torch.empty(shape_numel, device=device, dtype=dtype)
511
+ assert self.buffer.device == device, \
512
+ f"Buffer device mismatch: {self.buffer.device} != {device}"
513
+ assert self.buffer.dtype == dtype, \
514
+ f"Buffer dtype mismatch: {self.buffer.dtype} != {dtype}"
515
+ return self.buffer[:shape_numel].view(*shape)
516
+
517
+
518
@final
519
class FusedMoEModularKernel(torch.nn.Module):
520
"""
@@ -509,6 +528,9 @@ class FusedMoEModularKernel(torch.nn.Module):
528
layer due to any layer specific state that may be used by the component
529
objects.
530
"""
531
+ fused_out_buffer = SharedResizableBuffer()
532
+ workspace13_buffer = SharedResizableBuffer()
533
+ workspace2_buffer = SharedResizableBuffer()
534
535
def __init__(
536
self,
@@ -559,12 +581,12 @@ class FusedMoEModularKernel(torch.nn.Module):
581
582
# We can reuse the memory between cache1 and cache3 because by the
583
# time we need cache3, we're done with cache1.
562
- workspace13 = torch.empty(prod(workspace13_shape),
563
- device=a1.device,
564
- dtype=workspace_dtype)
565
- workspace2 = torch.empty(prod(workspace2_shape),
566
- device=a1.device,
567
- dtype=workspace_dtype)
584
+ workspace13 = self.workspace13_buffer.get(workspace13_shape,
585
+ device=a1.device,
586
+ dtype=workspace_dtype)
587
+ workspace2 = self.workspace2_buffer.get(workspace2_shape,
588
+ device=a1.device,
589
+ dtype=workspace_dtype)
590
591
assert fused_out is None or fused_out.shape == fused_out_shape, (
592
f"fused_out {fused_out.shape} but expected {fused_out_shape}")
@@ -656,9 +678,9 @@ class FusedMoEModularKernel(torch.nn.Module):
678
(_, _, fused_out_shape, _) = self.fused_experts.workspace_shapes(
679
a1, a1q, M, N, K, top_k, global_num_experts, local_num_experts,
680
expert_tokens_meta)
659
- fused_out = torch.empty(fused_out_shape,
660
- device=a1q.device,
661
- dtype=a1.dtype)
681
+ fused_out = self.fused_out_buffer.get(fused_out_shape,
682
+ device=a1q.device,
683
+ dtype=a1.dtype)
684
685
def slice_input_tensors(
686
chunk_idx: int
@@ -801,8 +823,10 @@ class FusedMoEModularKernel(torch.nn.Module):
823
824
shared_output: torch.Tensor
825
804
- if (not self.prepare_finalize.supports_async()
805
- or self.shared_experts is None):
826
+ if not self.prepare_finalize.supports_async():
827
+ # We shouldn't be running an a2a kernel that doesn't
828
+ # support async prepare/finalize
829
+ assert not dbo_enabled()
830
831
# Run shared experts serially with dispatch.
832
if self.shared_experts is not None:
@@ -822,7 +846,8 @@ class FusedMoEModularKernel(torch.nn.Module):
846
)
847
else:
848
# Overlap shared expert compute with all2all dispatch.
825
- receiver = self.prepare_finalize.prepare_async(
849
+ dbo_maybe_run_recv_hook()
850
+ hook, receiver = self.prepare_finalize.prepare_async(
851
a1,
852
a1_scale,
853
a2_scale,
@@ -834,8 +859,16 @@ class FusedMoEModularKernel(torch.nn.Module):
859
self.fused_experts.quant_config,
860
)
861
837
- assert self.shared_experts is not None
838
- shared_output = self.shared_experts(a1)
862
+ if self.shared_experts is not None:
863
+ shared_output = self.shared_experts(a1)
864
+
865
+ # If DBO is being used, register the hook with the ubatch context
866
+ # and call it in dbo_maybe_run_recv_hook instead of passing it to
867
+ # the receiver.
868
+ dbo_register_recv_hook(hook)
869
+ dbo_yield()
870
+ if not dbo_enabled():
871
+ hook()
872
873
(a1q, a1q_scale, expert_tokens_meta, _expert_topk_ids,
874
_expert_topk_weights) = receiver()
vllm/model_executor/layers/fused_moe/pplx_prepare_finalize.py
+18
-25
@@ -1,6 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-from typing import Optional, Union
3
+from typing import Callable, Optional, Union
4
5
import pplx_kernels as pplx
6
import torch
@@ -103,7 +103,7 @@ class PplxPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
103
expert_map: Optional[torch.Tensor],
104
apply_router_weight_on_input: bool,
105
quant_config: FusedMoEQuantConfig,
106
- ) -> mk.ReceiverType:
106
+ ) -> tuple[Callable, mk.ReceiverType]:
107
num_tokens = a1.size(0) # M
108
hidden_dim = a1.size(-1) # K
109
@@ -214,41 +214,33 @@ class PplxPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
214
do_recv=False,
215
)
216
217
- return lambda: self._receiver(
217
+ hook = lambda: self.a2a.dispatch(
218
+ out_expert_num_tokens=expert_num_tokens,
219
+ out_expert_x=expert_x,
220
+ out_expert_x_scale=expert_x_scale,
221
+ dp_x=a1q,
222
+ dp_x_scale=a1q_scale,
223
+ indices=topk_ids,
224
+ bound_m=bound_m,
225
+ do_send=False,
226
+ do_recv=True,
227
+ )
228
+
229
+ return (hook, lambda: self._receiver(
230
expert_num_tokens,
231
expert_x,
232
expert_x_scale,
221
- a1q,
222
- a1q_scale,
223
- topk_ids,
224
- bound_m,
233
orig_a_scale_block_shape,
226
- )
234
+ ))
235
236
def _receiver(
237
self,
238
expert_num_tokens: torch.Tensor,
239
expert_x: torch.Tensor,
240
expert_x_scale: Optional[torch.Tensor],
233
- a1q: torch.Tensor,
234
- a1q_scale: Optional[torch.Tensor],
235
- topk_ids: torch.Tensor,
236
- bound_m: Optional[torch.Tensor],
241
orig_a_scale_block_shape: Optional[int],
242
) -> mk.PrepareResultType:
243
240
- self.a2a.dispatch(
241
- out_expert_num_tokens=expert_num_tokens,
242
- out_expert_x=expert_x,
243
- out_expert_x_scale=expert_x_scale,
244
- dp_x=a1q,
245
- dp_x_scale=a1q_scale,
246
- indices=topk_ids,
247
- bound_m=bound_m,
248
- do_send=False,
249
- do_recv=True,
250
- )
251
-
244
if expert_x_scale is not None:
245
expert_x_scale = expert_x_scale[:, :, :orig_a_scale_block_shape]
246
assert expert_x_scale.ndim == 3
@@ -270,7 +262,7 @@ class PplxPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
262
apply_router_weight_on_input: bool,
263
quant_config: FusedMoEQuantConfig,
264
) -> mk.PrepareResultType:
273
- receiver = self.prepare_async(
265
+ hook, receiver = self.prepare_async(
266
a1,
267
a1_scale,
268
a2_scale,
@@ -281,6 +273,7 @@ class PplxPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
273
apply_router_weight_on_input,
274
quant_config,
275
)
276
+ hook()
277
return receiver()
278
279
def finalize(
vllm/v1/attention/backends/utils.py
+9
-9
@@ -28,6 +28,7 @@ from vllm.distributed.kv_transfer.kv_connector.utils import (
28
get_kv_connector_cache_layout)
29
from vllm.logger import init_logger
30
from vllm.v1.kv_cache_interface import AttentionSpec
31
+from vllm.v1.worker.ubatch_utils import UBatchSlice
32
33
logger = init_logger(__name__)
34
KVCacheLayoutType = Literal["NHD", "HND"]
@@ -81,12 +82,6 @@ class CommonAttentionMetadata:
82
encoder_seq_lens: Optional[np.ndarray] = None
83
84
84
-@dataclass
85
-class UbatchSlice:
86
- request_slice: slice
87
- token_slice: slice
88
-
89
-
85
def slice_query_start_locs(
86
query_start_loc: torch.Tensor,
87
request_slice: slice,
@@ -103,7 +98,7 @@ def slice_query_start_locs(
98
99
100
def _make_metadata_with_slice(
106
- ubatch_slice: UbatchSlice,
101
+ ubatch_slice: UBatchSlice,
102
attn_metadata: CommonAttentionMetadata) -> CommonAttentionMetadata:
103
"""
104
This function creates a new CommonAttentionMetadata that corresponds to
@@ -133,6 +128,11 @@ def _make_metadata_with_slice(
128
torch.max(torch.abs(query_start_loc_cpu[1:] -
129
query_start_loc_cpu[:-1])).item())
130
131
+ # This is to account for the case where we are in a dummy
132
+ # run and query_start_loc_cpu is full of 0s
133
+ if max_query_len == 0:
134
+ max_query_len = attn_metadata.max_query_len
135
+
136
block_table_tensor = attn_metadata.block_table_tensor[request_slice]
137
slot_mapping = attn_metadata.slot_mapping[token_slice]
138
@@ -152,12 +152,12 @@ def _make_metadata_with_slice(
152
153
154
def split_attn_metadata(
155
- ubatch_slices: list[UbatchSlice],
155
+ ubatch_slices: list[UBatchSlice],
156
common_attn_metadata: CommonAttentionMetadata,
157
) -> list[CommonAttentionMetadata]:
158
"""
159
Creates a new CommonAttentionMetadata instance that corresponds to the
160
- requests for each UbatchSlice in ubatch_slices.
160
+ requests for each UBatchSlice in ubatch_slices.
161
162
Note: This function does not modify common_attn_metadata
163
"""
vllm/v1/spec_decode/eagle.py
+8
-4
@@ -27,6 +27,7 @@ from vllm.v1.attention.backends.triton_attn import TritonAttentionMetadata
27
from vllm.v1.attention.backends.utils import CommonAttentionMetadata
28
from vllm.v1.kv_cache_interface import KVCacheConfig
29
from vllm.v1.sample.metadata import SamplingMetadata
30
+from vllm.v1.worker.ubatching import dbo_current_ubatch_id
31
32
logger = init_logger(__name__)
33
@@ -179,9 +180,11 @@ class EagleProposer:
180
assert self.runner is not None
181
182
# FIXME: need to consider multiple kv_cache_groups
182
- attn_metadata = self.runner.attn_groups[0][0].metadata_builder\
183
- .build_for_drafting(common_attn_metadata=common_attn_metadata,
184
- draft_index=0)
183
+ ubatch_id = dbo_current_ubatch_id()
184
+ attn_metadata_builder = \
185
+ self.runner.attn_groups[0][0].metadata_builders[ubatch_id]
186
+ attn_metadata = attn_metadata_builder.build_for_drafting(
187
+ common_attn_metadata=common_attn_metadata, draft_index=0)
188
189
# At this moment, we assume all eagle layers belong to the same KV
190
# cache group, thus using the same attention metadata.
@@ -355,8 +358,9 @@ class EagleProposer:
358
hidden_states: torch.Tensor,
359
common_attn_metadata: CommonAttentionMetadata,
360
) -> list[torch.Tensor]:
361
+ ubatch_id = dbo_current_ubatch_id()
362
tree_attn_metadata_builder = \
359
- self.runner.attn_groups[0][0].metadata_builder
363
+ self.runner.attn_groups[0][0].metadata_builders[ubatch_id]
364
assert isinstance(tree_attn_metadata_builder,
365
TreeAttentionMetadataBuilder)
366
vllm/v1/worker/cpu_model_runner.py
+7
-2
@@ -64,8 +64,13 @@ class CPUModelRunner(GPUModelRunner):
64
if not self.attn_groups[0]:
65
return
66
67
- mb = getattr(self.attn_groups[0][0], "metadata_builder", None)
68
- if not isinstance(mb, TorchSDPAMetadataBuilderV1):
67
+ mb = getattr(self.attn_groups[0][0], "metadata_builders", None)
68
+ if isinstance(mb, list):
69
+ if not isinstance(mb[0], TorchSDPAMetadataBuilderV1):
70
+ return
71
+ mb[0].reorder_batch(self.input_batch, scheduler_output)
72
+ return
73
+ elif not isinstance(mb, TorchSDPAMetadataBuilderV1):
74
# Encoder-only / rerank models do not benefit from reordering,
75
# so we safely skip here.
76
return
vllm/v1/worker/gpu_model_runner.py
+236
-39
@@ -15,6 +15,7 @@ import torch
15
import torch.distributed
16
import torch.nn as nn
17
from tqdm import tqdm
18
+from typing_extensions import TypeAlias
19
20
import vllm.envs as envs
21
from vllm.attention import Attention, AttentionType
@@ -55,11 +56,12 @@ from vllm.tasks import GenerationTask, PoolingTask, SupportedTask
56
from vllm.utils import (STR_DTYPE_TO_TORCH_DTYPE, DeviceMemoryProfiler,
57
GiB_bytes, LazyLoader, check_use_alibi, get_dtype_size,
58
is_pin_memory_available, round_up, supports_dynamo)
59
+from vllm.v1.attention.backends.flash_attn import AttentionMetadata
60
from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadataBuilder
61
from vllm.v1.attention.backends.utils import (
62
AttentionCGSupport, AttentionMetadataBuilder, CommonAttentionMetadata,
63
create_fast_prefill_custom_backend,
62
- reorder_batch_to_split_decodes_and_prefills)
64
+ reorder_batch_to_split_decodes_and_prefills, split_attn_metadata)
65
from vllm.v1.cudagraph_dispatcher import CudagraphDispatcher
66
# yapf conflicts with isort for this block
67
# yapf: disable
@@ -85,9 +87,12 @@ from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
87
from vllm.v1.spec_decode.ngram_proposer import NgramProposer
88
from vllm.v1.utils import CpuGpuBuffer, record_function_or_nullcontext
89
from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
90
+from vllm.v1.worker.gpu_ubatch_wrapper import UBatchWrapper
91
from vllm.v1.worker.kv_connector_model_runner_mixin import (
92
KVConnectorModelRunnerMixin)
93
from vllm.v1.worker.lora_model_runner_mixin import LoRAModelRunnerMixin
94
+from vllm.v1.worker.ubatch_splitting import get_dp_padding_ubatch, ubatch_split
95
+from vllm.v1.worker.ubatch_utils import UBatchSlice, UBatchSlices
96
from vllm.v1.worker.utils import is_residual_scattered_for_sp
97
98
from .utils import (AttentionGroup, MultiModalBudget,
@@ -105,6 +110,11 @@ else:
110
111
logger = init_logger(__name__)
112
113
+AttnMetadataDict: TypeAlias = dict[str, AttentionMetadata]
114
+# list when ubatching is enabled
115
+PerLayerAttnMetadata: TypeAlias = Union[list[AttnMetadataDict],
116
+ AttnMetadataDict]
117
+
118
119
# Wrapper for ModelRunnerOutput to support overlapped execution.
120
class AsyncGPUModelRunnerOutput(AsyncModelRunnerOutput):
@@ -274,6 +284,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
284
285
# Request states.
286
self.requests: dict[str, CachedRequestState] = {}
287
+ self.comm_stream = torch.cuda.Stream()
288
289
# Input Batch
290
# NOTE(Chen): Ideally, we should initialize the input batch inside
@@ -872,10 +883,11 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
883
return encoder_seq_lens
884
885
def _prepare_inputs(
875
- self,
876
- scheduler_output: "SchedulerOutput",
877
- ) -> tuple[dict[str, Any], torch.Tensor, Optional[SpecDecodeMetadata],
878
- np.ndarray, Optional[CommonAttentionMetadata], int]:
886
+ self, scheduler_output: "SchedulerOutput"
887
+ ) -> tuple[PerLayerAttnMetadata, torch.Tensor,
888
+ Optional[SpecDecodeMetadata], np.ndarray,
889
+ Optional[CommonAttentionMetadata], int, Optional[UBatchSlices],
890
+ Optional[torch.Tensor]]:
891
"""
892
:return: tuple[
893
attn_metadata: layer-to-attention_metadata mapping,
@@ -947,6 +959,15 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
959
self.query_start_loc.copy_to_gpu()
960
query_start_loc = self.query_start_loc.gpu[:num_reqs + 1]
961
962
+ num_tokens_unpadded = scheduler_output.total_num_scheduled_tokens
963
+ num_tokens_padded = num_tokens_unpadded + self.get_local_padding(
964
+ num_tokens_unpadded)
965
+ ubatch_slices, num_tokens_after_padding = \
966
+ ubatch_split(max_num_scheduled_tokens,
967
+ num_tokens_unpadded,
968
+ num_tokens_padded,
969
+ self.vllm_config)
970
+
971
self.seq_lens.np[:num_reqs] = (
972
self.input_batch.num_computed_tokens_cpu[:num_reqs] +
973
num_scheduled_tokens)
@@ -1001,7 +1022,9 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1022
logits_indices_padded = self._prepare_kv_sharing_fast_prefill(
1023
logits_indices)
1024
1004
- attn_metadata: dict[str, Any] = {}
1025
+ attn_metadata: PerLayerAttnMetadata = {}
1026
+ if ubatch_slices is not None:
1027
+ attn_metadata = [dict() for _ in range(len(ubatch_slices))]
1028
1029
# Used in the below loop.
1030
query_start_loc_cpu = self.query_start_loc.cpu[:num_reqs + 1]
@@ -1075,7 +1098,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1098
for attn_group in self.attn_groups[kv_cache_group_id]:
1099
# Prepare for cascade attention if enabled & beneficial.
1100
common_prefix_len = 0
1078
- builder = attn_group.metadata_builder
1101
+ builder = attn_group.get_metadata_builder()
1102
if self.cascade_attn_enabled:
1103
common_prefix_len = self._compute_cascade_attn_prefix_len(
1104
num_scheduled_tokens,
@@ -1093,13 +1116,27 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1116
num_draft_tokens=self.num_draft_tokens.gpu[:num_reqs],
1117
)
1118
1096
- attn_metadata_i = builder.build(
1097
- common_prefix_len=common_prefix_len,
1098
- common_attn_metadata=common_attn_metadata,
1099
- **extra_attn_metadata_args)
1100
-
1101
- for layer_name in attn_group.layer_names:
1102
- attn_metadata[layer_name] = attn_metadata_i
1119
+ if ubatch_slices is not None:
1120
+ common_attn_metadata_list = split_attn_metadata(
1121
+ ubatch_slices, common_attn_metadata)
1122
+ for ubid, common_attn_metadata in enumerate(
1123
+ common_attn_metadata_list):
1124
+ assert common_attn_metadata.max_query_len == 1
1125
+ attn_metadata_i = (attn_group.get_metadata_builder(
1126
+ ubatch_id=ubid).build(
1127
+ common_prefix_len=common_prefix_len,
1128
+ common_attn_metadata=common_attn_metadata))
1129
+ for layer_name in kv_cache_group_spec.layer_names:
1130
+ assert type(attn_metadata) is list
1131
+ attn_metadata[ubid][layer_name] = attn_metadata_i
1132
+ else:
1133
+ assert isinstance(attn_metadata, dict)
1134
+ attn_metadata_i = builder.build(
1135
+ common_prefix_len=common_prefix_len,
1136
+ common_attn_metadata=common_attn_metadata,
1137
+ **extra_attn_metadata_args)
1138
+ for layer_name in attn_group.layer_names:
1139
+ attn_metadata[layer_name] = attn_metadata_i
1140
1141
# Hot-Swap lora model
1142
if self.lora_config:
@@ -1107,7 +1144,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1144
1145
return (attn_metadata, logits_indices, spec_decode_metadata,
1146
num_scheduled_tokens, spec_decode_common_attn_metadata,
1110
- max_num_scheduled_tokens)
1147
+ max_num_scheduled_tokens, ubatch_slices,
1148
+ num_tokens_after_padding)
1149
1150
def _compute_cascade_attn_prefix_len(
1151
self,
@@ -1508,7 +1546,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1546
1547
def get_model(self) -> nn.Module:
1548
# get raw model out of the cudagraph wrapper.
1511
- if isinstance(self.model, CUDAGraphWrapper):
1549
+ if isinstance(self.model, (CUDAGraphWrapper, UBatchWrapper)):
1550
return self.model.unwrap()
1551
return self.model
1552
@@ -1675,6 +1713,17 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1713
1714
def get_dp_padding(self,
1715
num_tokens: int) -> tuple[int, Optional[torch.Tensor]]:
1716
+ """
1717
+ Determines the total number of tokens that each rank will run.
1718
+ All ranks will be padded out so that they run with the same number
1719
+ of tokens
1720
+
1721
+ Returns: tuple[
1722
+ num_pad_tokens: The number of tokens that will be added to the batch
1723
+ num_tokens_after_padding: A tensor containing the total number of
1724
+ tokens for each DP rank including padding.
1725
+ ]
1726
+ """
1727
dp_size = self.vllm_config.parallel_config.data_parallel_size
1728
dp_rank = self.vllm_config.parallel_config.data_parallel_rank
1729
@@ -1698,6 +1747,39 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1747
dtype=torch.int32)
1748
return max_tokens_across_dp_cpu - num_tokens, num_tokens_after_padding
1749
1750
+ def get_local_padding(self, num_tokens_unpadded: int) -> int:
1751
+
1752
+ num_tokens_padded = num_tokens_unpadded
1753
+
1754
+ if (self.compilation_config.cudagraph_mode != CUDAGraphMode.NONE
1755
+ and num_tokens_unpadded <= self.cudagraph_batch_sizes[-1]):
1756
+ # Use piecewise CUDA graphs.
1757
+ # Add padding to the batch size.
1758
+ num_tokens_padded = self.vllm_config.pad_for_cudagraph(
1759
+ num_tokens_unpadded)
1760
+ else:
1761
+ # Eager mode.
1762
+ # Pad tokens to multiple of tensor_parallel_size when
1763
+ # enabled collective fusion for SP
1764
+ tp_size = self.vllm_config.parallel_config.tensor_parallel_size
1765
+ if self.vllm_config.compilation_config.pass_config. \
1766
+ enable_sequence_parallelism and tp_size > 1:
1767
+ num_tokens_padded = round_up(num_tokens_unpadded, tp_size)
1768
+
1769
+ num_pad_tokens = num_tokens_padded - num_tokens_unpadded
1770
+ return num_pad_tokens
1771
+
1772
+ # This is where the second ubatch is adjusted to account for the padding.
1773
+ # Should be called after attention metadata creation. This just pads
1774
+ # the second ubatch slice out to the total number of tokens
1775
+ # (num_tokens + padding)
1776
+ def pad_out_ubatch_slice(self, ubatch_slices: UBatchSlices,
1777
+ num_total_tokens: int):
1778
+ padded_second_ubatch_slice = slice(ubatch_slices[1].token_slice.start,
1779
+ num_total_tokens)
1780
+ ubatch_slices[1] = UBatchSlice(padded_second_ubatch_slice,
1781
+ padded_second_ubatch_slice)
1782
+
1783
def _pool(
1784
self,
1785
hidden_states: torch.Tensor,
@@ -1758,15 +1840,22 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1840
self,
1841
scheduler_output: "SchedulerOutput",
1842
intermediate_tensors: Optional[IntermediateTensors] = None,
1843
+ ubatch_slices: Optional[UBatchSlices] = None,
1844
+ num_tokens_after_padding: Optional[torch.Tensor] = None,
1845
) -> tuple[int, int, Optional[torch.Tensor], Optional[torch.Tensor],
1846
Optional[torch.Tensor], torch.Tensor,
1847
Optional[IntermediateTensors], dict[str, Any]]:
1848
1849
num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
1766
- num_input_tokens = self._get_num_input_tokens(num_scheduled_tokens)
1767
- # Padding for DP
1768
- num_pad, num_tokens_across_dp = self.get_dp_padding(num_input_tokens)
1769
- num_input_tokens += num_pad
1850
+ if ubatch_slices:
1851
+ assert num_tokens_after_padding is not None
1852
+ num_input_tokens = int(num_tokens_after_padding[0].item() * 2)
1853
+ self.pad_out_ubatch_slice(ubatch_slices, num_input_tokens)
1854
+ elif ubatch_slices is None:
1855
+ num_input_tokens = self._get_num_input_tokens(num_scheduled_tokens)
1856
+ num_pad, num_tokens_after_padding = self.get_dp_padding(
1857
+ num_input_tokens)
1858
+ num_input_tokens += num_pad
1859
1860
# _prepare_inputs may reorder the batch, so we must gather multi
1861
# modal outputs after that to ensure the correct order
@@ -1821,7 +1910,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1910
return (
1911
num_scheduled_tokens,
1912
num_input_tokens,
1824
- num_tokens_across_dp,
1913
+ num_tokens_after_padding,
1914
input_ids,
1915
inputs_embeds,
1916
positions,
@@ -2027,7 +2116,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2116
# Prepare the decoder inputs.
2117
(attn_metadata, logits_indices, spec_decode_metadata,
2118
num_scheduled_tokens_np, spec_decode_common_attn_metadata,
2030
- max_query_len) = self._prepare_inputs(scheduler_output)
2119
+ max_query_len, ubatch_slices, num_tokens_after_padding
2120
+ ) = self._prepare_inputs(scheduler_output)
2121
2122
finally:
2123
if self.prepare_inputs_event is not None:
@@ -2042,7 +2132,11 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2132
positions,
2133
intermediate_tensors,
2134
model_kwargs,
2045
- ) = self._preprocess(scheduler_output, intermediate_tensors)
2135
+ ) = self._preprocess(scheduler_output, intermediate_tensors,
2136
+ ubatch_slices, num_tokens_after_padding)
2137
+
2138
+ if ubatch_slices is not None:
2139
+ num_input_tokens = num_input_tokens // 2
2140
2141
uniform_decode = (max_query_len
2142
== self.uniform_decode_query_len) and (
@@ -2062,6 +2156,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2156
num_tokens_across_dp=num_tokens_across_dp,
2157
cudagraph_runtime_mode=cudagraph_runtime_mode,
2158
batch_descriptor=batch_descriptor,
2159
+ ubatch_slices=ubatch_slices,
2160
), record_function_or_nullcontext("Forward"),
2161
self.maybe_get_kv_connector_output(scheduler_output) as
2162
kv_connector_output):
@@ -2441,10 +2536,18 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2536
# CudagraphWraper and CudagraphDispatcher of vllm.
2537
2538
# wrap the model with full cudagraph wrapper if needed.
2444
- if self.compilation_config.cudagraph_mode.has_full_cudagraphs():
2539
+ if self.compilation_config.cudagraph_mode.has_full_cudagraphs() \
2540
+ and not self.parallel_config.enable_dbo:
2541
self.model = CUDAGraphWrapper(self.model,
2542
self.vllm_config,
2543
runtime_mode=CUDAGraphMode.FULL)
2544
+ elif self.parallel_config.enable_dbo:
2545
+ if self.compilation_config.cudagraph_mode.has_full_cudagraphs():
2546
+ self.model = UBatchWrapper(self.model, self.vllm_config,
2547
+ CUDAGraphMode.FULL, self.device)
2548
+ else:
2549
+ self.model = UBatchWrapper(self.model, self.vllm_config,
2550
+ CUDAGraphMode.NONE, self.device)
2551
2552
def reload_weights(self) -> None:
2553
assert getattr(self, "model", None) is not None, \
@@ -2642,6 +2745,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2745
cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
2746
force_attention: bool = False,
2747
uniform_decode: bool = False,
2748
+ allow_microbatching: bool = False,
2749
skip_eplb: bool = False,
2750
is_profile: bool = False,
2751
create_mixed_batch: bool = False,
@@ -2667,12 +2771,30 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2771
(1 token) and prefill (multiple tokens) requests.
2772
remove_lora: If False, dummy LoRAs are not destroyed after the run
2773
"""
2774
+ ubatch_enabled = self.parallel_config.enable_dbo
2775
+ num_tokens_across_dp = None
2776
+ num_pad = 0
2777
+ should_ubatch = False
2778
+ if ubatch_enabled:
2779
+ should_ubatch = num_tokens >= \
2780
+ self.parallel_config.dbo_decode_token_threshold and \
2781
+ allow_microbatching
2782
+
2783
+ (should_ubatch, num_tokens_across_dp) = get_dp_padding_ubatch(
2784
+ num_tokens, num_tokens, should_ubatch, self.vllm_config)
2785
+
2786
+ # Currently the dummy run should only be ubatching during
2787
+ # cuda graph capture, meaning all DP ranks should already
2788
+ # have the same batch size
2789
+ if num_tokens_across_dp is not None:
2790
+ assert int(num_tokens_across_dp[0]) == num_tokens // 2
2791
+
2792
assert cudagraph_runtime_mode in {
2793
CUDAGraphMode.NONE, CUDAGraphMode.PIECEWISE, CUDAGraphMode.FULL
2794
}
2795
2674
- # Padding for DP
2675
- num_pad, num_tokens_across_dp = self.get_dp_padding(num_tokens)
2796
+ if not should_ubatch:
2797
+ num_pad, num_tokens_across_dp = self.get_dp_padding(num_tokens)
2798
num_tokens += num_pad
2799
2800
# If cudagraph_mode.decode_mode() == FULL and
@@ -2690,6 +2812,10 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2812
# for GQA/MQA.
2813
max_query_len = self.uniform_decode_query_len if uniform_decode else \
2814
num_tokens
2815
+ if allow_microbatching:
2816
+ assert self.uniform_decode_query_len == 1
2817
+ assert uniform_decode is True
2818
+ assert max_query_len == 1
2819
2820
# Set num_scheduled_tokens based on num_tokens and max_num_seqs
2821
# for dummy run with LoRA so that the num_reqs collectively
@@ -2728,12 +2854,28 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2854
num_scheduled_tokens = np.array(num_scheduled_tokens_list,
2855
dtype=np.int32)
2856
2731
- attn_metadata: Optional[dict[str, Any]] = None
2857
+ ubatch_slices = None
2858
+ # We currently only microbatch if the number of tokens is
2859
+ # over a certain threshold.
2860
+ if should_ubatch:
2861
+ # We only support decode-only cudagraphs
2862
+ assert num_reqs == num_tokens
2863
+ assert num_tokens % 2 == 0
2864
+ ubatch_slices = [
2865
+ UBatchSlice(slice(0, num_reqs // 2), slice(0,
2866
+ num_tokens // 2)),
2867
+ UBatchSlice(slice(num_reqs // 2, num_reqs),
2868
+ slice(num_tokens // 2, num_tokens))
2869
+ ]
2870
+
2871
+ attn_metadata: Optional[PerLayerAttnMetadata] = None
2872
2873
# If force_attention is True, we always capture attention. Otherwise,
2874
# it only happens for cudagraph_runtime_mode=FULL.
2875
if force_attention or cudagraph_runtime_mode == CUDAGraphMode.FULL:
2876
attn_metadata = {}
2877
+ if ubatch_slices is not None:
2878
+ attn_metadata = [dict() for _ in range(len(ubatch_slices))]
2879
2880
if create_mixed_batch:
2881
# In the mixed batch mode (used for FI warmup), we use
@@ -2766,12 +2908,26 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2908
slot_mapping=self.input_batch.
2909
block_table[kv_cache_group_id].slot_mapping[:num_tokens],
2910
causal=True)
2769
-
2911
for attn_group in self.attn_groups[kv_cache_group_id]:
2771
- attn_metadata_i = attn_group.metadata_builder\
2772
- .build_for_cudagraph_capture(common_attn_metadata)
2773
- for layer_name in kv_cache_group_spec.layer_names:
2774
- attn_metadata[layer_name] = attn_metadata_i
2912
+ if ubatch_slices is not None:
2913
+ common_attn_metadata_list = split_attn_metadata(
2914
+ ubatch_slices, common_attn_metadata)
2915
+ for ubid, common_attn_metadata in enumerate(
2916
+ common_attn_metadata_list):
2917
+ assert common_attn_metadata.max_query_len == 1
2918
+ attn_metadata_i = (attn_group\
2919
+ .get_metadata_builder(ubatch_id=ubid)\
2920
+ .build_for_cudagraph_capture(common_attn_metadata))
2921
+ for layer_name in kv_cache_group_spec.layer_names:
2922
+ assert type(attn_metadata) is list
2923
+ attn_metadata[ubid][
2924
+ layer_name] = attn_metadata_i
2925
+ else:
2926
+ assert type(attn_metadata) is dict
2927
+ attn_metadata_i = attn_group.get_metadata_builder()\
2928
+ .build_for_cudagraph_capture(common_attn_metadata)
2929
+ for layer_name in kv_cache_group_spec.layer_names:
2930
+ attn_metadata[layer_name] = attn_metadata_i
2931
2932
with self.maybe_dummy_run_with_lora(self.lora_config,
2933
num_scheduled_tokens, remove_lora):
@@ -2818,13 +2974,16 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2974
f"Cudagraph runtime mode mismatch at dummy_run. "
2975
f"Expected {_cg_mode}, but got {cudagraph_runtime_mode}.")
2976
2977
+ if ubatch_slices is not None:
2978
+ num_tokens = num_tokens // 2
2979
with self.maybe_randomize_inputs(input_ids), set_forward_context(
2980
attn_metadata,
2981
self.vllm_config,
2982
num_tokens=num_tokens,
2983
num_tokens_across_dp=num_tokens_across_dp,
2984
cudagraph_runtime_mode=cudagraph_runtime_mode,
2827
- batch_descriptor=batch_descriptor):
2985
+ batch_descriptor=batch_descriptor,
2986
+ ubatch_slices=ubatch_slices):
2987
outputs = self.model(
2988
input_ids=input_ids,
2989
positions=positions,
@@ -3096,6 +3255,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3255
set_cudagraph_capturing_enabled(True)
3256
with freeze_gc(), graph_capture(device=self.device):
3257
cudagraph_mode = self.compilation_config.cudagraph_mode
3258
+ assert cudagraph_mode is not None
3259
if cudagraph_mode.mixed_mode() != CUDAGraphMode.NONE:
3260
cudagraph_runtime_mode = cudagraph_mode.mixed_mode()
3261
@@ -3153,6 +3313,35 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3313
desc="Capturing CUDA graphs ({}, {})".format(
3314
"decode" if uniform_decode else "mixed prefill-decode",
3315
cudagraph_runtime_mode.name))
3316
+ enable_dbo = self.parallel_config.enable_dbo
3317
+ # DBO Only supports running Full cudagraphs with uniform
3318
+ # decode lengths
3319
+ if enable_dbo and uniform_decode:
3320
+ for num_tokens in compilation_cases:
3321
+ # If the number of tokens is greater than the microbatching
3322
+ # threshold, don't generate a microbatched cudagraph
3323
+ if (num_tokens
3324
+ < self.parallel_config.dbo_decode_token_threshold):
3325
+ continue
3326
+
3327
+ # Warmup
3328
+ for _ in range(
3329
+ self.compilation_config.cudagraph_num_of_warmups):
3330
+ force_attention = (
3331
+ cudagraph_runtime_mode == CUDAGraphMode.FULL)
3332
+ self._dummy_run(num_tokens,
3333
+ cudagraph_runtime_mode=CUDAGraphMode.NONE,
3334
+ force_attention=force_attention,
3335
+ uniform_decode=True,
3336
+ allow_microbatching=True,
3337
+ skip_eplb=True)
3338
+
3339
+ # Graph Capture
3340
+ self._dummy_run(num_tokens,
3341
+ cudagraph_runtime_mode=CUDAGraphMode.FULL,
3342
+ uniform_decode=True,
3343
+ allow_microbatching=True,
3344
+ skip_eplb=True)
3345
# We skip EPLB here since we don't want to record dummy metrics
3346
for num_tokens in compilation_cases:
3347
for _ in range(self.compilation_config.cudagraph_num_of_warmups):
@@ -3219,14 +3408,23 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3408
) -> list[AttentionGroup]:
3409
attn_groups: list[AttentionGroup] = []
3410
for attn_backend, layer_names in attn_backends_map.items():
3222
- attn_metadata_builder_i = attn_backend.get_builder_cls()(
3411
+ attn_metadata_builders = []
3412
+ attn_metadata_builders.append(attn_backend.get_builder_cls()(
3413
kv_cache_spec,
3414
layer_names,
3415
self.vllm_config,
3416
self.device,
3227
- )
3417
+ ))
3418
+ if self.parallel_config.enable_dbo:
3419
+ attn_metadata_builders.append(
3420
+ attn_backend.get_builder_cls()(
3421
+ kv_cache_spec,
3422
+ layer_names,
3423
+ self.vllm_config,
3424
+ self.device,
3425
+ ))
3426
attn_group = AttentionGroup(attn_backend,
3229
- attn_metadata_builder_i,
3427
+ attn_metadata_builders,
3428
layer_names)
3429
attn_groups.append(attn_group)
3430
return attn_groups
@@ -3246,11 +3444,10 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3444
min_cg_builder_name = None
3445
3446
for attn_group in self._attn_group_iterator():
3249
- builder = attn_group.metadata_builder
3447
+ builder = attn_group.get_metadata_builder()
3448
if builder.cudagraph_support.value < min_cg_support.value:
3449
min_cg_support = builder.cudagraph_support
3450
min_cg_builder_name = builder.__class__.__name__
3253
-
3451
# Flexible resolve the cudagraph mode
3452
cudagraph_mode = self.compilation_config.cudagraph_mode
3453
# check cudagraph for mixed batch is supported
@@ -3316,7 +3513,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3513
is compatible (e.g., decode threshold is the same)
3514
"""
3515
for group in self._attn_group_iterator():
3319
- attn_metadata_builder_i = group.metadata_builder
3516
+ attn_metadata_builder_i = group.get_metadata_builder()
3517
3518
# check that if any backends reorder batches; that the reordering
3519
# is compatible (e.g., decode threshold is the same)
vllm/v1/worker/gpu_ubatch_wrapper.py
new
+303
@@ -0,0 +1,303 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import dataclasses
5
+import threading
6
+from typing import Any, Callable, Optional
7
+
8
+import torch
9
+
10
+from vllm.compilation.cuda_graph import CUDAGraphWrapper
11
+from vllm.config import CUDAGraphMode, VllmConfig
12
+from vllm.forward_context import (create_forward_context, get_forward_context,
13
+ override_forward_context)
14
+from vllm.logger import init_logger
15
+from vllm.platforms import current_platform
16
+from vllm.sequence import IntermediateTensors
17
+from vllm.v1.worker.ubatching import UBatchContext, make_ubatch_contexts
18
+
19
+logger = init_logger(__name__)
20
+
21
+
22
+@dataclasses.dataclass
23
+class UbatchMetadata:
24
+ context: UBatchContext
25
+ input_ids: torch.Tensor
26
+ positions: torch.Tensor
27
+ inputs_embeds: Optional[torch.Tensor]
28
+ intermediate_tensors: Optional[IntermediateTensors]
29
+ num_tokens: int
30
+
31
+
32
+@dataclasses.dataclass
33
+class CUDAGraphMetaData:
34
+ cudagraph: torch.cuda.CUDAGraph
35
+ ubatch_metadata: UbatchMetadata
36
+ outputs: Optional[Any] = None
37
+
38
+
39
+class UBatchWrapper:
40
+
41
+ def __init__(self, runnable: Callable, vllm_config: VllmConfig,
42
+ runtime_mode: CUDAGraphMode, device: torch.cuda.device):
43
+ self.runnable = runnable
44
+ self.vllm_config = vllm_config
45
+ self.compilation_config = vllm_config.compilation_config
46
+ self.comm_stream = torch.cuda.Stream(device=device)
47
+ # Two ubatch threads plus the main thread
48
+ self.ready_barrier = threading.Barrier(3)
49
+
50
+ self.cudagraphs: dict[int, CUDAGraphMetaData] = {}
51
+
52
+ self.cudagraph_wrapper = None
53
+ self.graph_pool = None
54
+ if runtime_mode is not CUDAGraphMode.NONE:
55
+ self.cudagraph_wrapper = CUDAGraphWrapper(
56
+ runnable, vllm_config, runtime_mode=runtime_mode)
57
+ self.graph_pool = current_platform.get_global_graph_pool()
58
+
59
+ def __getattr__(self, key: str):
60
+ # allow accessing the attributes of the runnable.
61
+ if hasattr(self.runnable, key):
62
+ return getattr(self.runnable, key)
63
+ raise AttributeError(f"Attribute {key} not exists in the runnable of "
64
+ f"cudagraph wrapper: {self.runnable}")
65
+
66
+ def unwrap(self) -> Callable:
67
+ # in case we need to access the original runnable.
68
+ return self.runnable
69
+
70
+ def _capture_ubatches(self, ubatch_metadata, model) -> torch.Tensor:
71
+ """
72
+ Capture a cudagraph for a microbatched run.
73
+
74
+ The logic here is somewhat complicated because we need to make sure that
75
+ each of the ubatch threads initialize the cuda context before we start
76
+ the graph capture.
77
+
78
+ The flow is as follows:
79
+ 1. The main thread starts up each ubatch thread. Each thread will
80
+ initialize its cuda context (torch.cuda.current_blas_handle())
81
+ before going to sleep upon entering the ubatch_context.
82
+
83
+ 2. The main thread starts the graph capture and wakes up the first
84
+ ubatch thread.
85
+
86
+ 3. Each ubatch thread runs the model to completion and returns the
87
+ completed output tensors back to the main thread.
88
+
89
+ 4. The main thread stores the captured cudagraph along with its metadata
90
+ and returns
91
+ """
92
+
93
+ @torch.inference_mode()
94
+ def _capture_ubatch_thread(results, ubatch_metadata):
95
+ ubatch_context = ubatch_metadata.context
96
+ with torch.cuda.stream(ubatch_context.compute_stream):
97
+ _ = torch.cuda.current_blas_handle()
98
+ with torch.cuda.stream(ubatch_context.comm_stream):
99
+ _ = torch.cuda.current_blas_handle()
100
+ with ubatch_context:
101
+ model_output = model(
102
+ input_ids=ubatch_metadata.input_ids,
103
+ positions=ubatch_metadata.positions,
104
+ intermediate_tensors=ubatch_metadata.intermediate_tensors,
105
+ inputs_embeds=ubatch_metadata.inputs_embeds,
106
+ )
107
+
108
+ results.append((ubatch_metadata.context.id, model_output))
109
+
110
+ results: list[tuple[int, torch.Tensor]] = []
111
+ compute_stream = ubatch_metadata[0].context.compute_stream
112
+ num_tokens = ubatch_metadata[0].num_tokens + \
113
+ ubatch_metadata[1].num_tokens
114
+
115
+ # Ubatches will manually manage the forward context, so we override
116
+ # it to None here so we can have it restored correctly later
117
+ with override_forward_context(None):
118
+ ubatch_threads = []
119
+ for metadata in ubatch_metadata:
120
+ thread = threading.Thread(target=_capture_ubatch_thread,
121
+ args=(
122
+ results,
123
+ metadata,
124
+ ))
125
+ ubatch_threads.append(thread)
126
+ thread.start()
127
+ self.ready_barrier.wait() # Wait for both threads to be ready
128
+
129
+ # Capture the cudagraph
130
+ cudagraph_metadata = \
131
+ CUDAGraphMetaData(
132
+ cudagraph=torch.cuda.CUDAGraph(),
133
+ ubatch_metadata=ubatch_metadata,
134
+ )
135
+ with torch.cuda.graph(cudagraph_metadata.cudagraph,
136
+ stream=compute_stream,
137
+ pool=self.graph_pool):
138
+ ubatch_metadata[0].context.cpu_wait_event.set()
139
+ for thread in ubatch_threads:
140
+ thread.join()
141
+ sorted_results = [value for position, value in sorted(results)]
142
+ result = torch.cat(sorted_results, dim=0)
143
+ cudagraph_metadata.outputs = result
144
+ self.cudagraphs[num_tokens] = cudagraph_metadata
145
+ return cudagraph_metadata.outputs
146
+
147
+ def _run_ubatches(self, ubatch_metadata, model) -> torch.Tensor:
148
+
149
+ @torch.inference_mode()
150
+ def _ubatch_thread(results, model, ubatch_metadata):
151
+ with ubatch_metadata.context:
152
+ model_output = model(
153
+ input_ids=ubatch_metadata.input_ids,
154
+ positions=ubatch_metadata.positions,
155
+ intermediate_tensors=ubatch_metadata.intermediate_tensors,
156
+ inputs_embeds=ubatch_metadata.inputs_embeds,
157
+ )
158
+ results.append((ubatch_metadata.context.id, model_output))
159
+
160
+ results: list[tuple[int, torch.Tensor]] = []
161
+
162
+ # Ubatch threads will manually manage the forward context, so we
163
+ # override it to None here so we can have it restored correctly
164
+ # after both threads have finished
165
+ with override_forward_context(None):
166
+ ubatch_threads = []
167
+ for metadata in ubatch_metadata:
168
+ thread = threading.Thread(target=_ubatch_thread,
169
+ args=(
170
+ results,
171
+ model,
172
+ metadata,
173
+ ))
174
+ ubatch_threads.append(thread)
175
+ thread.start()
176
+ self.ready_barrier.wait() # Wait for both threads to be ready
177
+ ubatch_metadata[0].context.cpu_wait_event.set()
178
+ for thread in ubatch_threads:
179
+ thread.join()
180
+ sorted_results = [value for position, value in sorted(results)]
181
+ result = torch.cat(sorted_results, dim=0)
182
+ return result
183
+
184
+ def _make_ubatch_metadata(self, ubatch_slices, attn_metadata, input_ids,
185
+ positions, inputs_embeds, intermediate_tensors,
186
+ compute_stream, dp_metadata, batch_descriptor,
187
+ cudagraph_runtime_mode) -> list[UbatchMetadata]:
188
+
189
+ # Create one forward context per ubatch
190
+ forward_contexts = []
191
+ for i, ubatch_slice in enumerate(ubatch_slices):
192
+ forward_contexts.append(
193
+ create_forward_context(
194
+ attn_metadata[i] if attn_metadata is not None else None,
195
+ self.vllm_config,
196
+ dp_metadata=dp_metadata,
197
+ batch_descriptor=batch_descriptor,
198
+ cudagraph_runtime_mode=cudagraph_runtime_mode))
199
+
200
+ ubatch_ctxs = make_ubatch_contexts(
201
+ num_micro_batches=len(ubatch_slices),
202
+ comm_stream=self.comm_stream,
203
+ compute_stream=compute_stream,
204
+ forward_contexts=forward_contexts,
205
+ ready_barrier=self.ready_barrier)
206
+
207
+ ubatch_metadata: list[UbatchMetadata] = []
208
+ for i, ubatch_slice in enumerate(ubatch_slices):
209
+ sliced_input_ids, sliced_positions, sliced_inputs_embeds, \
210
+ sliced_intermediate_tensors = \
211
+ self._slice_model_inputs(
212
+ ubatch_slice.token_slice, input_ids, positions,
213
+ inputs_embeds, intermediate_tensors)
214
+ ubatch_metadata.append(
215
+ UbatchMetadata(
216
+ context=ubatch_ctxs[i],
217
+ input_ids=sliced_input_ids,
218
+ positions=sliced_positions,
219
+ inputs_embeds=sliced_inputs_embeds,
220
+ intermediate_tensors=sliced_intermediate_tensors,
221
+ num_tokens=ubatch_slice.token_slice.stop -
222
+ ubatch_slice.token_slice.start))
223
+
224
+ return ubatch_metadata
225
+
226
+ def _slice_model_inputs(self, tokens_slice: slice, input_ids, positions,
227
+ inputs_embeds, intermediate_tensors):
228
+ sliced_input_ids = input_ids[tokens_slice]
229
+ # if we are using mrope. Mrope adds an additional dimension to the
230
+ # positions tensor
231
+ if positions.ndim == 2:
232
+ sliced_positions = positions[:, tokens_slice]
233
+ else:
234
+ sliced_positions = positions[tokens_slice]
235
+ sliced_inputs_embeds = inputs_embeds[
236
+ tokens_slice] if inputs_embeds else None
237
+ sliced_intermediate_tensors = intermediate_tensors[
238
+ tokens_slice] if intermediate_tensors else None
239
+
240
+ return (sliced_input_ids, sliced_positions, sliced_inputs_embeds,
241
+ sliced_intermediate_tensors)
242
+
243
+ def __call__(self, *args, **kwargs):
244
+ forward_context = get_forward_context()
245
+ batch_descriptor = forward_context.batch_descriptor
246
+ ubatch_slices = forward_context.ubatch_slices
247
+ cudagraph_runtime_mode = forward_context.cudagraph_runtime_mode
248
+
249
+ # If there's no ubatching, just run the runnable object
250
+ if ubatch_slices is None:
251
+ if cudagraph_runtime_mode in (CUDAGraphMode.NONE,
252
+ CUDAGraphMode.PIECEWISE):
253
+ return self.runnable(*args, **kwargs)
254
+ else:
255
+ assert self.cudagraph_wrapper is not None
256
+ return self.cudagraph_wrapper(*args, **kwargs)
257
+
258
+ attn_metadata = forward_context.attn_metadata
259
+ num_tokens = (ubatch_slices[0].token_slice.stop -
260
+ ubatch_slices[0].token_slice.start) * 2
261
+ input_ids = kwargs['input_ids']
262
+ positions = kwargs['positions']
263
+ intermediate_tensors = kwargs['intermediate_tensors']
264
+ inputs_embeds = kwargs['inputs_embeds']
265
+ compute_stream = torch.cuda.current_stream()
266
+
267
+ dp_metadata = forward_context.dp_metadata
268
+
269
+ # We shouldn't be here unless we are running with multiple DP ranks
270
+ assert dp_metadata is not None
271
+
272
+ if num_tokens not in self.cudagraphs \
273
+ and cudagraph_runtime_mode is CUDAGraphMode.FULL:
274
+ ubatch_metadata = self._make_ubatch_metadata(
275
+ ubatch_slices=ubatch_slices,
276
+ attn_metadata=attn_metadata,
277
+ input_ids=input_ids,
278
+ positions=positions,
279
+ intermediate_tensors=intermediate_tensors,
280
+ inputs_embeds=inputs_embeds,
281
+ compute_stream=compute_stream,
282
+ dp_metadata=dp_metadata,
283
+ batch_descriptor=batch_descriptor,
284
+ cudagraph_runtime_mode=CUDAGraphMode.NONE)
285
+
286
+ return self._capture_ubatches(ubatch_metadata, self.model)
287
+ elif num_tokens in self.cudagraphs:
288
+ cudagraph_metadata = self.cudagraphs[num_tokens]
289
+ cudagraph_metadata.cudagraph.replay()
290
+ return cudagraph_metadata.outputs
291
+ else:
292
+ ubatch_metadata = self._make_ubatch_metadata(
293
+ ubatch_slices=ubatch_slices,
294
+ attn_metadata=attn_metadata,
295
+ input_ids=input_ids,
296
+ positions=positions,
297
+ intermediate_tensors=intermediate_tensors,
298
+ inputs_embeds=inputs_embeds,
299
+ compute_stream=compute_stream,
300
+ dp_metadata=dp_metadata,
301
+ batch_descriptor=batch_descriptor,
302
+ cudagraph_runtime_mode=CUDAGraphMode.NONE)
303
+ return self._run_ubatches(ubatch_metadata, self.model)
vllm/v1/worker/ubatch_splitting.py
new
+155
@@ -0,0 +1,155 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from typing import Optional
5
+
6
+import torch
7
+
8
+from vllm.config import VllmConfig
9
+from vllm.forward_context import DPMetadata
10
+from vllm.logger import init_logger
11
+from vllm.utils import round_up
12
+from vllm.v1.worker.ubatch_utils import (UBatchSlice, UBatchSlices,
13
+ is_second_ubatch_empty)
14
+
15
+logger = init_logger(__name__)
16
+
17
+
18
+def should_ubatch_with_num_tokens(
19
+ should_ubatch: bool,
20
+ orig_num_tokens_per_ubatch: int,
21
+ padded_num_tokens_per_ubatch: int,
22
+ vllm_config: VllmConfig,
23
+) -> tuple[bool, Optional[torch.Tensor]]:
24
+ dp_size = vllm_config.parallel_config.data_parallel_size
25
+ dp_rank = vllm_config.parallel_config.data_parallel_rank
26
+ return DPMetadata.should_ubatch_across_dp(should_ubatch,
27
+ orig_num_tokens_per_ubatch,
28
+ padded_num_tokens_per_ubatch,
29
+ dp_size, dp_rank)
30
+
31
+
32
+def get_dp_padding_ubatch(
33
+ num_tokens_unpadded: int, num_tokens_padded: int,
34
+ should_attempt_ubatching: bool,
35
+ vllm_config: VllmConfig) -> tuple[bool, Optional[torch.Tensor]]:
36
+ """
37
+ 1. Decides if each DP rank is going to microbatch. Either all ranks
38
+ run with microbatching or none of them do. If this function decides
39
+ not to run with microbatching. It will "abort" meaning that no padding
40
+ information will be returned to the caller. It will return (False, None)
41
+
42
+ 2. Determines the total number of tokens that each rank will run.
43
+ All ranks will be padded out so that the run with the same number
44
+ of tokens
45
+
46
+ Returns: tuple[
47
+ should_ubatch: Are all DP ranks going to microbatch
48
+ num_tokens_after_padding: A tensor containing the total number of
49
+ tokens per-microbatch for each DP rank including padding. Will be
50
+ None if should_ubatch if False
51
+ ]
52
+
53
+ """
54
+ assert num_tokens_padded >= num_tokens_unpadded
55
+ dp_size = vllm_config.parallel_config.data_parallel_size
56
+ if dp_size == 1:
57
+ # Early exit.
58
+ return False, None
59
+
60
+ # If this DP rank doesn't want to attempt microbatching
61
+ if not should_attempt_ubatching:
62
+ (should_ubatch, num_tokens_across_dp) = should_ubatch_with_num_tokens(
63
+ False, 0, 0, vllm_config)
64
+ assert should_ubatch is False
65
+ assert num_tokens_across_dp is None
66
+ return should_ubatch, num_tokens_across_dp
67
+
68
+ # Round up to the next multiple of two for even divisibility
69
+ num_tokens_padded = round_up(num_tokens_padded, 2)
70
+ num_tokens_per_ubatch = num_tokens_padded // 2
71
+ should_ubatch = True
72
+
73
+ # Sanity Check that the existing padding isn't giving us an empty second
74
+ # ubatch. Abort if so
75
+ if is_second_ubatch_empty(num_tokens_unpadded, num_tokens_padded):
76
+ logger.debug(
77
+ "Empty second µbatch detected: unpadded tokens: %s, padded "
78
+ "tokens: %s", num_tokens_unpadded, num_tokens_padded)
79
+ should_ubatch = False
80
+
81
+ # Note that we compute the number of padded tokens per ubatch
82
+ (should_ubatch, num_tokens_across_dp) = should_ubatch_with_num_tokens(
83
+ should_ubatch, num_tokens_unpadded // 2, num_tokens_per_ubatch,
84
+ vllm_config)
85
+ if not should_ubatch:
86
+ assert num_tokens_across_dp is None
87
+ return should_ubatch, num_tokens_across_dp
88
+
89
+ assert num_tokens_across_dp is not None
90
+
91
+ max_tokens_across_dp_cpu = int(torch.max(num_tokens_across_dp).item())
92
+ num_tokens_after_padding = torch.tensor([max_tokens_across_dp_cpu] *
93
+ dp_size,
94
+ device="cpu",
95
+ dtype=torch.int32)
96
+ return should_ubatch, num_tokens_after_padding
97
+
98
+
99
+def ubatch_split(
100
+ max_num_scheduled_tokens: int,
101
+ num_tokens_unpadded: int,
102
+ num_tokens_padded: int,
103
+ vllm_config: VllmConfig,
104
+) -> tuple[Optional[UBatchSlices], Optional[torch.Tensor]]:
105
+ """
106
+ Coordinates amongst all DP ranks to determine if and how the full batch
107
+ should be split into microbatches.
108
+
109
+ Returns: tuple[
110
+ ubatch_slices: if this is set then all DP ranks have agreed to
111
+ microbatch
112
+ num_tokens_after_padding: A tensor containing the total number of
113
+ tokens per-microbatch for each DP rank including padding. Will be
114
+ None if ubatch_slices is None
115
+ ]
116
+
117
+ """
118
+ parallel_config = vllm_config.parallel_config
119
+ # Don't bother with the should_ubatch handshaking unless microbatching
120
+ # is enabled
121
+ if not parallel_config.enable_dbo:
122
+ return (None, None)
123
+
124
+ # 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
130
+
131
+ # 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)
136
+ if not should_ubatch:
137
+ return (None, None)
138
+
139
+ # This doesn't actually pad the ubatch slices. It just initializes the
140
+ # split point to the padded value so that padding can be applied
141
+ # to the second ubatch in pad_out_ubatch_slice after attention
142
+ # metadata creation
143
+ 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
+ ]
154
+
155
+ return (ubatch_slices, num_tokens_after_padding)
vllm/v1/worker/ubatch_utils.py
new
+19
@@ -0,0 +1,19 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from dataclasses import dataclass
4
+
5
+from typing_extensions import TypeAlias
6
+
7
+
8
+@dataclass
9
+class UBatchSlice:
10
+ request_slice: slice
11
+ token_slice: slice
12
+
13
+
14
+UBatchSlices: TypeAlias = list[UBatchSlice]
15
+
16
+
17
+def is_second_ubatch_empty(orig_num_tokens_per_ubatch: int,
18
+ padded_num_tokens_per_ubatch: int) -> bool:
19
+ return padded_num_tokens_per_ubatch >= 2 * orig_num_tokens_per_ubatch
vllm/v1/worker/ubatching.py
new
+211
@@ -0,0 +1,211 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import threading
4
+from typing import Optional
5
+
6
+import torch
7
+
8
+from vllm import forward_context
9
+from vllm.forward_context import ForwardContext
10
+from vllm.utils import current_stream
11
+
12
+_THREAD_ID_TO_CONTEXT: dict = {}
13
+_CURRENT_CONTEXTS: list[Optional['UBatchContext']] = [None, None]
14
+
15
+
16
+class UBatchContext:
17
+ """
18
+ Context manager for micro-batching synchronization using threading events.
19
+ """
20
+
21
+ def __init__(self,
22
+ id: int,
23
+ comm_stream: torch.cuda.Stream,
24
+ compute_stream: torch.cuda.Stream,
25
+ forward_context: ForwardContext,
26
+ ready_barrier: threading.Barrier,
27
+ cpu_wait_event: threading.Event,
28
+ cpu_signal_event: threading.Event,
29
+ gpu_comm_done_event: torch.cuda.Event,
30
+ gpu_compute_done_event: torch.cuda.Event,
31
+ schedule: str = "default"):
32
+ self.id = id
33
+ self.comm_stream = comm_stream
34
+ self.compute_stream = compute_stream
35
+ self.forward_context = forward_context
36
+ self.ready_barrier = ready_barrier
37
+ self.cpu_wait_event = cpu_wait_event
38
+ self.cpu_signal_event = cpu_signal_event
39
+ self.current_stream = compute_stream
40
+ self.gpu_comm_done_event = gpu_comm_done_event
41
+ self.gpu_compute_done_event = gpu_compute_done_event
42
+ self.schedule = schedule
43
+ self.recv_hook = None
44
+
45
+ def __enter__(self):
46
+ global _CURRENT_CONTEXTS, _THREAD_ID_TO_CONTEXT
47
+ _THREAD_ID_TO_CONTEXT[threading.get_ident()] = self.id
48
+ _CURRENT_CONTEXTS[self.id] = self
49
+ self.ready_barrier.wait()
50
+
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
56
+ return self
57
+
58
+ def __exit__(self, exc_type, exc_val, exc_tb):
59
+ global _CURRENT_CONTEXTS, _THREAD_ID_TO_CONTEXT
60
+ _CURRENT_CONTEXTS[self.id] = None
61
+ del _THREAD_ID_TO_CONTEXT[threading.get_ident()]
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)
67
+ return False
68
+
69
+ def _restore_context(self):
70
+ forward_context._forward_context = self.forward_context
71
+ torch.cuda.set_stream(self.current_stream)
72
+
73
+ def update_stream(self, stream):
74
+ self.current_stream = stream
75
+ torch.cuda.set_stream(self.current_stream)
76
+
77
+ def _signal_comm_done(self):
78
+ self.gpu_comm_done_event.record(self.comm_stream)
79
+
80
+ def _signal_compute_done(self):
81
+ self.gpu_compute_done_event.record(self.compute_stream)
82
+
83
+ def _wait_compute_done(self):
84
+ self.comm_stream.wait_event(self.gpu_compute_done_event)
85
+
86
+ def _wait_comm_done(self):
87
+ self.compute_stream.wait_event(self.gpu_comm_done_event)
88
+
89
+ def _cpu_yield(self):
90
+ # It is critical for correctness that only one thread is running
91
+ # at a time. These asserts just make sure that this is the only
92
+ # thread running before waking the other one up and going to sleep
93
+ assert forward_context._forward_context == self.forward_context
94
+ assert current_stream() == self.current_stream
95
+ assert not self.cpu_wait_event.is_set()
96
+
97
+ self.cpu_signal_event.set()
98
+ self.cpu_wait_event.wait()
99
+ self.cpu_wait_event.clear()
100
+ self._restore_context()
101
+
102
+ def switch_to_comm_sync(self):
103
+ self._signal_compute_done()
104
+ self.update_stream(self.comm_stream)
105
+ self._wait_comm_done()
106
+
107
+ def maybe_run_recv_hook(self):
108
+ if self.recv_hook is not None:
109
+ self.recv_hook()
110
+ self.recv_hook = None
111
+
112
+ def yield_(self):
113
+ self.current_stream = current_stream()
114
+ self._cpu_yield()
115
+ if self.current_stream != current_stream():
116
+ self.update_stream(self.current_stream)
117
+
118
+ def yield_and_switch_from_compute_to_comm(self):
119
+ assert current_stream() == self.compute_stream
120
+ self._signal_compute_done()
121
+ self._cpu_yield()
122
+ assert self.current_stream == self.compute_stream
123
+ self.update_stream(self.comm_stream)
124
+ self._wait_compute_done()
125
+
126
+ def yield_and_switch_from_comm_to_compute(self):
127
+ assert current_stream() == self.comm_stream
128
+ self._signal_comm_done()
129
+ self._cpu_yield()
130
+ assert self.current_stream == self.comm_stream
131
+ self.update_stream(self.compute_stream)
132
+ self._wait_comm_done()
133
+
134
+
135
+def dbo_enabled() -> bool:
136
+ return len(_THREAD_ID_TO_CONTEXT) > 0
137
+
138
+
139
+def dbo_current_ubatch_id() -> int:
140
+ if len(_THREAD_ID_TO_CONTEXT) == 0:
141
+ return 0
142
+ return _THREAD_ID_TO_CONTEXT[threading.get_ident()]
143
+
144
+
145
+def _register_ubatch_function(func):
146
+
147
+ def wrapper(*args, **kwargs):
148
+ if len(_THREAD_ID_TO_CONTEXT) > 0:
149
+ ctx_idx = _THREAD_ID_TO_CONTEXT[threading.get_ident()]
150
+ ctx = _CURRENT_CONTEXTS[ctx_idx]
151
+ func(ctx, *args, **kwargs)
152
+
153
+ return wrapper
154
+
155
+
156
+dbo_yield_and_switch_from_compute_to_comm = _register_ubatch_function(
157
+ UBatchContext.yield_and_switch_from_compute_to_comm)
158
+dbo_yield_and_switch_from_comm_to_compute = _register_ubatch_function(
159
+ 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)
163
+dbo_switch_to_comm_sync = _register_ubatch_function(
164
+ UBatchContext.switch_to_comm_sync)
165
+
166
+
167
+def dbo_register_recv_hook(recv_hook):
168
+ if len(_THREAD_ID_TO_CONTEXT) > 0:
169
+ ctx_idx = _THREAD_ID_TO_CONTEXT[threading.get_ident()]
170
+ next_ctx = _CURRENT_CONTEXTS[(ctx_idx + 1) % 2]
171
+ next_ctx.recv_hook = recv_hook
172
+
173
+
174
+def make_ubatch_contexts(
175
+ num_micro_batches: int,
176
+ compute_stream: torch.cuda.Stream,
177
+ comm_stream: torch.cuda.Stream,
178
+ forward_contexts: list[ForwardContext],
179
+ ready_barrier: threading.Barrier,
180
+ schedule: str = "default",
181
+) -> list[UBatchContext]:
182
+ assert num_micro_batches == 2, "only been tested with 2 micro-batches"
183
+ """
184
+ Create a context manager for micro-batching synchronization.
185
+ """
186
+ cpu_events = [threading.Event() for _ in range(num_micro_batches)]
187
+ gpu_comm_done_events = [
188
+ torch.cuda.Event() for _ in range(num_micro_batches)
189
+ ]
190
+ gpu_compute_done_events = [
191
+ torch.cuda.Event() for _ in range(num_micro_batches)
192
+ ]
193
+
194
+ assert len(forward_contexts) == 2
195
+
196
+ ctxs = []
197
+ for i in range(num_micro_batches):
198
+ ctx = UBatchContext(id=i,
199
+ compute_stream=compute_stream,
200
+ comm_stream=comm_stream,
201
+ forward_context=forward_contexts[i],
202
+ ready_barrier=ready_barrier,
203
+ cpu_wait_event=cpu_events[i],
204
+ cpu_signal_event=cpu_events[(i + 1) %
205
+ num_micro_batches],
206
+ gpu_comm_done_event=gpu_comm_done_events[i],
207
+ gpu_compute_done_event=gpu_compute_done_events[i],
208
+ schedule=schedule)
209
+ ctxs.append(ctx)
210
+
211
+ return ctxs
vllm/v1/worker/utils.py
+9
-1
@@ -130,9 +130,17 @@ class MultiModalBudget:
130
@dataclass
131
class AttentionGroup:
132
backend: type[AttentionBackend]
133
- metadata_builder: AttentionMetadataBuilder
133
+ metadata_builders: list[AttentionMetadataBuilder]
134
layer_names: list[str]
135
136
+ def get_metadata_builder(self,
137
+ ubatch_id: Optional[int] = None
138
+ ) -> AttentionMetadataBuilder:
139
+ if ubatch_id is None:
140
+ return self.metadata_builders[0]
141
+ assert len(self.metadata_builders) > ubatch_id
142
+ return self.metadata_builders[ubatch_id]
143
+
144
145
def sanity_check_mm_encoder_outputs(
146
mm_embeddings: MultiModalEmbeddings,