main
py 1,654 lines 68.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """A GPU worker class."""
4
5 import gc
6 import os
7 import time
8 from collections.abc import Callable
9 from contextlib import AbstractContextManager, contextmanager, nullcontext
10 from datetime import timedelta
11 from fnmatch import filter as fnmatch_filter
12 from types import NoneType
13 from typing import TYPE_CHECKING, Any, cast
14
15 import numpy as np
16 import torch
17 import torch.nn as nn
18
19 import vllm.envs as envs
20 from vllm.config import CUDAGraphMode, VllmConfig, set_current_vllm_config
21 from vllm.config.compilation import CompilationMode
22 from vllm.device_allocator import get_mem_allocator_instance
23 from vllm.distributed import (
24 ensure_model_parallel_initialized,
25 init_distributed_environment,
26 set_custom_all_reduce,
27 )
28 from vllm.distributed.ec_transfer import (
29 ensure_ec_transfer_initialized,
30 ensure_ec_transfer_shutdown,
31 get_ec_transfer,
32 has_ec_transfer,
33 )
34 from vllm.distributed.eplb.eplb_utils import override_envs_for_eplb
35 from vllm.distributed.kv_transfer import (
36 ensure_kv_transfer_initialized,
37 ensure_kv_transfer_shutdown,
38 get_kv_transfer_group,
39 has_kv_transfer_group,
40 )
41 from vllm.distributed.kv_transfer.kv_connector.v1.base import (
42 KVConnectorHandshakeMetadata,
43 )
44 from vllm.distributed.parallel_state import (
45 Handle,
46 checkpoint_prepare_distributed_state,
47 checkpoint_restore_distributed_state,
48 get_pcp_group,
49 get_pp_group,
50 get_tp_group,
51 resume_device_comms,
52 suspend_device_comms,
53 )
54 from vllm.distributed.weight_transfer import (
55 WeightTransferEngine,
56 WeightTransferEngineFactory,
57 )
58 from vllm.logger import init_logger
59 from vllm.lora.request import LoRARequest
60 from vllm.model_executor.model_loader import get_model_loader
61 from vllm.model_executor.model_loader.weight_checksum import (
62 compute_tensor_digests,
63 zero_weights,
64 )
65 from vllm.model_executor.warmup.kernel_warmup import kernel_warmup
66 from vllm.multimodal.gpu_ipc_memory import reserve_mm_ipc_gpu_memory
67 from vllm.platforms import current_platform
68 from vllm.profiler.wrapper import (
69 create_graph_capture_profiler,
70 create_worker_profiler,
71 validate_worker_profiler_config,
72 )
73 from vllm.sequence import IntermediateTensors
74 from vllm.tasks import SupportedTask
75 from vllm.tracing import instrument
76 from vllm.utils.gc_utils import freeze_gc_heap, maybe_attach_gc_debug_callback
77 from vllm.utils.gpu_sync_debug import enable_gpu_sync_check, with_gpu_sync_check
78 from vllm.utils.mem_constants import GiB_bytes
79 from vllm.utils.mem_utils import (
80 MemoryProfilingResult,
81 MemorySnapshot,
82 format_gib,
83 memory_profiling,
84 )
85 from vllm.utils.torch_utils import set_random_seed, set_torch_threads_for_runtime
86 from vllm.v1.attention.backends.utils import record_kv_cache_layout
87 from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
88 from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheSpec
89 from vllm.v1.outputs import (
90 AsyncModelRunnerOutput,
91 DraftTokenIds,
92 ModelRunnerOutput,
93 )
94 from vllm.v1.utils import compute_iteration_details, report_usage_stats
95 from vllm.v1.worker.sentinel.gpu_worker_sentinel import WorkerSentinel
96 from vllm.v1.worker.startup_plan import (
97 maybe_apply_startup_plan,
98 maybe_save_startup_plan,
99 )
100 from vllm.v1.worker.utils import is_residual_scattered_for_sp
101 from vllm.v1.worker.worker_base import CompilationTimes, WorkerBase
102 from vllm.v1.worker.workspace import init_workspace_manager
103
104 from ...model_executor.model_loader import TensorizerLoader
105 from .gpu.cudagraph_utils import has_compiled_submodule
106 from .gpu.warmup import warmup_kernels
107 from .utils import request_memory
108
109 logger = init_logger(__name__)
110
111
112 def _num_workspace_lanes(vllm_config: VllmConfig, use_v2_model_runner: bool) -> int:
113 spec_config = vllm_config.speculative_config
114 return (
115 2
116 if use_v2_model_runner and spec_config is not None and spec_config.use_dspark()
117 else 1
118 )
119
120
121 def maybe_rocm_profiling_fallback(profile_result: MemoryProfilingResult) -> int | None:
122 """Memory in bytes to size the KV cache from when profiling measured a
123 release on ROCm, or None to keep what profiling measured.
124
125 The measurement is the drop in free device memory across startup, so it comes
126 out negative when anything else on the device released memory meanwhile. Torch
127 reserved is process local, so it stands in as a lower bound instead of refusing
128 to start. Kept to ROCm, where the AMD CI groups hit this, following #45490.
129 """
130 if profile_result.total_consumed >= 0 or not current_platform.is_rocm():
131 return None
132
133 baseline = profile_result.before_create
134 after_profile = profile_result.after_profile
135 torch_reserved = max(after_profile.torch_memory - baseline.torch_memory, 0)
136 logger.warning(
137 "Free memory grew by %s GiB during profiling (initial %s GiB, current "
138 "%s GiB). Sizing the KV cache from the %s GiB torch reserved instead of "
139 "the profiling difference.",
140 format_gib(-profile_result.total_consumed),
141 format_gib(baseline.free_memory),
142 format_gib(after_profile.free_memory),
143 format_gib(torch_reserved),
144 )
145 return torch_reserved
146
147
148 if TYPE_CHECKING:
149 from vllm.device_allocator.sleep_mode_backend import SleepModeBackend
150 from vllm.model_executor.model_loader.tensorizer import TensorizerConfig
151 from vllm.v1.worker.gpu.model_runner import GPUModelRunner as GPUModelRunnerV2
152 from vllm.v1.worker.gpu_model_runner import GPUModelRunner
153
154
155 class AsyncIntermediateTensors(IntermediateTensors):
156 """IntermediateTensors with lazy comm synchronization"""
157
158 def __init__(
159 self,
160 tensors: dict[str, torch.Tensor],
161 comm_handles: list[Handle] | None = None,
162 comm_postprocess: list[Callable[[], None]] | None = None,
163 ) -> None:
164 super().__init__(tensors)
165 self._comm_handles = comm_handles
166 self._comm_postprocess = comm_postprocess
167 self._comm_waited = False
168
169 def wait_for_comm(self) -> None:
170 if self._comm_waited:
171 return
172 if self._comm_handles:
173 for handle in self._comm_handles:
174 handle.wait()
175 if self._comm_postprocess:
176 for fn in self._comm_postprocess:
177 fn()
178 self._comm_waited = True
179
180 def __getattribute__(self, name: str):
181 # ensure `.tensors` is ready before use
182 if name == "tensors" and not object.__getattribute__(self, "_comm_waited"):
183 object.__getattribute__(self, "wait_for_comm")()
184 return object.__getattribute__(self, name)
185
186
187 class Worker(WorkerBase):
188 def __init__(
189 self,
190 vllm_config: VllmConfig,
191 local_rank: int,
192 rank: int,
193 distributed_init_method: str,
194 is_driver_worker: bool = False,
195 ):
196 super().__init__(
197 vllm_config=vllm_config,
198 local_rank=local_rank,
199 rank=rank,
200 distributed_init_method=distributed_init_method,
201 is_driver_worker=is_driver_worker,
202 )
203
204 # configure float32 matmul precision according to vLLM env.
205 precision = envs.VLLM_FLOAT32_MATMUL_PRECISION
206 torch.set_float32_matmul_precision(precision)
207
208 from vllm.distributed.elastic_ep.elastic_execute import ElasticEPScalingExecutor
209
210 self.elastic_ep_executor = ElasticEPScalingExecutor(self)
211 self.worker_sentinel: WorkerSentinel | None = None
212 if self.parallel_config.enable_fault_tolerance:
213 self.worker_sentinel = WorkerSentinel(worker=self)
214 # Buffers saved before sleep
215 self._sleep_saved_buffers: dict[str, torch.Tensor] = {}
216 self._sleep_saved_parameters: dict[str, torch.Tensor] = {}
217 self._sleep_saved_draft_buffers: dict[str, torch.Tensor] = {}
218
219 # Weight transfer engine is created in `load_model` once the model
220 # is available, since the engine needs a reference to the model.
221 self.weight_transfer_engine: WeightTransferEngine | None = None
222 self._weight_update_active = False
223 self._weight_update_is_draft = False
224
225 # Worker profiler. Enabled and configured through profiler_config.
226 # Profiler wrapper is created lazily in profile() when start is called,
227 # so we have all the information needed for proper trace naming.
228 self.profiler: Any | None = None
229 self.profiler_config = vllm_config.profiler_config
230 validate_worker_profiler_config(self.profiler_config)
231
232 self.use_v2_model_runner = vllm_config.use_v2_model_runner
233
234 # Device handles of the previous step's PP intermediate-tensor send.
235 self._pp_send_work: list[Handle] = []
236
237 # Resolved lazily on first sleep/wake; persists worker-process state.
238 self._sleep_mode_backend: SleepModeBackend | None = None
239
240 @property
241 def sleep_mode_backend(self) -> "SleepModeBackend":
242 if self._sleep_mode_backend is None:
243 from vllm.device_allocator.sleep_mode_backend import (
244 SleepModeBackendFactory,
245 )
246
247 self._sleep_mode_backend = SleepModeBackendFactory.create_backend(
248 self.vllm_config.model_config
249 )
250 return self._sleep_mode_backend
251
252 def _save_sleep_parameters(self, model: nn.Module) -> None:
253 patterns = self.model_config.sleep_preserve_parameter_names
254 if not patterns:
255 self._sleep_saved_parameters = {}
256 return
257 parameters = dict(model.named_parameters())
258 names: set[str] = set()
259 for pattern in patterns:
260 matches = fnmatch_filter(parameters, pattern)
261 if not matches:
262 raise ValueError(f"No parameter matches sleep retention: {pattern}")
263 names.update(matches)
264 self._sleep_saved_parameters = {
265 name: param.detach().to("cpu")
266 for name, param in parameters.items()
267 if name in names and not param.is_cpu
268 }
269
270 @torch.no_grad()
271 def _restore_sleep_parameters(self, model: nn.Module) -> None:
272 for name, value in self._sleep_saved_parameters.items():
273 model.get_parameter(name).copy_(value)
274 self._sleep_saved_parameters.clear()
275
276 def sleep(self, level: int = 1) -> None:
277 torch.accelerator.synchronize()
278 free_bytes_before_sleep = torch.accelerator.get_memory_info()[0]
279
280 # Save the buffers before level 2 sleep
281 if level == 2:
282 model = self.model_runner.model
283 self._save_sleep_parameters(model)
284 self._sleep_saved_buffers = {
285 name: buffer.cpu().clone() for name, buffer in model.named_buffers()
286 }
287 draft = self.get_draft_model()
288 if draft is not None:
289 self._sleep_saved_draft_buffers = {
290 name: buffer.cpu().clone() for name, buffer in draft.named_buffers()
291 }
292
293 self.sleep_mode_backend.suspend(level)
294 if self.vllm_config.model_config.enable_nccl_comm_suspend:
295 suspend_device_comms()
296
297 torch.accelerator.synchronize()
298 deadline = time.monotonic() + (5.0 if current_platform.is_rocm() else 0)
299 while True:
300 free_bytes_after_sleep, total = torch.accelerator.get_memory_info()
301 freed_bytes = free_bytes_after_sleep - free_bytes_before_sleep
302 if freed_bytes >= 0 or time.monotonic() >= deadline:
303 break
304 time.sleep(0.1)
305
306 used_bytes = total - free_bytes_after_sleep
307 assert freed_bytes >= 0, "Memory usage increased after sleeping."
308 logger.info(
309 "Sleep mode freed %s GiB memory, %s GiB memory is still in use.",
310 format_gib(freed_bytes),
311 format_gib(used_bytes),
312 )
313
314 def wake_up(self, tags: list[str] | None = None) -> None:
315 self.sleep_mode_backend.resume(tags)
316 if self.vllm_config.model_config.enable_nccl_comm_suspend:
317 resume_device_comms()
318
319 # Restore the buffers after level 2 sleep
320 wake_weights = tags is None or "weights" in tags
321 if wake_weights and self._sleep_saved_parameters:
322 self._restore_sleep_parameters(self.model_runner.model)
323 if wake_weights and len(self._sleep_saved_buffers):
324 model = self.model_runner.model
325 for name, buffer in model.named_buffers():
326 if name in self._sleep_saved_buffers:
327 buffer.data.copy_(self._sleep_saved_buffers[name].data)
328 self._sleep_saved_buffers = {}
329
330 if wake_weights and len(self._sleep_saved_draft_buffers):
331 draft = self.get_draft_model()
332 if draft is not None:
333 for name, buffer in draft.named_buffers():
334 if name in self._sleep_saved_draft_buffers:
335 buffer.data.copy_(self._sleep_saved_draft_buffers[name].data)
336 self._sleep_saved_draft_buffers = {}
337
338 self.synchronize_device()
339
340 def discard(self, tags: tuple[str, ...]) -> None:
341 self.sleep_mode_backend.discard(tags)
342
343 def checkpoint_prepare(self) -> None:
344 checkpoint_prepare_distributed_state()
345
346 def checkpoint_restore(self) -> None:
347 checkpoint_restore_distributed_state()
348
349 def _weight_checksum_prefix(self) -> str:
350 # data_parallel_rank is 0 in dense workers; only the index is unique.
351 dp = self.parallel_config.data_parallel_index
352 pp = get_pp_group().rank_in_group
353 pcp = get_pcp_group().rank_in_group
354 tp = get_tp_group().rank_in_group
355 return f"dp{dp}:pp{pp}:pcp{pcp}:tp{tp}:"
356
357 def compute_weight_checksums(self) -> dict[str, str]:
358 prefix = self._weight_checksum_prefix()
359 digests = compute_tensor_digests(self.model_runner.get_model())
360 return {prefix + name: digest for name, digest in digests.items()}
361
362 def reset_weights(self) -> None:
363 zero_weights(self.model_runner.get_model())
364
365 def _maybe_get_memory_pool_context(self, tag: str) -> AbstractContextManager:
366 if (
367 current_platform.is_cuda_alike()
368 and not self.vllm_config.model_config.enable_cumem_allocator
369 ):
370 return nullcontext()
371
372 if (
373 current_platform.is_xpu()
374 and not self.vllm_config.model_config.enable_sleep_mode
375 ):
376 return nullcontext()
377
378 if current_platform.is_cpu():
379 return nullcontext()
380
381 allocator = get_mem_allocator_instance()
382 if tag == "weights":
383 assert allocator.get_current_usage() == 0, (
384 "CuMem allocator can only be used for one instance per process."
385 )
386 return allocator.use_memory_pool(tag=tag)
387
388 @contextmanager
389 def _scoped_allocator_max_split(self, max_split_size_mb: int):
390 """Limit CUDA/ROCm allocator splitting, restoring settings on exit."""
391 if (
392 not current_platform.is_cuda_alike()
393 or torch.cuda.memory.get_allocator_backend() != "native"
394 ):
395 yield
396 return
397
398 get_settings = getattr(torch._C, "_accelerator_getAllocatorSettings", None)
399 if get_settings is not None:
400 original_settings = get_settings()
401 else:
402 original_settings = torch.cuda.memory._snapshot()["allocator_settings"][
403 "PYTORCH_CUDA_ALLOC_CONF"
404 ]
405
406 # The setter resets other allocator options unless they are included.
407 prefix = original_settings.strip().rstrip(",")
408 settings = f"max_split_size_mb:{max_split_size_mb}"
409 if prefix:
410 settings = f"{prefix},{settings}"
411 try:
412 torch._C._accelerator_setAllocatorSettings(settings)
413 yield
414 finally:
415 torch._C._accelerator_setAllocatorSettings(original_settings)
416
417 @instrument(span_name="Init device")
418 def init_device(self):
419 if self.device_config.device_type == "cuda":
420 # This env var set by Ray causes exceptions with graph building.
421 os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
422 parallel_config = self.parallel_config
423 if (
424 parallel_config.distributed_executor_backend
425 not in ("ray", "external_launcher")
426 and parallel_config.data_parallel_backend != "ray"
427 and parallel_config.nnodes_within_dp == 1
428 ):
429 # Use local DP rank if available, otherwise use global DP rank.
430 dp_local_rank = self.parallel_config.data_parallel_rank_local
431 if dp_local_rank is None:
432 dp_local_rank = self.parallel_config.data_parallel_index
433
434 tp_pcp_pp_world_size = (
435 self.parallel_config.pipeline_parallel_size
436 * self.parallel_config.prefill_context_parallel_size
437 * self.parallel_config.tensor_parallel_size
438 )
439
440 # DP_LOCAL_RANK * TP_PCP_PP_WORLD_SIZE + TP_LOCAL_RANK
441 self.local_rank += dp_local_rank * tp_pcp_pp_world_size
442
443 # Publish the logical-to-physical mapping for topology queries
444 # such as NIC affinity and P2P checks.
445 assigned_physical_gpu_ids = parallel_config.assigned_physical_gpu_ids
446 if assigned_physical_gpu_ids is not None:
447 from vllm.platforms.interface import set_assigned_physical_gpu_ids
448
449 set_assigned_physical_gpu_ids(assigned_physical_gpu_ids)
450 assert self.local_rank < len(assigned_physical_gpu_ids), (
451 f"local_rank {self.local_rank} is out of bounds for "
452 f"assigned_physical_gpu_ids {assigned_physical_gpu_ids}"
453 )
454 # NOTE(patch pr45026): local_world_size is derived from
455 # parallel_config.nnodes, which is only set for the "mp"
456 # multi-node backend. With the "ray"/"external_launcher"
457 # backends nnodes stays 1, so local_world_size collapses to
458 # the full world_size and this check wrongly fires on
459 # cross-node deployments. assigned_physical_gpu_ids is already
460 # per-node and the local_rank bound above fully validates the
461 # mapping for these backends, so skip the check for them.
462 if parallel_config.distributed_executor_backend not in (
463 "ray",
464 "external_launcher",
465 ):
466 assert self.parallel_config.local_world_size <= len(
467 assigned_physical_gpu_ids
468 ), (
469 f"local_world_size ({self.parallel_config.local_world_size})"
470 " exceeds assigned_physical_gpu_ids count "
471 f"({len(assigned_physical_gpu_ids)})"
472 )
473 else:
474 assert self.local_rank < torch.accelerator.device_count(), (
475 f"DP adjusted local rank {self.local_rank} is out of "
476 f"bounds for {torch.accelerator.device_count()} devices."
477 )
478
479 visible_device_index = (
480 current_platform.logical_device_id_to_visible_device_id(self.local_rank)
481 )
482 self.device = torch.device(f"cuda:{visible_device_index}")
483 torch.accelerator.set_device_index(self.device)
484
485 current_platform.check_if_supports_dtype(self.model_config.dtype)
486
487 # Initialize the distributed environment BEFORE taking
488 # memory snapshot
489 # This ensures NCCL buffers are allocated before we measure
490 # available memory
491 init_worker_distributed_environment(
492 self.vllm_config,
493 self.rank,
494 self.distributed_init_method,
495 self.local_rank,
496 current_platform.dist_backend,
497 )
498
499 if self.use_v2_model_runner:
500 logger.info_once("Using V2 Model Runner")
501
502 # Set random seed.
503 set_random_seed(self.model_config.seed)
504
505 # Now take memory snapshot after NCCL is initialized
506 gc.collect()
507 torch.accelerator.empty_cache()
508
509 # take current memory snapshot
510 self.init_snapshot = init_snapshot = MemorySnapshot(device=self.device)
511 # Weights from external model loader process
512 external_weight_memory = get_model_loader(
513 self.load_config
514 ).get_external_weight_memory(self.vllm_config)
515 self.requested_memory = request_memory(
516 init_snapshot, self.cache_config, external_weight_memory
517 )
518 logger.debug("worker init memory snapshot: %r", self.init_snapshot)
519 logger.debug(
520 "worker requested memory: %sGiB", format_gib(self.requested_memory)
521 )
522 else:
523 raise RuntimeError(f"Unsupported device type: {self.device_config.device}")
524
525 self._init_workspace_and_model_runner()
526
527 def _init_workspace_and_model_runner(self) -> None:
528 """Set up the workspace manager, build the model runner, report usage."""
529 # DSpark target and draft CUDA graphs retain workspace views concurrently.
530 num_ubatches = 2 if self.vllm_config.parallel_config.enable_dbo else 1
531 init_workspace_manager(
532 self.device,
533 num_ubatches,
534 _num_workspace_lanes(self.vllm_config, self.use_v2_model_runner),
535 # Scratch holds no state across steps: discard it on sleep.
536 alloc_context=lambda: self._maybe_get_memory_pool_context("workspace"),
537 )
538 self.model_runner: GPUModelRunner = self._make_model_runner()
539 if self.rank == 0:
540 # If usage stat is enabled, collect relevant info.
541 report_usage_stats(self.vllm_config)
542
543 def _make_model_runner(self) -> "GPUModelRunner":
544 """Construct the platform's model runner; subclasses override this."""
545 if self.use_v2_model_runner:
546 if self.vllm_config.is_mm_encoder_only:
547 from vllm.v1.worker.mm_encoder_model_runner import (
548 MMEncoderModelRunner as GPUModelRunnerV2,
549 )
550 else:
551 from vllm.v1.worker.gpu.model_runner import ( # type: ignore[assignment]
552 GPUModelRunner as GPUModelRunnerV2,
553 )
554
555 # HACK(woosuk): This is a temporary fix to avoid type errors.
556 return GPUModelRunnerV2(self.vllm_config, self.device) # type: ignore
557 from vllm.v1.worker.gpu_model_runner import GPUModelRunner as GPUModelRunnerV1
558
559 return GPUModelRunnerV1(self.vllm_config, self.device)
560
561 def handle_ft_command(self, ft_request):
562 assert self.worker_sentinel is not None
563 return self.worker_sentinel.handle_command(ft_request)
564
565 # FIXME(youkaichao & ywang96): Use TorchDispatchMode instead of memory pool
566 # to hijack tensor allocation.
567 def load_model(self, *, load_dummy_weights: bool = False) -> None:
568 with (
569 self._maybe_get_memory_pool_context(tag="weights"),
570 set_current_vllm_config(self.vllm_config),
571 # 20 MiB is the minimum PyTorch allows for max_split_size_mb.
572 self._scoped_allocator_max_split(max_split_size_mb=20),
573 ):
574 self.model_runner.load_model(load_dummy_weights=load_dummy_weights)
575
576 if has_ec_transfer():
577 get_ec_transfer().start_worker_services()
578
579 if self.vllm_config.weight_transfer_config is not None:
580 self.weight_transfer_engine = WeightTransferEngineFactory.create_engine(
581 self.vllm_config.weight_transfer_config,
582 self.vllm_config,
583 self.device,
584 self.model_runner.get_model(),
585 )
586
587 # Preserve parallel weight loading, then use serving's thread count
588 # for profiling and compilation so Dynamo's global-state guards remain
589 # valid when requests arrive.
590 set_torch_threads_for_runtime()
591
592 def update_config(self, overrides: dict[str, Any]) -> None:
593 self.model_runner.update_config(overrides)
594
595 def reload_weights(self, *args, **kwargs) -> None:
596 with set_current_vllm_config(self.vllm_config):
597 self.model_runner.reload_weights(*args, **kwargs)
598
599 @torch.inference_mode()
600 def determine_available_memory(self) -> int:
601 """Profiles the peak memory usage of the model to determine how much
602 memory can be used for KV cache without OOMs.
603
604 The engine will first conduct a profiling of the existing memory usage.
605 Then, it calculates the free memory that can be used for KV cache in
606 bytes.
607
608 Tip:
609 You may limit the usage of GPU memory
610 by adjusting the `gpu_memory_utilization` parameter.
611
612 """
613 maybe_apply_startup_plan(self)
614
615 if kv_cache_memory_bytes := self.cache_config.kv_cache_memory_bytes:
616 # still need a profile run which compiles the model for
617 # max_num_batched_tokens
618 self.model_runner.profile_run(randomize_inputs=self.randomize_dummy_inputs)
619
620 msg = (
621 f"Initial free memory {format_gib(self.init_snapshot.free_memory)} "
622 f"GiB, reserved {format_gib(kv_cache_memory_bytes)} GiB memory for "
623 "KV Cache as specified by kv_cache_memory_bytes config and "
624 "skipped memory profiling. This does not respect the "
625 "gpu_memory_utilization config. Only use kv_cache_memory_bytes "
626 "config when you want manual control of KV cache memory "
627 "size. If OOM'ed, check the difference of initial free "
628 "memory between the current run and the previous run "
629 "where kv_cache_memory_bytes is suggested and update it "
630 "correspondingly."
631 )
632 logger.info(msg)
633 return reserve_mm_ipc_gpu_memory(
634 kv_cache_memory_bytes,
635 self.model_config.multimodal_config,
636 getattr(self.parallel_config, "_api_process_count", 1),
637 )
638
639 # Execute a forward pass with dummy inputs to profile the memory usage
640 # of the model.
641 with (
642 memory_profiling(
643 self.init_snapshot,
644 weights_memory=int(self.model_runner.model_memory_usage),
645 ) as profile_result,
646 # Workspaces (e.g. the MoE workspace) grow in steps during this pass,
647 # freeing each smaller buffer. Without a split limit, a later small
648 # allocation can be carved out of a freed multi-GiB block and pin the
649 # whole segment past the empty_cache() in memory_profiling, so it is
650 # counted as consumed and taken from the KV cache. Blocks above the
651 # limit are never split, so they stay releasable. Exits before
652 # memory_profiling measures, restoring the original limit.
653 self._scoped_allocator_max_split(max_split_size_mb=20),
654 ):
655 self.model_runner.profile_run(randomize_inputs=self.randomize_dummy_inputs)
656
657 # Profile CUDA graph memory if graphs will be captured.
658 # ROCm is included: #44825 moved the profiler to
659 # torch.accelerator.get_memory_info (reliable on ROCm, as used by
660 # the AMD-CI mem tests), and graph_pool_handle resolves to the same
661 # torch.cuda handle the live capture path already uses on ROCm.
662 cudagraph_memory_estimate = 0
663 if (
664 current_platform.is_cuda_alike() or current_platform.is_xpu()
665 ) and self.vllm_config.compilation_config.cudagraph_mode != CUDAGraphMode.NONE:
666 cudagraph_memory_estimate = self.model_runner.profile_cudagraph_memory()
667
668 # Respect the opt-in flag as originally designed.
669 cudagraph_memory_estimate_applied = (
670 cudagraph_memory_estimate
671 if envs.VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS
672 else 0
673 )
674
675 init_free_memory = self.init_snapshot.free_memory
676 free_gpu_memory = profile_result.after_profile.free_memory
677 rocm_fallback = maybe_rocm_profiling_fallback(profile_result)
678 if rocm_fallback is None:
679 # NOTE(woosuk): Here we assume that the other processes using the same
680 # GPU did not change their memory usage during the profiling.
681 assert init_free_memory >= free_gpu_memory, (
682 "Error in memory profiling. "
683 f"Initial free memory {format_gib(init_free_memory)} GiB, "
684 f"current free memory {format_gib(free_gpu_memory)} GiB. "
685 "This happens when other processes sharing the same container "
686 "release GPU memory while vLLM is profiling during initialization. "
687 "To fix this, ensure consistent GPU memory allocation or "
688 "isolate vLLM in its own container."
689 )
690 else:
691 profile_result.total_consumed = rocm_fallback
692 profile_result.non_kv_cache_memory = (
693 profile_result.total_consumed + profile_result.transient_peak_headroom
694 )
695
696 self.total_consumed = profile_result.total_consumed
697 self.peak_activation_memory = (
698 profile_result.transient_peak_headroom + cudagraph_memory_estimate_applied
699 )
700 self.cudagraph_memory_estimate = cudagraph_memory_estimate
701
702 self.available_kv_cache_memory_bytes = (
703 self.requested_memory
704 - profile_result.non_kv_cache_memory
705 - cudagraph_memory_estimate_applied
706 )
707
708 unrequested_memory = self.init_snapshot.free_memory - self.requested_memory
709 logger.debug(
710 "Initial free memory: %s GiB; Requested memory: %f (util), %s GiB",
711 format_gib(self.init_snapshot.free_memory),
712 self.cache_config.gpu_memory_utilization,
713 format_gib(self.requested_memory),
714 )
715 logger.debug(
716 "Free memory after profiling: %s GiB (total), %s GiB (within requested)",
717 format_gib(free_gpu_memory),
718 format_gib(free_gpu_memory - unrequested_memory),
719 )
720 logger.debug(profile_result)
721 logger.info_once(
722 "Available KV cache memory: %s GiB",
723 format_gib(self.available_kv_cache_memory_bytes),
724 )
725
726 if cudagraph_memory_estimate > 0:
727 total_mem = self.init_snapshot.total_memory
728 current_util = self.cache_config.gpu_memory_utilization
729 cg_util_delta = cudagraph_memory_estimate / total_mem
730 if envs.VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS:
731 equiv_util = round(current_util - cg_util_delta, 4)
732 suggested_util = min(
733 round(current_util + cg_util_delta, 4),
734 1.0,
735 )
736 logger.info(
737 "CUDA graph memory profiling is enabled (default since "
738 "v0.21.0). The current --gpu-memory-utilization=%.4f is "
739 "equivalent to --gpu-memory-utilization=%.4f without "
740 "CUDA graph memory profiling. To maintain the same "
741 "effective KV cache size as before, increase "
742 "--gpu-memory-utilization to %.4f. To disable, set "
743 "VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0.",
744 current_util,
745 equiv_util,
746 suggested_util,
747 )
748 else:
749 suggested_util = min(
750 round(current_util + cg_util_delta, 4),
751 1.0,
752 )
753 logger.warning(
754 "CUDA graph memory profiling is disabled "
755 "(VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0). "
756 "Without it, CUDA graph memory is not accounted for "
757 "during KV cache allocation, which may require lowering "
758 "--gpu-memory-utilization to avoid OOM. Consider "
759 "re-enabling it (the default as of v0.21.0) and increasing "
760 "--gpu-memory-utilization from %.4f to %.4f.",
761 current_util,
762 suggested_util,
763 )
764
765 return reserve_mm_ipc_gpu_memory(
766 int(self.available_kv_cache_memory_bytes),
767 self.model_config.multimodal_config,
768 getattr(self.parallel_config, "_api_process_count", 1),
769 )
770
771 def get_kv_connector_handshake_metadata(
772 self,
773 ) -> dict[tuple[int, int], KVConnectorHandshakeMetadata] | None:
774 """Get KV connector metadata from this worker if available.
775
776 Returned dict is keyed by `(pp_rank, tp_rank)`.
777 """
778 if not has_kv_transfer_group():
779 return None
780
781 connector = get_kv_transfer_group()
782 # Return None for connectors that don't need to exchange handshake
783 # metadata across workers.
784 if (metadata := connector.get_handshake_metadata()) is None:
785 return None
786
787 pp_rank = get_pp_group().rank_in_group
788 tp_rank = get_tp_group().rank_in_group
789 parallel_config = self.vllm_config.parallel_config
790 if (
791 parallel_config.prefill_context_parallel_size > 1
792 and parallel_config.decode_context_parallel_size > 1
793 ):
794 tp_rank += (
795 get_pcp_group().rank_in_group * parallel_config.tensor_parallel_size
796 )
797 return {(pp_rank, tp_rank): metadata}
798
799 def get_kv_cache_spec(self) -> dict[str, KVCacheSpec]:
800 return self.model_runner.get_kv_cache_spec()
801
802 def update_max_model_len(self, max_model_len: int) -> None:
803 """Update max_model_len after auto-fit to GPU memory.
804 This is called when max_model_len=-1 is used and the engine
805 automatically determines the maximum context length that fits
806 in GPU memory. Workers need to update their cached max_model_len
807 to match the engine's decision.
808 """
809 self.model_config.max_model_len = max_model_len
810 if self.model_runner is not None:
811 self.model_runner.update_max_model_len(max_model_len)
812 logger.debug("Updated max_model_len to %d", max_model_len)
813
814 @instrument(span_name="Allocate KV cache")
815 def initialize_from_config(self, kv_cache_config: KVCacheConfig) -> None:
816 """Allocate GPU KV cache with the specified kv_cache_config."""
817 # Update local config with adjusted num blocks after profiling,
818 # so that it's available to the warmup stage.
819 self.cache_config.num_gpu_blocks = kv_cache_config.num_blocks
820
821 # Adopt the engine core's layout; workers spawned after resolution
822 # (e.g. elastic EP scale-up) only see it through the config.
823 if kv_cache_config.kv_cache_layout is not None:
824 record_kv_cache_layout(self.cache_config, kv_cache_config.kv_cache_layout)
825
826 # Init kv cache connector here, because it requires
827 # `kv_cache_config`.
828 # NOTE(Kuntai): This need to be done before `initialize_kv_cache`,
829 # because `initialize_kv_cache` will inject kv cache groups not
830 # related to kv cache connector (e.g. kv cache sharing layers).
831 ensure_kv_transfer_initialized(self.vllm_config, kv_cache_config)
832
833 # If the connector provides a custom memory pool (e.g. Mooncake
834 # NVLink/BAREX), use it for KV cache allocation; otherwise fall
835 # back to the standard CuMem pool.
836 mem_pool_context = (
837 get_kv_transfer_group().get_mem_pool_context()
838 if has_kv_transfer_group()
839 else None
840 )
841 if mem_pool_context is None:
842 mem_pool_context = self._maybe_get_memory_pool_context(tag="kv_cache")
843
844 # Offload KV-init state, except on XPU, whose outermost pool would win.
845 runtime_pool = (
846 nullcontext()
847 if current_platform.is_xpu()
848 else self._maybe_get_memory_pool_context(tag="runtime")
849 )
850 with runtime_pool:
851 self.model_runner.initialize_kv_cache(
852 kv_cache_config,
853 kv_cache_allocation_context=mem_pool_context,
854 )
855
856 # Build KV-zero metadata outside the CuMem pool so the bookkeeping
857 # GPU tensors (seg_addrs, block-id buffers) use the standard PyTorch
858 # allocator and are not discarded during sleep/wake cycles.
859 if kv_cache_config.needs_kv_cache_zeroing and hasattr(
860 self.model_runner, "_init_kv_zero_meta"
861 ):
862 self.model_runner._init_kv_zero_meta()
863
864 @instrument(span_name="Warmup (GPU)")
865 def compile_or_warm_up_model(self) -> CompilationTimes:
866 warmup_sizes: list[int] = []
867
868 if (
869 self.vllm_config.compilation_config.mode == CompilationMode.VLLM_COMPILE
870 and has_compiled_submodule(self.model_runner.get_model())
871 ):
872 # warm up sizes that are not in cudagraph capture sizes,
873 # but users still want to compile for better performance,
874 # e.g. for the max-num-batched token size in chunked prefill.
875 compile_sizes = self.vllm_config.compilation_config.compile_sizes
876 warmup_sizes = compile_sizes.copy() if compile_sizes is not None else [] # type: ignore[assignment]
877 cg_capture_sizes: list[int] = []
878
879 if self.vllm_config.compilation_config.cudagraph_mode != CUDAGraphMode.NONE:
880 cg_sizes = self.vllm_config.compilation_config.cudagraph_capture_sizes
881 cg_capture_sizes = [] if cg_sizes is None else cg_sizes
882 warmup_sizes = [x for x in warmup_sizes if x not in cg_capture_sizes]
883
884 compile_ranges = self.vllm_config.compilation_config.get_compile_ranges()
885 # For each compile_range, if none of the batch sizes
886 # in warmup_sizes or cudagraph_capture_sizes are in the range,
887 # add the end of the range to ensure compilation/warmup.
888 all_sizes = set(cg_capture_sizes)
889 all_sizes.update([x for x in warmup_sizes if isinstance(x, int)])
890 for compile_range in compile_ranges:
891 if not any(x in compile_range for x in all_sizes):
892 warmup_sizes.append(compile_range.end)
893
894 # We skip EPLB here since we don't want to record dummy metrics
895 for size in sorted(warmup_sizes, reverse=True):
896 logger.info("Compile and warming up model for size %d", size)
897 self.model_runner._dummy_run(
898 size,
899 skip_eplb=True,
900 remove_lora=False,
901 randomize_inputs=self.randomize_dummy_inputs,
902 )
903 self.model_runner.maybe_remove_all_loras(self.model_runner.lora_config)
904
905 # Warmup and tune the kernels used during model execution before
906 # cuda graph capture.
907 kernel_warmup(self)
908
909 if self.use_v2_model_runner: # noqa: SIM102
910 if self.vllm_config.kernel_config.enable_jit_warmup:
911 # A workspace resize after capture frees what the graphs point at.
912 warmup_kernels(
913 self.model_runner, self.execute_model, self.sample_tokens
914 )
915
916 cuda_graph_memory_bytes = 0
917 if not self.model_config.enforce_eager:
918 with self._get_cudagraph_capture_context():
919 cuda_graph_memory_bytes = self.model_runner.capture_model()
920
921 # Compare actual vs estimated CUDA graph memory (if we did profiling)
922 if (
923 hasattr(self, "cudagraph_memory_estimate")
924 and self.cudagraph_memory_estimate > 0
925 ):
926 GiB = lambda b: round(b / GiB_bytes, 2)
927 diff = abs(cuda_graph_memory_bytes - self.cudagraph_memory_estimate)
928 logger.info(
929 "CUDA graph pool memory: %s GiB (actual), %s GiB (estimated), "
930 "difference: %s GiB (%.1f%%).",
931 GiB(cuda_graph_memory_bytes),
932 GiB(self.cudagraph_memory_estimate),
933 GiB(diff),
934 100 * diff / max(cuda_graph_memory_bytes, 1),
935 )
936
937 if self.cache_config.kv_cache_memory_bytes is None and hasattr(
938 self, "peak_activation_memory"
939 ):
940 # Suggests optimal kv cache memory size if we rely on
941 # memory_profiling to guess the kv cache memory size which
942 # provides peak_activation_memory and a few other memory
943 # consumption. `memory_profiling` does not consider
944 # CUDAGraph memory size and may not utilize all gpu memory.
945 # Users may want fine-grained control to specify kv cache
946 # memory size.
947
948 # empirically observed that the memory profiling may
949 # slightly underestimate the memory consumption.
950 # So leave a small buffer (=150MiB) to avoid OOM.
951 redundancy_buffer_memory = 150 * (1 << 20)
952
953 non_kv_cache_memory = (
954 self.total_consumed
955 + self.peak_activation_memory
956 + cuda_graph_memory_bytes
957 )
958 kv_cache_memory_bytes_to_gpu_limit = (
959 self.init_snapshot.free_memory
960 - non_kv_cache_memory
961 - redundancy_buffer_memory
962 )
963 kv_cache_memory_bytes_to_requested_limit = (
964 int(self.requested_memory)
965 - non_kv_cache_memory
966 - redundancy_buffer_memory
967 )
968
969 msg = (
970 f"Free memory on device "
971 f"({format_gib(self.init_snapshot.free_memory)}/"
972 f"{format_gib(self.init_snapshot.total_memory)} GiB) on startup. "
973 f"Desired GPU memory utilization is "
974 f"({self.cache_config.gpu_memory_utilization}, "
975 f"{format_gib(self.requested_memory)} GiB). "
976 f"Actual usage is {format_gib(self.total_consumed)} "
977 f"GiB for consumed memory (weights + non-torch), "
978 f"{format_gib(self.peak_activation_memory)} GiB "
979 f"for peak activation, and {format_gib(cuda_graph_memory_bytes)} "
980 f"GiB for CUDAGraph memory. Replace gpu_memory_utilization "
981 f"config with `--kv-cache-memory="
982 f"{kv_cache_memory_bytes_to_requested_limit}` "
983 f"({format_gib(kv_cache_memory_bytes_to_requested_limit)} GiB) to fit "
984 f"into requested memory, or `--kv-cache-memory="
985 f"{kv_cache_memory_bytes_to_gpu_limit}` "
986 f"({format_gib(kv_cache_memory_bytes_to_gpu_limit)} GiB) to fully "
987 f"utilize gpu memory. Current kv cache memory in use is "
988 f"{format_gib(self.available_kv_cache_memory_bytes)} GiB."
989 )
990
991 logger.info(msg)
992
993 maybe_save_startup_plan(self, kv_cache_memory_bytes_to_requested_limit)
994
995 if not self.use_v2_model_runner and get_pp_group().is_last_rank:
996 # V1: Warm up sampler and preallocate memory buffer for logits and other
997 # sampling related tensors of max possible shape to avoid memory
998 # fragmentation issue.
999 # NOTE: This is called after `capture_model` on purpose to prevent
1000 # memory buffers from being cleared by `torch.accelerator.empty_cache`.
1001 max_num_reqs = min(
1002 self.scheduler_config.max_num_seqs,
1003 self.scheduler_config.max_num_batched_tokens,
1004 )
1005
1006 # We skip EPLB here since we don't want to record dummy metrics
1007 hidden_states, last_hidden_states = self.model_runner._dummy_run(
1008 num_tokens=max_num_reqs,
1009 skip_eplb=True,
1010 cudagraph_runtime_mode=CUDAGraphMode.NONE,
1011 )
1012 if self.model_runner.is_pooling_model:
1013 self.model_runner._dummy_pooler_run(hidden_states)
1014 else:
1015 self.model_runner._dummy_sampler_run(hidden_states=last_hidden_states)
1016
1017 # Reset the seed to ensure that the random state is not affected by
1018 # the model initialization and profiling.
1019 set_random_seed(self.model_config.seed)
1020
1021 # Eagerly trigger inductor's once-per-process lazy inits during
1022 # warmup (rather than on a later compile cache-miss at runtime).
1023 c_config = self.compilation_config
1024 if c_config.mode != CompilationMode.NONE and c_config.backend == "inductor":
1025 from vllm.compilation.compiler_interface import (
1026 trigger_inductor_lazy_init,
1027 )
1028
1029 trigger_inductor_lazy_init(self.device)
1030
1031 # All warmup is done — start monitoring for unexpected JIT
1032 # compilations that would cause latency spikes during inference.
1033 self._maybe_activate_jit_monitor()
1034
1035 # Freeze the worker heap so the GC won't scan static objects
1036 # (model weights, KV caches, CUDA graphs) during inference.
1037 freeze_gc_heap()
1038 maybe_attach_gc_debug_callback()
1039
1040 # Warmup / first-compile is done — activate the `VLLM_GPU_SYNC_CHECK`
1041 # gate so subsequent `execute_model` / `sample_tokens` calls enforce it.
1042 enable_gpu_sync_check()
1043
1044 return CompilationTimes(
1045 language_model=self.compilation_config.compilation_time,
1046 encoder=self.compilation_config.encoder_compilation_time,
1047 )
1048
1049 def _maybe_activate_jit_monitor(self) -> None:
1050 # When JIT warmup is disabled (e.g. enforce_eager), runtime JIT
1051 # compilation is expected, so monitoring would only produce noise
1052 # (or spurious errors in "error" mode).
1053 if not self.vllm_config.kernel_config.enable_jit_warmup:
1054 return
1055
1056 from vllm.utils.jit_monitor import activate as activate_jit_monitor
1057
1058 activate_jit_monitor(
1059 mode=self.observability_config.jit_monitor_mode,
1060 verbose=self.observability_config.jit_monitor_verbose,
1061 )
1062
1063 def _get_cudagraph_capture_context(self) -> AbstractContextManager[None]:
1064 """Let the configured profiler observe CUDA graph capture."""
1065 if not self.use_v2_model_runner:
1066 return nullcontext()
1067 if self.profiler is None:
1068 model_runner = cast("GPUModelRunnerV2", self.model_runner)
1069 if not model_runner.needs_cudagraph_capture():
1070 return nullcontext()
1071 self.profiler = create_graph_capture_profiler(
1072 self.profiler_config, global_rank=self.rank
1073 )
1074 if self.profiler is None:
1075 return nullcontext()
1076 return self.profiler.capture_cuda_graphs()
1077
1078 def reset_mm_cache(self) -> None:
1079 self.model_runner.reset_mm_cache()
1080
1081 def reset_encoder_cache(self) -> None:
1082 self.model_runner.reset_encoder_cache()
1083
1084 def get_model(self) -> nn.Module:
1085 return self.model_runner.get_model()
1086
1087 def get_draft_model(self) -> nn.Module | None:
1088 return self.model_runner.get_draft_model()
1089
1090 def supports_draft_weight_updates(self) -> bool:
1091 engine = self.weight_transfer_engine
1092 speculative_config = self.speculative_config
1093 get_draft_model = getattr(self.model_runner, "get_draft_model", None)
1094 return (
1095 engine is not None
1096 and engine.supports_draft_weight_update
1097 and callable(get_draft_model)
1098 and get_draft_model() is not None
1099 and speculative_config is not None
1100 and speculative_config.draft_model_config is not None
1101 )
1102
1103 def _set_draft_weight_update_target(self) -> None:
1104 assert self.weight_transfer_engine is not None
1105
1106 draft_model = self.get_draft_model()
1107 if draft_model is None:
1108 raise RuntimeError(
1109 "Draft model weight update requested, but no draft model is configured."
1110 )
1111
1112 speculative_config = self.speculative_config
1113 if speculative_config is None or speculative_config.draft_model_config is None:
1114 raise RuntimeError(
1115 "Draft model weight update requested, but no draft model "
1116 "config is configured."
1117 )
1118
1119 self.weight_transfer_engine.set_weight_update_target(
1120 draft_model, speculative_config.draft_model_config
1121 )
1122
1123 def get_supported_tasks(self) -> tuple[SupportedTask, ...]:
1124 return self.model_runner.get_supported_tasks()
1125
1126 def get_compilation_match_table(self) -> dict[str, int]:
1127 from vllm.compilation.passes.vllm_inductor_pass import get_match_table
1128
1129 return get_match_table()
1130
1131 def get_encoder_timing_stats(self) -> dict[str, dict[str, float | int]]:
1132 """Get encoder timing stats from model runner."""
1133 return self.model_runner.get_encoder_timing_stats()
1134
1135 def annotate_profile(self, scheduler_output):
1136 # add trace annotation so that we can easily distinguish
1137 # context/generation request numbers in each iteration.
1138 # A context request is a request that has not yet generated any tokens
1139 if not self.profiler:
1140 return nullcontext()
1141
1142 self.profiler.step()
1143 if not self.profiler.should_annotate:
1144 return nullcontext()
1145
1146 iteration_details = compute_iteration_details(scheduler_output)
1147
1148 if self.vllm_config.profiler_config.detailed_trace_annotation:
1149 # Compute roofline-model metrics per request, split by phase
1150 # (context vs generation). These help estimate compute and
1151 # memory intensity from the trace.
1152 #
1153 # Per-request quantities:
1154 # query_len = number of scheduled (new) tokens for this request
1155 # seq_len = total sequence length (computed + scheduled tokens)
1156 #
1157 # Aggregated across requests in each phase
1158 # (ctx_=context, gen_=generation):
1159 # seq_len_sum = sum of seq_len (total KV length)
1160 # qq_compute = sum of query_len*query_len
1161 # (proxy for QK^T compute cost)
1162 # qk_compute = sum of query_len*seq_len
1163 # (proxy for QK^T compute cost for decode and
1164 # chunked prefill)
1165 # total_scheduled_tokens = scheduled tokens across all requests
1166 ctx_seq_len_sum = 0
1167 ctx_qq_compute = 0
1168 ctx_qk_compute = 0
1169 gen_seq_len_sum = 0
1170 gen_qq_compute = 0
1171 gen_qk_compute = 0
1172 total_scheduled_tokens = 0
1173
1174 # Build a map of req_id -> num_computed_tokens for all requests
1175 new_req_ids = {
1176 new_req.req_id for new_req in scheduler_output.scheduled_new_reqs
1177 }
1178 num_computed_tokens_ids = {
1179 new_req.req_id: new_req.num_computed_tokens
1180 for new_req in scheduler_output.scheduled_new_reqs
1181 }
1182 for req_id, num_computed_tokens in zip(
1183 scheduler_output.scheduled_cached_reqs.req_ids,
1184 scheduler_output.scheduled_cached_reqs.num_computed_tokens,
1185 ):
1186 num_computed_tokens_ids[req_id] = num_computed_tokens
1187
1188 # Accumulate per-phase metrics
1189 for req_id, num_tokens in scheduler_output.num_scheduled_tokens.items():
1190 query_len = num_tokens
1191 total_scheduled_tokens += query_len
1192 seq_len = num_computed_tokens_ids.get(req_id, 0) + query_len
1193 if (
1194 scheduler_output.scheduled_cached_reqs.is_context_phase(req_id)
1195 or req_id in new_req_ids
1196 ):
1197 ctx_seq_len_sum += seq_len
1198 ctx_qq_compute += query_len * query_len
1199 ctx_qk_compute += query_len * seq_len
1200 else:
1201 gen_seq_len_sum += seq_len
1202 gen_qq_compute += query_len * query_len
1203 gen_qk_compute += query_len * seq_len
1204 annotation = "".join(
1205 [
1206 "execute_",
1207 str(total_scheduled_tokens),
1208 "_context_",
1209 str(iteration_details.num_ctx_requests),
1210 "(sq",
1211 str(iteration_details.num_ctx_tokens),
1212 "sk",
1213 str(ctx_seq_len_sum),
1214 "sqsq",
1215 str(ctx_qq_compute),
1216 "sqsk",
1217 str(ctx_qk_compute),
1218 ")_generation_",
1219 str(iteration_details.num_generation_requests),
1220 "(sq",
1221 str(iteration_details.num_generation_tokens),
1222 "sk",
1223 str(gen_seq_len_sum),
1224 "sqsq",
1225 str(gen_qq_compute),
1226 "sqsk",
1227 str(gen_qk_compute),
1228 ")",
1229 ]
1230 )
1231 else:
1232 annotation = "".join(
1233 [
1234 "execute_context_",
1235 str(iteration_details.num_ctx_requests),
1236 "(",
1237 str(iteration_details.num_ctx_tokens),
1238 ")",
1239 "_generation_",
1240 str(iteration_details.num_generation_requests),
1241 "(",
1242 str(iteration_details.num_generation_tokens),
1243 ")",
1244 ]
1245 )
1246 return self.profiler.annotate_context_manager(annotation)
1247
1248 @torch.inference_mode()
1249 @with_gpu_sync_check
1250 def sample_tokens(
1251 self, grammar_output: "GrammarOutput | None"
1252 ) -> ModelRunnerOutput | AsyncModelRunnerOutput:
1253 return self.model_runner.sample_tokens(grammar_output)
1254
1255 @torch.inference_mode()
1256 @with_gpu_sync_check
1257 def execute_model(
1258 self, scheduler_output: "SchedulerOutput"
1259 ) -> ModelRunnerOutput | AsyncModelRunnerOutput | None:
1260 # Wait for the previous step's sends so this forward pass cannot
1261 # overwrite buffers they are still reading.
1262 if self._pp_send_work:
1263 for handle in self._pp_send_work:
1264 handle.wait()
1265 self._pp_send_work = []
1266
1267 intermediate_tensors = None
1268 forward_pass = scheduler_output.total_num_scheduled_tokens > 0
1269 num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
1270 all_gather_tensors = {}
1271 compilation_config = self.vllm_config.compilation_config
1272 parallel_config = self.vllm_config.parallel_config
1273
1274 if (
1275 parallel_config.pipeline_parallel_size > 1
1276 and compilation_config.pass_config.enable_sp
1277 and forward_pass
1278 ):
1279 # currently only supported by V1 GPUModelRunner
1280 assert not self.use_v2_model_runner
1281 num_scheduled_tokens_np = np.array(
1282 list(scheduler_output.num_scheduled_tokens.values()),
1283 dtype=np.int32,
1284 )
1285 # TODO(lucas): This is pretty gross; ideally we should only ever call
1286 # `_determine_batch_execution_and_padding` once (will get called again
1287 # in `execute_model`) but this requires a larger refactor of PP.
1288 _, batch_desc, _, _, _ = (
1289 self.model_runner._determine_batch_execution_and_padding(
1290 num_tokens=num_scheduled_tokens,
1291 num_reqs=len(num_scheduled_tokens_np),
1292 num_scheduled_tokens_np=num_scheduled_tokens_np,
1293 max_num_scheduled_tokens=num_scheduled_tokens_np.max(),
1294 use_cascade_attn=False, # TODO(lucas): Handle cascade attention
1295 )
1296 )
1297 all_gather_tensors = {
1298 "residual": not is_residual_scattered_for_sp(
1299 self.vllm_config, batch_desc.num_tokens
1300 )
1301 }
1302
1303 if forward_pass and not get_pp_group().is_first_rank:
1304 tensor_dict, comm_handles, comm_postprocess = (
1305 get_pp_group().irecv_tensor_dict(
1306 all_gather_group=get_tp_group(),
1307 all_gather_tensors=all_gather_tensors,
1308 )
1309 )
1310 assert tensor_dict is not None
1311 intermediate_tensors = AsyncIntermediateTensors(
1312 tensor_dict,
1313 comm_handles=comm_handles,
1314 comm_postprocess=comm_postprocess,
1315 )
1316
1317 with self.annotate_profile(scheduler_output):
1318 output = self.model_runner.execute_model(
1319 scheduler_output, intermediate_tensors
1320 )
1321 if (
1322 self.use_v2_model_runner
1323 and self.model_runner.is_pooling_model
1324 and output is None
1325 ):
1326 output = self.model_runner.pool() # type: ignore
1327 if isinstance(
1328 output, ModelRunnerOutput | AsyncModelRunnerOutput | NoneType
1329 ):
1330 return output
1331
1332 assert isinstance(output, IntermediateTensors)
1333 parallel_config = self.vllm_config.parallel_config
1334 assert (
1335 parallel_config.distributed_executor_backend != "external_launcher"
1336 and not get_pp_group().is_last_rank
1337 )
1338
1339 # Non-blocking send of the intermediate tensors. The metadata handle
1340 # is reaped lazily by the GroupCoordinator; the device handles are
1341 # waited at the top of the next step.
1342 handles = get_pp_group().isend_tensor_dict(
1343 output.tensors,
1344 all_gather_group=get_tp_group(),
1345 all_gather_tensors=all_gather_tensors,
1346 )
1347 self._pp_send_work = handles[1:]
1348
1349 if self.use_v2_model_runner and self.model_runner.is_pooling_model:
1350 return self.model_runner.pool() # type: ignore
1351 return None
1352
1353 def take_draft_token_ids(self) -> DraftTokenIds | None:
1354 return self.model_runner.take_draft_token_ids()
1355
1356 def profile(self, is_start: bool = True, profile_prefix: str | None = None):
1357 # Check if profiling is enabled
1358 if self.profiler_config is None or self.profiler_config.profiler is None:
1359 raise RuntimeError(
1360 "Profiling is not enabled. Please set --profiler-config to enable "
1361 "profiling. Example: "
1362 "'--profiler-config.profiler=torch --profiler-config.torch_profiler_dir"
1363 "=YOUR_DIR_PATH_TO_DUMP_TRACE'"
1364 )
1365
1366 if is_start:
1367 # Generate the trace name by combining prefix with comprehensive rank suffix
1368 from vllm.distributed.utils import get_worker_rank_suffix
1369
1370 rank_suffix = get_worker_rank_suffix(global_rank=self.rank)
1371
1372 # Build the full trace name
1373 if profile_prefix:
1374 trace_name = f"{profile_prefix}_{rank_suffix}"
1375 else:
1376 trace_name = rank_suffix
1377
1378 if self.profiler_config.profiler == "proton" and self.profiler is not None:
1379 self.profiler.set_output_name(trace_name)
1380
1381 # Create the profiler wrapper only on the first start call
1382 if self.profiler is None:
1383 self.profiler = create_worker_profiler(
1384 self.profiler_config,
1385 worker_name=trace_name,
1386 local_rank=self.local_rank,
1387 )
1388
1389 self.profiler.start()
1390 else:
1391 if self.profiler is None:
1392 logger.warning("Profiler was not started, nothing to stop.")
1393 return
1394 try:
1395 self.profiler.stop()
1396 finally:
1397 if self.profiler_config.profiler == "proton" and not (
1398 self.profiler.has_cuda_graph_session
1399 ):
1400 # Proton output names are fixed when the wrapper is constructed.
1401 # Recreate it so the next profile_prefix is honored.
1402 self.profiler = None
1403
1404 @property
1405 def randomize_dummy_inputs(self) -> bool:
1406 # Not cached: elastic EP rewrites parallel_config in place on reconfigure.
1407 return (
1408 envs.VLLM_RANDOMIZE_DP_DUMMY_INPUTS
1409 and self.parallel_config.data_parallel_size > 1
1410 )
1411
1412 def execute_dummy_batch(self) -> None:
1413 num_tokens = getattr(self.model_runner, "uniform_decode_query_len", 1)
1414 self.model_runner._dummy_run(
1415 num_tokens,
1416 uniform_decode=True,
1417 randomize_inputs=self.randomize_dummy_inputs,
1418 )
1419
1420 def add_lora(self, lora_request: LoRARequest) -> bool:
1421 return self.model_runner.add_lora(lora_request)
1422
1423 def remove_lora(self, lora_id: int) -> bool:
1424 return self.model_runner.remove_lora(lora_id)
1425
1426 def list_loras(self) -> set[int]:
1427 return self.model_runner.list_loras()
1428
1429 def pin_lora(self, lora_id: int) -> bool:
1430 return self.model_runner.pin_lora(lora_id)
1431
1432 def check_health(self) -> None:
1433 # worker will always be healthy as long as it's running.
1434 return
1435
1436 def save_sharded_state(
1437 self,
1438 path: str,
1439 pattern: str | None = None,
1440 max_size: int | None = None,
1441 ) -> None:
1442 from vllm.model_executor.model_loader import ShardedStateLoader
1443
1444 ShardedStateLoader.save_model(
1445 self.model_runner.model,
1446 path,
1447 pattern=pattern,
1448 max_size=max_size,
1449 )
1450
1451 def save_tensorized_model(self, tensorizer_config: "TensorizerConfig") -> None:
1452 TensorizerLoader.save_model(
1453 self.get_model(),
1454 tensorizer_config=tensorizer_config,
1455 model_config=self.model_config,
1456 )
1457
1458 def _check_weight_transfer_engine(self) -> None:
1459 if self.weight_transfer_engine is None:
1460 raise RuntimeError(
1461 "Weight transfer not configured. "
1462 "Please set weight_transfer_config to enable weight transfer."
1463 )
1464
1465 def init_weight_transfer_engine(self, init_info: dict) -> None:
1466 """Initialize weight transfer mechanism.
1467 For NCCL backend, this creates a process group with the trainer.
1468
1469 Args:
1470 init_info: Dictionary containing backend-specific initialization info
1471
1472 """
1473 self._check_weight_transfer_engine()
1474 assert self.weight_transfer_engine is not None
1475 # Parse dict into backend-specific typed dataclass
1476 typed_init_info = self.weight_transfer_engine.parse_init_info(init_info)
1477 self.weight_transfer_engine.init_transfer_engine(typed_init_info)
1478
1479 def start_weight_update(self) -> None:
1480 """Start a new weight update session.
1481
1482 Delegates engine-specific preparation (e.g. layerwise reload setup) to
1483 the configured weight transfer engine. The worker only tracks that a
1484 session is active.
1485 """
1486 with set_current_vllm_config(self.vllm_config):
1487 self._start_weight_update()
1488
1489 def start_draft_weight_update(self) -> None:
1490 """Like start_weight_update, but retargets the engine at the speculative
1491 draft model for this session.
1492 """
1493 with set_current_vllm_config(self.vllm_config):
1494 self._start_weight_update(is_draft=True)
1495
1496 def _start_weight_update(self, is_draft: bool = False) -> None:
1497 self._check_weight_transfer_engine()
1498 assert self.weight_transfer_engine is not None
1499
1500 if is_draft and not self.weight_transfer_engine.supports_draft_weight_update:
1501 raise RuntimeError(
1502 f"{type(self.weight_transfer_engine).__name__} does not support "
1503 "draft model weight updates."
1504 )
1505
1506 if self._weight_update_active:
1507 raise RuntimeError(
1508 "start_weight_update called while a weight update is already "
1509 "active. Call finish_weight_update first."
1510 )
1511
1512 try:
1513 if is_draft:
1514 self._set_draft_weight_update_target()
1515 self.weight_transfer_engine.start_weight_update()
1516 except BaseException:
1517 self.weight_transfer_engine.reset_weight_update_target()
1518 raise
1519 self._weight_update_active = True
1520 self._weight_update_is_draft = is_draft
1521
1522 def update_weights(self, update_info: dict | list[dict]) -> None:
1523 """Receive one weight update chunk from the trainer.
1524
1525 start_weight_update must be called before update_weights and
1526 finish_weight_update must be called after all chunks have been sent.
1527 Every chunk loads into whichever model the session's start_weight_update
1528 / start_draft_weight_update call selected.
1529
1530 Args:
1531 update_info: Backend-specific update info, or a list indexed by
1532 global worker rank across data parallel replicas.
1533
1534 """
1535 self._check_weight_transfer_engine()
1536 assert self.weight_transfer_engine is not None
1537
1538 if not self._weight_update_active:
1539 raise RuntimeError(
1540 "start_weight_update must be called before update_weights."
1541 )
1542
1543 with set_current_vllm_config(self.vllm_config):
1544 try:
1545 if isinstance(update_info, list):
1546 parallel_config = self.vllm_config.parallel_config
1547 local_update_info = update_info[
1548 parallel_config.data_parallel_index * parallel_config.world_size
1549 + self.rank
1550 ]
1551 else:
1552 local_update_info = update_info
1553 self.weight_transfer_engine.update_weights(local_update_info)
1554 except BaseException:
1555 self._weight_update_active = False
1556 self.weight_transfer_engine.reset_weight_update_target()
1557 raise
1558
1559 def finish_weight_update(self) -> None:
1560 """Finish the current weight update session."""
1561 self._check_weight_transfer_engine()
1562 assert self.weight_transfer_engine is not None
1563
1564 if not self._weight_update_active:
1565 raise RuntimeError(
1566 "finish_weight_update called without a matching start_weight_update."
1567 )
1568
1569 with set_current_vllm_config(self.vllm_config):
1570 self.weight_transfer_engine.finish_weight_update()
1571 self.weight_transfer_engine.reset_weight_update_target()
1572 self._weight_update_active = False
1573
1574 # Weight transfer bypasses GPUModelRunner.reload_weights().
1575 if not self._weight_update_is_draft:
1576 self.model_runner.reset_lora_state()
1577
1578 def shutdown(self) -> None:
1579 gc.unfreeze()
1580
1581 # has_kv_transfer_group can be None during interpreter shutdown.
1582 if ensure_kv_transfer_shutdown is not None:
1583 ensure_kv_transfer_shutdown()
1584 if ensure_ec_transfer_shutdown is not None:
1585 ensure_ec_transfer_shutdown()
1586 if self.profiler is not None:
1587 self.profiler.shutdown()
1588
1589 if weight_transfer_engine := getattr(self, "weight_transfer_engine", None):
1590 weight_transfer_engine.shutdown()
1591
1592 self.elastic_ep_executor.shutdown()
1593
1594 # Release GPU resources held by the model runner so that memory
1595 # can be reclaimed when running in-process
1596 if model_runner := getattr(self, "model_runner", None):
1597 model_runner.shutdown()
1598
1599 # Release kept-alive cumem pools while the pluggable allocator wrappers
1600 # and callbacks are still alive, so MemPool teardown is not deferred to
1601 # interpreter finalization (pytorch/pytorch#145168).
1602 if current_platform.is_cuda_alike():
1603 from vllm.device_allocator.cumem import CuMemAllocator
1604
1605 if CuMemAllocator.instance is not None:
1606 CuMemAllocator.instance.release_pools()
1607
1608 def elastic_ep_execute(self, execute_method: str, *args, **kwargs):
1609 return self.elastic_ep_executor.execute(execute_method, *args, **kwargs)
1610
1611
1612 def init_worker_distributed_environment(
1613 vllm_config: VllmConfig,
1614 rank: int,
1615 distributed_init_method: str | None = None,
1616 local_rank: int = -1,
1617 backend: str = "nccl",
1618 ) -> None:
1619 """Initialize the distributed environment."""
1620 parallel_config = vllm_config.parallel_config
1621 from vllm.model_executor.determinism.batch_invariant import init_batch_invariance
1622
1623 init_batch_invariance()
1624 override_envs_for_eplb(
1625 parallel_config,
1626 moe_backend=getattr(vllm_config.kernel_config, "moe_backend", None),
1627 )
1628 set_custom_all_reduce(not parallel_config.disable_custom_all_reduce)
1629
1630 init_method = distributed_init_method or "env://"
1631
1632 timeout = None
1633 if parallel_config.distributed_timeout_seconds is not None:
1634 timeout = timedelta(seconds=parallel_config.distributed_timeout_seconds)
1635
1636 init_distributed_environment(
1637 parallel_config.world_size,
1638 rank,
1639 init_method,
1640 local_rank,
1641 backend,
1642 timeout,
1643 )
1644
1645 ensure_model_parallel_initialized(
1646 parallel_config.tensor_parallel_size,
1647 parallel_config.pipeline_parallel_size,
1648 parallel_config.prefill_context_parallel_size,
1649 parallel_config.decode_context_parallel_size,
1650 )
1651
1652 # Init ec connector here before KV caches init
1653 # NOTE: We do not init KV caches for Encoder-only instance in EPD disagg mode
1654 ensure_ec_transfer_initialized(vllm_config)