v1/worker/gpu_worker.py: the GPU Worker
Code version: vLLM @ 3e44c0d · 2026-10-22
Purpose#
vllm/v1/worker/gpu_worker.py defines Worker, the CUDA worker. One Worker object owns one GPU. The executor calls its methods by name, and most of them forward to the model runner.
| File | vllm/v1/worker/gpu_worker.py, 1,720 lines of Python. |
|---|---|
| Main class | Worker(WorkerBase) (vllm/v1/worker/gpu_worker.py:196). The CUDA platform sets worker_cls to it when worker_cls is "auto" (vllm/platforms/cuda.py:330-331). |
| Runs in | A worker process (VllmWorker-<rank>), or the engine core process with the uni executor. Always in the main thread of that process. |
| Called by | WorkerWrapperBase, which the executor drives: directly (vllm/v1/executor/uniproc_executor.py:68-74) or through worker_busy_loop (vllm/v1/executor/multiproc_executor.py:1041-1047), which looks up the method by name (vllm/v1/executor/multiproc_executor.py:1057-1059). |
| Calls | Model Runner V2 (vllm/v1/worker/gpu/model_runner.py), init_distributed_environment, the memory profiler (vllm/utils/mem_utils.py), the sleep-mode backend, KV and EC connectors. |
| Not in this file | Inputs, attention metadata, the forward pass and sampling (the model runner); the KV cache planning (v1/core/kv_cache_planning.py); the process loop (multiproc_executor.py). |
This page reads the methods that vllm serve uses. The page Executors and workers: from the engine core to the GPUs explains where the worker sits.
Where it sits#
Who calls it#
- Construction.
WorkerWrapperBase.init_workerresolvesparallel_config.worker_clsand makes the object (vllm/v1/worker/worker_base.py:292-347). - Start-up. The executor calls
init_deviceandload_model. ThenEngineCore._initialize_kv_cachescallsget_kv_cache_spec,determine_available_memory,initialize_from_configandcompile_or_warm_up_modelthroughcollective_rpc(vllm/v1/engine/core.py:253-379). - Every step.
execute_model, thensample_tokens(vllm/v1/executor/abstract.py:261-287). With MoE data parallelism, idle ranks callexecute_dummy_batch(vllm/v1/engine/core.py:2493-2495). - Control.
sleep,wake_up, LoRA and profiling calls come as utility calls from the front end.
What it calls#
init_worker_distributed_environment: process groups and NCCL (vllm/v1/worker/gpu_worker.py:1678-1716)._make_model_runner: Model Runner V2 by default (vllm/v1/worker/gpu_worker.py:567-583).memory_profilingandmodel_runner.profile_run(vllm/v1/worker/gpu_worker.py:682-697).model_runner.execute_model,model_runner.sample_tokens(vllm/v1/worker/gpu_worker.py:1318-1353).
Key classes and functions#
| Name | Line | What it does |
|---|---|---|
AsyncIntermediateTensors | vllm/v1/worker/gpu_worker.py:164-193 | PP only: received activations that wait for their communication on first use. |
Worker.__init__ | vllm/v1/worker/gpu_worker.py:197-247 | Store config, matmul precision, sleep and profiler state. |
sleep / wake_up / discard | vllm/v1/worker/gpu_worker.py:298-369 | Free and restore GPU memory through the sleep-mode backend. |
init_device | vllm/v1/worker/gpu_worker.py:446-549 | Device, NCCL, cuBLAS handles, memory snapshot, model runner. |
_make_model_runner | vllm/v1/worker/gpu_worker.py:567-583 | Choose Model Runner V2, the encoder-only runner, or the V1 fallback. |
load_model | vllm/v1/worker/gpu_worker.py:591-614 | Load weights (in the "weights" memory pool when sleep mode is on). |
determine_available_memory | vllm/v1/worker/gpu_worker.py:637-811 | Profile run; return the bytes for the KV cache. |
initialize_from_config | vllm/v1/worker/gpu_worker.py:856-909 | Allocate the KV cache tensors. |
compile_or_warm_up_model | vllm/v1/worker/gpu_worker.py:923-1115 | Compile sizes, kernel warm-up, CUDA graph capture, GC freeze. |
sample_tokens / execute_model | vllm/v1/worker/gpu_worker.py:1316-1383 | The two calls of each step. |
execute_dummy_batch | vllm/v1/worker/gpu_worker.py:1454-1460 | A one-token decode for idle DP ranks. |
init_worker_distributed_environment | vllm/v1/worker/gpu_worker.py:1678-1720 | World group, then TP/PP/PCP/DCP groups. |
Walk through the main methods#
init_device: the GPU, NCCL and the memory baseline#
visible_device_index = (
current_platform.logical_device_id_to_visible_device_id(self.local_rank)
)
self.device = torch.device(f"cuda:{visible_device_index}")
torch.accelerator.set_device_index(self.device)
current_platform.check_if_supports_dtype(self.model_config.dtype)
# Initialize the distributed environment BEFORE taking
# memory snapshot
# This ensures NCCL buffers are allocated before we measure
# available memory
init_worker_distributed_environment(
self.vllm_config,
self.rank,
self.distributed_init_method,
self.local_rank,
current_platform.dist_backend,
)
if self.use_v2_model_runner:
logger.info_once("Using V2 Model Runner")
warn_if_flashinfer_kernels_missing()
# Set random seed.
set_random_seed(self.model_config.seed)
# cuBLAS(Lt) asks CUDA for its handle memory directly. When torch's own
# cudaMalloc fails, torch frees its unused cache and retries, but a cuBLAS
# request never triggers that because torch doesn't see it. Profiling can
# grow torch's cache until the GPU is full, and the first cuBLAS call would
# then fail even though torch holds idle memory it could have given back.
# Create the handles now, before the snapshot, so they count as baseline
# like the NCCL buffers. mm uses cuBLAS; linear with a bias uses cuBLASLt.
x = torch.ones(16, 16, dtype=self.model_config.dtype, device=self.device)
torch.mm(x, x)
torch.nn.functional.linear(x, x, x[0])
del x
# Now take memory snapshot after NCCL is initialized
gc.collect()
torch.accelerator.empty_cache()
# Weights from external model loader process
external_weight_memory = get_model_loader(
self.load_config
).get_external_weight_memory(self.vllm_config)
# take current memory snapshot after external weights are ready
self.init_snapshot = init_snapshot = MemorySnapshot(device=self.device)
self.requested_memory = request_memory(
init_snapshot, self.cache_config, external_weight_memory
)
logger.debug("worker init memory snapshot: %r", self.init_snapshot)
logger.debug(
"worker requested memory: %sGiB", format_gib(self.requested_memory)
)
else:
raise RuntimeError(f"Unsupported device type: {self.device_config.device}")
self._init_workspace_and_model_runner()- Lines 492-493. The worker binds its thread to its GPU.
- Lines 501-507. NCCL and the process groups start before the snapshot, as the comment says, so their buffers count as baseline.
- Lines 523-526. A tiny
mmandlinearmake the cuBLAS handles now. The comment (lines 516-522) explains the reason: cuBLAS memory is invisible to torch's cache, so it must exist before the snapshot. - Lines 538-541. The snapshot records free and total memory.
request_memorycomputes the budget and fails at once if the GPU has less free memory than that (vllm/v1/worker/utils.py:724-726,vllm/v1/worker/utils.py:761-772). - Line 549. The model runner is built here, before the weights load.
_make_model_runner: Model Runner V2#
def _make_model_runner(self) -> "GPUModelRunner":
"""Construct the platform's model runner; subclasses override this."""
if self.use_v2_model_runner:
if self.vllm_config.is_mm_encoder_only:
from vllm.v1.worker.mm_encoder_model_runner import (
MMEncoderModelRunner as GPUModelRunnerV2,
)
else:
from vllm.v1.worker.gpu.model_runner import ( # type: ignore[assignment]
GPUModelRunner as GPUModelRunnerV2,
)
# HACK(woosuk): This is a temporary fix to avoid type errors.
return GPUModelRunnerV2(self.vllm_config, self.device) # type: ignore
from vllm.v1.worker.gpu_model_runner import GPUModelRunner as GPUModelRunnerV1
return GPUModelRunnerV1(self.vllm_config, self.device)use_v2_model_runner is the default when Triton is present and no unsupported feature is set (vllm/config/vllm.py:713-748). Lines 581-583 are the Model Runner V1 fallback; the page The model runner (Model Runner V2) names its cases.
determine_available_memory: size the KV cache#
# Execute a forward pass with dummy inputs to profile the memory usage
# of the model.
with (
memory_profiling(
self.init_snapshot,
weights_memory=int(self.model_runner.model_memory_usage),
) as profile_result,
# Workspaces (e.g. the MoE workspace) grow in steps during this pass,
# freeing each smaller buffer. Without a split limit, a later small
# allocation can be carved out of a freed multi-GiB block and pin the
# whole segment past the empty_cache() in memory_profiling, so it is
# counted as consumed and taken from the KV cache. Blocks above the
# limit are never split, so they stay releasable. Exits before
# memory_profiling measures, restoring the original limit.
self._scoped_allocator_max_split(max_split_size_mb=20),
set_current_vllm_config(self.vllm_config),
):
self.model_runner.profile_run(randomize_inputs=self.randomize_dummy_inputs)
# Profile CUDA graph memory if graphs will be captured.
# ROCm is included: #44825 moved the profiler to
# torch.accelerator.get_memory_info (reliable on ROCm, as used by
# the AMD-CI mem tests), and graph_pool_handle resolves to the same
# torch.cuda handle the live capture path already uses on ROCm.
cudagraph_memory_estimate = 0
if (
current_platform.is_cuda_alike() or current_platform.is_xpu()
) and self.vllm_config.compilation_config.cudagraph_mode != CUDAGraphMode.NONE:
cudagraph_memory_estimate = self.model_runner.profile_cudagraph_memory()
# Respect the opt-in flag as originally designed.
cudagraph_memory_estimate_applied = (
cudagraph_memory_estimate
if envs.VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS
else 0
)- Lines 683-686.
memory_profilingclears the cache, resets the peak counter and measures before and after the block. - Line 697. The profile run: one forward pass with
max_num_batched_tokensdummy tokens, then a dummy sampler run (vllm/v1/worker/gpu/model_runner.py:1010-1040). Attention is skipped in this pass (skip_attn=True, line 1024 there). - Lines 704-708. When CUDA graphs will be captured, the runner estimates their memory.
- Lines 711-715. The estimate counts only with
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=1, the default (vllm/envs.py:2217-2219).
self.total_consumed = profile_result.total_consumed
self.peak_activation_memory = (
profile_result.transient_peak_headroom + cudagraph_memory_estimate_applied
)
self.cudagraph_memory_estimate = cudagraph_memory_estimate
self.available_kv_cache_memory_bytes = (
self.requested_memory
- profile_result.non_kv_cache_memory
- cudagraph_memory_estimate_applied
)The two numbers in line 746 come from the profiler:
# Measure total consumption via mem_get_info() instead of
# memory_reserved(), which goes negative when pluggable allocators
# (e.g. cumem) bypass PyTorch's tracking.
result.total_consumed = (
result.before_create.free_memory - result.after_profile.free_memory
)
# total_consumed already covers persistent torch allocations; add only the
# transient peak headroom to avoid double-counting.
result.transient_peak_headroom = (
result.after_profile.torch_peak - result.after_profile.torch_allocated
)
result.non_kv_cache_memory = result.total_consumed + result.transient_peak_headroomSo the formula is:
requested_memory = ceil(total_memory × gpu_memory_utilization)
total_consumed = free(snapshot in init_device) − free(after profile run)
peak_headroom = torch_peak − torch_allocated (after profile run)
available_kv = requested_memory − (total_consumed + peak_headroom) − cudagraph_estimate
The default gpu_memory_utilization is 0.92 (vllm/config/cache.py:212). The result goes back to the engine core, which divides it by the bytes per block (vllm/v1/core/kv_cache_planning.py:678-681). See the worked example in the deep dive.
initialize_from_config: allocate the KV cache#
def initialize_from_config(self, kv_cache_config: KVCacheConfig) -> None:
"""Allocate GPU KV cache with the specified kv_cache_config."""
# Update local config with adjusted num blocks after profiling,
# so that it's available to the warmup stage.
self.cache_config.num_gpu_blocks = kv_cache_config.num_blocks
# Adopt the engine core's layout and prefix-cache granularity; workers
# spawned after resolution (e.g. elastic EP scale-up) only see them
# through the config.
if kv_cache_config.kv_cache_layout is not None:
record_kv_cache_layout(self.cache_config, kv_cache_config.kv_cache_layout)
self.cache_config.hash_block_size = kv_cache_config.hash_block_size
self.cache_config.cache_hit_alignment_tokens = (
kv_cache_config.cache_hit_alignment_tokens
)
# Init kv cache connector here, because it requires
# `kv_cache_config`.
# NOTE(Kuntai): This need to be done before `initialize_kv_cache`,
# because `initialize_kv_cache` will inject kv cache groups not
# related to kv cache connector (e.g. kv cache sharing layers).
ensure_kv_transfer_initialized(self.vllm_config, kv_cache_config)
# If the connector provides a custom memory pool (e.g. Mooncake
# NVLink/BAREX), use it for KV cache allocation; otherwise fall
# back to the standard CuMem pool.
mem_pool_context = (
get_kv_transfer_group().get_mem_pool_context()
if has_kv_transfer_group()
else None
)
if mem_pool_context is None:
mem_pool_context = self._maybe_get_memory_pool_context(tag="kv_cache")
# Offload KV-init state, except on XPU, whose outermost pool would win.
runtime_pool = (
nullcontext()
if current_platform.is_xpu()
else self._maybe_get_memory_pool_context(tag="runtime")
)
with runtime_pool:
self.model_runner.initialize_kv_cache(
kv_cache_config,
kv_cache_allocation_context=mem_pool_context,
)- Line 861. The worker learns the final number of blocks.
- Line 878. A KV connector starts here, because it needs the KV cache config.
- Lines 883-889. The KV cache goes into the "kv_cache" memory pool, so sleep mode can free it on its own.
- Lines 898-901. The model runner allocates and binds the tensors. The page The KV cache manager: blocks, the block pool and memory sizing explains the layout.
compile_or_warm_up_model: the last start-up step#
# We skip EPLB here since we don't want to record dummy metrics
for size in sorted(warmup_sizes, reverse=True):
logger.info("Compile and warming up model for size %d", size)
self.model_runner._dummy_run(
size,
skip_eplb=True,
remove_lora=False,
randomize_inputs=self.randomize_dummy_inputs,
)
self.model_runner.maybe_remove_all_loras(self.model_runner.lora_config)
# Warmup and tune the kernels used during model execution before
# cuda graph capture.
kernel_warmup(self)
if self.use_v2_model_runner: # noqa: SIM102
if self.vllm_config.kernel_config.enable_jit_warmup:
# A workspace resize after capture frees what the graphs point at.
warmup_kernels(
self.model_runner, self.execute_model, self.sample_tokens
)
cuda_graph_memory_bytes = 0
if not self.model_config.enforce_eager:
with self._get_cudagraph_capture_context():
cuda_graph_memory_bytes = self.model_runner.capture_model()- Lines 961-968. One dummy run per extra compile size, largest first.
- Line 973. Kernel warm-up and tuning before capture.
- Lines 983-985. CUDA graph capture, unless
--enforce-eager. See torch.compile and CUDA graphs in vLLM.
After capture, when --kv-cache-memory-bytes is not set, the worker logs a suggested KV cache size that includes the real CUDA graph memory and a 150 MiB buffer (vllm/v1/worker/gpu_worker.py:1003-1057). The log line writes the flag as --kv-cache-memory=; the flag that the parser defines is --kv-cache-memory-bytes (vllm/engine/arg_utils.py:1385). Then it resets the random seed, freezes the GC heap and turns on the GPU sync check (vllm/v1/worker/gpu_worker.py:1085-1110).
execute_model and sample_tokens: every step#
@torch.inference_mode()
@with_gpu_sync_check
def sample_tokens(
self, grammar_output: "GrammarOutput | None"
) -> ModelRunnerOutput | AsyncModelRunnerOutput:
return self.model_runner.sample_tokens(grammar_output)
@torch.inference_mode()
@with_gpu_sync_check
def execute_model(
self, scheduler_output: "SchedulerOutput"
) -> ModelRunnerOutput | AsyncModelRunnerOutput | None:
# Wait for the previous step's sends so this forward pass cannot
# overwrite buffers they are still reading.
if self._pp_send_work:
for handle in self._pp_send_work:
handle.wait()
self._pp_send_work = []
intermediate_tensors = None
forward_pass = scheduler_output.total_num_scheduled_tokens > 0
if forward_pass and not get_pp_group().is_first_rank:
tensor_dict, comm_handles, comm_postprocess = (
get_pp_group().irecv_tensor_dict(
all_gather_group=get_tp_group(),
)
)
assert tensor_dict is not None
intermediate_tensors = AsyncIntermediateTensors(
tensor_dict,
comm_handles=comm_handles,
comm_postprocess=comm_postprocess,
)
with self.annotate_profile(scheduler_output):
output = self.model_runner.execute_model(
scheduler_output, intermediate_tensors
)
if (
self.use_v2_model_runner
and self.model_runner.is_pooling_model
and output is None
):
output = self.model_runner.pool() # type: ignore
if isinstance(
output, ModelRunnerOutput | AsyncModelRunnerOutput | NoneType
):
return output- Lines 1317 and 1324.
with_gpu_sync_checkcan report any GPU-to-CPU wait in these calls (only withVLLM_GPU_SYNC_CHECK; see Async scheduling: overlap the CPU and the GPU). - Line 1321.
sample_tokensis a plain forward to the model runner. On the last PP rank it returns anAsyncOutput. - Lines 1330-1348. PP only: wait for the previous send, and start receiving the activations of the stage before.
- Lines 1351-1353. The model runner does the step. With Model Runner V2 on the last rank, it returns
None(vllm/v1/worker/gpu/model_runner.py:2103-2110). - Lines 1360-1363.
Noneor an output goes straight back. Only a non-last PP rank continues: it sends itsIntermediateTensorsto the next stage without a wait (lines 1375-1379).
sleep and wake_up in brief#
Sleep mode frees GPU memory while the engine is not in use. It needs enable_sleep_mode. On CUDA this turns on the CuMem allocator; on a platform without sleep mode it raises ValueError (vllm/config/model.py:666-673). The engine core docstring gives the levels: level 0 only pauses scheduling; level 1 offloads the weights to the CPU and discards the KV cache; level 2 discards all GPU memory (vllm/v1/engine/core.py:984-992, vllm/v1/engine/core.py:997-998).
def sleep(self, level: int = 1) -> None:
torch.accelerator.synchronize()
free_bytes_before_sleep = torch.accelerator.get_memory_info()[0]
# Save the buffers before level 2 sleep
if level == 2:
model = self.model_runner.model
self._save_sleep_parameters(model)
self._sleep_saved_buffers = {
name: buffer.cpu().clone() for name, buffer in model.named_buffers()
}
draft = self.get_draft_model()
if draft is not None:
self._sleep_saved_draft_buffers = {
name: buffer.cpu().clone() for name, buffer in draft.named_buffers()
}
if has_kv_transfer_group():
get_kv_transfer_group().unregister_kv_caches()
self.sleep_mode_backend.suspend(level)
if self.vllm_config.model_config.enable_nccl_comm_suspend:
suspend_device_comms()
For level 2, the worker first copies the model buffers (and the parameters named in sleep_preserve_parameter_names, and the buffers of a draft model) to the CPU (lines 303-313). wake_up restores them (vllm/v1/worker/gpu_worker.py:345-362). The backend decides what to offload: runtime state and graph pools at every level, and the weights only at level 1. Everything else, for example the KV cache, is discarded (vllm/device_allocator/sleep_mode_backend.py:127-137, vllm/device_allocator/cumem.py:233-241). So the code does not discard all GPU memory at level 2, as the docstring says: it keeps the runtime and CUDA graph pools.
Deep dive#
The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.
Deep diveWorked example: memory profiling on an 80 GiB GPU
Assumptions (illustrative, not measured): an 8B model with 32 layers, 8 KV heads, head size 128, BF16 weights of about 15 GiB; one GPU with 80 GiB; default settings.
requested_memory = ceil(80 × 0.92) = 73.6 GiB(vllm/v1/worker/utils.py:724-726).- The snapshot in
init_devicesaw 79.0 GiB free. The weights loaded after it. After the profile run, 63.0 GiB are free. Sototal_consumed = 16.0 GiB: 15.0 GiB of weights and 1.0 GiB of runner buffers and non-torch memory (vllm/utils/mem_utils.py:312-314). - During the run, torch's peak was 2.5 GiB above what stays allocated:
peak_headroom = 2.5 GiB. - CUDA graph estimate: 0.5 GiB.
available = 73.6 − 16.0 − 2.5 − 0.5 = 54.6 GiB(vllm/v1/worker/gpu_worker.py:744-748).- One block holds 16 tokens in all 32 layers:
32 × 2 × 16 × 8 × 128 × 2 bytes = 2 MiB. The planner adds up the page sizes of all layers in a group (vllm/v1/core/kv_cache_planning.py:574-593). num_blocks = 54.6 GiB // 2 MiB = 27,955(vllm/v1/core/kv_cache_planning.py:681), so the KV cache holds 447,280 tokens. (Derived.)
The real values are in the log: "Available KV cache memory: ... GiB" (vllm/v1/worker/gpu_worker.py:763-766). The KV size math in general is in The KV cache.
Deep diveData flow: what each method takes and returns
| Method | Input | Output |
|---|---|---|
determine_available_memory | none | int bytes |
get_kv_cache_spec | none | dict[str, KVCacheSpec], one spec per attention layer (vllm/v1/worker/gpu_worker.py:841-842) |
initialize_from_config | KVCacheConfig of this rank (the wrapper picks it, vllm/v1/worker/worker_base.py:350-354) | none |
compile_or_warm_up_model | none | CompilationTimes(language_model, encoder) |
execute_model | SchedulerOutput | None (MRV2, last PP rank), ModelRunnerOutput, or AsyncModelRunnerOutput |
sample_tokens | GrammarOutput | None | AsyncOutput on the last PP rank; None if execute_model failed (vllm/v1/worker/gpu/model_runner.py:2117-2119) |
Deep diveError paths: what raises, what only logs
| Situation | Result | Where |
|---|---|---|
| free memory at start-up below the budget | ValueError: "Free memory on device ... is less than desired GPU memory utilization" | vllm/v1/worker/utils.py:761-770 |
| free memory grew during profiling (another process released memory), and there is no ROCm fallback | AssertionError "Error in memory profiling." | vllm/v1/worker/gpu_worker.py:719-731 |
| device type is not CUDA | RuntimeError | vllm/v1/worker/gpu_worker.py:546-547 |
| model dtype not supported by the GPU | raised by check_if_supports_dtype | vllm/v1/worker/gpu_worker.py:495 |
| memory grew after sleep | AssertionError | vllm/v1/worker/gpu_worker.py:331 |
check_health | always passes while the process runs | vllm/v1/worker/gpu_worker.py:1474-1476 |
Deep diveEnvironment variables and config fields
| Name | Default | Read at | Effect |
|---|---|---|---|
--gpu-memory-utilization | 0.92 (vllm/config/cache.py:212) | vllm/v1/worker/utils.py:724-726 | Share of the total GPU memory that this instance may use. |
--kv-cache-memory-bytes | unset (vllm/config/cache.py:371) | vllm/v1/worker/gpu_worker.py:653 | Fixed KV cache size (minus a multimodal reservation, if any); ignores the utilization budget. |
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS | 1 (vllm/envs.py:2217-2219) | vllm/v1/worker/gpu_worker.py:711-715 | Subtract the CUDA graph estimate from the KV cache. |
VLLM_FLOAT32_MATMUL_PRECISION | see Environment variables | vllm/v1/worker/gpu_worker.py:214-215 | torch.set_float32_matmul_precision. |
VLLM_RANDOMIZE_DP_DUMMY_INPUTS | see Environment variables | vllm/v1/worker/gpu_worker.py:1447-1452 | DP only: random inputs in dummy runs. |
--enforce-eager | off | vllm/v1/worker/gpu_worker.py:983 | No CUDA graph capture. |
Related concepts#
- Executors and workers: from the engine core to the GPUs: who calls the worker, and in which process.
- The model runner (Model Runner V2): what
execute_modelandsample_tokensdo inside. - The KV cache manager: blocks, the block pool and memory sizing: how the bytes from this page become blocks.
- torch.compile and CUDA graphs in vLLM: warm-up and capture.
- Tuning vLLM: throughput and latency knobs, their real defaults and what each one does:
gpu_memory_utilizationin practice.
Check yourself#
- 1Which class does the GPU worker build as its model runner by default?easy
Show answer
Model Runner V2,
vllm.v1.worker.gpu.model_runner.GPUModelRunner, whenuse_v2_model_runneris true and the instance is not encoder-only (vllm/v1/worker/gpu_worker.py:569-580). - 2Why doesmedium
init_devicestart NCCL and make the cuBLAS handles before it takes the memory snapshot?Show answer
So their memory counts as the baseline and not as memory that the model consumed. The comments say this for NCCL (
vllm/v1/worker/gpu_worker.py:497-500) and for cuBLAS, whose memory torch cannot free on demand (vllm/v1/worker/gpu_worker.py:516-522). - 3On an 80 GiB GPU withhard
--gpu-memory-utilization 0.92, the profile run consumes 16 GiB, the peak headroom is 2.5 GiB and the CUDA graph estimate is 0.5 GiB. How much memory goes to the KV cache, and what changes withVLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0?Show answer
73.6 − 16 − 2.5 − 0.5 = 54.6 GiB(vllm/v1/worker/gpu_worker.py:744-748,vllm/v1/worker/utils.py:724-726). With the variable at 0, the estimate is not subtracted, so the KV cache gets 55.1 GiB, and the worker warns that you may need a lower utilization to avoid OOM (vllm/v1/worker/gpu_worker.py:711-715,vllm/v1/worker/gpu_worker.py:790-805). - 4What doesmedium
Worker.execute_modelreturn for a normal step with Model Runner V2 and no PP, and where does the sampled token come from then?Show answer
None: the model runner stores the hidden states and returns nothing (vllm/v1/worker/gpu/model_runner.py:2089-2110). The engine core then callssample_tokens, which returns anAsyncOutput(vllm/v1/worker/gpu_worker.py:1318-1321,vllm/v1/worker/gpu/model_runner.py:2210-2218).