main
py 394 lines 14.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import time
5 from collections import defaultdict
6 from contextlib import contextmanager
7 from dataclasses import dataclass, field
8 from typing import Any
9
10 import torch
11
12 import vllm.envs as envs
13 from vllm.config import CUDAGraphMode, ParallelConfig, VllmConfig
14 from vllm.logger import init_logger
15 from vllm.platforms import current_platform
16 from vllm.v1.attention.backend import AttentionMetadata
17 from vllm.v1.worker.dp_utils import coordinate_batch_across_dp
18 from vllm.v1.worker.ubatch_utils import UBatchSlices
19
20 logger = init_logger(__name__)
21
22 track_batchsize: bool = envs.VLLM_LOG_BATCHSIZE_INTERVAL >= 0
23 last_logging_time: float = 0
24 forward_start_time: float = 0
25 batchsize_logging_interval: float = envs.VLLM_LOG_BATCHSIZE_INTERVAL
26 batchsize_forward_time: defaultdict = defaultdict(list)
27
28
29 @dataclass(frozen=True)
30 class BatchDescriptor:
31 """Batch descriptor for cudagraph dispatching. We should keep the num of
32 items as minimal as possible to properly and uniquely describe the padded
33 batch for cudagraph.
34 """
35
36 num_tokens: int
37 num_reqs: int | None = None
38 """
39 Number of requests in the batch. Can be None for PIECEWISE cudagraphs where
40 the cudagraphs can handle any number of requests.
41 """
42 uniform: bool = False
43 """
44 True if all the requests in the batch have the same number of tokens.
45 """
46 has_lora: bool = False
47 """
48 Whether this batch has active LoRA adapters.
49 """
50 num_active_loras: int = 0
51 """
52 Number of distinct active LoRA adapters in this batch.
53 When cudagraph_specialize_lora_count is enabled, separate CUDA graphs
54 are captured for each num_active_loras value. This allows kernels
55 (like fused_moe_lora) whose grid size depends on num_active_loras
56 to be properly captured.
57 """
58
59
60 def _compute_sp_num_tokens(
61 num_tokens_across_dp_cpu: torch.Tensor,
62 sequence_parallel_size: int,
63 pcp_size: int = 1,
64 use_ep: bool = True,
65 ) -> list[int]:
66 if pcp_size > 1:
67 num_tokens_across_dp_cpu = (
68 num_tokens_across_dp_cpu.repeat_interleave(pcp_size)
69 if use_ep
70 else num_tokens_across_dp_cpu * pcp_size
71 )
72 sp_tokens = (
73 num_tokens_across_dp_cpu + sequence_parallel_size - 1
74 ) // sequence_parallel_size
75
76 sp_tokens = sp_tokens.repeat_interleave(sequence_parallel_size)
77 return sp_tokens.tolist()
78
79
80 @dataclass
81 class DPMetadata:
82 num_tokens_across_dp_cpu: torch.Tensor
83
84 # NOTE: local_sizes should only be set by the chunked_sizes context manager
85 local_sizes: list[int] | None = None
86
87 @staticmethod
88 def make(
89 parallel_config: ParallelConfig,
90 num_tokens: int,
91 num_tokens_across_dp_cpu: torch.Tensor,
92 ) -> "DPMetadata":
93 assert num_tokens_across_dp_cpu is not None
94 assert (
95 parallel_config.data_parallel_size > 1
96 or parallel_config.use_sequence_parallel_moe
97 )
98 assert parallel_config.is_moe_model is not False
99 dp_rank = parallel_config.data_parallel_rank
100 batchsize = num_tokens
101
102 # If num_tokens_across_dp is None, it will be computed by all_reduce
103 # Otherwise, num_tokens_across_dp[dp_rank] should be equal to batchsize
104 assert num_tokens_across_dp_cpu[dp_rank] == batchsize, (
105 f"{num_tokens_across_dp_cpu[dp_rank]} {batchsize}"
106 )
107 return DPMetadata(num_tokens_across_dp_cpu)
108
109 @contextmanager
110 def sp_local_sizes(
111 self, sequence_parallel_size: int, pcp_size: int = 1, use_ep: bool = False
112 ):
113 """Context manager for setting self.local_sizes. Same as self.chunked_sizes
114 but without any chunking.
115 """
116 self.local_sizes = _compute_sp_num_tokens(
117 self.num_tokens_across_dp_cpu, sequence_parallel_size, pcp_size, use_ep
118 )
119 try:
120 yield self.local_sizes
121 finally:
122 self.local_sizes = None
123
124 def get_chunk_sizes_across_dp_rank(self) -> list[int] | None:
125 assert self.local_sizes is not None
126 return self.local_sizes
127
128 # Get the cumulative tokens across sequence parallel ranks.
129 # In this case the input to the MoEs will be distributed w.r.t both
130 # DP and TP rank.
131 # When sp_size==1, this is just the cumulative num tokens across DP.
132 def cu_tokens_across_sp(self, sp_size: int) -> torch.Tensor:
133 num_tokens_across_sp_cpu = (
134 self.num_tokens_across_dp_cpu - 1 + sp_size
135 ) // sp_size
136 num_tokens_across_sp_cpu = num_tokens_across_sp_cpu.repeat_interleave(sp_size)
137 return torch.cumsum(num_tokens_across_sp_cpu, dim=0)
138
139
140 @dataclass
141 class ForwardContext:
142 # copy from vllm_config.compilation_config.static_forward_context
143 no_compile_layers: dict[str, Any]
144 attn_metadata: dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]]
145 slot_mapping: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]]
146 """
147 Type Dict[str, AttentionMetadata] for v1, map from layer_name of each
148 attention layer to its attention metadata
149 Type List[Dict[str, AttentionMetadata]] for DBO. List of size two, one
150 for each microbatch.
151 Set dynamically for each forward pass
152 """
153 # set dynamically for each forward pass
154 dp_metadata: DPMetadata | None = None
155 # determine the cudagraph style at runtime to be FULL, PIECEWISE, or NONE.
156 # by default NONE, no cudagraph is used.
157 cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE
158 batch_descriptor: BatchDescriptor | None = None
159
160 ubatch_slices: UBatchSlices | None = None
161
162 # Boolean mask over the token axis: True for padding rows that are not real
163 # tokens. Consumers can use it to skip work for padded tokens. None when
164 # the producer does not set it.
165 is_padding: torch.Tensor | None = None
166
167 # If True, bypass the compiled model call, e.g. by using .forward() directly
168 skip_compiled: bool = False
169
170 # For torch.compile cold start times, we need to avoid hard-coding
171 # any strings into the graph. Right now, the vllm.moe_forward
172 # and vllm.moe_forward_shared custom operators hard-code strings into
173 # the graph.
174 #
175 # The workaround is to store a list of the strings that each of those
176 # custom ops needs in the ForwardContext (all_moe_layers)
177 # as well as a counter (moe_layer_index).
178 # The ForwardContext object is alive for the duration of the forward pass.
179 # When the custom op needs a layer string, get the next string
180 # from all_moe_layers and increment the counter.
181 #
182 # This assumes that the custom operators will always be executed in
183 # order and that torch.compile will not try to reorder these
184 # operations with respect to each other.
185 #
186 # TODO(https://github.com/vllm-project/vllm/issues/31985):
187 # There are longer-term solutions, like unwrapping the moe custom operator,
188 # that aren't ready yet.
189 # We could also treat the string as a "symbolic input" to the graph but
190 # the PyTorch-side bits for that aren't ready yet either.
191 #
192 # If this value is None (like in some tests), then we end up baking the string
193 # into the graph. Otherwise, the moe custom ops will pop a string from this list.
194 all_moe_layers: list[str] | None = None
195 moe_layer_index: int = 0
196
197 additional_kwargs: dict[str, Any] = field(default_factory=dict)
198
199 def __post_init__(self):
200 assert self.cudagraph_runtime_mode.is_valid_runtime_mode(), (
201 f"Invalid cudagraph runtime mode: {self.cudagraph_runtime_mode}"
202 )
203
204
205 _forward_context: ForwardContext | None = None
206
207
208 def get_forward_context() -> ForwardContext:
209 """Get the current forward context."""
210 assert _forward_context is not None, (
211 "Forward context is not set. "
212 "Please use `set_forward_context` to set the forward context."
213 )
214 return _forward_context
215
216
217 def is_forward_context_available() -> bool:
218 return _forward_context is not None
219
220
221 def in_piecewise_cudagraph() -> bool:
222 """Whether the current forward runs in piecewise cudagraph mode (graph
223 segments separated by eager breaks), at capture or replay time."""
224 return (
225 is_forward_context_available()
226 and get_forward_context().cudagraph_runtime_mode == CUDAGraphMode.PIECEWISE
227 )
228
229
230 def create_forward_context(
231 attn_metadata: Any,
232 vllm_config: VllmConfig,
233 dp_metadata: DPMetadata | None = None,
234 cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
235 batch_descriptor: BatchDescriptor | None = None,
236 ubatch_slices: UBatchSlices | None = None,
237 slot_mapping: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None = None,
238 additional_kwargs: dict[str, Any] | None = None,
239 skip_compiled: bool = False,
240 is_padding: torch.Tensor | None = None,
241 ):
242 if vllm_config.compilation_config.fast_moe_cold_start:
243 all_moe_layers = vllm_config.compilation_config.static_all_moe_layers
244 else:
245 all_moe_layers = None
246
247 return ForwardContext(
248 no_compile_layers=vllm_config.compilation_config.static_forward_context,
249 all_moe_layers=all_moe_layers,
250 attn_metadata=attn_metadata,
251 slot_mapping=slot_mapping or {},
252 dp_metadata=dp_metadata,
253 cudagraph_runtime_mode=cudagraph_runtime_mode,
254 batch_descriptor=batch_descriptor,
255 ubatch_slices=ubatch_slices,
256 skip_compiled=skip_compiled,
257 additional_kwargs=additional_kwargs or {},
258 is_padding=is_padding,
259 )
260
261
262 @contextmanager
263 def override_forward_context(forward_context: ForwardContext | None):
264 """A context manager that overrides the current forward context.
265 This is used to override the forward context for a specific
266 forward pass.
267 """
268 global _forward_context
269 prev_context = _forward_context
270 _forward_context = forward_context
271 try:
272 yield
273 finally:
274 _forward_context = prev_context
275
276
277 @contextmanager
278 def set_forward_context(
279 attn_metadata: Any,
280 vllm_config: VllmConfig,
281 num_tokens: int | None = None,
282 num_tokens_across_dp: torch.Tensor | None = None,
283 cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
284 batch_descriptor: BatchDescriptor | None = None,
285 ubatch_slices: UBatchSlices | None = None,
286 slot_mapping: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None = None,
287 skip_compiled: bool = False,
288 is_padding: torch.Tensor | None = None,
289 ):
290 """A context manager that stores the current forward context,
291 can be attention metadata, etc.
292 Here we can inject common logic for every model forward pass.
293 """
294 global forward_start_time
295 need_to_track_batchsize = track_batchsize and attn_metadata is not None
296 if need_to_track_batchsize:
297 forward_start_time = time.perf_counter()
298
299 dp_metadata: DPMetadata | None = None
300 if (
301 (
302 vllm_config.parallel_config.data_parallel_size > 1
303 or vllm_config.parallel_config.use_sequence_parallel_moe
304 )
305 and vllm_config.parallel_config.is_moe_model is not False
306 and (attn_metadata is not None or num_tokens is not None)
307 ):
308 # If num_tokens_across_dp hasn't already been initialized, then
309 # initialize it here. Both DP padding and Microbatching will be
310 # disabled.
311 if (
312 num_tokens_across_dp is None
313 and vllm_config.parallel_config.data_parallel_size > 1
314 ):
315 assert ubatch_slices is None
316 assert num_tokens is not None
317 _, num_tokens_across_dp, _ = coordinate_batch_across_dp(
318 num_tokens_unpadded=num_tokens,
319 parallel_config=vllm_config.parallel_config,
320 allow_microbatching=False,
321 )
322 assert num_tokens_across_dp is not None
323 elif num_tokens_across_dp is None:
324 assert num_tokens is not None
325 num_tokens_across_dp = torch.tensor([num_tokens], dtype=torch.int32)
326 dp_metadata = DPMetadata.make(
327 vllm_config.parallel_config, num_tokens or 0, num_tokens_across_dp
328 )
329
330 # Convenience: if cudagraph is used and num_tokens is given, we can just
331 # create a batch descriptor here if not given (there's no harm since if it
332 # doesn't match in the wrapper it'll fall through).
333 if cudagraph_runtime_mode != CUDAGraphMode.NONE and num_tokens is not None:
334 batch_descriptor = batch_descriptor or BatchDescriptor(num_tokens=num_tokens)
335
336 additional_kwargs = current_platform.set_additional_forward_context(
337 attn_metadata=attn_metadata,
338 vllm_config=vllm_config,
339 dp_metadata=dp_metadata,
340 num_tokens=num_tokens,
341 num_tokens_across_dp=num_tokens_across_dp,
342 cudagraph_runtime_mode=cudagraph_runtime_mode,
343 batch_descriptor=batch_descriptor,
344 ubatch_slices=ubatch_slices,
345 )
346
347 forward_context = create_forward_context(
348 attn_metadata,
349 vllm_config,
350 dp_metadata,
351 cudagraph_runtime_mode,
352 batch_descriptor,
353 ubatch_slices,
354 slot_mapping,
355 additional_kwargs,
356 skip_compiled,
357 is_padding=is_padding,
358 )
359
360 try:
361 with override_forward_context(forward_context):
362 yield
363 finally:
364 global last_logging_time, batchsize_logging_interval
365 if need_to_track_batchsize:
366 batchsize = num_tokens
367 # we use synchronous scheduling right now,
368 # adding a sync point here should not affect
369 # scheduling of the next batch
370 synchronize = current_platform.synchronize
371 if synchronize is not None:
372 synchronize()
373 now = time.perf_counter()
374 # time measurement is in milliseconds
375 batchsize_forward_time[batchsize].append((now - forward_start_time) * 1000)
376 if now - last_logging_time > batchsize_logging_interval:
377 last_logging_time = now
378 forward_stats = []
379 for bs, times in batchsize_forward_time.items():
380 if len(times) <= 1:
381 # can be cudagraph / profiling run
382 continue
383 medium = torch.quantile(torch.tensor(times), q=0.5).item()
384 medium = round(medium, 2)
385 forward_stats.append((bs, len(times), medium))
386 forward_stats.sort(key=lambda x: x[1], reverse=True)
387 if forward_stats:
388 logger.info(
389 (
390 "Batchsize forward time stats "
391 "(batchsize, count, median_time(ms)): %s"
392 ),
393 forward_stats,
394 )