vLLM Explained

v1/worker/gpu_worker.py: the GPU Worker

AdvancedCode mapabout 10 min read + 3 min deep dives

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.

Filevllm/v1/worker/gpu_worker.py, 1,720 lines of Python.
Main classWorker(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 inA worker process (VllmWorker-<rank>), or the engine core process with the uni executor. Always in the main thread of that process.
Called byWorkerWrapperBase, 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).
CallsModel 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 fileInputs, 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_worker resolves parallel_config.worker_cls and makes the object (vllm/v1/worker/worker_base.py:292-347).
  • Start-up. The executor calls init_device and load_model. Then EngineCore._initialize_kv_caches calls get_kv_cache_spec, determine_available_memory, initialize_from_config and compile_or_warm_up_model through collective_rpc (vllm/v1/engine/core.py:253-379).
  • Every step. execute_model, then sample_tokens (vllm/v1/executor/abstract.py:261-287). With MoE data parallelism, idle ranks call execute_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#

Key classes and functions#

NameLineWhat it does
AsyncIntermediateTensorsvllm/v1/worker/gpu_worker.py:164-193PP only: received activations that wait for their communication on first use.
Worker.__init__vllm/v1/worker/gpu_worker.py:197-247Store config, matmul precision, sleep and profiler state.
sleep / wake_up / discardvllm/v1/worker/gpu_worker.py:298-369Free and restore GPU memory through the sleep-mode backend.
init_devicevllm/v1/worker/gpu_worker.py:446-549Device, NCCL, cuBLAS handles, memory snapshot, model runner.
_make_model_runnervllm/v1/worker/gpu_worker.py:567-583Choose Model Runner V2, the encoder-only runner, or the V1 fallback.
load_modelvllm/v1/worker/gpu_worker.py:591-614Load weights (in the "weights" memory pool when sleep mode is on).
determine_available_memoryvllm/v1/worker/gpu_worker.py:637-811Profile run; return the bytes for the KV cache.
initialize_from_configvllm/v1/worker/gpu_worker.py:856-909Allocate the KV cache tensors.
compile_or_warm_up_modelvllm/v1/worker/gpu_worker.py:923-1115Compile sizes, kernel warm-up, CUDA graph capture, GC freeze.
sample_tokens / execute_modelvllm/v1/worker/gpu_worker.py:1316-1383The two calls of each step.
execute_dummy_batchvllm/v1/worker/gpu_worker.py:1454-1460A one-token decode for idle DP ranks.
init_worker_distributed_environmentvllm/v1/worker/gpu_worker.py:1678-1720World 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()
  1. Lines 492-493. The worker binds its thread to its GPU.
  2. Lines 501-507. NCCL and the process groups start before the snapshot, as the comment says, so their buffers count as baseline.
  3. Lines 523-526. A tiny mm and linear make 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.
  4. Lines 538-541. The snapshot records free and total memory. request_memory computes 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).
  5. 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
        )
  1. Lines 683-686. memory_profiling clears the cache, resets the peak counter and measures before and after the block.
  2. Line 697. The profile run: one forward pass with max_num_batched_tokens dummy 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).
  3. Lines 704-708. When CUDA graphs will be captured, the runner estimates their memory.
  4. 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_headroom

So 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,
            )
  1. Line 861. The worker learns the final number of blocks.
  2. Line 878. A KV connector starts here, because it needs the KV cache config.
  3. Lines 883-889. The KV cache goes into the "kv_cache" memory pool, so sleep mode can free it on its own.
  4. 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()
  1. Lines 961-968. One dummy run per extra compile size, largest first.
  2. Line 973. Kernel warm-up and tuning before capture.
  3. 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
  1. Lines 1317 and 1324. with_gpu_sync_check can report any GPU-to-CPU wait in these calls (only with VLLM_GPU_SYNC_CHECK; see Async scheduling: overlap the CPU and the GPU).
  2. Line 1321. sample_tokens is a plain forward to the model runner. On the last PP rank it returns an AsyncOutput.
  3. Lines 1330-1348. PP only: wait for the previous send, and start receiving the activations of the stage before.
  4. 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).
  5. Lines 1360-1363. None or an output goes straight back. Only a non-last PP rank continues: it sends its IntermediateTensors to 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 dive

Worked example: memory profiling on an 80 GiB GPU

Figure: how determine_available_memory splits an 80 GiB GPU for an 8B BF16 model. The GiB values are illustrative; the formula is from the code.
Open full size ↗

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.

  1. requested_memory = ceil(80 × 0.92) = 73.6 GiB (vllm/v1/worker/utils.py:724-726).
  2. The snapshot in init_device saw 79.0 GiB free. The weights loaded after it. After the profile run, 63.0 GiB are free. So total_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).
  3. During the run, torch's peak was 2.5 GiB above what stays allocated: peak_headroom = 2.5 GiB.
  4. CUDA graph estimate: 0.5 GiB.
  5. available = 73.6 − 16.0 − 2.5 − 0.5 = 54.6 GiB (vllm/v1/worker/gpu_worker.py:744-748).
  6. 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).
  7. 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 dive

Data flow: what each method takes and returns

MethodInputOutput
determine_available_memorynoneint bytes
get_kv_cache_specnonedict[str, KVCacheSpec], one spec per attention layer (vllm/v1/worker/gpu_worker.py:841-842)
initialize_from_configKVCacheConfig of this rank (the wrapper picks it, vllm/v1/worker/worker_base.py:350-354)none
compile_or_warm_up_modelnoneCompilationTimes(language_model, encoder)
execute_modelSchedulerOutputNone (MRV2, last PP rank), ModelRunnerOutput, or AsyncModelRunnerOutput
sample_tokensGrammarOutput | NoneAsyncOutput on the last PP rank; None if execute_model failed (vllm/v1/worker/gpu/model_runner.py:2117-2119)
Deep dive

Error paths: what raises, what only logs

SituationResultWhere
free memory at start-up below the budgetValueError: "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 fallbackAssertionError "Error in memory profiling."vllm/v1/worker/gpu_worker.py:719-731
device type is not CUDARuntimeErrorvllm/v1/worker/gpu_worker.py:546-547
model dtype not supported by the GPUraised by check_if_supports_dtypevllm/v1/worker/gpu_worker.py:495
memory grew after sleepAssertionErrorvllm/v1/worker/gpu_worker.py:331
check_healthalways passes while the process runsvllm/v1/worker/gpu_worker.py:1474-1476
Deep dive

Environment variables and config fields

NameDefaultRead atEffect
--gpu-memory-utilization0.92 (vllm/config/cache.py:212)vllm/v1/worker/utils.py:724-726Share of the total GPU memory that this instance may use.
--kv-cache-memory-bytesunset (vllm/config/cache.py:371)vllm/v1/worker/gpu_worker.py:653Fixed KV cache size (minus a multimodal reservation, if any); ignores the utilization budget.
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS1 (vllm/envs.py:2217-2219)vllm/v1/worker/gpu_worker.py:711-715Subtract the CUDA graph estimate from the KV cache.
VLLM_FLOAT32_MATMUL_PRECISIONsee Environment variablesvllm/v1/worker/gpu_worker.py:214-215torch.set_float32_matmul_precision.
VLLM_RANDOMIZE_DP_DUMMY_INPUTSsee Environment variablesvllm/v1/worker/gpu_worker.py:1447-1452DP only: random inputs in dummy runs.
--enforce-eageroffvllm/v1/worker/gpu_worker.py:983No CUDA graph capture.

Check yourself#

  1. 1
    Which 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, when use_v2_model_runner is true and the instance is not encoder-only (vllm/v1/worker/gpu_worker.py:569-580).

  2. 2
    Why does init_device start NCCL and make the cuBLAS handles before it takes the memory snapshot?
    medium
    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).

  3. 3
    On an 80 GiB GPU with --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 with VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0?
    hard
    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).

  4. 4
    What does Worker.execute_model return for a normal step with Model Runner V2 and no PP, and where does the sampled token come from then?
    medium
    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 calls sample_tokens, which returns an AsyncOutput (vllm/v1/worker/gpu_worker.py:1318-1321, vllm/v1/worker/gpu/model_runner.py:2210-2218).