Download vllm_patch/gpu_model_runner.py from LeoMaxwell/vlm-twin-spec-decoding: direct link, hf CLI and curl.
- Browser
- Download file 353 kB
-
https://huggingface.co/LeoMaxwell/vlm-twin-spec-decoding/resolve/main/vllm_patch/gpu_model_runner.py
- Command line
-
hf download hf://LeoMaxwell/vlm-twin-spec-decoding/vllm_patch/gpu_model_runner.py
-
curl -L -o gpu_model_runner.py https://huggingface.co/LeoMaxwell/vlm-twin-spec-decoding/resolve/main/vllm_patch/gpu_model_runner.py
353 kB
| # SPDX-License-Identifier: Apache-2.0 | |
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | |
| import functools | |
| import gc | |
| import os | |
| import itertools | |
| import threading | |
| import time | |
| from collections import defaultdict | |
| from collections.abc import Callable, Iterable, Iterator, Sequence | |
| from contextlib import AbstractContextManager, contextmanager, nullcontext | |
| from copy import copy, deepcopy | |
| from dataclasses import dataclass, replace | |
| from functools import reduce | |
| from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias, cast | |
| import numpy as np | |
| import torch | |
| import torch.distributed | |
| import torch.nn as nn | |
| from tqdm import tqdm | |
| import vllm.envs as envs | |
| from vllm.compilation.breakable_cudagraph import ( | |
| BreakableCUDAGraphWrapper, | |
| is_breakable_cudagraph_enabled, | |
| ) | |
| from vllm.compilation.counter import compilation_counter | |
| from vllm.compilation.cuda_graph import CUDAGraphStat, CUDAGraphWrapper | |
| from vllm.compilation.monitor import set_cudagraph_capturing_enabled | |
| from vllm.config import ( | |
| CompilationMode, | |
| CUDAGraphMode, | |
| VllmConfig, | |
| get_layers_from_vllm_config, | |
| set_current_vllm_config, | |
| update_config, | |
| ) | |
| from vllm.config.cache import CacheConfig | |
| from vllm.config.ec_manager_config import EncoderCacheManagerMetadata | |
| from vllm.config.model import PROCESSED_LOGPROBS_MODES | |
| from vllm.distributed.ec_transfer import get_ec_transfer, has_ec_transfer | |
| from vllm.distributed.eplb.eplb_state import EplbState | |
| from vllm.distributed.kv_transfer import get_kv_transfer_group, has_kv_transfer_group | |
| from vllm.distributed.kv_transfer.kv_connector.utils import copy_kv_blocks | |
| from vllm.distributed.parallel_state import ( | |
| GraphCaptureContext, | |
| get_dcp_group, | |
| get_pp_group, | |
| get_tp_group, | |
| graph_capture, | |
| is_global_first_rank, | |
| prepare_communication_buffer_for_model, | |
| ) | |
| from vllm.forward_context import ( | |
| BatchDescriptor, | |
| set_forward_context, | |
| ) | |
| from vllm.logger import init_logger | |
| from vllm.lora.layers import LoRAMapping, LoRAMappingType | |
| from vllm.model_executor.layers.attention import Attention, MLAAttention | |
| from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase | |
| from vllm.model_executor.layers.fused_moe.all2all_utils import get_ep_all2all_manager | |
| from vllm.model_executor.layers.fused_moe.routed_experts_capturer import ( | |
| RoutedExpertsCapturer, | |
| ) | |
| from vllm.model_executor.layers.mamba.ops.ssu_dispatch import ( | |
| initialize_mamba_ssu_backend, | |
| ) | |
| from vllm.model_executor.layers.rotary_embedding import ( | |
| MRotaryEmbedding, | |
| XDRotaryEmbedding, | |
| ) | |
| from vllm.model_executor.model_loader import get_model_loader | |
| from vllm.model_executor.model_loader.reload import ( | |
| finalize_layerwise_reload, | |
| initialize_layerwise_reload, | |
| ) | |
| from vllm.model_executor.models.interfaces import ( | |
| MixtureOfExperts, | |
| MultiModalEmbeddings, | |
| SupportsMRoPE, | |
| SupportsMultiModal, | |
| SupportsXDRoPE, | |
| is_mixture_of_experts, | |
| supports_eagle3, | |
| supports_mrope, | |
| supports_multimodal_pruning, | |
| supports_realtime, | |
| supports_transcription, | |
| supports_xdrope, | |
| ) | |
| from vllm.model_executor.models.interfaces_base import ( | |
| VllmModelForPooling, | |
| is_pooling_model, | |
| is_text_generation_model, | |
| ) | |
| from vllm.model_executor.offloader import ( | |
| create_offloader, | |
| get_offloader, | |
| set_offloader, | |
| ) | |
| from vllm.multimodal import MULTIMODAL_REGISTRY | |
| from vllm.multimodal.encoder_budget import MultiModalBudget | |
| from vllm.multimodal.inputs import ( | |
| BatchedTensorInputs, | |
| MultiModalKwargsItem, | |
| PlaceholderRange, | |
| ) | |
| from vllm.multimodal.utils import ( | |
| copy_mm_embedding_modality, | |
| get_mm_features_in_window, | |
| group_and_batch_mm_kwargs, | |
| set_mm_embedding_modality, | |
| ) | |
| from vllm.platforms import current_platform | |
| from vllm.pooling_params import PoolingParams | |
| from vllm.sampling_params import SamplingType | |
| from vllm.sequence import IntermediateTensors | |
| from vllm.tasks import GenerationTask, PoolingTask, SupportedTask | |
| from vllm.tracing import instrument | |
| from vllm.utils import length_from_prompt_token_ids_or_embeds | |
| from vllm.utils.math_utils import cdiv, round_up | |
| from vllm.utils.mem_utils import DeviceMemoryProfiler, format_gib | |
| from vllm.utils.nvtx_pytorch_hooks import PytHooks | |
| from vllm.utils.platform_utils import num_compute_units | |
| from vllm.utils.torch_utils import ( | |
| PIN_MEMORY, | |
| async_tensor_h2d, | |
| current_stream, | |
| is_quantized_kv_cache, | |
| kv_cache_dtype_str_to_dtype, | |
| ) | |
| from vllm.v1.attention.backend import ( | |
| AttentionBackend, | |
| AttentionCGSupport, | |
| AttentionMetadata, | |
| AttentionMetadataBuilder, | |
| AttentionType, | |
| CommonAttentionMetadata, | |
| ) | |
| from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadataBuilder | |
| from vllm.v1.attention.backends.linear_attn import ( | |
| BailingLinearAttentionMetadataBuilder, | |
| ) | |
| from vllm.v1.attention.backends.mamba2_attn import Mamba2AttentionMetadataBuilder | |
| from vllm.v1.attention.backends.utils import ( | |
| NULL_BLOCK_ID, | |
| create_fast_prefill_custom_backend, | |
| get_dcp_local_seq_lens, | |
| reorder_batch_to_split_decodes_and_prefills, | |
| ) | |
| from vllm.v1.core.sched.output import NewRequestData | |
| from vllm.v1.cudagraph_dispatcher import CudagraphDispatcher | |
| from vllm.v1.kv_cache_interface import ( | |
| AttentionSpec, | |
| ChunkedLocalAttentionSpec, | |
| CrossAttentionSpec, | |
| EncoderOnlyAttentionSpec, | |
| FullAttentionSpec, | |
| KVCacheConfig, | |
| KVCacheGroupSpec, | |
| KVCacheSpec, | |
| KVCacheSpecKind, | |
| KVQuantMode, | |
| MambaSpec, | |
| SlidingWindowSpec, | |
| UniformTypeKVCacheSpecs, | |
| get_kv_cache_spec_kind, | |
| ) | |
| from vllm.v1.kv_cache_spec_registry import KVCacheSpecRegistry | |
| from vllm.v1.outputs import ( | |
| EMPTY_MODEL_RUNNER_OUTPUT, | |
| AsyncModelRunnerOutput, | |
| DraftTokenIds, | |
| ECConnectorOutput, | |
| KVConnectorOutput, | |
| LogprobsLists, | |
| LogprobsTensors, | |
| ModelRunnerOutput, | |
| PoolerOutput, | |
| RoutedExpertsLists, | |
| RoutedExpertsTensors, | |
| SamplerOutput, | |
| make_empty_encoder_model_runner_output, | |
| ) | |
| from vllm.v1.pool.late_interaction_runner import LateInteractionRunner | |
| from vllm.v1.pool.metadata import PoolingMetadata, PoolingStates | |
| from vllm.v1.sample.logits_processor import LogitsProcessors, build_logitsprocs | |
| from vllm.v1.sample.logits_processor.interface import LogitsProcessor | |
| from vllm.v1.sample.metadata import SamplingMetadata | |
| from vllm.v1.sample.rejection_sampler import RejectionSampler | |
| from vllm.v1.sample.sampler import Sampler | |
| from vllm.v1.spec_decode.custom_class_proposer import create_custom_proposer | |
| from vllm.v1.spec_decode.dflash import DFlashProposer | |
| from vllm.v1.spec_decode.draft_model import DraftModelProposer | |
| from vllm.v1.spec_decode.eagle import EagleProposer | |
| from vllm.v1.spec_decode.extract_hidden_states import ExtractHiddenStatesProposer | |
| from vllm.v1.spec_decode.gemma4 import Gemma4Proposer | |
| from vllm.v1.spec_decode.medusa import MedusaProposer | |
| from vllm.v1.spec_decode.metadata import SpecDecodeMetadata | |
| from vllm.v1.spec_decode.ngram_proposer_gpu import ( | |
| NgramProposerGPU, | |
| copy_num_valid_draft_tokens, | |
| update_ngram_gpu_tensors_incremental, | |
| update_scheduler_for_invalid_drafts, | |
| ) | |
| from vllm.v1.spec_decode.step3p5 import Step3p5MTPProposer | |
| from vllm.v1.spec_decode.suffix_decoding import SuffixDecodingProposer | |
| from vllm.v1.spec_decode.utils import update_num_computed_tokens_for_batch_change | |
| from vllm.v1.structured_output.utils import apply_grammar_bitmask | |
| from vllm.v1.utils import CpuGpuBuffer, record_function_or_nullcontext | |
| from vllm.v1.worker import mamba_utils | |
| from vllm.v1.worker.block_table import SlotMappingMode | |
| from vllm.v1.worker.cp_utils import ( | |
| check_attention_cp_compatibility, | |
| get_dcp_dummy_context_len, | |
| prepare_dcp_dummy_context_metadata, | |
| ) | |
| from vllm.v1.worker.dp_utils import coordinate_batch_across_dp | |
| from vllm.v1.worker.ec_connector_model_runner_mixin import ECConnectorModelRunnerMixin | |
| from vllm.v1.worker.gpu.attn_utils import _reshape_attention_kv_cache | |
| from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch | |
| from vllm.v1.worker.gpu_ubatch_wrapper import UBatchWrapper | |
| from vllm.v1.worker.kv_connector_model_runner_mixin import KVConnectorModelRunnerMixin | |
| from vllm.v1.worker.lora_model_runner_mixin import LoRAModelRunnerMixin | |
| from vllm.v1.worker.ubatch_utils import ( | |
| UBatchSlices, | |
| check_ubatch_thresholds, | |
| maybe_create_ubatch_slices, | |
| split_attn_metadata, | |
| ) | |
| from vllm.v1.worker.utils import is_residual_scattered_for_sp | |
| from vllm.v1.worker.workspace import lock_workspace | |
| from .utils import ( | |
| AttentionGroup, | |
| KVBlockZeroer, | |
| add_kv_sharing_layers_to_kv_cache_groups, | |
| bind_kv_cache, | |
| copy_kv_cache_blocks_inplace, | |
| prepare_kernel_block_sizes, | |
| sanity_check_mm_encoder_outputs, | |
| ) | |
| if TYPE_CHECKING: | |
| from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput | |
| from vllm.v1.spec_decode.ngram_proposer import NgramProposer | |
| from vllm.v1.worker.encoder_cudagraph import EncoderCudaGraphManager | |
| logger = init_logger(__name__) | |
| AttnMetadataDict: TypeAlias = dict[str, AttentionMetadata] | |
| # list when ubatching is enabled | |
| PerLayerAttnMetadata: TypeAlias = list[AttnMetadataDict] | AttnMetadataDict | |
| # Wrapper for ModelRunnerOutput to support overlapped execution. | |
| class AsyncGPUModelRunnerOutput(AsyncModelRunnerOutput): | |
| def __init__( | |
| self, | |
| model_runner_output: ModelRunnerOutput, | |
| sampled_token_ids: torch.Tensor, | |
| logprobs_tensors: LogprobsTensors | None, | |
| invalid_req_indices: list[int], | |
| async_output_copy_stream: torch.cuda.Stream, | |
| vocab_size: int, | |
| routed_experts: RoutedExpertsTensors | None = None, | |
| check_ep_fault: bool = False, | |
| ): | |
| self._model_runner_output = model_runner_output | |
| self._invalid_req_indices = invalid_req_indices | |
| # Event on the copy stream so we can synchronize the non-blocking copy. | |
| # Blocking (sleep) event to avoid busy-polling the CUDA driver lock. | |
| self.async_copy_ready_event = torch.cuda.Event(blocking=True) | |
| # Keep a reference to the device tensor to avoid it being | |
| # deallocated until we finish copying it to the host. | |
| self._sampled_token_ids = sampled_token_ids | |
| self.vocab_size = vocab_size | |
| self._logprobs_tensors = logprobs_tensors | |
| self._routed_experts = routed_experts | |
| self._has_fault: torch.Tensor | None = None | |
| # Initiate the copy on a separate stream, but do not synchronize it. | |
| default_stream = torch.cuda.current_stream() | |
| with torch.cuda.stream(async_output_copy_stream): | |
| async_output_copy_stream.wait_stream(default_stream) | |
| self.sampled_token_ids_cpu = self._sampled_token_ids.to( | |
| "cpu", non_blocking=True | |
| ) | |
| self._logprobs_tensors_cpu = ( | |
| self._logprobs_tensors.to_cpu_nonblocking() | |
| if self._logprobs_tensors | |
| else None | |
| ) | |
| self._routed_experts_cpu = ( | |
| self._routed_experts.to_cpu_nonblocking() | |
| if self._routed_experts is not None | |
| else None | |
| ) | |
| if check_ep_fault: | |
| has_fault = get_ep_all2all_manager().query_fault() | |
| self._has_fault = has_fault.to("cpu", non_blocking=True) | |
| self.async_copy_ready_event.record() | |
| def get_output(self) -> ModelRunnerOutput: | |
| """Copy the device tensors to the host and return a ModelRunnerOutput. | |
| This function blocks until the copy is finished. | |
| """ | |
| max_gen_len = self.sampled_token_ids_cpu.shape[-1] | |
| self.async_copy_ready_event.synchronize() | |
| # Release the device tensors once the copy has completed. | |
| del self._logprobs_tensors | |
| del self._sampled_token_ids | |
| if max_gen_len == 1: | |
| valid_sampled_token_ids = self.sampled_token_ids_cpu.tolist() | |
| for i in self._invalid_req_indices: | |
| valid_sampled_token_ids[i].clear() | |
| logprobs_lists = None | |
| if self._logprobs_tensors_cpu is not None: | |
| logprobs_lists = self._logprobs_tensors_cpu.tolists() | |
| else: | |
| valid_sampled_token_ids, logprobs_lists = RejectionSampler.parse_output( | |
| self.sampled_token_ids_cpu, | |
| self.vocab_size, | |
| self._invalid_req_indices, | |
| logprobs_tensors=self._logprobs_tensors_cpu, | |
| ) | |
| output = self._model_runner_output | |
| output.sampled_token_ids = valid_sampled_token_ids | |
| output.logprobs = logprobs_lists | |
| if self._routed_experts_cpu is not None: | |
| output.routed_experts = self._routed_experts_cpu.tolists() | |
| del self._routed_experts | |
| if self._has_fault is not None and self._has_fault.item(): | |
| mask = get_ep_all2all_manager().query_active_mask() | |
| raise RuntimeError( | |
| "Fault detected in EP all2all communication: " | |
| "one or more ranks timed out during dispatch/combine. " | |
| f"Mask: {mask.cpu().tolist()}" | |
| ) | |
| return output | |
| def _copy_pooler_output_to_cpu( | |
| raw_pooler_output: PoolerOutput, finished_mask: list[bool] | |
| ) -> list[torch.Tensor | None]: | |
| num_reqs = len(finished_mask) | |
| if isinstance(raw_pooler_output, torch.Tensor): | |
| if raw_pooler_output.shape[0] != num_reqs: | |
| raise ValueError( | |
| "Pooler output batch size does not match finished mask size: " | |
| f"{raw_pooler_output.shape[0]} != {num_reqs}." | |
| ) | |
| num_finished = sum(finished_mask) | |
| if num_finished == 0: | |
| return [None] * num_reqs | |
| if num_finished == num_reqs: | |
| return list(raw_pooler_output.to("cpu", non_blocking=True)) | |
| # partial finished | |
| finished_indices = [i for i, include in enumerate(finished_mask) if include] | |
| index_tensor = torch.tensor( | |
| finished_indices, device=raw_pooler_output.device, dtype=torch.long | |
| ) | |
| finished_outputs = raw_pooler_output.index_select(0, index_tensor).to( | |
| "cpu", non_blocking=True | |
| ) | |
| partial_pooler_output: list[torch.Tensor | None] = [None] * num_reqs | |
| for i, out in zip(finished_indices, finished_outputs): | |
| partial_pooler_output[i] = out | |
| return partial_pooler_output | |
| assert isinstance(raw_pooler_output, list) | |
| if len(raw_pooler_output) != num_reqs: | |
| raise ValueError( | |
| "Pooler output batch size does not match finished mask size: " | |
| f"{len(raw_pooler_output)} != {num_reqs}." | |
| ) | |
| pooler_output: list[torch.Tensor | None] = [None] * num_reqs | |
| for i, (out, include) in enumerate(zip(raw_pooler_output, finished_mask)): | |
| if include and out is not None: | |
| pooler_output[i] = out.to("cpu", non_blocking=True) | |
| return pooler_output | |
| class AsyncGPUPoolingModelRunnerOutput(AsyncModelRunnerOutput): | |
| def __init__( | |
| self, | |
| model_runner_output: ModelRunnerOutput, | |
| raw_pooler_output: PoolerOutput, | |
| finished_mask: list[bool], | |
| async_output_copy_stream: torch.cuda.Stream, | |
| ): | |
| self._model_runner_output = model_runner_output | |
| # Event on the copy stream so we can synchronize the non-blocking copy. | |
| # Blocking (sleep) event to avoid busy-polling the CUDA driver lock. | |
| self.async_copy_ready_event = torch.cuda.Event(blocking=True) | |
| # Keep a reference to the device tensors to avoid them being | |
| # deallocated until we finish copying it to the host. | |
| self._raw_pooler_output = raw_pooler_output | |
| # Initiate the copy on a separate stream, but do not synchronize it. | |
| default_stream = torch.cuda.current_stream() | |
| with torch.cuda.stream(async_output_copy_stream): | |
| async_output_copy_stream.wait_stream(default_stream) | |
| self._model_runner_output.pooler_output = _copy_pooler_output_to_cpu( | |
| raw_pooler_output=self._raw_pooler_output, | |
| finished_mask=finished_mask, | |
| ) | |
| self.async_copy_ready_event.record() | |
| def get_output(self) -> ModelRunnerOutput: | |
| """Copy the device tensors to the host and return a ModelRunnerOutput. | |
| This function blocks until the copy is finished. | |
| """ | |
| self.async_copy_ready_event.synchronize() | |
| # Release the device tensors once the copy has completed. | |
| del self._raw_pooler_output | |
| return self._model_runner_output | |
| class ExecuteModelState(NamedTuple): | |
| """Ephemeral cached state transferred between execute_model() and | |
| sample_tokens(), after execute_model() returns None.""" | |
| scheduler_output: "SchedulerOutput" | |
| logits: torch.Tensor | |
| spec_decode_metadata: SpecDecodeMetadata | None | |
| spec_decode_common_attn_metadata: CommonAttentionMetadata | None | |
| hidden_states: torch.Tensor | |
| sample_hidden_states: torch.Tensor | |
| aux_hidden_states: list[torch.Tensor] | None | |
| ec_connector_output: ECConnectorOutput | None | |
| cudagraph_stats: CUDAGraphStat | None | |
| slot_mappings: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None | |
| class GPUModelRunner( | |
| LoRAModelRunnerMixin, KVConnectorModelRunnerMixin, ECConnectorModelRunnerMixin | |
| ): | |
| def __init__( | |
| self, | |
| vllm_config: VllmConfig, | |
| device: torch.device, | |
| ): | |
| self.vllm_config = vllm_config | |
| self.model_config = vllm_config.model_config | |
| self.cache_config = vllm_config.cache_config | |
| self.offload_config = vllm_config.offload_config | |
| self.compilation_config = vllm_config.compilation_config | |
| self.lora_config = vllm_config.lora_config | |
| self.load_config = vllm_config.load_config | |
| self.parallel_config = vllm_config.parallel_config | |
| self.scheduler_config = vllm_config.scheduler_config | |
| self.speculative_config = vllm_config.speculative_config | |
| self.observability_config = vllm_config.observability_config | |
| model_config = self.model_config | |
| cache_config = self.cache_config | |
| scheduler_config = self.scheduler_config | |
| parallel_config = self.parallel_config | |
| self.device = device | |
| self.dtype = self.model_config.dtype | |
| self.check_ep_fault = False | |
| if parallel_config.data_parallel_size > 1 and self.model_config.is_moe: | |
| self.check_ep_fault = get_ep_all2all_manager().support_fault_tolerance | |
| self.kv_cache_dtype = kv_cache_dtype_str_to_dtype( | |
| cache_config.cache_dtype, self.model_config | |
| ) | |
| self.is_pooling_model = model_config.runner_type == "pooling" | |
| self.enable_prompt_embeds = model_config.enable_prompt_embeds | |
| self.is_multimodal_raw_input_only_model = ( | |
| model_config.is_multimodal_raw_input_only_model | |
| ) | |
| # These will be overridden in load_model() | |
| self.is_multimodal_pruning_enabled = False | |
| self.requires_sequential_video_encoding = False | |
| # Set to True after init_routed_experts_capturer() completes. | |
| # Prevents routed experts code from running during profiling/dummy run. | |
| self.routed_experts_initialized = False | |
| self.max_model_len = model_config.max_model_len | |
| # Always set to false after the first forward pass | |
| self.calculate_kv_scales = self.cache_config.calculate_kv_scales | |
| self.dcp_world_size = self.parallel_config.decode_context_parallel_size | |
| self.dcp_rank = 0 if self.dcp_world_size <= 1 else get_dcp_group().rank_in_group | |
| self.max_num_tokens = scheduler_config.max_num_batched_tokens | |
| self.max_num_reqs = scheduler_config.max_num_seqs | |
| # Broadcast PP output for external_launcher (torchrun) | |
| # to make sure we are synced across pp ranks | |
| # TODO: Support overlapping micro-batches | |
| # https://github.com/vllm-project/vllm/issues/18019 | |
| self.broadcast_pp_output = ( | |
| self.parallel_config.distributed_executor_backend == "external_launcher" | |
| and len(get_pp_group().ranks) > 1 | |
| ) | |
| # Model-related. | |
| self.num_query_heads = model_config.get_num_attention_heads(parallel_config) | |
| self.inputs_embeds_size = model_config.get_inputs_embeds_size() | |
| # Only relevant for models using ALiBi (e.g, MPT) | |
| self.use_alibi = model_config.uses_alibi | |
| self.cascade_attn_enabled = not self.model_config.disable_cascade_attn | |
| self.is_mm_prefix_lm = self.model_config.is_mm_prefix_lm | |
| # Multi-modal data support | |
| self.mm_registry = MULTIMODAL_REGISTRY | |
| self.uses_mrope = model_config.uses_mrope | |
| self.uses_xdrope_dim = model_config.uses_xdrope_dim | |
| self.supports_mm_inputs = self.mm_registry.supports_multimodal_inputs( | |
| model_config | |
| ) | |
| if self.model_config.is_encoder_decoder: | |
| # Maximum length of the encoder input, only for encoder-decoder | |
| # models. | |
| self.max_encoder_len = scheduler_config.max_num_encoder_input_tokens | |
| else: | |
| self.max_encoder_len = 0 | |
| # Async scheduling | |
| self.use_async_scheduling = self.scheduler_config.async_scheduling | |
| # Sampler | |
| self.sampler = Sampler( | |
| logprobs_mode=self.model_config.logprobs_mode, | |
| use_fp64_gumbel=self.model_config.use_fp64_gumbel, | |
| ) | |
| self.eplb_state: EplbState | None = None | |
| self._moe_model: MixtureOfExperts | None = None | |
| # NOTE(yongji): flag to temporarily disable EPLB during scaling up/down | |
| self.eep_eplb_suppressed = False | |
| """ | |
| State of the expert parallelism load balancer. | |
| Will be lazily initialized when the model is loaded. | |
| """ | |
| # Lazy initializations | |
| # self.model: nn.Module # Set after load_model | |
| # Initialize in initialize_kv_cache | |
| self.kv_caches: list[torch.Tensor] = [] | |
| # Initialize in initialize_kv_cache_tensors | |
| self.cross_layers_kv_cache: torch.Tensor | None = None | |
| self.cross_layers_attn_backend: type[AttentionBackend] | None = None | |
| # indexes: [kv_cache_group_id][attn_group] | |
| self.attn_groups: list[list[AttentionGroup]] = [] | |
| # self.kv_cache_config: KVCacheConfig | |
| # mm_hash -> encoder_output | |
| self.encoder_cache: dict[str, torch.Tensor] = {} | |
| self.late_interaction_runner = LateInteractionRunner() | |
| # Encoder CUDA graph manager (initialized after model load if enabled) | |
| self.encoder_cudagraph_manager: EncoderCudaGraphManager | None = None | |
| self.use_aux_hidden_state_outputs = False | |
| # Set up speculative decoding. | |
| # NOTE(Jiayi): currently we put the entire draft model on | |
| # the last PP rank. This is not ideal if there are many | |
| # layers in the draft model. | |
| if self.speculative_config and get_pp_group().is_last_rank: | |
| self.drafter: ( | |
| NgramProposer # noqa: F823 | |
| | NgramProposerGPU | |
| | SuffixDecodingProposer | |
| | EagleProposer | |
| | DFlashProposer | |
| | DraftModelProposer | |
| | MedusaProposer | |
| | ExtractHiddenStatesProposer | |
| | Gemma4Proposer | |
| | Step3p5MTPProposer | |
| ) | |
| if self.speculative_config.method == "custom_class": | |
| self.drafter = create_custom_proposer( # type: ignore[assignment] | |
| self.vllm_config | |
| ) | |
| elif self.speculative_config.method == "ngram": | |
| from vllm.v1.spec_decode.ngram_proposer import NgramProposer | |
| self.drafter = NgramProposer(self.vllm_config) | |
| elif self.speculative_config.uses_draft_model(): | |
| self.drafter = DraftModelProposer( | |
| vllm_config=self.vllm_config, | |
| device=self.device, | |
| runner=self, | |
| ) | |
| elif self.speculative_config.use_ngram_gpu(): | |
| self.drafter = NgramProposerGPU(self.vllm_config, self.device, self) | |
| self.num_tokens_no_spec_gpu = torch.zeros( | |
| self.max_num_reqs, dtype=torch.int32, device=device | |
| ) | |
| self.token_ids_gpu_tensor = torch.zeros( | |
| self.max_num_reqs, | |
| self.max_model_len, | |
| dtype=torch.int32, | |
| device=device, | |
| ) | |
| self._ngram_pinned_idx_buf = torch.zeros( | |
| self.max_num_reqs, dtype=torch.long, pin_memory=True | |
| ) | |
| self._ngram_pinned_val_buf = torch.zeros( | |
| self.max_num_reqs, dtype=torch.int32, pin_memory=True | |
| ) | |
| elif self.speculative_config.use_gemma4_mtp(): | |
| self.drafter = Gemma4Proposer(self.vllm_config, self.device, self) | |
| elif self.speculative_config.use_step3p5_mtp(): | |
| self.drafter = Step3p5MTPProposer(self.vllm_config, self.device, self) | |
| elif self.speculative_config.use_dflash(): | |
| self.drafter = DFlashProposer(self.vllm_config, self.device, self) | |
| self.use_aux_hidden_state_outputs = True | |
| elif self.speculative_config.method == "suffix": | |
| self.drafter = SuffixDecodingProposer(self.vllm_config) | |
| elif self.speculative_config.use_eagle(): | |
| self.drafter = EagleProposer(self.vllm_config, self.device, self) | |
| if self.speculative_config.method == "eagle3": | |
| self.use_aux_hidden_state_outputs = ( | |
| self.drafter.eagle3_use_aux_hidden_state | |
| ) | |
| elif self.speculative_config.method == "medusa": | |
| self.drafter = MedusaProposer( | |
| vllm_config=self.vllm_config, device=self.device | |
| ) | |
| elif self.speculative_config.method == "extract_hidden_states": | |
| self.drafter = ExtractHiddenStatesProposer( | |
| vllm_config=self.vllm_config, device=self.device | |
| ) | |
| self.use_aux_hidden_state_outputs = True | |
| else: | |
| raise ValueError( | |
| "Unknown speculative decoding method: " | |
| f"{self.speculative_config.method}" | |
| ) | |
| self.rejection_sampler = RejectionSampler( | |
| self.sampler, self.speculative_config, self.device | |
| ) | |
| self.num_spec_tokens = 0 | |
| self.prev_num_spec_tokens = 0 | |
| self.valid_sampled_token_count_gpu: torch.Tensor | None = None | |
| if self.speculative_config: | |
| self.num_spec_tokens = self.speculative_config.num_speculative_tokens | |
| self.prev_num_spec_tokens = self.num_spec_tokens | |
| draft_config = self.speculative_config.draft_model_config | |
| if draft_config is not None and draft_config.max_model_len is not None: | |
| self.effective_drafter_max_model_len = draft_config.max_model_len | |
| else: | |
| self.effective_drafter_max_model_len = self.max_model_len | |
| self.use_async_spec_decode = ( | |
| self.use_async_scheduling and self.num_spec_tokens > 0 | |
| ) | |
| # Request states. | |
| self.requests: dict[str, CachedRequestState] = {} | |
| # NOTE(rob): num_prompt_logprobs only includes reqs | |
| # that are currently in the prefill phase. | |
| self.num_prompt_logprobs: dict[str, int] = {} | |
| # Input Batch | |
| # NOTE(Chen): Ideally, we should initialize the input batch inside | |
| # `initialize_kv_cache` based on the kv cache config. However, as in | |
| # https://github.com/vllm-project/vllm/pull/18298, due to some unknown | |
| # reasons, we have to initialize the input batch before `load_model`, | |
| # quantization + weight offloading will fail otherwise. As a temporary | |
| # solution, we initialize the input batch here, and re-initialize it | |
| # in `initialize_kv_cache` if the block_sizes here is different from | |
| # the block_sizes in the kv cache config. | |
| logits_processors = model_config.logits_processors | |
| custom_logitsprocs: Sequence[str | type[LogitsProcessor]] = ( | |
| tuple(logits_processors) if logits_processors is not None else () | |
| ) | |
| placeholder_block_size = ( | |
| self.cache_config.block_size or CacheConfig.DEFAULT_BLOCK_SIZE | |
| ) | |
| placeholder_max_num_blocks = cdiv( | |
| max(self.max_model_len, self.max_encoder_len), placeholder_block_size | |
| ) | |
| self._init_block_sizes = [placeholder_block_size] | |
| self._init_kernel_block_sizes = [placeholder_block_size] | |
| self._init_max_num_blocks = [placeholder_max_num_blocks] | |
| self._init_slot_mapping_modes = [SlotMappingMode.TOKEN_TO_KV_SLOT] | |
| self.input_batch = InputBatch( | |
| max_num_reqs=self.max_num_reqs, | |
| # We need to use the encoder length for encoder-decoder | |
| # because of KV cache for cross-attention. | |
| max_model_len=max(self.max_model_len, self.max_encoder_len), | |
| max_num_batched_tokens=self.max_num_tokens, | |
| device=self.device, | |
| vocab_size=self.model_config.get_vocab_size(), | |
| block_sizes=[placeholder_block_size], | |
| kernel_block_sizes=[placeholder_block_size], | |
| max_num_blocks_per_req=[placeholder_max_num_blocks], | |
| num_spec_tokens=self.num_spec_tokens, | |
| logitsprocs=build_logitsprocs( | |
| self.vllm_config, | |
| self.device, | |
| PIN_MEMORY, | |
| self.is_pooling_model, | |
| custom_logitsprocs, | |
| ), | |
| # We currently don't know whether a particular custom logits processor | |
| # uses output token ids so we set this conservatively. Thinking-budget | |
| # tracking is requested dynamically when a budgeted request is in the batch. | |
| logitsprocs_need_output_token_ids=bool(custom_logitsprocs), | |
| is_pooling_model=self.is_pooling_model, | |
| cp_kv_cache_interleave_size=self.parallel_config.cp_kv_cache_interleave_size, | |
| reasoning_config=self.vllm_config.reasoning_config, | |
| use_replayssm=self.cache_config.use_replayssm, | |
| ) | |
| # Separate cuda stream for overlapping transfer of sampled token ids from | |
| # GPU to CPU when async scheduling is enabled. | |
| self.async_output_copy_stream: torch.cuda.Stream | None = None | |
| # cuda event to synchronize use of reused CPU tensors between steps | |
| # when async scheduling is enabled. | |
| self.prepare_inputs_event: torch.Event | None = None | |
| if self.use_async_scheduling: | |
| self.async_output_copy_stream = torch.cuda.Stream() | |
| # Blocking (sleep) event to avoid busy-polling the CUDA driver lock; | |
| # under TP contention that spin can balloon and make the rank a straggler. | |
| self.prepare_inputs_event = torch.cuda.Event(blocking=True) | |
| # self.cudagraph_batch_sizes sorts in ascending order. | |
| if ( | |
| self.compilation_config.cudagraph_capture_sizes | |
| and self.compilation_config.cudagraph_mode != CUDAGraphMode.NONE | |
| ): | |
| self.cudagraph_batch_sizes = sorted( | |
| self.compilation_config.cudagraph_capture_sizes | |
| ) | |
| else: | |
| self.cudagraph_batch_sizes = [] | |
| # Cache the device properties. | |
| self._init_device_properties() | |
| # Encoder timing registry for observability | |
| self.encoder_timing_registry: dict[str, EncoderTimingStats] = {} | |
| self._encoder_timing_lock = threading.Lock() | |
| # Persistent buffers for CUDA graphs. | |
| self.input_ids = self._make_buffer(self.max_num_tokens, dtype=torch.int32) | |
| self.positions = torch.zeros( | |
| self.max_num_tokens, dtype=torch.int64, device=self.device | |
| ) | |
| self.query_start_loc = self._make_buffer( | |
| self.max_num_reqs + 1, dtype=torch.int32 | |
| ) | |
| self.seq_lens = torch.zeros( | |
| self.max_num_reqs, dtype=torch.int32, device=self.device | |
| ) | |
| self.optimistic_seq_lens_cpu = torch.zeros( | |
| self.max_num_reqs, dtype=torch.int32, pin_memory=PIN_MEMORY | |
| ) | |
| self.num_computed_tokens = torch.zeros( | |
| self.max_num_reqs, dtype=torch.int32, device=self.device | |
| ) | |
| self.prev_num_draft_tokens = self._make_buffer( | |
| self.max_num_reqs, dtype=torch.int32 | |
| ) | |
| self.req_indices = self._make_buffer(self.max_num_tokens, dtype=torch.int64) | |
| # Maps current batch position -> previous batch position (-1 for new reqs) | |
| self.prev_positions = self._make_buffer(self.max_num_reqs, dtype=torch.int64) | |
| self.num_scheduled_tokens = self._make_buffer( | |
| self.max_num_reqs, dtype=torch.int32 | |
| ) | |
| self.encoder_seq_lens = self._make_buffer(self.max_num_reqs, dtype=torch.int32) | |
| if self.dcp_world_size > 1: | |
| self.dcp_local_seq_lens = self._make_buffer( | |
| self.max_num_reqs, dtype=torch.int32 | |
| ) | |
| # Because inputs_embeds may be bfloat16 and we don't need a numpy | |
| # version of this tensor, avoid a RuntimeError by not creating a | |
| # numpy buffer. | |
| self.inputs_embeds = self._make_buffer( | |
| self.max_num_tokens, self.inputs_embeds_size, dtype=self.dtype, numpy=False | |
| ) | |
| self.is_token_ids = self._make_buffer(self.max_num_tokens, dtype=torch.bool) | |
| self.discard_request_mask = self._make_buffer( | |
| self.max_num_reqs, dtype=torch.bool | |
| ) | |
| self.num_decode_draft_tokens = self._make_buffer( | |
| self.max_num_reqs, dtype=torch.int32 | |
| ) | |
| self.num_accepted_tokens = self._make_buffer( | |
| self.max_num_reqs, dtype=torch.int32 | |
| ) | |
| # Only relevant for models using M-RoPE (e.g, Qwen2-VL) | |
| if self.uses_mrope: | |
| # NOTE: `mrope_positions` is implemented with one additional dummy | |
| # position on purpose to make it non-contiguous so that it can work | |
| # with torch compile. | |
| # See detailed explanation in https://github.com/vllm-project/vllm/pull/12128#discussion_r1926431923 | |
| # NOTE: When M-RoPE is enabled, position ids are 3D regardless of | |
| # the modality of inputs. For text-only inputs, each dimension has | |
| # identical position IDs, making M-RoPE functionally equivalent to | |
| # 1D-RoPE. | |
| # See page 5 of https://arxiv.org/abs/2409.12191 | |
| self.mrope_positions = self._make_buffer( | |
| (3, self.max_num_tokens + 1), dtype=torch.int64 | |
| ) | |
| # Only relevant for models using XD-RoPE (e.g, HunYuan-VL) | |
| if self.uses_xdrope_dim > 0: | |
| # Similar to mrope but use assigned dimension number for RoPE, 4 as default. | |
| self.xdrope_positions = self._make_buffer( | |
| (self.uses_xdrope_dim, self.max_num_tokens + 1), dtype=torch.int64 | |
| ) | |
| # None in the first PP rank. The rest are set after load_model. | |
| self.intermediate_tensors: IntermediateTensors | None = None | |
| # OPTIMIZATION: Cache the arange tensors rather than creating them | |
| # every step. Keep in int64 to avoid overflow with long context. | |
| # - arange_np: immutable [0, 1, 2, ...] used as source for batched computation | |
| # - query_pos: CpuGpuBuffer for the computed batched arange result | |
| arange_size = max(self.max_num_reqs + 1, self.max_num_tokens) | |
| self.arange_np = np.arange(arange_size, dtype=np.int64) | |
| self.query_pos = self._make_buffer(arange_size, dtype=torch.int64) | |
| self._arange_scratch = np.empty(arange_size, dtype=np.int64) | |
| # Layer pairings for cross-layer KV sharing. | |
| # If an Attention layer `layer_name` is in the keys of this dict, it | |
| # means this layer will perform attention using the keys and values | |
| # from the KV cache of `shared_kv_cache_layers[layer_name]`. | |
| self.shared_kv_cache_layers: dict[str, str] = {} | |
| self.kv_sharing_fast_prefill_eligible_layers: set[str] = set() | |
| self.kv_sharing_fast_prefill_logits_indices = None | |
| if self.cache_config.kv_sharing_fast_prefill: | |
| self.kv_sharing_fast_prefill_logits_indices = torch.zeros( | |
| self.max_num_tokens, dtype=torch.int32, device=self.device | |
| ) | |
| self.uniform_decode_query_len = 1 + self.num_spec_tokens | |
| # Cudagraph dispatcher for runtime cudagraph dispatching. | |
| self.cudagraph_dispatcher = CudagraphDispatcher(self.vllm_config) | |
| self.mm_budget = ( | |
| MultiModalBudget(self.vllm_config, self.mm_registry) | |
| if self.supports_mm_inputs | |
| else None | |
| ) | |
| self.reorder_batch_threshold: int | None = None | |
| # Attention layers that are only in the KVCacheConfig of the runner | |
| # (e.g., KV sharing, encoder-only attention), but not in the | |
| # KVCacheConfig of the scheduler. | |
| self.runner_only_attn_layers: set[str] = set() | |
| # Cached outputs. | |
| self._draft_token_ids: list[list[int]] | torch.Tensor | None = None | |
| self._draft_probs: torch.Tensor | None = None | |
| self._draft_prob_req_ids: list[str] | None = None | |
| # N-gram GPU path: async D2H buffer/event for per-request valid draft counts. | |
| self._num_valid_draft_tokens: torch.Tensor | None = None | |
| self._num_valid_draft_tokens_cpu: torch.Tensor | None = None | |
| self._num_valid_draft_tokens_event: torch.cuda.Event | None = None | |
| self._num_valid_draft_tokens_copy_stream: torch.cuda.Stream | None = None | |
| if ( | |
| self.speculative_config is not None | |
| and self.speculative_config.use_ngram_gpu() | |
| ): | |
| self._num_valid_draft_tokens_cpu = torch.empty( | |
| self.max_num_reqs, dtype=torch.int32, pin_memory=PIN_MEMORY | |
| ) | |
| self._num_valid_draft_tokens_event = torch.cuda.Event() | |
| self._num_valid_draft_tokens_copy_stream = torch.cuda.Stream() | |
| self._draft_token_req_ids: list[str] | None = None | |
| self.transfer_event = torch.Event() | |
| self.sampled_token_ids_pinned_cpu = torch.empty( | |
| (self.max_num_reqs, 1), | |
| dtype=torch.int64, | |
| device="cpu", | |
| pin_memory=PIN_MEMORY, | |
| ) | |
| # Pre-allocated tensor for copying valid sampled token counts to CPU, | |
| # with dedicated stream for overlapping and event for coordination. | |
| self.valid_sampled_token_count_event: torch.Event | None = None | |
| self.valid_sampled_token_count_copy_stream: torch.cuda.Stream | None = None | |
| # We also copy the drafted tokens to the CPU asynchronously, | |
| # in case we need them for structured outputs. | |
| self.draft_token_ids_event: torch.Event | None = None | |
| self.draft_token_ids_copy_stream: torch.cuda.Stream | None = None | |
| self.valid_sampled_token_count_cpu: torch.Tensor | None = None | |
| self.draft_token_ids_cpu: torch.Tensor | None = None | |
| self.num_accepted_tokens_event: torch.Event | None = None | |
| if self.num_spec_tokens: | |
| self.draft_token_ids_event = torch.Event() | |
| self.num_accepted_tokens_event = torch.Event() | |
| self.draft_token_ids_copy_stream = torch.cuda.Stream() | |
| self.draft_token_ids_cpu = torch.empty( | |
| (self.max_num_reqs, self.num_spec_tokens), | |
| dtype=torch.int64, | |
| device="cpu", | |
| pin_memory=PIN_MEMORY, | |
| ) | |
| if self.use_async_scheduling: | |
| self.valid_sampled_token_count_event = torch.Event() | |
| self.valid_sampled_token_count_copy_stream = torch.cuda.Stream() | |
| self.valid_sampled_token_count_cpu = torch.empty( | |
| self.max_num_reqs, | |
| dtype=torch.int32, | |
| device="cpu", | |
| pin_memory=PIN_MEMORY, | |
| ) | |
| # Model weight offloader | |
| # Make sure this is called before any get_offloader call | |
| set_offloader(create_offloader(self.offload_config)) | |
| # Ephemeral state transferred between execute_model() and sample_tokens(). | |
| self.execute_model_state: ExecuteModelState | None = None | |
| self.kv_connector_output: KVConnectorOutput | None = None | |
| self.mamba_state_idx: dict[str, int] = {} | |
| self._mamba_bufs: mamba_utils.MambaBuffers | None = None | |
| self.mamba_prev_last_scheduled_idx: CpuGpuBuffer | None = None | |
| if self.cache_config.mamba_cache_mode == "all" and self.num_spec_tokens > 0: | |
| self.mamba_prev_last_scheduled_idx = self._make_buffer( | |
| self.max_num_reqs, dtype=torch.int32 | |
| ) | |
| self.layerwise_nvtx_hooks_registered = False | |
| def update_max_model_len(self, max_model_len: int) -> None: | |
| self.max_model_len = max_model_len | |
| if self.speculative_config: | |
| draft_config = self.speculative_config.draft_model_config | |
| if draft_config is None or draft_config.max_model_len is None: | |
| self.effective_drafter_max_model_len = self.max_model_len | |
| def reset_mm_cache(self) -> None: | |
| """ | |
| Clear the multi-modal cache that was used during profiling, | |
| but no longer needed during inference. | |
| """ | |
| if self.mm_budget: | |
| self.mm_budget.reset_cache() | |
| self.late_interaction_runner.clear() | |
| def reset_encoder_cache(self) -> None: | |
| """Clear the GPU-side encoder cache storing vision embeddings. | |
| This should be called when model weights are updated to ensure | |
| stale embeddings computed with old weights are not reused. | |
| """ | |
| self.encoder_cache.clear() | |
| self.late_interaction_runner.clear() | |
| def post_kv_cache_wake_up(self) -> None: | |
| self.init_fp8_kv_scales() | |
| def init_fp8_kv_scales(self) -> None: | |
| """ | |
| Re-initialize the KV cache and FP8 scales after waking from sleep. | |
| 1. Zero out the KV cache tensors to remove garbage data from re-allocation. | |
| 2. Reset Attention layer scaling factors (_k_scale, _v_scale) to 1.0. | |
| If these are left at 0.0 (default after wake_up), all KV cache values | |
| become effectively zero, causing gibberish output. | |
| """ | |
| if not is_quantized_kv_cache(self.cache_config.cache_dtype): | |
| return | |
| kv_caches = getattr(self, "kv_caches", []) | |
| for cache_entry in kv_caches: | |
| if cache_entry is None: | |
| continue | |
| # Hybrid models (Mamba, DeltaNet) store per-layer state as a | |
| # list of tensors rather than a single tensor. | |
| if isinstance(cache_entry, list): | |
| for t in cache_entry: | |
| t.zero_() | |
| else: | |
| cache_entry.zero_() | |
| k_attr_names = ("_k_scale", "k_scale") | |
| v_attr_names = ("_v_scale", "v_scale") | |
| attn_layers = self.compilation_config.static_forward_context | |
| for name, module in attn_layers.items(): | |
| if isinstance(module, (Attention, MLAAttention)): | |
| # TODO: Generally, scale is 1.0 if user uses on-the-fly fp8 | |
| # kvcache quant. However, to get better accuracy, compression | |
| # frameworks like llm-compressors allow users to tune the | |
| # scale. We may need to restore the specific calibrated scales | |
| # here in the future. | |
| k_scale_val, v_scale_val = 1.0, 1.0 | |
| # Processing K Scale | |
| for attr in k_attr_names: | |
| if hasattr(module, attr): | |
| param = getattr(module, attr) | |
| if isinstance(param, torch.Tensor): | |
| param.fill_(k_scale_val) | |
| # Processing V Scale | |
| for attr in v_attr_names: | |
| if hasattr(module, attr): | |
| param = getattr(module, attr) | |
| if isinstance(param, torch.Tensor): | |
| param.fill_(v_scale_val) | |
| def _get_positions(self, num_tokens: Any): | |
| if isinstance(num_tokens, int): | |
| if self.uses_mrope: | |
| return self.mrope_positions.gpu[:, :num_tokens] | |
| if self.uses_xdrope_dim > 0: | |
| return self.xdrope_positions.gpu[:, :num_tokens] | |
| return self.positions[:num_tokens] | |
| else: | |
| if self.uses_mrope: | |
| return self.mrope_positions.gpu[:, num_tokens] | |
| if self.uses_xdrope_dim > 0: | |
| return self.xdrope_positions.gpu[:, num_tokens] | |
| return self.positions[num_tokens] | |
| def _make_buffer( | |
| self, *size: int | torch.SymInt, dtype: torch.dtype, numpy: bool = True | |
| ) -> CpuGpuBuffer: | |
| return CpuGpuBuffer( | |
| *size, | |
| dtype=dtype, | |
| device=self.device, | |
| with_numpy=numpy, | |
| ) | |
| def _get_mamba_bufs(self) -> mamba_utils.MambaBuffers: | |
| # Only reachable on the ``mamba_cache_mode == "align"`` path. | |
| # The postprocess sub-object is additionally gated on spec | |
| # decode + hybrid model. | |
| assert self.cache_config.mamba_cache_mode == "align" | |
| if self._mamba_bufs is None: | |
| self._mamba_bufs = mamba_utils.MambaBuffers.create( | |
| max_num_reqs=self.max_num_reqs, | |
| kv_cache_config=self.kv_cache_config, | |
| copy_funcs=self.model.get_mamba_state_copy_func(), | |
| make_buffer=self._make_buffer, | |
| device=self.device, | |
| with_postprocess_align=( | |
| self.speculative_config is not None and self.model_config.is_hybrid | |
| ), | |
| ) | |
| return self._mamba_bufs | |
| def _init_model_kwargs(self): | |
| model_kwargs = dict[str, Any]() | |
| if not self.is_pooling_model: | |
| return model_kwargs | |
| num_reqs = self.input_batch.num_reqs | |
| pooling_params = self.input_batch.get_pooling_params() | |
| token_type_id_requests = dict[int, Any]() | |
| for i, param in enumerate(pooling_params): | |
| if ( | |
| param.extra_kwargs is not None | |
| and (token_types := param.extra_kwargs.get("compressed_token_type_ids")) | |
| is not None | |
| ): | |
| token_type_id_requests[i] = token_types | |
| if len(token_type_id_requests) == 0: | |
| return model_kwargs | |
| # Build ids on CPU using the CPU-resident upper bound for seq_lens; | |
| # `torch.arange(seq_lens[i])` with a GPU scalar would force a sync. | |
| seq_lens_cpu = self.optimistic_seq_lens_cpu[:num_reqs].tolist() | |
| token_type_ids = [] | |
| for i in range(num_reqs): | |
| seq_len_i = seq_lens_cpu[i] | |
| pos = token_type_id_requests.get(i, seq_len_i) | |
| ids = (torch.arange(seq_len_i) >= pos).int() | |
| token_type_ids.append(ids) | |
| token_type_ids_cpu = torch.empty( | |
| sum(seq_lens_cpu), dtype=torch.int32, pin_memory=PIN_MEMORY | |
| ) | |
| torch.cat(token_type_ids, out=token_type_ids_cpu) | |
| model_kwargs["token_type_ids"] = token_type_ids_cpu.to( | |
| device=self.device, non_blocking=True | |
| ) | |
| return model_kwargs | |
| def _may_reorder_batch(self, scheduler_output: "SchedulerOutput") -> None: | |
| """ | |
| Update the order of requests in the batch based on the attention | |
| backend's needs. For example, some attention backends (namely MLA) may | |
| want to separate requests based on if the attention computation will be | |
| compute-bound or memory-bound. | |
| Args: | |
| scheduler_output: The scheduler output. | |
| """ | |
| # Attention free models have zero kv_cache_groups, however models | |
| # like Mamba are also attention free but use the kv_cache for | |
| # keeping its internal state. This is why we check the number | |
| # of kv_cache groups instead of solely checking | |
| # for self.model_config.is_attention_free. | |
| if len(self.kv_cache_config.kv_cache_groups) == 0: | |
| return | |
| if self.reorder_batch_threshold is not None: | |
| reorder_batch_to_split_decodes_and_prefills( | |
| self.input_batch, | |
| scheduler_output, | |
| decode_threshold=self.reorder_batch_threshold, | |
| ) | |
| def _init_kv_zero_meta(self) -> None: | |
| """One-time precomputation for _zero_block_ids. | |
| Called from gpu_worker.py outside the CuMem pool context. | |
| """ | |
| self._kv_block_zeroer = KVBlockZeroer( | |
| self.device, | |
| attn_groups_iter=self._kv_cache_spec_attn_group_iterator(), | |
| kernel_block_sizes=self._kernel_block_sizes, | |
| cache_dtype=self.cache_config.cache_dtype, | |
| runner_only_attn_layers=self.runner_only_attn_layers, | |
| static_forward_context=self.compilation_config.static_forward_context, | |
| ) | |
| def _zero_block_ids(self, block_ids: list[int]) -> None: | |
| """Zero the KV cache memory for the given block IDs.""" | |
| if hasattr(self, "_kv_block_zeroer"): | |
| self._kv_block_zeroer.zero_block_ids(block_ids) | |
| # Note: used for model runner override. | |
| def _init_device_properties(self) -> None: | |
| """Initialize attributes from torch.cuda.get_device_properties""" | |
| self.num_sms = num_compute_units(self.device.index) | |
| # Note: used for model runner override. | |
| def _sync_device(self) -> None: | |
| torch.accelerator.synchronize() | |
| def _get_or_create_async_output_copy_stream(self) -> torch.cuda.Stream: | |
| stream = self.async_output_copy_stream | |
| if stream is None: | |
| stream = torch.cuda.Stream() | |
| self.async_output_copy_stream = stream | |
| return stream | |
| def _on_request_state_removed( | |
| self, | |
| req_id: str, | |
| req_state: CachedRequestState | None, | |
| ) -> None: | |
| """Hook for platform runners to clean request-scoped side caches.""" | |
| del req_id, req_state | |
| def _process_encoder_cache_scheduler_output( | |
| self, | |
| scheduler_output: "SchedulerOutput", | |
| ) -> None: | |
| """Apply scheduler-side encoder cache lifecycle updates.""" | |
| for mm_hash in scheduler_output.free_encoder_mm_hashes: | |
| self.encoder_cache.pop(mm_hash, None) | |
| def _update_states(self, scheduler_output: "SchedulerOutput") -> Callable | None: | |
| """Update the cached states and the persistent batch with the scheduler | |
| output. | |
| The updated states are used by the `_prepare_inputs` function to create | |
| the input GPU tensors for the model. | |
| The SamplingMetadata is updated and copied to the GPU if there is a | |
| new/resumed/paused/finished request in the batch. | |
| """ | |
| # Remove finished requests from the cached states. | |
| for req_id in scheduler_output.finished_req_ids: | |
| req_state = self.requests.pop(req_id, None) | |
| self._on_request_state_removed(req_id, req_state) | |
| self.num_prompt_logprobs.pop(req_id, None) | |
| self.late_interaction_runner.on_requests_finished( | |
| scheduler_output.finished_req_ids | |
| ) | |
| # Remove the finished requests from the persistent batch. | |
| # NOTE(woosuk): There could be an edge case where finished_req_ids and | |
| # scheduled_req_ids overlap. This happens when a request is aborted and | |
| # then resubmitted with the same ID. In this case, we treat them as two | |
| # distinct requests - clearing the cached states for the first request | |
| # and handling the second as a new request. | |
| for req_id in scheduler_output.finished_req_ids: | |
| self.input_batch.remove_request(req_id) | |
| # Zero GPU memory for freshly allocated cache blocks to prevent | |
| # stale NaN/data from corrupting attention or SSM computation. | |
| if scheduler_output.new_block_ids_to_zero: | |
| self._zero_block_ids(scheduler_output.new_block_ids_to_zero) | |
| if scheduler_output.kv_cache_block_copies: | |
| copy_kv_cache_blocks_inplace( | |
| self.kv_caches, | |
| self.kv_cache_config.num_blocks, | |
| scheduler_output.kv_cache_block_copies, | |
| ) | |
| # Free the cached encoder outputs. | |
| self._process_encoder_cache_scheduler_output(scheduler_output) | |
| # Remove the unscheduled requests from the persistent batch. | |
| # NOTE(woosuk): The unscheduled requests are either preempted requests | |
| # or running requests that are not scheduled in this step. We remove | |
| # them from the persistent batch but keep their cached states since | |
| # they will be scheduled again sometime in the future. | |
| scheduled_req_ids = scheduler_output.num_scheduled_tokens.keys() | |
| cached_req_ids = self.input_batch.req_id_to_index.keys() | |
| resumed_req_ids = scheduler_output.scheduled_cached_reqs.resumed_req_ids | |
| # NOTE(zhuohan): cached_req_ids and resumed_req_ids are usually disjoint, | |
| # so `(scheduled_req_ids - resumed_req_ids) == scheduled_req_ids` holds | |
| # apart from the forced-preemption case in reset_prefix_cache. And in | |
| # that case we include the resumed_req_ids in the unscheduled set so | |
| # that they get cleared from the persistent batch before being re-scheduled | |
| # in the normal resumed request path. | |
| unscheduled_req_ids = cached_req_ids - (scheduled_req_ids - resumed_req_ids) | |
| # NOTE(woosuk): The persistent batch optimization assumes that | |
| # consecutive batches contain mostly the same requests. If batches | |
| # have low request overlap (e.g., alternating between two distinct | |
| # sets of requests), this optimization becomes very inefficient. | |
| for req_id in unscheduled_req_ids: | |
| self.input_batch.remove_request(req_id) | |
| is_ngram_gpu = ( | |
| self.speculative_config is not None | |
| and self.speculative_config.use_ngram_gpu() | |
| ) | |
| if is_ngram_gpu: | |
| ngram_gpu_new_reqs: list[CachedRequestState] = [] | |
| reqs_to_add: list[CachedRequestState] = [] | |
| deferred_spec_decode_corrections = [] | |
| # Add new requests to the cached states. | |
| for new_req_data in scheduler_output.scheduled_new_reqs: | |
| req_id = new_req_data.req_id | |
| if req_id in self.requests: | |
| # For streaming case only. | |
| req_state = self._update_streaming_request(req_id, new_req_data) | |
| reqs_to_add.append(req_state) | |
| continue | |
| sampling_params = new_req_data.sampling_params | |
| pooling_params = new_req_data.pooling_params | |
| if ( | |
| sampling_params | |
| and sampling_params.sampling_type == SamplingType.RANDOM_SEED | |
| ): | |
| generator = torch.Generator(device=self.device) | |
| generator.manual_seed(sampling_params.seed) | |
| else: | |
| generator = None | |
| if self.is_pooling_model: | |
| assert pooling_params is not None | |
| task = pooling_params.task | |
| assert task is not None, "You did not set `task` in the API" | |
| model = cast(VllmModelForPooling, self.get_model()) | |
| to_update = model.pooler.get_pooling_updates(task) | |
| to_update.apply(pooling_params) | |
| req_state = CachedRequestState( | |
| req_id=req_id, | |
| prompt_token_ids=new_req_data.prompt_token_ids, | |
| prompt_embeds=new_req_data.prompt_embeds, | |
| prompt_is_token_ids=new_req_data.prompt_is_token_ids, | |
| mm_features=new_req_data.mm_features, | |
| sampling_params=sampling_params, | |
| pooling_params=pooling_params, | |
| generator=generator, | |
| block_ids=new_req_data.block_ids, | |
| num_computed_tokens=new_req_data.num_computed_tokens, | |
| output_token_ids=[], | |
| lora_request=new_req_data.lora_request, | |
| ) | |
| self.requests[req_id] = req_state | |
| self.late_interaction_runner.register_request(req_id, pooling_params) | |
| if sampling_params and sampling_params.prompt_logprobs is not None: | |
| self.num_prompt_logprobs[req_id] = ( | |
| self.input_batch.vocab_size | |
| if sampling_params.prompt_logprobs == -1 | |
| else sampling_params.prompt_logprobs | |
| ) | |
| # Only relevant for models using M-RoPE (e.g, Qwen2-VL) | |
| if self.uses_mrope: | |
| self._init_mrope_positions(req_state) | |
| # Only relevant for models using XD-RoPE (e.g, HunYuan-VL) | |
| if self.uses_xdrope_dim > 0: | |
| self._init_xdrope_positions(req_state) | |
| reqs_to_add.append(req_state) | |
| # Track new requests for ngram_gpu full tensor copy | |
| if is_ngram_gpu: | |
| ngram_gpu_new_reqs.append(req_state) | |
| # Update the states of the running/resumed requests. | |
| is_last_rank = get_pp_group().is_last_rank | |
| req_data = scheduler_output.scheduled_cached_reqs | |
| scheduled_spec_tokens = scheduler_output.scheduled_spec_decode_tokens | |
| # Save scheduler-allocated spec lengths before trimming so | |
| # prev_num_draft_len keeps the optimistic count for rejection correction. | |
| original_num_spec_per_req: dict[str, int] = {} | |
| if ( | |
| self.speculative_config is not None | |
| and self.speculative_config.use_ngram_gpu() | |
| ): | |
| for req_id, toks in scheduled_spec_tokens.items(): | |
| original_num_spec_per_req[req_id] = len(toks) | |
| update_scheduler_for_invalid_drafts( | |
| self._num_valid_draft_tokens_event, | |
| self._num_valid_draft_tokens_cpu, | |
| scheduler_output, | |
| self.input_batch.req_id_to_index, | |
| ) | |
| if self.use_async_spec_decode: | |
| self.prev_num_draft_tokens.np.fill(0) | |
| for i, req_id in enumerate(req_data.req_ids): | |
| req_state = self.requests[req_id] | |
| num_computed_tokens = req_data.num_computed_tokens[i] | |
| new_block_ids = req_data.new_block_ids[i] | |
| resumed_from_preemption = req_id in req_data.resumed_req_ids | |
| num_output_tokens = req_data.num_output_tokens[i] | |
| req_index = self.input_batch.req_id_to_index.get(req_id) | |
| if req_state.prev_num_draft_len and self.use_async_scheduling: | |
| # prev_num_draft_len is used in async scheduling mode with | |
| # spec decode. it indicates if need to update num_computed_tokens | |
| # of the request. for example: | |
| # first step: num_computed_tokens = 0, spec_tokens = [], | |
| # prev_num_draft_len = 0. | |
| # second step: num_computed_tokens = 100(prompt length), | |
| # spec_tokens = [a,b], prev_num_draft_len = 0. | |
| # third step: num_computed_tokens = 100 + 2, spec_tokens = [c,d], | |
| # prev_num_draft_len = 2. | |
| # num_computed_tokens in first step and second step doesn't contain | |
| # the spec tokens length, but in third step it contains the | |
| # spec tokens length. we only need to update num_computed_tokens | |
| # when prev_num_draft_len > 0. | |
| if req_index is None: | |
| req_state.prev_num_draft_len = 0 | |
| else: | |
| # Optimistically assume all accepted; queue up a correction | |
| # to be called after the model forward to preserve async | |
| # scheduling. Corrected on GPU in _prepare_inputs. | |
| optimistic_num_accepted = req_state.prev_num_draft_len | |
| req_state.output_token_ids.extend([-1] * optimistic_num_accepted) | |
| deferred_spec_decode_corrections.append( | |
| (req_id, optimistic_num_accepted, req_state) | |
| ) | |
| prev_req_index = ( | |
| self.input_batch.prev_req_id_to_index.get(req_id) | |
| if self.input_batch.prev_req_id_to_index | |
| else None | |
| ) | |
| if prev_req_index is not None: | |
| self.prev_num_draft_tokens.np[prev_req_index] = ( | |
| optimistic_num_accepted | |
| ) | |
| if is_ngram_gpu and optimistic_num_accepted > 0: | |
| self.input_batch.num_tokens_no_spec[req_index] += ( | |
| optimistic_num_accepted | |
| ) | |
| # Update the cached states. | |
| req_state.num_computed_tokens = num_computed_tokens | |
| if not is_last_rank: | |
| if not req_data.new_token_ids: | |
| # Async scheduled PP: Sampled tokens propagated via GPU broadcast. | |
| new_token_ids: list[int] = [] | |
| else: | |
| # Non-async scheduling with PP: The scheduler sends | |
| # sampled token ids back because there's no direct communication | |
| # between the first-stage worker and the last-stage worker. | |
| new_token_ids = req_data.new_token_ids[i] | |
| # Add the sampled token(s) from the previous step (if any). | |
| # This doesn't include "unverified" tokens like spec tokens. | |
| num_new_tokens = ( | |
| num_computed_tokens + len(new_token_ids) - req_state.num_tokens | |
| ) | |
| if num_new_tokens == 1: | |
| # Avoid slicing list in most common case. | |
| req_state.output_token_ids.append(new_token_ids[-1]) | |
| elif num_new_tokens > 0: | |
| req_state.output_token_ids.extend( | |
| new_token_ids[-num_new_tokens:] | |
| ) | |
| elif num_output_tokens < len(req_state.output_token_ids): | |
| # Some output tokens were discarded due to a sync-KV-load | |
| # failure, or output_token_ids was inflated by the optimistic | |
| # extend above (async spec decode). Align the cached state. | |
| del req_state.output_token_ids[num_output_tokens:] | |
| if req_index is not None: | |
| end_idx = ( | |
| self.input_batch.num_prompt_tokens[req_index] | |
| + num_output_tokens | |
| ) | |
| self.input_batch.num_tokens_no_spec[req_index] = end_idx | |
| # Update the block IDs. | |
| if not resumed_from_preemption: | |
| if new_block_ids is not None: | |
| # Append the new blocks to the existing block IDs. | |
| for block_ids, new_ids in zip(req_state.block_ids, new_block_ids): | |
| block_ids.extend(new_ids) | |
| else: | |
| assert req_index is None | |
| assert new_block_ids is not None | |
| # The request is resumed from preemption. | |
| # Replace the existing block IDs with the new ones. | |
| req_state.block_ids = new_block_ids | |
| if req_index is None: | |
| # The request is not in the persistent batch. | |
| # The request was either preempted and resumed later, or was not | |
| # scheduled in the previous step and needs to be added again. | |
| if self.use_async_scheduling and num_output_tokens > 0: | |
| # We must recover the output token ids for resumed requests in the | |
| # async scheduling case, so that correct input_ids are obtained. | |
| resumed_token_ids = req_data.all_token_ids[req_id] | |
| req_state.output_token_ids = resumed_token_ids[-num_output_tokens:] | |
| reqs_to_add.append(req_state) | |
| # Track resumed requests for ngram_gpu full tensor copy | |
| if is_ngram_gpu: | |
| ngram_gpu_new_reqs.append(req_state) | |
| continue | |
| # Update the persistent batch. | |
| self.input_batch.num_computed_tokens_cpu[req_index] = num_computed_tokens | |
| if new_block_ids is not None: | |
| self.input_batch.block_table.append_row(new_block_ids, req_index) | |
| # For the last rank, we don't need to update the token_ids_cpu | |
| # because the sampled tokens are already cached. | |
| if not is_last_rank: | |
| start_token_index = self.input_batch.num_tokens_no_spec[req_index] | |
| # For chunked prefill, num_computed_tokens may less | |
| # than num_tokens_no_spec. | |
| # Async scheduled PP: no new_token_ids, advance num_tokens_no_spec | |
| # according to num_computed_tokens. | |
| end_token_index = max( | |
| start_token_index, | |
| num_computed_tokens + len(new_token_ids), | |
| ) | |
| if end_token_index > start_token_index: | |
| if new_token_ids: | |
| # Add new_token_ids to token_ids_cpu. | |
| num_new_tokens = end_token_index - start_token_index | |
| tokens_to_append = new_token_ids[-num_new_tokens:] | |
| self.input_batch.token_ids_cpu[ | |
| req_index, start_token_index:end_token_index | |
| ] = tokens_to_append | |
| self.input_batch.is_token_ids[ | |
| req_index, start_token_index:end_token_index | |
| ] = True | |
| self.input_batch.num_tokens_no_spec[req_index] = end_token_index | |
| # Add spec_token_ids to token_ids_cpu. | |
| self.input_batch.update_req_spec_token_ids(req_state, scheduled_spec_tokens) | |
| # Restore scheduler-side draft count after ngram trimming. | |
| if original_num_spec_per_req: | |
| orig = original_num_spec_per_req.get(req_id, 0) | |
| if orig != req_state.prev_num_draft_len: | |
| req_state.prev_num_draft_len = orig | |
| # Add the new or resumed requests to the persistent batch. | |
| # The smaller empty indices are filled first. | |
| for request in reqs_to_add: | |
| self.input_batch.add_request(request) | |
| self.input_batch.update_req_spec_token_ids(request, scheduled_spec_tokens) | |
| # Condense the batched states if there are gaps left by removed requests | |
| self.input_batch.condense() | |
| # Allow attention backend to reorder the batch, potentially | |
| self._may_reorder_batch(scheduler_output) | |
| # Refresh batch metadata with any pending updates. | |
| self.input_batch.refresh_metadata() | |
| # Incrementally update ngram_gpu tensors after batch is stable | |
| if is_ngram_gpu: | |
| update_ngram_gpu_tensors_incremental( | |
| self.input_batch, | |
| self.token_ids_gpu_tensor, | |
| self.num_tokens_no_spec_gpu, | |
| ngram_gpu_new_reqs, | |
| self.device, | |
| _pinned_idx_buf=self._ngram_pinned_idx_buf, | |
| _pinned_val_buf=self._ngram_pinned_val_buf, | |
| ) | |
| if deferred_spec_decode_corrections: | |
| def correct_spec_decode_token_counts(): | |
| valid_sampled_token_count = self._get_valid_sampled_token_count() | |
| if not valid_sampled_token_count: | |
| return | |
| prev_req_id_to_index = self.input_batch.prev_req_id_to_index | |
| if not prev_req_id_to_index: | |
| return | |
| for ( | |
| req_id, | |
| optimistic_num_accepted, | |
| req_state, | |
| ) in deferred_spec_decode_corrections: | |
| prev_req_index = prev_req_id_to_index.get(req_id) | |
| if prev_req_index is None: | |
| continue | |
| num_accepted = valid_sampled_token_count[prev_req_index] - 1 | |
| correction = optimistic_num_accepted - num_accepted | |
| req_state.num_computed_tokens -= correction | |
| cur_req_index = self.input_batch.req_id_to_index.get(req_id) | |
| if cur_req_index is None: | |
| continue | |
| self.input_batch.num_computed_tokens_cpu[cur_req_index] -= ( | |
| correction | |
| ) | |
| if is_ngram_gpu and correction > 0: | |
| self.input_batch.num_tokens_no_spec[cur_req_index] -= correction | |
| self.num_tokens_no_spec_gpu[cur_req_index] -= correction | |
| return correct_spec_decode_token_counts | |
| else: | |
| return None | |
| def _update_states_after_model_execute( | |
| self, output_token_ids: torch.Tensor, scheduler_output: "SchedulerOutput" | |
| ) -> None: | |
| """Update the cached states after model execution. | |
| This is used for MTP/EAGLE for hybrid models, as in linear attention, | |
| only the last token's state is kept. In MTP/EAGLE, for draft tokens | |
| the state are kept util we decide how many tokens are accepted for | |
| each sequence, and a shifting is done during the next iteration | |
| based on the number of accepted tokens. | |
| """ | |
| if not self.speculative_config or not self.model_config.is_hybrid: | |
| return | |
| # Count the number of accepted tokens for each sequence. | |
| # Valid tokens are contiguous from position 0, so counting non-(-1) | |
| # tokens gives us the first -1 position (i.e., number of accepted). | |
| num_reqs = output_token_ids.size(0) | |
| self.num_accepted_tokens.gpu[:num_reqs] = (output_token_ids != -1).sum(dim=1) | |
| if self.cache_config.mamba_cache_mode == "align": | |
| # Fused GPU postprocess: state copies + per-request accepted-token | |
| # update without CPU-GPU sync. The metadata | |
| # (num_scheduled_tokens, num_draft_tokens, num_computed_tokens) is | |
| # pre-staged to GPU buffers in _prepare_inputs. | |
| mamba_utils.postprocess_mamba_align_gpu( | |
| bufs=self._get_mamba_bufs(), | |
| num_reqs=num_reqs, | |
| num_accepted_tokens_gpu=self.num_accepted_tokens.gpu, | |
| num_accepted_tokens_cpu_tensor=( | |
| self.input_batch.num_accepted_tokens_cpu_tensor | |
| ), | |
| input_batch=self.input_batch, | |
| kv_cache_config=self.kv_cache_config, | |
| forward_context=self.compilation_config.static_forward_context, | |
| mamba_state_copy_funcs=self.model.get_mamba_state_copy_func(), | |
| ) | |
| assert self.num_accepted_tokens_event is not None | |
| self.num_accepted_tokens_event.record() | |
| else: | |
| self.input_batch.num_accepted_tokens_cpu_tensor[:num_reqs].copy_( | |
| self.num_accepted_tokens.gpu[:num_reqs], non_blocking=True | |
| ) | |
| assert self.num_accepted_tokens_event is not None | |
| self.num_accepted_tokens_event.record() | |
| if self.cache_config.mamba_cache_mode == "all": | |
| mamba_utils.postprocess_mamba_all( | |
| scheduler_output, | |
| self.kv_cache_config, | |
| self.input_batch, | |
| self.requests, | |
| self.mamba_state_idx, | |
| self.num_spec_tokens, | |
| num_reqs, | |
| ) | |
| def _update_streaming_request( | |
| self, req_id: str, new_req_data: NewRequestData | |
| ) -> CachedRequestState: | |
| """Updates streaming session request from `scheduled_new_reqs`. | |
| Removes the request from InputBatch (if present), updates the cached | |
| state, and prepares it for re-addition to the batch. | |
| NOTE: prompt_token_ids includes intermediate output tokens - tokens | |
| previously generated but now are input context (part of the prompt). | |
| """ | |
| self.input_batch.remove_request(req_id) | |
| req_state = self.requests[req_id] | |
| req_state.prompt_token_ids = new_req_data.prompt_token_ids | |
| req_state.mm_features = new_req_data.mm_features | |
| req_state.prompt_embeds = new_req_data.prompt_embeds | |
| req_state.sampling_params = new_req_data.sampling_params | |
| req_state.pooling_params = new_req_data.pooling_params | |
| self.late_interaction_runner.register_request(req_id, req_state.pooling_params) | |
| req_state.block_ids = new_req_data.block_ids | |
| req_state.num_computed_tokens = new_req_data.num_computed_tokens | |
| req_state.num_prompt_tokens = length_from_prompt_token_ids_or_embeds( | |
| req_state.prompt_token_ids, req_state.prompt_embeds | |
| ) | |
| # Clear `output_token_ids` as previous output tokens are now part of | |
| # `prompt_token_ids`. | |
| req_state.output_token_ids.clear() | |
| if self.uses_mrope: | |
| self._init_mrope_positions(req_state) | |
| return req_state | |
| def _init_mrope_positions(self, req_state: CachedRequestState): | |
| model = self.get_model() | |
| assert supports_mrope(model), "M-RoPE support is not implemented." | |
| mrope_model = cast(SupportsMRoPE, model) | |
| # `prompt_embeds` is a passthrough modality (no grid_thw), models' | |
| # M-RoPE code assumes per-feature grid info, so filter it out. The | |
| # prompt_embeds positions are treated as text positions for M-RoPE. | |
| mrope_features = [ | |
| f for f in req_state.mm_features if f.modality != "prompt_embeds" | |
| ] | |
| if req_state.prompt_token_ids is not None: | |
| input_tokens = req_state.prompt_token_ids | |
| elif req_state.prompt_embeds is not None: | |
| # For embeddings-only inputs, get_mrope_input_positions only | |
| # needs the sequence length when mm_features is empty (which is | |
| # the case here since prompt_embeds are filtered out above). | |
| seq_len = req_state.prompt_embeds.shape[0] | |
| input_tokens = list(range(seq_len)) | |
| else: | |
| raise ValueError( | |
| "M-RoPE requires either prompt_token_ids or prompt_embeds." | |
| ) | |
| req_state.mrope_positions, req_state.mrope_position_delta = ( | |
| mrope_model.get_mrope_input_positions( | |
| input_tokens, | |
| mrope_features, | |
| ) | |
| ) | |
| def _init_xdrope_positions(self, req_state: CachedRequestState): | |
| model = self.get_model() | |
| xdrope_model = cast(SupportsXDRoPE, model) | |
| assert req_state.prompt_token_ids is not None, ( | |
| "XD-RoPE requires prompt_token_ids to be available." | |
| ) | |
| assert supports_xdrope(model), "XD-RoPE support is not implemented." | |
| req_state.xdrope_positions = xdrope_model.get_xdrope_input_positions( | |
| req_state.prompt_token_ids, | |
| req_state.mm_features, | |
| ) | |
| def _extract_mm_kwargs( | |
| self, | |
| scheduler_output: "SchedulerOutput", | |
| ) -> BatchedTensorInputs: | |
| if not scheduler_output or not self.is_multimodal_raw_input_only_model: | |
| return {} | |
| mm_kwargs = list[tuple[str, MultiModalKwargsItem]]() | |
| for req in scheduler_output.scheduled_new_reqs: | |
| for feature in req.mm_features: | |
| if feature.data is not None: | |
| mm_kwargs.append((feature.modality, feature.data)) | |
| # Input all modalities at once | |
| mm_kwargs_combined: BatchedTensorInputs = {} | |
| for _, _, mm_kwargs_batch in group_and_batch_mm_kwargs( | |
| mm_kwargs, | |
| device=self.device, | |
| pin_memory=PIN_MEMORY, | |
| ): | |
| mm_kwargs_combined.update(mm_kwargs_batch) | |
| return mm_kwargs_combined | |
| def _dummy_mm_kwargs(self, num_seqs: int) -> BatchedTensorInputs: | |
| if not self.is_multimodal_raw_input_only_model: | |
| return {} | |
| mm_budget = self.mm_budget | |
| assert mm_budget is not None | |
| if not mm_budget.mm_max_toks_per_item: | |
| return {} # No tower modalities (embed-only mode) | |
| dummy_modality = mm_budget.get_modality_with_max_tokens() | |
| return self._get_mm_dummy_batch(dummy_modality, num_seqs) | |
| def _get_cumsum_and_arange( | |
| self, | |
| num_tokens: np.ndarray, | |
| arange_out: np.ndarray, | |
| cumsum_dtype: np.dtype | None = None, | |
| ) -> np.ndarray: | |
| """Get the cumulative sum and batched arange of the given array. | |
| E.g., [2, 5, 3] -> [2, 7, 10], arange written to | |
| arange_out[:10] as [0, 1, 0, 1, 2, 3, 4, 0, 1, 2]. | |
| Equivalent to but faster than: | |
| np.concatenate([np.arange(n) for n in num_tokens]) | |
| """ | |
| # Step 1. [2, 5, 3] -> [2, 7, 10] | |
| cu_num_tokens = np.cumsum(num_tokens, dtype=cumsum_dtype) | |
| total_num_tokens = cu_num_tokens[-1] | |
| # Step 2. [2, 7, 10] -> [0, 0, 2, 2, 2, 2, 2, 7, 7, 7] | |
| cumsums_offsets = np.repeat(cu_num_tokens - num_tokens, num_tokens) | |
| # Step 3. [0, 1, 0, 1, 2, 3, 4, 0, 1, 2] | |
| np.subtract( | |
| self.arange_np[:total_num_tokens], | |
| cumsums_offsets, | |
| out=arange_out[:total_num_tokens], | |
| ) | |
| return cu_num_tokens | |
| def _compute_prev_positions(self, num_reqs: int) -> None: | |
| """Build prev_positions mapping: current pos -> previous pos (-1 if new). | |
| Populates self.prev_positions.np[:num_reqs] with the mapping. | |
| """ | |
| prev_req_id_to_index = self.input_batch.prev_req_id_to_index | |
| prev_positions = self.prev_positions.np[:num_reqs] | |
| if not prev_req_id_to_index: | |
| prev_positions.fill(-1) | |
| return | |
| for i, req_id in enumerate(self.input_batch.req_ids[:num_reqs]): | |
| prev_positions[i] = prev_req_id_to_index.get(req_id, -1) | |
| def _prepare_input_ids( | |
| self, | |
| scheduler_output: "SchedulerOutput", | |
| num_reqs: int, | |
| total_num_scheduled_tokens: int, | |
| cu_num_tokens: np.ndarray, | |
| ) -> None: | |
| """Prepare the input IDs for the current batch. | |
| Carefully handles the `prev_sampled_token_ids` which can be cached | |
| from the previous engine iteration, in which case those tokens on the | |
| GPU need to be copied into the corresponding slots into input_ids. | |
| Uses self.prev_positions[:num_reqs] which maps current pos -> prev pos | |
| (-1 for new requests). | |
| """ | |
| if self.input_batch.prev_sampled_token_ids is None: | |
| # Normal scheduling case | |
| self.input_ids.copy_to_gpu(total_num_scheduled_tokens) | |
| if self.enable_prompt_embeds: | |
| self.inputs_embeds.copy_to_gpu(total_num_scheduled_tokens) | |
| self.is_token_ids.copy_to_gpu(total_num_scheduled_tokens) | |
| return | |
| # Async scheduling case, where some decode requests from the previous | |
| # iteration won't have entries in input_ids_cpu and need to be copied | |
| # on the GPU from prev_sampled_token_ids. | |
| prev_positions = self.prev_positions.np[:num_reqs] | |
| scheduled_spec_tokens = scheduler_output.scheduled_spec_decode_tokens | |
| sample_flattened_indices: list[int] = [] | |
| spec_flattened_indices: list[int] = [] | |
| prev_draft_token_indices: list[int] = [] | |
| prev_indices: list[int] = [] | |
| common_indices_match = True | |
| max_flattened_index = -1 | |
| total_num_spec_tokens = 0 | |
| for cur_index in range(num_reqs): | |
| prev_index = prev_positions[cur_index] | |
| if prev_index < 0: | |
| continue | |
| prev_indices.append(prev_index) | |
| req_id = self.input_batch.req_ids[cur_index] | |
| # We need to compute the flattened input_ids index of the | |
| # last token in each common request. | |
| draft_len = len(scheduled_spec_tokens.get(req_id, ())) | |
| total_num_spec_tokens += draft_len | |
| flattened_index = cu_num_tokens[cur_index].item() - 1 | |
| # example: cu_num_tokens = [2, 5, 8], draft_tokens = [1, 2, 2] | |
| # sample_flattened_indices = [0, 2, 5] | |
| # spec_flattened_indices = [1, 3, 4, 6, 7] | |
| sample_flattened_indices.append(flattened_index - draft_len) | |
| spec_flattened_indices.extend( | |
| range(flattened_index - draft_len + 1, flattened_index + 1) | |
| ) | |
| start = prev_index * self.prev_num_spec_tokens | |
| # prev_draft_token_indices is used to find which draft_tokens_id | |
| # should be copied to input_ids | |
| # example: prev draft_tokens_id [[1,2], [3,4], [5, 6]] | |
| # flatten draft_tokens_id [1,2,3,4,5,6] | |
| # draft_len of each request [1, 2, 1] | |
| # then prev_draft_token_indices is [0, 2, 3, 4] | |
| prev_draft_token_indices.extend(range(start, start + draft_len)) | |
| common_indices_match &= prev_index == flattened_index | |
| max_flattened_index = max(max_flattened_index, flattened_index) | |
| num_common_tokens = len(sample_flattened_indices) | |
| total_without_spec = total_num_scheduled_tokens - total_num_spec_tokens | |
| if self.enable_prompt_embeds: | |
| # The multimodal embed path reads is_token_ids.gpu; its .cpu copy is | |
| # refreshed every step but the async fast paths below only scatter | |
| # input_ids.gpu, so refresh is_token_ids.gpu here too. | |
| self.is_token_ids.copy_to_gpu(total_num_scheduled_tokens) | |
| if num_common_tokens < total_without_spec: | |
| # If not all requests are decodes from the last iteration, | |
| # we need to copy the input_ids_cpu to the GPU first. | |
| self.input_ids.copy_to_gpu(total_num_scheduled_tokens) | |
| if self.enable_prompt_embeds: | |
| self.inputs_embeds.copy_to_gpu(total_num_scheduled_tokens) | |
| if num_common_tokens == 0: | |
| # No requests in common with the previous iteration | |
| # So input_ids.cpu will have all the input ids. | |
| return | |
| if common_indices_match and max_flattened_index == (num_common_tokens - 1): | |
| # Common-case optimization: the batch is unchanged | |
| # and no reordering happened. | |
| # The indices are both the same permutation of 0..N-1 so | |
| # we can copy directly using a single slice. | |
| self.input_ids.gpu[:num_common_tokens].copy_( | |
| self.input_batch.prev_sampled_token_ids[:num_common_tokens, 0], | |
| non_blocking=True, | |
| ) | |
| return | |
| # Upload the index tensors asynchronously so the scatter can be non-blocking. | |
| sampled_tokens_index_tensor = torch.tensor( | |
| sample_flattened_indices, dtype=torch.int64, pin_memory=PIN_MEMORY | |
| ).to(self.device, non_blocking=True) | |
| prev_common_req_indices_tensor = torch.tensor( | |
| prev_indices, dtype=torch.int64, pin_memory=PIN_MEMORY | |
| ).to(self.device, non_blocking=True) | |
| self.input_ids.gpu.scatter_( | |
| dim=0, | |
| index=sampled_tokens_index_tensor, | |
| src=self.input_batch.prev_sampled_token_ids[ | |
| prev_common_req_indices_tensor, 0 | |
| ], | |
| ) | |
| # Scatter the draft tokens after the sampled tokens are scattered. | |
| if self._draft_token_ids is None or not spec_flattened_indices: | |
| return | |
| assert isinstance(self._draft_token_ids, torch.Tensor) | |
| draft_tokens_index_tensor = torch.tensor( | |
| spec_flattened_indices, dtype=torch.int64, pin_memory=PIN_MEMORY | |
| ).to(self.device, non_blocking=True) | |
| prev_draft_token_indices_tensor = torch.tensor( | |
| prev_draft_token_indices, dtype=torch.int64, pin_memory=PIN_MEMORY | |
| ).to(self.device, non_blocking=True) | |
| # because input_ids dtype is torch.int32, | |
| # so convert draft_token_ids to torch.int32 here. | |
| draft_token_ids = self._draft_token_ids.to(dtype=torch.int32) | |
| self.input_ids.gpu.scatter_( | |
| dim=0, | |
| index=draft_tokens_index_tensor, | |
| src=draft_token_ids.flatten()[prev_draft_token_indices_tensor], | |
| ) | |
| def _get_encoder_seq_lens( | |
| self, | |
| num_scheduled_tokens: dict[str, int], | |
| kv_cache_spec: KVCacheSpec, | |
| num_reqs: int, | |
| for_cudagraph_capture: bool = False, | |
| ) -> tuple[torch.Tensor | None, np.ndarray | None]: | |
| if not isinstance(kv_cache_spec, CrossAttentionSpec): | |
| return None, None | |
| # Zero out buffer for padding requests that are not actually scheduled (CGs) | |
| self.encoder_seq_lens.np[:num_reqs] = 0 | |
| # Build encoder_seq_lens array mapping request indices to | |
| # encoder lengths for inputs scheduled in this batch | |
| for req_id in num_scheduled_tokens: | |
| req_index = self.input_batch.req_id_to_index[req_id] | |
| req_state = self.requests[req_id] | |
| if req_state.mm_features is None: | |
| self.encoder_seq_lens.np[req_index] = 0 | |
| continue | |
| # Get the total number of encoder input tokens for running encoder requests | |
| # whether encoding is finished or not so that cross-attention knows how | |
| # many encoder tokens to attend to. | |
| encoder_input_tokens = sum( | |
| feature.mm_position.length for feature in req_state.mm_features | |
| ) | |
| self.encoder_seq_lens.np[req_index] = encoder_input_tokens | |
| if for_cudagraph_capture: | |
| # During CUDA graph capture, we need to use realistic encoder lengths | |
| # so that max_seqlen_k is captured with the correct value. | |
| max_encoder_len = getattr( | |
| self.model_config.hf_config, | |
| "max_source_positions", | |
| self.max_encoder_len, | |
| ) | |
| self.encoder_seq_lens.np[:num_reqs] = max_encoder_len | |
| self.encoder_seq_lens.copy_to_gpu(num_reqs) | |
| encoder_seq_lens = self.encoder_seq_lens.gpu[:num_reqs] | |
| encoder_seq_lens_cpu = self.encoder_seq_lens.np[:num_reqs] | |
| return encoder_seq_lens, encoder_seq_lens_cpu | |
| def _prepare_inputs( | |
| self, | |
| scheduler_output: "SchedulerOutput", | |
| num_scheduled_tokens: np.ndarray, | |
| ) -> tuple[ | |
| torch.Tensor, | |
| SpecDecodeMetadata | None, | |
| ]: | |
| """ | |
| Returns: | |
| tuple[logits_indices, spec_decode_metadata] | |
| """ | |
| total_num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens | |
| assert total_num_scheduled_tokens > 0 | |
| num_reqs = self.input_batch.num_reqs | |
| assert num_reqs > 0 | |
| # OPTIMIZATION: Start copying the block table first. | |
| # This way, we can overlap the copy with the following CPU operations. | |
| self.input_batch.block_table.commit_block_table(num_reqs) | |
| # Get request indices. | |
| # E.g., [2, 5, 3] -> [0, 0, 1, 1, 1, 1, 1, 2, 2, 2] | |
| req_indices = np.repeat(self.arange_np[:num_reqs], num_scheduled_tokens) | |
| # cu_num_tokens: [2, 5, 3] -> [2, 7, 10] | |
| # self.query_pos.np[:10]: [0, 1, 0, 1, 2, 3, 4, 0, 1, 2] | |
| cu_num_tokens = self._get_cumsum_and_arange( | |
| num_scheduled_tokens, self.query_pos.np | |
| ) | |
| # Get positions. | |
| positions_np = ( | |
| self.input_batch.num_computed_tokens_cpu[req_indices] | |
| + self.query_pos.np[: cu_num_tokens[-1]] | |
| ) | |
| # Calculate M-RoPE positions. | |
| # Only relevant for models using M-RoPE (e.g, Qwen2-VL) | |
| if self.uses_mrope: | |
| self._calc_mrope_positions(scheduler_output) | |
| # Calculate XD-RoPE positions. | |
| # Only relevant for models using XD-RoPE (e.g, HunYuan-VL) | |
| if self.uses_xdrope_dim > 0: | |
| self._calc_xdrope_positions(scheduler_output) | |
| # Get token indices. | |
| # E.g., [0, 1, 0, 1, 2, 3, 4, 0, 1, 2] | |
| # -> [0, 1, M, M + 1, M + 2, M + 3, M + 4, 2 * M, 2 * M + 1, 2 * M + 2] | |
| # where M is the max_model_len. | |
| token_indices = ( | |
| positions_np + req_indices * self.input_batch.token_ids_cpu.shape[1] | |
| ) | |
| token_indices_tensor = torch.from_numpy(token_indices) | |
| # NOTE(woosuk): We use torch.index_select instead of np.take here | |
| # because torch.index_select is much faster than np.take for large | |
| # tensors. | |
| torch.index_select( | |
| self.input_batch.token_ids_cpu_tensor.flatten(), | |
| 0, | |
| token_indices_tensor, | |
| out=self.input_ids.cpu[:total_num_scheduled_tokens], | |
| ) | |
| if self.enable_prompt_embeds: | |
| is_token_ids = self.input_batch.is_token_ids_tensor.flatten() | |
| torch.index_select( | |
| is_token_ids, | |
| 0, | |
| token_indices_tensor, | |
| out=self.is_token_ids.cpu[:total_num_scheduled_tokens], | |
| ) | |
| # Because we did not pre-allocate a massive prompt_embeds CPU tensor on | |
| # the InputBatch, we need to fill in the prompt embeds into the expected | |
| # spots in the GpuModelRunner's pre-allocated prompt_embeds tensor. | |
| if self.input_batch.req_prompt_embeds: | |
| output_idx = 0 | |
| for req_idx in range(num_reqs): | |
| num_sched = num_scheduled_tokens[req_idx] | |
| # Skip if this request doesn't have embeddings | |
| if req_idx not in self.input_batch.req_prompt_embeds: | |
| output_idx += num_sched | |
| continue | |
| # Skip if no tokens scheduled | |
| if num_sched <= 0: | |
| output_idx += num_sched | |
| continue | |
| req_embeds = self.input_batch.req_prompt_embeds[req_idx] | |
| start_pos = self.input_batch.num_computed_tokens_cpu[req_idx] | |
| # Skip if trying to read beyond available embeddings | |
| if start_pos >= req_embeds.shape[0]: | |
| output_idx += num_sched | |
| continue | |
| # Copy available embeddings | |
| end_pos = start_pos + num_sched | |
| actual_end = min(end_pos, req_embeds.shape[0]) | |
| actual_num_sched = actual_end - start_pos | |
| if actual_num_sched > 0: | |
| self.inputs_embeds.cpu[ | |
| output_idx : output_idx + actual_num_sched | |
| ].copy_(req_embeds[start_pos:actual_end]) | |
| output_idx += num_sched | |
| # Prepare the attention metadata. | |
| self.query_start_loc.np[0] = 0 | |
| self.query_start_loc.np[1 : num_reqs + 1] = cu_num_tokens | |
| # Note: pad query_start_loc to be non-decreasing, as kernels | |
| # like FlashAttention requires that | |
| self.query_start_loc.np[num_reqs + 1 :].fill(cu_num_tokens[-1]) | |
| self.query_start_loc.copy_to_gpu() | |
| query_start_loc = self.query_start_loc.gpu[: num_reqs + 1] | |
| # Compute optimistic seq_lens (assumes all draft tokens from previous | |
| # iteration accepted). Store in optimistic_seq_lens_cpu for use by | |
| # _build_attention_metadata (max_seq_len) and discard_request_mask. | |
| # seq_lens (GPU) will be computed later using the same optimistic values. | |
| torch.add( | |
| self.input_batch.num_computed_tokens_cpu_tensor[:num_reqs], | |
| torch.from_numpy(num_scheduled_tokens), | |
| out=self.optimistic_seq_lens_cpu[:num_reqs], | |
| ) | |
| self.optimistic_seq_lens_cpu[num_reqs:].fill_(0) | |
| # Build prev_positions mapping: current pos -> prev pos (-1 if new). | |
| # Used for gathering from previous iteration's GPU tensors. | |
| prev_req_id_to_index = self.input_batch.prev_req_id_to_index | |
| self._compute_prev_positions(num_reqs) | |
| num_tokens = [self.requests[r].num_tokens for r in self.input_batch.req_ids] | |
| num_tokens_np = np.array(num_tokens, dtype=np.int32) | |
| # Record which requests should not be sampled, | |
| # so that we could clear the sampled tokens before returning | |
| self.discard_request_mask.np[:num_reqs] = ( | |
| self.optimistic_seq_lens_cpu[:num_reqs].numpy() < num_tokens_np | |
| ) | |
| self.discard_request_mask.copy_to_gpu(num_reqs) | |
| # Sync num_accepted_tokens from CPU (set by | |
| # _update_states_after_model_execute for hybrid models). | |
| # Skipped under async scheduling (non-align): the CPU copy races with | |
| # the in-flight D2H copy and with input-batch row moves. | |
| needs_cpu_accepted_counts = self.num_accepted_tokens_event is not None and not ( | |
| self.use_async_scheduling and self.cache_config.mamba_cache_mode != "align" | |
| ) | |
| if needs_cpu_accepted_counts: | |
| assert self.num_accepted_tokens_event is not None | |
| self.num_accepted_tokens_event.synchronize() | |
| # Async mode: condense() reordered indices, use prev_positions mapping | |
| if self.use_async_scheduling and prev_req_id_to_index: | |
| prev_idx = self.prev_positions.np[:num_reqs] | |
| new_mask = prev_idx < 0 | |
| self.num_accepted_tokens.np[:num_reqs] = ( | |
| self.input_batch.num_accepted_tokens_cpu[ | |
| np.where(new_mask, 0, prev_idx) | |
| ] | |
| ) | |
| self.num_accepted_tokens.np[:num_reqs][new_mask] = 1 | |
| self.input_batch.num_accepted_tokens_cpu[:num_reqs] = ( | |
| self.num_accepted_tokens.np[:num_reqs] | |
| ) | |
| else: | |
| # Non-async mode: use values directly | |
| self.num_accepted_tokens.np[:num_reqs] = ( | |
| self.input_batch.num_accepted_tokens_cpu[:num_reqs] | |
| ) | |
| self.num_accepted_tokens.np[num_reqs:].fill(1) | |
| self.num_accepted_tokens.copy_to_gpu() | |
| else: | |
| # Default to 1; update_num_computed_tokens_for_batch_change below | |
| # corrects rows that had drafts from valid_sampled_token_count. | |
| self.num_accepted_tokens.np.fill(1) | |
| self.num_accepted_tokens.gpu.fill_(1) | |
| if self.mamba_prev_last_scheduled_idx is not None: | |
| mamba_utils.preprocess_mamba_all_specdec( | |
| scheduler_output, | |
| self.input_batch, | |
| self.mamba_state_idx, | |
| num_reqs, | |
| self.mamba_prev_last_scheduled_idx, | |
| ) | |
| # Update num_computed_tokens on GPU. In async spec decode, | |
| # CPU values are optimistic (all drafts accepted). The kernel | |
| # corrects on GPU using the previous step's | |
| # valid_sampled_token_count_gpu. Otherwise, just copy from CPU. | |
| if ( | |
| self.use_async_spec_decode | |
| and self.valid_sampled_token_count_gpu is not None | |
| and prev_req_id_to_index | |
| ): | |
| self.prev_positions.copy_to_gpu(num_reqs) | |
| self.prev_num_draft_tokens.copy_to_gpu() | |
| cpu_values = self.input_batch.num_computed_tokens_cpu_tensor[:num_reqs].to( | |
| device=self.device, non_blocking=True | |
| ) | |
| update_num_computed_tokens_for_batch_change( | |
| self.num_computed_tokens, | |
| self.num_accepted_tokens.gpu[:num_reqs], | |
| self.prev_positions.gpu[:num_reqs], | |
| self.valid_sampled_token_count_gpu, | |
| self.prev_num_draft_tokens.gpu, | |
| cpu_values, | |
| ) | |
| else: | |
| self.num_computed_tokens[:num_reqs].copy_( | |
| self.input_batch.num_computed_tokens_cpu_tensor[:num_reqs], | |
| non_blocking=True, | |
| ) | |
| self.req_indices.np[:total_num_scheduled_tokens] = req_indices | |
| self.req_indices.copy_to_gpu(total_num_scheduled_tokens) | |
| req_indices_gpu = self.req_indices.gpu[:total_num_scheduled_tokens] | |
| self.query_pos.copy_to_gpu(total_num_scheduled_tokens) | |
| self.num_scheduled_tokens.np[:num_reqs] = num_scheduled_tokens | |
| self.num_scheduled_tokens.copy_to_gpu(num_reqs) | |
| num_scheduled_tokens_gpu = self.num_scheduled_tokens.gpu[:num_reqs] | |
| self.positions[:total_num_scheduled_tokens] = ( | |
| self.num_computed_tokens[req_indices_gpu].to(torch.int64) | |
| + self.query_pos.gpu[:total_num_scheduled_tokens] | |
| ) | |
| self.seq_lens[:num_reqs] = ( | |
| self.num_computed_tokens[:num_reqs] + num_scheduled_tokens_gpu | |
| ) | |
| self.seq_lens[num_reqs:].fill_(0) | |
| self.input_batch.block_table.compute_slot_mapping( | |
| num_reqs, | |
| self.query_start_loc.gpu[: num_reqs + 1], | |
| self.positions[:total_num_scheduled_tokens], | |
| ) | |
| # Copy the tensors to the GPU. | |
| self._prepare_input_ids( | |
| scheduler_output, | |
| num_reqs, | |
| total_num_scheduled_tokens, | |
| cu_num_tokens, | |
| ) | |
| if self.uses_mrope: | |
| # Only relevant for models using M-RoPE (e.g, Qwen2-VL) | |
| self.mrope_positions.gpu[:, :total_num_scheduled_tokens].copy_( | |
| self.mrope_positions.cpu[:, :total_num_scheduled_tokens], | |
| non_blocking=True, | |
| ) | |
| elif self.uses_xdrope_dim > 0: | |
| # Only relevant for models using XD-RoPE (e.g, HunYuan-VL) | |
| self.xdrope_positions.gpu[:, :total_num_scheduled_tokens].copy_( | |
| self.xdrope_positions.cpu[:, :total_num_scheduled_tokens], | |
| non_blocking=True, | |
| ) | |
| if self.use_async_spec_decode and (self.uses_mrope or self.uses_xdrope_dim > 0): | |
| drift = self.num_computed_tokens[req_indices_gpu].to( | |
| torch.int64 | |
| ) - self.input_batch.num_computed_tokens_cpu_tensor[req_indices].to( | |
| device=self.device, dtype=torch.int64, non_blocking=True | |
| ) | |
| target = self.mrope_positions if self.uses_mrope else self.xdrope_positions | |
| target.gpu[:, :total_num_scheduled_tokens] += drift | |
| use_spec_decode = len(scheduler_output.scheduled_spec_decode_tokens) > 0 | |
| if not use_spec_decode: | |
| # NOTE(woosuk): Due to chunked prefills, the batch may contain | |
| # partial requests. While we should not sample any token | |
| # from these partial requests, we do so for simplicity. | |
| # We will ignore the sampled tokens from the partial requests. | |
| # TODO: Support prompt logprobs. | |
| logits_indices = query_start_loc[1:] - 1 | |
| spec_decode_metadata = None | |
| num_sampled_tokens = np.ones(num_reqs, dtype=np.int32) | |
| else: | |
| # Get the number of draft tokens for each request. | |
| # Iterate over the dictionary rather than all requests since not all | |
| # requests have draft tokens. | |
| num_draft_tokens = np.zeros(num_reqs, dtype=np.int32) | |
| # For chunked prefills, use -1 as mask rather than 0, as guided | |
| # decoding may rollback speculative tokens. | |
| num_decode_draft_tokens = np.full(num_reqs, -1, dtype=np.int32) | |
| for ( | |
| req_id, | |
| draft_token_ids, | |
| ) in scheduler_output.scheduled_spec_decode_tokens.items(): | |
| req_idx = self.input_batch.req_id_to_index[req_id] | |
| draft_len = len(draft_token_ids) | |
| num_draft_tokens[req_idx] = draft_len | |
| if num_scheduled_tokens[req_idx] == draft_len + 1: | |
| num_decode_draft_tokens[req_idx] = draft_len | |
| spec_decode_metadata = self._calc_spec_decode_metadata( | |
| num_draft_tokens, cu_num_tokens | |
| ) | |
| logits_indices = spec_decode_metadata.logits_indices | |
| num_sampled_tokens = num_draft_tokens + 1 | |
| # For DECODE only cuda graph of some attention backends (e.g., GDN). | |
| self.num_decode_draft_tokens.np[:num_reqs] = num_decode_draft_tokens | |
| self.num_decode_draft_tokens.np[num_reqs:].fill(-1) | |
| self.num_decode_draft_tokens.copy_to_gpu() | |
| # Hot-Swap lora model | |
| if self.lora_config: | |
| assert ( | |
| np.sum(num_sampled_tokens) | |
| <= self.vllm_config.scheduler_config.max_num_batched_tokens | |
| ) | |
| self.set_active_loras( | |
| self.input_batch, num_scheduled_tokens, num_sampled_tokens | |
| ) | |
| return ( | |
| logits_indices, | |
| spec_decode_metadata, | |
| ) | |
| def _build_attention_metadata( | |
| self, | |
| num_tokens: int, | |
| num_reqs: int, | |
| max_query_len: int, | |
| num_tokens_padded: int | None = None, | |
| num_reqs_padded: int | None = None, | |
| ubatch_slices: UBatchSlices | None = None, | |
| logits_indices: torch.Tensor | None = None, | |
| use_spec_decode: bool = False, | |
| for_cudagraph_capture: bool = False, | |
| num_scheduled_tokens: dict[str, int] | None = None, | |
| cascade_attn_prefix_lens: list[list[int]] | None = None, | |
| slot_mappings: dict[int, torch.Tensor] | None = None, | |
| ) -> tuple[PerLayerAttnMetadata, CommonAttentionMetadata | None]: | |
| """ | |
| Returns: | |
| tuple[attn_metadata, spec_decode_common_attn_metadata] | |
| """ | |
| # Attention metadata is not needed for attention free models | |
| if len(self.kv_cache_config.kv_cache_groups) == 0: | |
| return {}, None | |
| num_tokens_padded = num_tokens_padded or num_tokens | |
| num_reqs_padded = num_reqs_padded or num_reqs | |
| assert num_reqs_padded is not None and num_tokens_padded is not None | |
| attn_metadata: PerLayerAttnMetadata = {} | |
| if ubatch_slices is not None: | |
| attn_metadata = [dict() for _ in range(len(ubatch_slices))] | |
| if for_cudagraph_capture: | |
| # For some attention backends (e.g. FA) with sliding window models we need | |
| # to make sure the backend see a max_seq_len that is larger to the sliding | |
| # window size when capturing to make sure the correct kernel is selected. | |
| max_seq_len = self.max_model_len | |
| else: | |
| max_seq_len = self.optimistic_seq_lens_cpu.numpy()[:num_reqs].max().item() | |
| kv_cache_groups = self.kv_cache_config.kv_cache_groups | |
| def _get_block_table(kv_cache_gid: int): | |
| assert num_reqs_padded is not None and num_tokens_padded is not None | |
| kv_cache_spec = kv_cache_groups[kv_cache_gid].kv_cache_spec | |
| if isinstance(kv_cache_spec, EncoderOnlyAttentionSpec): | |
| blk_table_tensor = torch.zeros( | |
| (num_reqs_padded, 1), | |
| dtype=torch.int32, | |
| device=self.device, | |
| ) | |
| else: | |
| blk_table = self.input_batch.block_table[kv_cache_gid] | |
| blk_table_tensor = blk_table.get_device_tensor(num_reqs_padded) | |
| # Fill unused block table entries with NULL_BLOCK_ID (null block) | |
| # for CUDAGraph padding. Block 0 is reserved for padding. | |
| blk_table_tensor[num_reqs:num_reqs_padded].fill_(NULL_BLOCK_ID) | |
| return blk_table_tensor | |
| assert slot_mappings is not None | |
| block_table_gid_0 = _get_block_table(0) | |
| slot_mapping_gid_0 = slot_mappings[0] | |
| if self.routed_experts_initialized: | |
| # Copy this step's attention slot_mapping into our private | |
| # device buffer. The shared ``slot_mappings[attn_gid]`` is | |
| # owned by the attention block table and will be overwritten | |
| # by the next ``_prepare_inputs``; we need a stable snapshot | |
| # because the async D2H may still be in flight on the copy | |
| # stream when the next step runs. | |
| attn_gid = self.routed_experts_attn_gid | |
| slot_mapping_attn = slot_mappings[attn_gid] | |
| self.routed_experts_slot_mapping_device[:num_tokens].copy_( | |
| slot_mapping_attn[:num_tokens] | |
| ) | |
| num_computed_tokens_cpu = self.input_batch.num_computed_tokens_cpu_tensor[ | |
| :num_reqs_padded | |
| ] | |
| num_prompt_tokens_cpu = self.input_batch.num_prompt_tokens_cpu_tensor[ | |
| :num_reqs_padded | |
| ] | |
| seq_lens_cpu = self.optimistic_seq_lens_cpu[:num_reqs_padded] | |
| seq_lens_cpu_upper_bound = seq_lens_cpu | |
| # is_prefilling: True if request is still in prefill phase. | |
| # Used by mamba backends to distinguish actual decodes from | |
| # short extends. | |
| is_prefilling = num_computed_tokens_cpu < num_prompt_tokens_cpu | |
| # Zero out padded rows so stale data from condense() doesn't | |
| # misclassify padding as prefill in CUDA graph mode. | |
| is_prefilling[num_reqs:] = False | |
| if self.use_async_spec_decode: | |
| # GPU tensors are authoritative in async mode. | |
| seq_lens_cpu = None | |
| num_computed_tokens_cpu = None | |
| # Compute mm_prefix bidirectional ranges before building | |
| # attention metadata so builders handle them during build(). | |
| # By default, ranges exceeding sliding_window are skipped to prevent | |
| # early tokens from attending across the entire image span. Models that | |
| # clamp mm_prefix to the sliding window *in-kernel* (e.g. Gemma4, which | |
| # needs HF's (causal OR blockwise) AND sliding_window on sliding layers) | |
| # opt out of the skip so the bidirectional range survives for images | |
| # larger than the window; the kernel then bounds it per-query. | |
| req_doc_ranges: dict[int, list[tuple[int, int]]] | None = None | |
| if self.is_mm_prefix_lm: | |
| req_doc_ranges = {} | |
| hf_text_config = self.model_config.hf_text_config | |
| _bidi_sw = getattr(hf_text_config, "sliding_window", None) | |
| _clamps_in_kernel = getattr( | |
| self.model, "mm_prefix_clamp_sliding_window", False | |
| ) | |
| for req_id in self.input_batch.req_ids: | |
| image_doc_ranges = [] | |
| req_state = self.requests[req_id] | |
| for mm_feature in req_state.mm_features: | |
| if mm_feature.modality == "audio": | |
| continue | |
| pos_info = mm_feature.mm_position | |
| img_doc_range = pos_info.extract_embeds_range() | |
| for r in img_doc_range: | |
| if ( | |
| not _clamps_in_kernel | |
| and _bidi_sw is not None | |
| and (r[1] - r[0] + 1) > _bidi_sw | |
| ): | |
| continue | |
| image_doc_ranges.append(r) | |
| req_idx = self.input_batch.req_id_to_index[req_id] | |
| req_doc_ranges[req_idx] = image_doc_ranges | |
| # Reference Sliding Window Attention (R-SWA): pass per-request prompt | |
| # lengths so the attention backend can keep the prefix globally visible. | |
| # The backend owns the persistent CUDA-graph-safe GPU buffer. | |
| rswa_prefix_lens = None | |
| if self.model_config.rswa_window is not None: | |
| rswa_prefix_lens = num_prompt_tokens_cpu | |
| replayssm_decode_base_cpu = None | |
| if self.cache_config.use_replayssm: | |
| replayssm_decode_base_cpu = ( | |
| self.input_batch.replayssm_decode_base_cpu_tensor[:num_reqs_padded] | |
| ) | |
| cm_base = CommonAttentionMetadata( | |
| query_start_loc=self.query_start_loc.gpu[: num_reqs_padded + 1], | |
| query_start_loc_cpu=self.query_start_loc.cpu[: num_reqs_padded + 1], | |
| seq_lens=self.seq_lens[:num_reqs_padded], | |
| _seq_lens_cpu=seq_lens_cpu, | |
| _num_computed_tokens_cpu=num_computed_tokens_cpu, | |
| seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound, | |
| replayssm_decode_base_cpu=replayssm_decode_base_cpu, | |
| num_reqs=num_reqs_padded, | |
| num_actual_tokens=num_tokens_padded, | |
| max_query_len=max_query_len, | |
| max_seq_len=max_seq_len, | |
| block_table_tensor=block_table_gid_0, | |
| slot_mapping=slot_mapping_gid_0, | |
| causal=True, | |
| is_prefilling=is_prefilling, | |
| positions=self.positions[:num_tokens_padded], | |
| mm_req_doc_ranges=req_doc_ranges, | |
| rswa_prefix_lens=rswa_prefix_lens, | |
| ) | |
| if self.dcp_world_size > 1: | |
| self.dcp_local_seq_lens.cpu[:num_reqs] = get_dcp_local_seq_lens( | |
| self.optimistic_seq_lens_cpu[:num_reqs], | |
| self.dcp_world_size, | |
| self.dcp_rank, | |
| self.parallel_config.cp_kv_cache_interleave_size, | |
| ) | |
| self.dcp_local_seq_lens.cpu[num_reqs:].fill_(0) | |
| self.dcp_local_seq_lens.copy_to_gpu(num_reqs_padded) | |
| cm_base.dcp_local_seq_lens = self.dcp_local_seq_lens.gpu[:num_reqs_padded] | |
| cm_base.dcp_local_seq_lens_cpu = self.dcp_local_seq_lens.cpu[ | |
| :num_reqs_padded | |
| ] | |
| if logits_indices is not None and self.cache_config.kv_sharing_fast_prefill: | |
| cm_base.num_logits_indices = logits_indices.size(0) | |
| cm_base.logits_indices_padded = self._prepare_kv_sharing_fast_prefill( | |
| logits_indices | |
| ) | |
| # Cache attention metadata builds across hybrid KV-cache groups | |
| # The only thing that changes between different hybrid KV-cache groups when the | |
| # same metadata builder and KVCacheSpec is the same is the block table, so we | |
| # can cache the attention metadata builds and just update the block table using | |
| # `builder.update_block_table` if the builder supports it. | |
| cached_attn_metadata: dict[ | |
| tuple[KVCacheSpec, type[AttentionMetadataBuilder]], AttentionMetadata | |
| ] = {} | |
| def _build_attn_group_metadata( | |
| kv_cache_gid: int, | |
| attn_gid: int, | |
| common_attn_metadata: CommonAttentionMetadata, | |
| ubid: int | None = None, | |
| ) -> None: | |
| attn_group = self.attn_groups[kv_cache_gid][attn_gid] | |
| builder = attn_group.get_metadata_builder(ubid or 0) | |
| kv_cache_spec = kv_cache_groups[kv_cache_gid].kv_cache_spec | |
| if isinstance(kv_cache_spec, UniformTypeKVCacheSpecs): | |
| kv_cache_spec = kv_cache_spec.kv_cache_specs[attn_group.layer_names[0]] | |
| cache_key = (kv_cache_spec, type(builder)) | |
| cascade_attn_prefix_len = ( | |
| cascade_attn_prefix_lens[kv_cache_gid][attn_gid] | |
| if cascade_attn_prefix_lens | |
| else 0 | |
| ) | |
| extra_attn_metadata_args = {} | |
| if use_spec_decode and isinstance( | |
| builder, | |
| ( | |
| Mamba2AttentionMetadataBuilder, | |
| GDNAttentionMetadataBuilder, | |
| BailingLinearAttentionMetadataBuilder, | |
| ), | |
| ): | |
| assert ubid is None, ( | |
| "UBatching not supported with GDN or linear attn yet" | |
| ) | |
| extra_attn_metadata_args = dict( | |
| num_accepted_tokens=self.num_accepted_tokens.gpu[:num_reqs_padded], | |
| num_decode_draft_tokens_cpu=self.num_decode_draft_tokens.cpu[ | |
| :num_reqs_padded | |
| ], | |
| ) | |
| if ( | |
| isinstance(builder, Mamba2AttentionMetadataBuilder) | |
| and self.mamba_prev_last_scheduled_idx is not None | |
| ): | |
| extra_attn_metadata_args["prev_last_scheduled_idx"] = ( | |
| self.mamba_prev_last_scheduled_idx.gpu[:num_reqs_padded] | |
| ) | |
| if for_cudagraph_capture: | |
| attn_metadata_i = builder.build_for_cudagraph_capture( | |
| common_attn_metadata | |
| ) | |
| elif ( | |
| cache_key in cached_attn_metadata | |
| and builder.supports_update_block_table | |
| ): | |
| attn_metadata_i = builder.update_block_table( | |
| cached_attn_metadata[cache_key], | |
| common_attn_metadata.block_table_tensor, | |
| common_attn_metadata.slot_mapping, | |
| ) | |
| else: | |
| attn_metadata_i = builder.build( | |
| common_prefix_len=cascade_attn_prefix_len, | |
| common_attn_metadata=common_attn_metadata, | |
| **extra_attn_metadata_args, | |
| ) | |
| if builder.supports_update_block_table: | |
| cached_attn_metadata[cache_key] = attn_metadata_i | |
| if ubid is None: | |
| assert isinstance(attn_metadata, dict) | |
| attn_metadata_dict = attn_metadata | |
| else: | |
| assert isinstance(attn_metadata, list) | |
| attn_metadata_dict = attn_metadata[ubid] | |
| for layer_name in attn_group.layer_names: | |
| attn_metadata_dict[layer_name] = attn_metadata_i | |
| # Prepare the attention metadata for each KV cache group and make layers | |
| # in the same group share the same metadata. | |
| spec_decode_common_attn_metadata = None | |
| for kv_cache_gid, kv_cache_group in enumerate(kv_cache_groups): | |
| cm = copy(cm_base) # shallow copy | |
| # Basically only the encoder seq_lens, block_table and slot_mapping change | |
| # for each kv_cache_group. | |
| cm.encoder_seq_lens, cm.encoder_seq_lens_cpu = self._get_encoder_seq_lens( | |
| num_scheduled_tokens or {}, | |
| kv_cache_group.kv_cache_spec, | |
| num_reqs_padded, | |
| for_cudagraph_capture=for_cudagraph_capture, | |
| ) | |
| if kv_cache_gid > 0: | |
| cm.block_table_tensor = _get_block_table(kv_cache_gid) | |
| cm.slot_mapping = slot_mappings[kv_cache_gid] | |
| if self.speculative_config and spec_decode_common_attn_metadata is None: | |
| if isinstance( | |
| self.drafter, | |
| ( | |
| EagleProposer, | |
| DFlashProposer, | |
| Gemma4Proposer, | |
| ExtractHiddenStatesProposer, | |
| ), | |
| ): | |
| if self.drafter.kv_cache_gid == kv_cache_gid: | |
| spec_decode_common_attn_metadata = cm | |
| else: | |
| spec_decode_common_attn_metadata = cm | |
| # Capture per-group block tables for multi-group proposers. | |
| if self.speculative_config and isinstance(self.drafter, Step3p5MTPProposer): | |
| self.drafter.set_per_group_attn_metadata( | |
| kv_cache_gid, cm.block_table_tensor, cm.slot_mapping | |
| ) | |
| elif self.speculative_config and isinstance(self.drafter, Gemma4Proposer): | |
| self.drafter.set_per_group_block_table( | |
| kv_cache_gid, cm.block_table_tensor | |
| ) | |
| for attn_gid in range(len(self.attn_groups[kv_cache_gid])): | |
| if ubatch_slices is not None: | |
| for ubid, _cm in enumerate(split_attn_metadata(ubatch_slices, cm)): | |
| _build_attn_group_metadata(kv_cache_gid, attn_gid, _cm, ubid) | |
| else: | |
| _build_attn_group_metadata(kv_cache_gid, attn_gid, cm) | |
| if spec_decode_common_attn_metadata is not None and ( | |
| num_reqs != num_reqs_padded or num_tokens != num_tokens_padded | |
| ): | |
| # Currently the drafter still only uses piecewise cudagraphs (and modifies | |
| # the attention metadata in directly), and therefore does not want to use | |
| # padded attention metadata. | |
| spec_decode_common_attn_metadata = ( | |
| spec_decode_common_attn_metadata.unpadded(num_tokens, num_reqs) | |
| ) | |
| return attn_metadata, spec_decode_common_attn_metadata | |
| def _compute_cascade_attn_prefix_lens( | |
| self, | |
| num_scheduled_tokens: np.ndarray, | |
| num_computed_tokens: np.ndarray, | |
| num_common_prefix_blocks: list[int], | |
| ) -> list[list[int]] | None: | |
| """ | |
| Returns: | |
| Optional[cascade_attn_prefix_lens] | |
| cascade_attn_prefix_lens is 2D: | |
| ``[kv_cache_group_id][attn_group_idx]``, | |
| None if we should not use cascade attention | |
| """ | |
| use_cascade_attn = False | |
| num_kv_cache_groups = len(self.kv_cache_config.kv_cache_groups) | |
| cascade_attn_prefix_lens: list[list[int]] = [ | |
| [] for _ in range(num_kv_cache_groups) | |
| ] | |
| for kv_cache_gid in range(num_kv_cache_groups): | |
| for attn_group in self.attn_groups[kv_cache_gid]: | |
| if isinstance(attn_group.kv_cache_spec, EncoderOnlyAttentionSpec): | |
| cascade_attn_prefix_len = 0 | |
| else: | |
| # 0 if cascade attention should not be used | |
| cascade_attn_prefix_len = self._compute_cascade_attn_prefix_len( | |
| num_scheduled_tokens, | |
| num_computed_tokens, | |
| num_common_prefix_blocks[kv_cache_gid], | |
| attn_group.kv_cache_spec, | |
| attn_group.get_metadata_builder(), | |
| ) | |
| cascade_attn_prefix_lens[kv_cache_gid].append(cascade_attn_prefix_len) | |
| use_cascade_attn |= cascade_attn_prefix_len > 0 | |
| return cascade_attn_prefix_lens if use_cascade_attn else None | |
| def _compute_cascade_attn_prefix_len( | |
| self, | |
| num_scheduled_tokens: np.ndarray, | |
| num_computed_tokens: np.ndarray, | |
| num_common_prefix_blocks: int, | |
| kv_cache_spec: KVCacheSpec, | |
| attn_metadata_builder: AttentionMetadataBuilder, | |
| ) -> int: | |
| """Compute the length of the common prefix for cascade attention. | |
| NOTE(woosuk): The common prefix length returned by this function | |
| represents the length used specifically for cascade attention, not the | |
| actual number of tokens shared between requests. When cascade attention | |
| is disabled (use_cascade=False), this function returns 0 even if | |
| requests share common tokens. Additionally, the common prefix length is | |
| truncated to a multiple of the block size and may be further truncated | |
| due to implementation details explained below. | |
| Args: | |
| num_scheduled_tokens: Number of tokens scheduled per request. | |
| num_common_prefix_blocks: Number of shared KV cache blocks. | |
| Returns: | |
| int: Length of common prefix in tokens. | |
| """ | |
| common_prefix_len = num_common_prefix_blocks * kv_cache_spec.block_size | |
| if common_prefix_len == 0: | |
| # Common case. | |
| return 0 | |
| # NOTE(woosuk): Cascade attention uses two attention kernels: one | |
| # for the common prefix and the other for the rest. For the first | |
| # kernel, we concatenate all the query tokens (possibly from | |
| # different requests) and treat them as if they are from the same | |
| # request. Then, we use bi-directional attention to process the | |
| # common prefix in the KV cache. Importantly, this means that the | |
| # first kernel does not do any masking. | |
| # Consider the following example: | |
| # Request 1's input query: [D, E, X] | |
| # Request 1's kv cache: [A, B, C, D, E, X] | |
| # Request 1's num_computed_tokens: 3 (i.e., [A, B, C]) | |
| # Request 2's input query: [E, Y] | |
| # Request 2's kv cache: [A, B, C, D, E, Y] | |
| # Request 2's num_computed_tokens: 4 (i.e., [A, B, C, D]) | |
| # If we use [A, B, C, D, E] as the common prefix, then the | |
| # first kernel will compute the bi-directional attention between | |
| # input query [D, E, X, E, Y] and common prefix [A, B, C, D, E]. | |
| # However, this is wrong because D in Request 1 should not attend to | |
| # E in the common prefix (i.e., we need masking). | |
| # To avoid this, [A, B, C, D] should be the common prefix. | |
| # That is, the common prefix should be capped by the minimum | |
| # num_computed_tokens among the requests, and plus one to include | |
| # the first token of the query. | |
| # In practice, we use [A, B, C] as the common prefix, instead of | |
| # [A, B, C, D] (i.e., the common prefix is capped by the minimum | |
| # num_computed_tokens, without plus one). | |
| # This is because of an implementation detail: We want to always | |
| # use two kernels for cascade attention. Let's imagine: | |
| # Request 3's input query: [D] | |
| # Request 3's kv cache: [A, B, C, D] | |
| # Request 3's num_computed_tokens: 3 (i.e., [A, B, C]) | |
| # If we use [A, B, C, D] as the common prefix for Request 1-3, | |
| # then Request 3 will be processed only by the first kernel, | |
| # and the second kernel will get an empty input. While this is not | |
| # a fundamental problem, our current implementation does not support | |
| # this case. | |
| common_prefix_len = min(common_prefix_len, num_computed_tokens.min()) | |
| # common_prefix_len should be a multiple of the block size. | |
| common_prefix_len = ( | |
| common_prefix_len // kv_cache_spec.block_size * kv_cache_spec.block_size | |
| ) | |
| use_sliding_window = isinstance(kv_cache_spec, SlidingWindowSpec) or ( | |
| isinstance(kv_cache_spec, FullAttentionSpec) | |
| and kv_cache_spec.sliding_window is not None | |
| ) | |
| use_local_attention = isinstance(kv_cache_spec, ChunkedLocalAttentionSpec) or ( | |
| isinstance(kv_cache_spec, FullAttentionSpec) | |
| and kv_cache_spec.attention_chunk_size is not None | |
| ) | |
| assert isinstance(kv_cache_spec, AttentionSpec) | |
| use_cascade = attn_metadata_builder.use_cascade_attention( | |
| common_prefix_len=common_prefix_len, | |
| query_lens=num_scheduled_tokens, | |
| num_query_heads=self.num_query_heads, | |
| num_kv_heads=kv_cache_spec.num_kv_heads, | |
| use_alibi=self.use_alibi, | |
| use_sliding_window=use_sliding_window, | |
| use_local_attention=use_local_attention, | |
| num_sms=self.num_sms, | |
| dcp_world_size=self.dcp_world_size, | |
| ) | |
| return common_prefix_len if use_cascade else 0 | |
| def _calc_mrope_positions(self, scheduler_output: "SchedulerOutput"): | |
| mrope_pos_ptr = 0 | |
| for index, req_id in enumerate(self.input_batch.req_ids): | |
| req = self.requests[req_id] | |
| assert req.mrope_positions is not None | |
| num_computed_tokens = self.input_batch.num_computed_tokens_cpu[index] | |
| num_scheduled_tokens = scheduler_output.num_scheduled_tokens[req_id] | |
| num_prompt_tokens = length_from_prompt_token_ids_or_embeds( | |
| req.prompt_token_ids, req.prompt_embeds | |
| ) | |
| if num_computed_tokens + num_scheduled_tokens > num_prompt_tokens: | |
| prompt_part_len = max(0, num_prompt_tokens - num_computed_tokens) | |
| completion_part_len = max(0, num_scheduled_tokens - prompt_part_len) | |
| else: | |
| prompt_part_len = num_scheduled_tokens | |
| completion_part_len = 0 | |
| assert num_scheduled_tokens == prompt_part_len + completion_part_len | |
| if prompt_part_len > 0: | |
| # prompt's mrope_positions are pre-computed | |
| dst_start = mrope_pos_ptr | |
| dst_end = mrope_pos_ptr + prompt_part_len | |
| src_start = num_computed_tokens | |
| src_end = num_computed_tokens + prompt_part_len | |
| self.mrope_positions.cpu[:, dst_start:dst_end] = req.mrope_positions[ | |
| :, src_start:src_end | |
| ] | |
| mrope_pos_ptr += prompt_part_len | |
| if completion_part_len > 0: | |
| # compute completion's mrope_positions on-the-fly | |
| dst_start = mrope_pos_ptr | |
| dst_end = mrope_pos_ptr + completion_part_len | |
| assert req.mrope_position_delta is not None | |
| MRotaryEmbedding.get_next_input_positions_tensor( | |
| out=self.mrope_positions.np, | |
| out_offset=dst_start, | |
| mrope_position_delta=req.mrope_position_delta, | |
| context_len=num_computed_tokens + prompt_part_len, | |
| num_new_tokens=completion_part_len, | |
| ) | |
| mrope_pos_ptr += completion_part_len | |
| def _calc_xdrope_positions(self, scheduler_output: "SchedulerOutput"): | |
| xdrope_pos_ptr = 0 | |
| for index, req_id in enumerate(self.input_batch.req_ids): | |
| req = self.requests[req_id] | |
| assert req.xdrope_positions is not None | |
| num_computed_tokens = self.input_batch.num_computed_tokens_cpu[index] | |
| num_scheduled_tokens = scheduler_output.num_scheduled_tokens[req_id] | |
| num_prompt_tokens = length_from_prompt_token_ids_or_embeds( | |
| req.prompt_token_ids, req.prompt_embeds | |
| ) | |
| if num_computed_tokens + num_scheduled_tokens > num_prompt_tokens: | |
| prompt_part_len = max(0, num_prompt_tokens - num_computed_tokens) | |
| completion_part_len = max(0, num_scheduled_tokens - prompt_part_len) | |
| else: | |
| prompt_part_len = num_scheduled_tokens | |
| completion_part_len = 0 | |
| assert num_scheduled_tokens == prompt_part_len + completion_part_len | |
| if prompt_part_len > 0: | |
| # prompt's xdrope_positions are pre-computed | |
| dst_start = xdrope_pos_ptr | |
| dst_end = xdrope_pos_ptr + prompt_part_len | |
| src_start = num_computed_tokens | |
| src_end = num_computed_tokens + prompt_part_len | |
| self.xdrope_positions.cpu[:, dst_start:dst_end] = req.xdrope_positions[ | |
| :, src_start:src_end | |
| ] | |
| xdrope_pos_ptr += prompt_part_len | |
| if completion_part_len > 0: | |
| # compute completion's xdrope_positions on-the-fly | |
| dst_start = xdrope_pos_ptr | |
| dst_end = xdrope_pos_ptr + completion_part_len | |
| XDRotaryEmbedding.get_next_input_positions_tensor( | |
| out=self.xdrope_positions.np, | |
| out_offset=dst_start, | |
| context_len=num_computed_tokens + prompt_part_len, | |
| num_new_tokens=completion_part_len, | |
| ) | |
| xdrope_pos_ptr += completion_part_len | |
| def _calc_spec_decode_metadata( | |
| self, | |
| num_draft_tokens: np.ndarray, | |
| cu_num_scheduled_tokens: np.ndarray, | |
| ) -> SpecDecodeMetadata: | |
| # Inputs: | |
| # cu_num_scheduled_tokens: [ 4, 104, 107, 207, 209] | |
| # num_draft_tokens: [ 3, 0, 2, 0, 1] | |
| # Outputs: | |
| # cu_num_draft_tokens: [ 3, 3, 5, 5, 6] | |
| # logits_indices: [ 0, 1, 2, 3, 103, 104, 105, 106, | |
| # 206, 207, 208] | |
| # target_logits_indices: [ 0, 1, 2, 5, 6, 9] | |
| # bonus_logits_indices: [ 3, 4, 7, 8, 10] | |
| # Compute the logits indices. | |
| # [4, 1, 3, 1, 2] | |
| num_sampled_tokens = num_draft_tokens + 1 | |
| # Step 1. | |
| # cu_num_sampled_tokens: [4, 5, 8, 9, 11] | |
| # _arange_scratch[:11]: [0, 1, 2, 3, 0, 0, 1, 2, 0, 0, 1] | |
| cu_num_sampled_tokens = self._get_cumsum_and_arange( | |
| num_sampled_tokens, self._arange_scratch, cumsum_dtype=np.int32 | |
| ) | |
| # Step 2. [0, 0, 0, 0, 103, 104, 104, 104, 206, 207, 207] | |
| logits_indices = np.repeat( | |
| cu_num_scheduled_tokens - num_sampled_tokens, num_sampled_tokens | |
| ) | |
| # Step 3. [0, 1, 2, 3, 103, 104, 105, 106, 206, 207, 208] | |
| logits_indices += self._arange_scratch[: cu_num_sampled_tokens[-1]] | |
| # Compute the bonus logits indices. | |
| bonus_logits_indices = cu_num_sampled_tokens - 1 | |
| # Compute the draft logits indices. | |
| # cu_num_draft_tokens: [3, 3, 5, 5, 6] | |
| # _arange_scratch[:6]: [0, 1, 2, 0, 1, 0] | |
| cu_num_draft_tokens = self._get_cumsum_and_arange( | |
| num_draft_tokens, self._arange_scratch, cumsum_dtype=np.int32 | |
| ) | |
| # [0, 0, 0, 5, 5, 9] | |
| target_logits_indices = np.repeat( | |
| cu_num_sampled_tokens - num_sampled_tokens, num_draft_tokens | |
| ) | |
| # [0, 1, 2, 5, 6, 9] | |
| target_logits_indices += self._arange_scratch[: cu_num_draft_tokens[-1]] | |
| cu_num_draft_tokens = async_tensor_h2d(cu_num_draft_tokens, device=self.device) | |
| cu_num_sampled_tokens = async_tensor_h2d( | |
| cu_num_sampled_tokens, device=self.device | |
| ) | |
| logits_indices = async_tensor_h2d(logits_indices, device=self.device) | |
| target_logits_indices = async_tensor_h2d( | |
| target_logits_indices, device=self.device | |
| ) | |
| bonus_logits_indices = async_tensor_h2d( | |
| bonus_logits_indices, device=self.device | |
| ) | |
| # Compute the draft token ids. | |
| # draft_token_indices: [ 1, 2, 3, 105, 106, 208] | |
| draft_token_ids = self.input_ids.gpu[logits_indices] | |
| draft_token_ids = draft_token_ids[target_logits_indices + 1] | |
| return SpecDecodeMetadata( | |
| draft_token_ids=draft_token_ids, | |
| num_draft_tokens=num_draft_tokens.tolist(), | |
| cu_num_draft_tokens=cu_num_draft_tokens, | |
| cu_num_sampled_tokens=cu_num_sampled_tokens, | |
| target_logits_indices=target_logits_indices, | |
| bonus_logits_indices=bonus_logits_indices, | |
| logits_indices=logits_indices, | |
| ) | |
| def _prepare_kv_sharing_fast_prefill( | |
| self, | |
| logits_indices: torch.Tensor, | |
| ) -> torch.Tensor: | |
| assert self.kv_sharing_fast_prefill_logits_indices is not None | |
| num_logits = logits_indices.shape[0] | |
| assert num_logits > 0 | |
| self.kv_sharing_fast_prefill_logits_indices[:num_logits].copy_(logits_indices) | |
| # There might have leftover indices in logits_indices[num_logits:] | |
| # from previous iterations, whose values may be greater than the | |
| # batch size in the current iteration. To ensure indices are always | |
| # valid, fill the padded indices with the last index. Broadcast the | |
| # scalar GPU-side to avoid a D2H sync on `.item()`. | |
| self.kv_sharing_fast_prefill_logits_indices[num_logits:] = logits_indices[-1] | |
| # Dispatch for the decoder portion of the model. | |
| _, batch_desc = self.cudagraph_dispatcher.dispatch( | |
| num_logits, invalid_modes={CUDAGraphMode.FULL} | |
| ) | |
| num_logits_padded = batch_desc.num_tokens | |
| logits_indices_padded = self.kv_sharing_fast_prefill_logits_indices[ | |
| :num_logits_padded | |
| ] | |
| return logits_indices_padded | |
| def _batch_mm_inputs_from_scheduler( | |
| self, | |
| scheduler_output: "SchedulerOutput", | |
| ) -> tuple[ | |
| list[str], | |
| list[tuple[str, MultiModalKwargsItem]], | |
| list[tuple[str, PlaceholderRange]], | |
| ]: | |
| """Batch multimodal inputs from scheduled encoder inputs. | |
| Args: | |
| scheduler_output: The scheduler output containing scheduled encoder | |
| inputs. | |
| Returns: | |
| A tuple of (mm_hashes, mm_kwargs, mm_lora_refs) where: | |
| - mm_hashes: List of multimodal hashes for each item | |
| - mm_kwargs: List of multimodal kwargs for each item | |
| - mm_lora_refs: List of (req_id, placeholder_range) for each item | |
| """ | |
| scheduled_encoder_inputs = scheduler_output.scheduled_encoder_inputs | |
| if not scheduled_encoder_inputs: | |
| return [], [], [] | |
| mm_hashes = list[str]() | |
| mm_kwargs = list[tuple[str, MultiModalKwargsItem]]() | |
| # Multimodal LoRA reference info to map each multimodal item | |
| # back to its request & position | |
| mm_lora_refs = list[tuple[str, PlaceholderRange]]() | |
| for req_id, encoder_input_ids in scheduled_encoder_inputs.items(): | |
| req_state = self.requests[req_id] | |
| for mm_input_id in encoder_input_ids: | |
| mm_feature = req_state.mm_features[mm_input_id] | |
| if mm_feature.data is None: | |
| continue | |
| mm_hashes.append(mm_feature.identifier) | |
| mm_kwargs.append((mm_feature.modality, mm_feature.data)) | |
| mm_lora_refs.append((req_id, mm_feature.mm_position)) | |
| return mm_hashes, mm_kwargs, mm_lora_refs | |
| def _cache_encoder_output( | |
| self, | |
| mm_hash: str, | |
| output: torch.Tensor, | |
| ec_manager_metadata: "EncoderCacheManagerMetadata | None", | |
| free_encoder_mm_hashes: list[str], | |
| ) -> None: | |
| """Store an encoder output for later multimodal embedding gather.""" | |
| del ec_manager_metadata, free_encoder_mm_hashes | |
| self.encoder_cache[mm_hash] = output | |
| self.maybe_save_ec_to_connector(self.encoder_cache, mm_hash) | |
| def _execute_mm_encoder( | |
| self, scheduler_output: "SchedulerOutput" | |
| ) -> list[torch.Tensor]: | |
| mm_hashes, mm_kwargs, mm_lora_refs = self._batch_mm_inputs_from_scheduler( | |
| scheduler_output | |
| ) | |
| if not mm_kwargs: | |
| return [] | |
| # `prompt_embeds` is a passthrough modality, the tensor is already in | |
| # the model embedding space, so no encoder runs. Inject each | |
| # `prompt_embeds` tensor directly into the encoder cache here so that | |
| # `_gather_mm_embeddings` can splice it via the standard `is_mm_embed` | |
| # path. | |
| pe_indices = [ | |
| i | |
| for i, (modality, _) in enumerate(mm_kwargs) | |
| if modality == "prompt_embeds" | |
| ] | |
| if pe_indices: | |
| for i in pe_indices: | |
| pe_tensor = mm_kwargs[i][1]["embedding"].data | |
| assert isinstance(pe_tensor, torch.Tensor) | |
| self._cache_encoder_output( | |
| mm_hashes[i], | |
| pe_tensor.to(self.device), | |
| scheduler_output.ec_manager_metadata, | |
| scheduler_output.free_encoder_mm_hashes, | |
| ) | |
| # Filter out `prompt_embeds` items from mm_kwargs/mm_hashes/mm_lora_refs | |
| # since they don't require further encoder processing. | |
| mm_hashes = [h for i, h in enumerate(mm_hashes) if i not in pe_indices] | |
| mm_kwargs = [k for i, k in enumerate(mm_kwargs) if i not in pe_indices] | |
| mm_lora_refs = [ | |
| r for i, r in enumerate(mm_lora_refs) if i not in pe_indices | |
| ] | |
| if not mm_kwargs: | |
| return [] # nothing left to encode after filtering out `prompt_embeds` | |
| should_time = bool( | |
| self.observability_config | |
| and self.observability_config.enable_mm_processor_stats | |
| and scheduler_output.scheduled_encoder_inputs | |
| ) | |
| # Batch mm inputs as much as we can: if a request in the batch has | |
| # multiple modalities or a different modality than the previous one, | |
| # we process it separately to preserve item order. | |
| # FIXME(ywang96): This is a hacky way to deal with multiple modalities | |
| # in the same batch while still being able to benefit from batching | |
| # multimodal inputs. The proper solution should be reordering the | |
| # encoder outputs. | |
| model = cast(SupportsMultiModal, self.model) | |
| if self.lora_config and self.lora_manager.supports_tower_connector_lora(): | |
| # Build LoRA mappings independently for encoder inputs | |
| # (encoder batch structure is different from main batch) | |
| prompt_lora_mapping = [] | |
| token_lora_mapping = [] | |
| lora_requests = set() | |
| encoder_token_counts = [] | |
| for req_id, pos_info in mm_lora_refs: | |
| req_idx = self.input_batch.req_id_to_index[req_id] | |
| lora_id = int(self.input_batch.request_lora_mapping[req_idx]) | |
| # Prefer pos_info.get_num_embeds to count precise MM embedding tokens. | |
| num_tokens = self.model.get_num_mm_encoder_tokens( # type: ignore[attr-defined] | |
| pos_info.get_num_embeds() | |
| ) | |
| prompt_lora_mapping.append(lora_id) | |
| token_lora_mapping.extend([lora_id] * num_tokens) | |
| encoder_token_counts.append(num_tokens) | |
| if lora_id > 0: | |
| lora_request = self.input_batch.lora_id_to_lora_request.get(lora_id) | |
| if lora_request is not None: | |
| lora_requests.add(lora_request) | |
| # Set tower adapter mapping | |
| tower_mapping = LoRAMapping( | |
| tuple(token_lora_mapping), | |
| tuple(prompt_lora_mapping), | |
| is_prefill=True, | |
| type=LoRAMappingType.TOWER, | |
| ) | |
| self.lora_manager.set_active_adapters(lora_requests, tower_mapping) | |
| # Only set connector mapping if the model actually has a connector. | |
| # Some multimodal models inherit a stub `get_num_mm_connector_tokens` | |
| # from `SupportsMultiModal`, which returns None and should not be | |
| # treated as a signal that connector LoRA is supported. | |
| mm_mapping = ( | |
| self.model.get_mm_mapping() # type: ignore[attr-defined] | |
| if hasattr(self.model, "get_mm_mapping") | |
| else None | |
| ) | |
| if ( | |
| mm_mapping is not None | |
| and mm_mapping.connector | |
| and hasattr(self.model, "get_num_mm_connector_tokens") | |
| ): | |
| post_op_counts = [ | |
| self.model.get_num_mm_connector_tokens(num_tokens) # type: ignore[attr-defined] | |
| for num_tokens in encoder_token_counts | |
| ] | |
| connector_token_mapping = np.repeat( | |
| np.array(prompt_lora_mapping, dtype=np.int32), | |
| np.array(post_op_counts, dtype=np.int32), | |
| ) | |
| connector_mapping = LoRAMapping( | |
| index_mapping=tuple(connector_token_mapping.tolist()), | |
| prompt_mapping=tuple(prompt_lora_mapping), | |
| is_prefill=True, | |
| type=LoRAMappingType.CONNECTOR, | |
| ) | |
| self.lora_manager.set_active_adapters( | |
| lora_requests, | |
| connector_mapping, | |
| ) | |
| encoder_outputs: list[torch.Tensor] = [] | |
| # Track the current index in mm_kwargs/mm_lora_refs to map groups to request IDs | |
| current_item_idx = 0 | |
| for modality, num_items, mm_kwargs_batch in group_and_batch_mm_kwargs( | |
| mm_kwargs, device=self.device, pin_memory=PIN_MEMORY | |
| ): | |
| batch_outputs: MultiModalEmbeddings | |
| # EVS and dynamic res video related change. | |
| # (ekhvedchenia): Temporary hack to limit peak memory usage when | |
| # processing multimodal data. This solves the issue with scheduler | |
| # putting too many video samples into a single batch. Scheduler | |
| # uses pruned vision tokens count to compare it versus compute | |
| # budget which is incorrect (Either input media size or non-pruned | |
| # output vision tokens count should be considered) | |
| # dynamic res video for nemotron temporarily uses this hack via | |
| # requires_sequential_video_encoding | |
| # because it doesn't yet support video batching. | |
| # TODO(ywang96): Fix memory profiling to take EVS into account and | |
| # remove this hack. | |
| if ( | |
| ( | |
| self.is_multimodal_pruning_enabled | |
| or self.requires_sequential_video_encoding | |
| ) | |
| and modality == "video" | |
| and num_items > 1 | |
| ): | |
| batch_outputs_lst = list[torch.Tensor]() | |
| for video_idx in range(num_items): | |
| video_mm_kwargs_item = mm_kwargs[current_item_idx + video_idx] | |
| with self.timed_encoder_operation( | |
| should_time, mm_lora_refs, current_item_idx + video_idx, 1 | |
| ): | |
| _, _, micro_batch_mm_inputs = next( | |
| group_and_batch_mm_kwargs( | |
| [video_mm_kwargs_item], | |
| device=self.device, | |
| pin_memory=PIN_MEMORY, | |
| ) | |
| ) | |
| micro_batch_outputs = model.embed_multimodal( | |
| **micro_batch_mm_inputs | |
| ) | |
| batch_outputs_lst.extend(micro_batch_outputs) | |
| batch_outputs = batch_outputs_lst | |
| else: | |
| # Run the encoder. | |
| # `batch_outputs` is either of the following: | |
| # 1. A tensor of shape (num_items, feature_size, hidden_size) | |
| # in case feature_size is fixed across all multimodal items. | |
| # 2. A list or tuple (length: num_items) of tensors, | |
| # each of shape (feature_size, hidden_size) in case the feature | |
| # size is dynamic depending on the input multimodal items. | |
| with self.timed_encoder_operation( | |
| should_time, mm_lora_refs, current_item_idx, num_items | |
| ): | |
| cudagraph_output = None | |
| if ( | |
| self.encoder_cudagraph_manager is not None | |
| and self.encoder_cudagraph_manager.supports_modality(modality) | |
| ): | |
| cudagraph_output = self.encoder_cudagraph_manager.execute( | |
| mm_kwargs_batch, | |
| ) | |
| if cudagraph_output is not None: | |
| batch_outputs = cudagraph_output | |
| else: | |
| batch_outputs = model.embed_multimodal(**mm_kwargs_batch) | |
| sanity_check_mm_encoder_outputs(batch_outputs, expected_num_items=num_items) | |
| encoder_outputs.extend(batch_outputs) | |
| current_item_idx += num_items | |
| # Cache the encoder outputs by mm_hash | |
| for mm_hash, output in zip(mm_hashes, encoder_outputs): | |
| self._cache_encoder_output( | |
| mm_hash, | |
| output, | |
| scheduler_output.ec_manager_metadata, | |
| scheduler_output.free_encoder_mm_hashes, | |
| ) | |
| logger.debug("Finish execute for mm hash %s", mm_hash) | |
| return encoder_outputs | |
| def _get_encoder_output_from_cache(self, mm_hash: str) -> torch.Tensor | None: | |
| """Return a cached encoder output for multimodal | |
| embedding gather.""" | |
| return self.encoder_cache.get(mm_hash, None) | |
| def _gather_mm_embeddings( | |
| self, | |
| scheduler_output: "SchedulerOutput", | |
| shift_computed_tokens: int = 0, | |
| ) -> tuple[list[torch.Tensor], torch.Tensor]: | |
| total_num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens | |
| mm_embeds = list[torch.Tensor]() | |
| is_mm_embed = torch.zeros( | |
| total_num_scheduled_tokens, | |
| dtype=torch.bool, | |
| device="cpu", | |
| pin_memory=PIN_MEMORY, | |
| ) | |
| req_start_idx = 0 | |
| should_sync_mrope_positions = False | |
| should_sync_xdrope_positions = False | |
| for req_id in self.input_batch.req_ids: | |
| mm_embeds_req: list[torch.Tensor] = [] | |
| num_scheduled_tokens = scheduler_output.num_scheduled_tokens[req_id] | |
| req_state = self.requests[req_id] | |
| num_computed_tokens = req_state.num_computed_tokens + shift_computed_tokens | |
| mm_features = req_state.mm_features | |
| lo, hi = get_mm_features_in_window( | |
| mm_features, | |
| start=num_computed_tokens, | |
| end=num_computed_tokens + num_scheduled_tokens, | |
| ) | |
| for i in range(lo, hi): | |
| mm_feature = mm_features[i] | |
| pos_info = mm_feature.mm_position | |
| start_pos = pos_info.offset | |
| num_encoder_tokens = pos_info.length | |
| start_idx = max(num_computed_tokens - start_pos, 0) | |
| end_idx = min( | |
| num_computed_tokens - start_pos + num_scheduled_tokens, | |
| num_encoder_tokens, | |
| ) | |
| assert start_idx < end_idx | |
| curr_embeds_start, curr_embeds_end = ( | |
| pos_info.get_embeds_indices_in_range(start_idx, end_idx) | |
| ) | |
| # If there are no embeddings in the current range, we skip | |
| # gathering the embeddings. | |
| if curr_embeds_start == curr_embeds_end: | |
| continue | |
| mm_hash = mm_feature.identifier | |
| encoder_output = self._get_encoder_output_from_cache(mm_hash) | |
| if encoder_output is None: | |
| # A feature starting at/after the processed boundary is only | |
| # reached via the drafter's +1 look-ahead and might not be | |
| # encoded yet; fall back to the token embedding for drafting. | |
| if ( | |
| start_pos | |
| >= req_state.num_computed_tokens + num_scheduled_tokens | |
| ): | |
| continue | |
| raise RuntimeError(f"Encoder cache miss for {mm_hash}.") | |
| if (is_embed := pos_info.is_embed) is not None: | |
| is_embed = is_embed[start_idx:end_idx] | |
| mm_embeds_item = encoder_output[curr_embeds_start:curr_embeds_end] | |
| else: | |
| mm_embeds_item = encoder_output[start_idx:end_idx] | |
| req_start_pos = req_start_idx + start_pos - num_computed_tokens | |
| # OR mask for overlapping mm_features (use_audio_in_video) | |
| if is_embed is None: | |
| is_mm_embed[req_start_pos + start_idx : req_start_pos + end_idx] = ( | |
| True | |
| ) | |
| else: | |
| is_mm_embed[ | |
| req_start_pos + start_idx : req_start_pos + end_idx | |
| ] |= is_embed | |
| set_mm_embedding_modality(mm_embeds_item, mm_feature.modality) | |
| mm_embeds_req.append(mm_embeds_item) | |
| if self.is_multimodal_pruning_enabled and self.uses_mrope: | |
| assert req_state.mrope_positions is not None | |
| should_sync_mrope_positions = True | |
| old_mm_embeds_req = mm_embeds_req | |
| mm_embeds_req, new_mrope_positions, new_delta = ( | |
| self.model.recompute_mrope_positions( | |
| input_ids=req_state.prompt_token_ids, | |
| multimodal_embeddings=mm_embeds_req, | |
| mrope_positions=req_state.mrope_positions, | |
| num_computed_tokens=req_state.num_computed_tokens, | |
| ) | |
| ) | |
| mm_embeds_req = [ | |
| copy_mm_embedding_modality(src, dst) | |
| for src, dst in zip(old_mm_embeds_req, mm_embeds_req) | |
| ] | |
| req_state.mrope_positions.copy_(new_mrope_positions) | |
| req_state.mrope_position_delta = new_delta | |
| mm_embeds.extend(mm_embeds_req) | |
| req_start_idx += num_scheduled_tokens | |
| if should_sync_mrope_positions: | |
| self._calc_mrope_positions(scheduler_output) | |
| self.mrope_positions.copy_to_gpu(total_num_scheduled_tokens) | |
| if should_sync_xdrope_positions: | |
| self._calc_xdrope_positions(scheduler_output) | |
| self.xdrope_positions.copy_to_gpu(total_num_scheduled_tokens) | |
| return mm_embeds, is_mm_embed | |
| def get_model(self) -> nn.Module: | |
| if not hasattr(self, "model"): | |
| raise ValueError("Cannot get model before model has been initialized") | |
| if isinstance( | |
| self.model, (CUDAGraphWrapper, UBatchWrapper, BreakableCUDAGraphWrapper) | |
| ): | |
| # get raw model out of the cudagraph wrapper. | |
| return self.model.unwrap() | |
| return self.model | |
| def get_draft_model(self) -> nn.Module | None: | |
| drafter = getattr(self, "drafter", None) | |
| if drafter is None: | |
| return None | |
| model = getattr(drafter, "model", None) | |
| if isinstance( | |
| model, (CUDAGraphWrapper, UBatchWrapper, BreakableCUDAGraphWrapper) | |
| ): | |
| return cast(nn.Module, model.unwrap()) | |
| return cast(nn.Module | None, model) | |
| def get_supported_generation_tasks(self) -> list[GenerationTask]: | |
| model = self.get_model() | |
| supported_tasks = list[GenerationTask]() | |
| if is_text_generation_model(model): | |
| supported_tasks.append("generate") | |
| if supports_transcription(model): | |
| if model.supports_transcription_only: | |
| return ["transcription"] | |
| supported_tasks.append("transcription") | |
| if supports_realtime(model): | |
| supported_tasks.append("realtime") | |
| return supported_tasks | |
| def get_supported_pooling_tasks(self) -> list[PoolingTask]: | |
| model = self.get_model() | |
| if not is_pooling_model(model): | |
| return [] | |
| return list(model.pooler.get_supported_tasks()) | |
| def get_supported_tasks(self) -> tuple[SupportedTask, ...]: | |
| tasks = list[SupportedTask]() | |
| if self.model_config.runner_type == "generate": | |
| tasks.extend(self.get_supported_generation_tasks()) | |
| if self.model_config.runner_type == "pooling": | |
| tasks.extend(self.get_supported_pooling_tasks()) | |
| return tuple(tasks) | |
| def sync_and_gather_intermediate_tensors( | |
| self, | |
| num_tokens: int, | |
| intermediate_tensors: IntermediateTensors | None, | |
| sync_self: bool, | |
| ) -> IntermediateTensors: | |
| assert self.intermediate_tensors is not None | |
| tp = self.vllm_config.parallel_config.tensor_parallel_size | |
| is_rs = is_residual_scattered_for_sp(self.vllm_config, num_tokens) | |
| # When sequence parallelism is enabled, the "residual" tensor is | |
| # sharded across TP ranks. All-gather it here because downstream | |
| # QKV + Attention needs the full residual before the SP split point. | |
| if sync_self: | |
| assert intermediate_tensors is not None | |
| for k, v in intermediate_tensors.items(): | |
| is_scattered = k == "residual" and is_rs | |
| if is_scattered: | |
| local_len = num_tokens // tp | |
| v = get_tp_group().all_gather(v[:local_len], dim=0) | |
| self.intermediate_tensors[k][:num_tokens].copy_( | |
| v[:num_tokens], non_blocking=True | |
| ) | |
| return IntermediateTensors( | |
| {k: v[:num_tokens] for k, v in self.intermediate_tensors.items()} | |
| ) | |
| def eplb_step(self, is_dummy: bool = False, is_profile: bool = False) -> None: | |
| """ | |
| Step for the EPLB (Expert Parallelism Load Balancing) state. | |
| """ | |
| if not self.parallel_config.enable_eplb or self.eep_eplb_suppressed: | |
| return | |
| assert self.eplb_state is not None | |
| assert self._moe_model is not None | |
| self.eplb_state.step( | |
| is_dummy, | |
| is_profile, | |
| log_stats=self.parallel_config.eplb_config.log_balancedness, | |
| ) | |
| def setup_eplb_from_mapping( | |
| self, | |
| expanded_physical_to_logical: torch.Tensor, | |
| old_num_physical_experts: int, | |
| ) -> None: | |
| assert self._moe_model is not None | |
| self.eplb_state = EplbState.from_mapping( | |
| model=self._moe_model, | |
| model_config=self.model_config, | |
| device=self.device, | |
| parallel_config=self.parallel_config, | |
| expanded_physical_to_logical=expanded_physical_to_logical, | |
| num_valid_physical_experts=old_num_physical_experts, | |
| ) | |
| def _pool( | |
| self, | |
| hidden_states: torch.Tensor, | |
| num_scheduled_tokens: int, | |
| num_scheduled_tokens_np: np.ndarray, | |
| kv_connector_output: KVConnectorOutput | None, | |
| ) -> ModelRunnerOutput | AsyncModelRunnerOutput: | |
| num_reqs = self.input_batch.num_reqs | |
| assert num_reqs == len(self.input_batch.pooling_params), ( | |
| "Either all or none of the requests in a batch must be pooling request" | |
| ) | |
| hidden_states = hidden_states[:num_scheduled_tokens] | |
| seq_lens_cpu = self.optimistic_seq_lens_cpu[:num_reqs] | |
| pooling_metadata = self.input_batch.get_pooling_metadata() | |
| pooling_metadata.build_pooling_cursor( | |
| num_scheduled_tokens_np, | |
| seq_lens_cpu, | |
| device=hidden_states.device, | |
| query_start_loc_gpu=self.query_start_loc.gpu[: num_reqs + 1], | |
| ) | |
| model = cast(VllmModelForPooling, self.model) | |
| raw_pooler_output: PoolerOutput = model.pooler( | |
| hidden_states=hidden_states, pooling_metadata=pooling_metadata | |
| ) | |
| finished_mask = [ | |
| seq_len == prompt_len | |
| for seq_len, prompt_len in zip(seq_lens_cpu, pooling_metadata.prompt_lens) | |
| ] | |
| raw_pooler_output = self.late_interaction_runner.postprocess_pooler_output( | |
| raw_pooler_output=raw_pooler_output, | |
| pooling_params=pooling_metadata.pooling_params, | |
| req_ids=self.input_batch.req_ids, | |
| finished_mask=finished_mask, | |
| ) | |
| model_runner_output = ModelRunnerOutput( | |
| req_ids=self.input_batch.req_ids.copy(), | |
| req_id_to_index=self.input_batch.req_id_to_index.copy(), | |
| kv_connector_output=kv_connector_output, | |
| ) | |
| if raw_pooler_output is None or not any(finished_mask): | |
| self._sync_device() | |
| model_runner_output.pooler_output = [None] * num_reqs | |
| return model_runner_output | |
| if not current_platform.is_cuda_alike(): | |
| # cpu/xpu runners cannot use the CUDA stream/event-based wrapper. | |
| model_runner_output.pooler_output = _copy_pooler_output_to_cpu( | |
| raw_pooler_output=raw_pooler_output, | |
| finished_mask=finished_mask, | |
| ) | |
| self._sync_device() | |
| return model_runner_output | |
| return AsyncGPUPoolingModelRunnerOutput( | |
| model_runner_output=model_runner_output, | |
| raw_pooler_output=raw_pooler_output, | |
| finished_mask=finished_mask, | |
| async_output_copy_stream=self._get_or_create_async_output_copy_stream(), | |
| ) | |
| def _pad_for_sequence_parallelism(self, num_scheduled_tokens: int) -> int: | |
| # Pad tokens to multiple of tensor_parallel_size when | |
| # enabled collective fusion for SP | |
| tp_size = self.vllm_config.parallel_config.tensor_parallel_size | |
| if self.compilation_config.pass_config.enable_sp and tp_size > 1: | |
| return round_up(num_scheduled_tokens, tp_size) | |
| return num_scheduled_tokens | |
| def _prepare_mm_inputs( | |
| self, num_tokens: int | |
| ) -> tuple[torch.Tensor | None, torch.Tensor]: | |
| if self.model.requires_raw_input_tokens: | |
| input_ids = self.input_ids.gpu[:num_tokens] | |
| else: | |
| input_ids = None | |
| inputs_embeds = self.inputs_embeds.gpu[:num_tokens] | |
| return input_ids, inputs_embeds | |
| def _preprocess( | |
| self, | |
| scheduler_output: "SchedulerOutput", | |
| num_input_tokens: int, # Padded | |
| intermediate_tensors: IntermediateTensors | None = None, | |
| ) -> tuple[ | |
| torch.Tensor | None, | |
| torch.Tensor | None, | |
| torch.Tensor, | |
| IntermediateTensors | None, | |
| dict[str, Any], | |
| ECConnectorOutput | None, | |
| ]: | |
| num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens | |
| is_first_rank = get_pp_group().is_first_rank | |
| is_encoder_decoder = self.model_config.is_encoder_decoder | |
| # Clamp speculative scheduler placeholders (-1) before embedding lookup. | |
| if self.speculative_config is not None: | |
| self.input_ids.gpu[:num_input_tokens].clamp_(min=0) | |
| # _prepare_inputs may reorder the batch, so we must gather multi | |
| # modal outputs after that to ensure the correct order | |
| ec_connector_output = None | |
| if self.supports_mm_inputs and is_first_rank and not is_encoder_decoder: | |
| # Run the multimodal encoder if any. | |
| with self.maybe_get_ec_connector_output( | |
| scheduler_output, | |
| encoder_cache=self.encoder_cache, | |
| ) as ec_connector_output: | |
| self._execute_mm_encoder(scheduler_output) | |
| mm_embeds, is_mm_embed = self._gather_mm_embeddings(scheduler_output) | |
| # NOTE(woosuk): To unify token ids and soft tokens (vision | |
| # embeddings), we always use embeddings (rather than token ids) | |
| # as input to the multimodal model, even when the input is text. | |
| if self.enable_prompt_embeds and self.input_batch.req_prompt_embeds: | |
| # Some positions carry precomputed prompt_embeds: they are | |
| # already in self.inputs_embeds and marked is_token_ids=False. | |
| # Embed only the token-id positions (zeroing the placeholder ids | |
| # at prompt_embeds positions so the embedding gather cannot read | |
| # out-of-range ids), and write them back without clobbering the | |
| # prompt_embeds positions. | |
| is_token_ids = self.is_token_ids.gpu[:num_scheduled_tokens] | |
| safe_input_ids = torch.where( | |
| is_token_ids, | |
| self.input_ids.gpu[:num_scheduled_tokens], | |
| 0, | |
| ) | |
| inputs_embeds_scheduled = self.model.embed_input_ids( | |
| safe_input_ids, | |
| multimodal_embeddings=mm_embeds, | |
| is_multimodal=is_mm_embed, | |
| ) | |
| target = self.inputs_embeds.gpu[:num_scheduled_tokens] | |
| self.inputs_embeds.gpu[:num_scheduled_tokens] = torch.where( | |
| is_token_ids.unsqueeze(-1), | |
| inputs_embeds_scheduled, | |
| target, | |
| ) | |
| else: | |
| inputs_embeds_scheduled = self.model.embed_input_ids( | |
| self.input_ids.gpu[:num_scheduled_tokens], | |
| multimodal_embeddings=mm_embeds, | |
| is_multimodal=is_mm_embed, | |
| ) | |
| # TODO(woosuk): Avoid the copy. Optimize. | |
| self.inputs_embeds.gpu[:num_scheduled_tokens].copy_( | |
| inputs_embeds_scheduled | |
| ) | |
| input_ids, inputs_embeds = self._prepare_mm_inputs(num_input_tokens) | |
| model_kwargs = { | |
| **self._init_model_kwargs(), | |
| **self._extract_mm_kwargs(scheduler_output), | |
| } | |
| elif self.enable_prompt_embeds and is_first_rank: | |
| # Get the input embeddings for the tokens that are not input embeds, | |
| # then put them into the appropriate positions. | |
| # TODO(qthequartermasterman): Since even when prompt embeds are | |
| # enabled, (a) not all requests will use prompt embeds, and (b) | |
| # after the initial prompt is processed, the rest of the generated | |
| # tokens will be token ids, it is not desirable to have the | |
| # embedding layer outside of the CUDA graph all the time. The v0 | |
| # engine avoids this by "double compiling" the CUDA graph, once | |
| # with input_ids and again with inputs_embeds, for all num_tokens. | |
| # If a batch only has token ids, then including the embedding layer | |
| # in the CUDA graph will be more performant (like in the else case | |
| # below). | |
| is_token_ids = self.is_token_ids.np[:num_scheduled_tokens] | |
| token_ids_idx_np = np.nonzero(is_token_ids)[0] | |
| # Some tokens ids may need to become embeds | |
| if token_ids_idx_np.size > 0: | |
| token_ids_idx = async_tensor_h2d(token_ids_idx_np, device=self.device) | |
| token_ids = self.input_ids.gpu[token_ids_idx] | |
| tokens_to_embeds = self.model.embed_input_ids(input_ids=token_ids) | |
| self.inputs_embeds.gpu[token_ids_idx] = tokens_to_embeds | |
| inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens] | |
| model_kwargs = self._init_model_kwargs() | |
| input_ids = None | |
| else: | |
| # For text-only models, we use token ids as input. | |
| # While it is possible to use embeddings as input just like the | |
| # multimodal models, it is not desirable for performance since | |
| # then the embedding layer is not included in the CUDA graph. | |
| input_ids = self.input_ids.gpu[:num_input_tokens] | |
| inputs_embeds = None | |
| model_kwargs = self._init_model_kwargs() | |
| if self.uses_mrope: | |
| positions = self.mrope_positions.gpu[:, :num_input_tokens] | |
| elif self.uses_xdrope_dim > 0: | |
| positions = self.xdrope_positions.gpu[:, :num_input_tokens] | |
| else: | |
| positions = self.positions[:num_input_tokens] | |
| if num_input_tokens > num_scheduled_tokens: | |
| self.positions[num_scheduled_tokens:num_input_tokens].zero_() | |
| if is_first_rank: | |
| intermediate_tensors = None | |
| else: | |
| assert intermediate_tensors is not None | |
| intermediate_tensors = self.sync_and_gather_intermediate_tensors( | |
| num_input_tokens, intermediate_tensors, True | |
| ) | |
| if is_encoder_decoder and scheduler_output.scheduled_encoder_inputs: | |
| # Run the encoder, just like we do with other multimodal inputs. | |
| # For an encoder-decoder model, our processing here is a bit | |
| # simpler, because the outputs are just passed to the decoder. | |
| # We are not doing any prompt replacement. We also will only | |
| # ever have a single encoder input. | |
| encoder_outputs = self._execute_mm_encoder(scheduler_output) | |
| model_kwargs.update({"encoder_outputs": encoder_outputs}) | |
| return ( | |
| input_ids, | |
| inputs_embeds, | |
| positions, | |
| intermediate_tensors, | |
| model_kwargs, | |
| ec_connector_output, | |
| ) | |
| def _sample( | |
| self, | |
| logits: torch.Tensor | None, | |
| spec_decode_metadata: SpecDecodeMetadata | None, | |
| ) -> SamplerOutput: | |
| # Sample the next token and get logprobs if needed. | |
| sampling_metadata = self.input_batch.sampling_metadata | |
| # Update output token ids with tokens sampled in last step | |
| # if async scheduling and required by current sampling params. | |
| self.input_batch.update_async_output_token_ids() | |
| if spec_decode_metadata is None: | |
| return self.sampler( | |
| logits=logits, | |
| sampling_metadata=sampling_metadata, | |
| ) | |
| # Update spec_token_ids with real draft tokens from pre step only when | |
| # output_token_ids is needed (penalties or bad_words are in use). | |
| if self.use_async_scheduling and self._draft_token_req_ids is not None: | |
| draft_token_ids_cpu, _ = self._get_draft_token_ids_cpu() | |
| self.input_batch.update_async_spec_token_ids(draft_token_ids_cpu) | |
| draft_probs = self._get_spec_decode_draft_probs(spec_decode_metadata) | |
| sampler_output = self.rejection_sampler( | |
| spec_decode_metadata, | |
| draft_probs, | |
| logits, | |
| sampling_metadata, | |
| ) | |
| return sampler_output | |
| def _bookkeeping_sync( | |
| self, | |
| scheduler_output: "SchedulerOutput", | |
| sampler_output: SamplerOutput, | |
| logits: torch.Tensor | None, | |
| hidden_states: torch.Tensor, | |
| num_scheduled_tokens: int, | |
| ) -> tuple[ | |
| dict[str, int], | |
| LogprobsLists | None, | |
| list[list[int]], | |
| dict[str, LogprobsTensors | None], | |
| list[str], | |
| dict[str, int], | |
| list[int], | |
| ]: | |
| num_nans_in_logits = {} | |
| if envs.VLLM_COMPUTE_NANS_IN_LOGITS: | |
| num_nans_in_logits = self._get_nans_in_logits(logits) | |
| num_reqs = self.input_batch.num_reqs | |
| discard_sampled_tokens_req_indices = np.nonzero( | |
| self.discard_request_mask.np[:num_reqs] | |
| )[0] | |
| for i in discard_sampled_tokens_req_indices: | |
| gen = self.input_batch.generators.get(int(i)) | |
| if gen is not None: | |
| gen.set_offset(gen.get_offset() - 4) | |
| # Copy some objects so they don't get modified after returning. | |
| # This is important when using async scheduling. | |
| req_ids_output_copy = self.input_batch.req_ids.copy() | |
| req_id_to_index_output_copy = self.input_batch.req_id_to_index.copy() | |
| num_sampled_tokens = sampler_output.sampled_token_ids.shape[0] | |
| sampled_token_ids = sampler_output.sampled_token_ids | |
| logprobs_tensors = sampler_output.logprobs_tensors | |
| invalid_req_indices = [] | |
| logprobs_lists = None | |
| if not self.use_async_scheduling: | |
| # Sync scheduling: issue routed experts D2H into the pinned | |
| # CPU buffer BEFORE ``_to_list`` below. ``_to_list`` does | |
| # ``event.synchronize()`` on the async copy stream which | |
| # waits for every D2H queued on the default stream since | |
| # the last sync, so this enqueue is naturally covered | |
| # without requiring its own synchronize. | |
| if self.routed_experts_initialized: | |
| buf = self.routed_experts_capturer.get_device_buffer() | |
| total = scheduler_output.total_num_scheduled_tokens | |
| self.routed_experts_cpu[:total].copy_(buf[:total], non_blocking=True) | |
| self.routed_experts_slot_mapping_cpu[:total].copy_( | |
| self.routed_experts_slot_mapping_device[:total], | |
| non_blocking=True, | |
| ) | |
| # Get the valid generated tokens. | |
| max_gen_len = sampled_token_ids.shape[-1] | |
| if max_gen_len == 1: | |
| # No spec decode tokens. | |
| valid_sampled_token_ids = self._to_list(sampled_token_ids) | |
| # Mask out the sampled tokens that should not be sampled. | |
| for i in discard_sampled_tokens_req_indices: | |
| valid_sampled_token_ids[int(i)].clear() | |
| if logprobs_tensors is not None: | |
| logprobs_lists = logprobs_tensors.tolists() | |
| else: | |
| # Includes spec decode tokens. | |
| valid_sampled_token_ids, logprobs_lists = RejectionSampler.parse_output( | |
| sampled_token_ids, | |
| self.input_batch.vocab_size, | |
| discard_sampled_tokens_req_indices, | |
| logprobs_tensors=logprobs_tensors, | |
| ) | |
| else: | |
| valid_sampled_token_ids = [] | |
| invalid_req_indices = discard_sampled_tokens_req_indices.tolist() | |
| invalid_req_indices_set = set(invalid_req_indices) | |
| # Cache the sampled tokens on the GPU and avoid CPU sync. | |
| # These will be copied into input_ids in the next step | |
| # when preparing inputs. | |
| # With spec decoding, this is done in propose_draft_token_ids(). | |
| if self.input_batch.prev_sampled_token_ids is None: | |
| assert sampled_token_ids.shape[-1] == 1 | |
| self.input_batch.prev_sampled_token_ids = sampled_token_ids | |
| self.input_batch.prev_req_id_to_index = { | |
| req_id: i | |
| for i, req_id in enumerate(self.input_batch.req_ids) | |
| if i not in invalid_req_indices_set | |
| } | |
| # Cache the sampled tokens in the model runner, so that the scheduler | |
| # doesn't need to send them back. | |
| # NOTE(woosuk): As an exception, when using PP, the scheduler sends | |
| # the sampled tokens back, because there's no direct communication | |
| # between the first-stage worker and the last-stage worker. | |
| req_ids = self.input_batch.req_ids | |
| for req_idx in range(num_sampled_tokens): | |
| if self.use_async_scheduling: | |
| sampled_ids = [-1] if req_idx not in invalid_req_indices_set else None | |
| else: | |
| sampled_ids = valid_sampled_token_ids[req_idx] | |
| num_sampled_ids: int = len(sampled_ids) if sampled_ids else 0 | |
| if not sampled_ids: | |
| continue | |
| start_idx = self.input_batch.num_tokens_no_spec[req_idx] | |
| end_idx = start_idx + num_sampled_ids | |
| assert end_idx <= self.max_model_len, ( | |
| "Sampled token IDs exceed the max model length. " | |
| f"Total number of tokens: {end_idx} > max_model_len: " | |
| f"{self.max_model_len}" | |
| ) | |
| self.input_batch.token_ids_cpu[req_idx, start_idx:end_idx] = sampled_ids | |
| self.input_batch.is_token_ids[req_idx, start_idx:end_idx] = True | |
| self.input_batch.num_tokens_no_spec[req_idx] = end_idx | |
| req_id = req_ids[req_idx] | |
| req_state = self.requests[req_id] | |
| req_state.output_token_ids.extend(sampled_ids) | |
| # Compute prompt logprobs if needed. | |
| prompt_logprobs_dict = self._get_prompt_logprobs_dict( | |
| hidden_states[:num_scheduled_tokens], | |
| scheduler_output.num_scheduled_tokens, | |
| ) | |
| return ( | |
| num_nans_in_logits, | |
| logprobs_lists, | |
| valid_sampled_token_ids, | |
| prompt_logprobs_dict, | |
| req_ids_output_copy, | |
| req_id_to_index_output_copy, | |
| invalid_req_indices, | |
| ) | |
| def synchronize_input_prep(self): | |
| if self.prepare_inputs_event is None: | |
| yield | |
| return | |
| # Ensure prior step has finished with reused CPU tensors. | |
| # This is required in the async scheduling case because | |
| # the CPU->GPU transfer happens async. | |
| self.prepare_inputs_event.synchronize() | |
| try: | |
| yield | |
| finally: | |
| self.prepare_inputs_event.record() | |
| def _model_forward( | |
| self, | |
| input_ids: torch.Tensor | None = None, | |
| positions: torch.Tensor | None = None, | |
| intermediate_tensors: IntermediateTensors | None = None, | |
| inputs_embeds: torch.Tensor | None = None, | |
| **model_kwargs: dict[str, Any], | |
| ) -> Any: | |
| """Helper method to call the model forward pass. | |
| This method can be overridden by subclasses for model execution. | |
| Motivation: We can inspect only this method versus | |
| the whole execute_model, which has additional logic. | |
| Args: | |
| input_ids: Input token IDs | |
| positions: Token positions | |
| intermediate_tensors: Tensors from previous pipeline stages | |
| inputs_embeds: Input embeddings (alternative to input_ids) | |
| **model_kwargs: Additional model arguments | |
| Returns: | |
| Model output tensor | |
| """ | |
| return self.model( | |
| input_ids=input_ids, | |
| positions=positions, | |
| intermediate_tensors=intermediate_tensors, | |
| inputs_embeds=inputs_embeds, | |
| **model_kwargs, | |
| ) | |
| def _is_uniform_decode( | |
| max_num_scheduled_tokens: int, | |
| uniform_decode_query_len: int, | |
| num_tokens: int, | |
| num_reqs: int, | |
| force_uniform_decode: bool | None = None, | |
| ) -> bool: | |
| """ | |
| Checks if it's a decode batch with same amount scheduled tokens | |
| across all requests. | |
| """ | |
| return ( | |
| ( | |
| (max_num_scheduled_tokens == uniform_decode_query_len) | |
| and (num_tokens == max_num_scheduled_tokens * num_reqs) | |
| ) | |
| if force_uniform_decode is None | |
| else force_uniform_decode | |
| ) | |
| def _determine_batch_execution_and_padding( | |
| self, | |
| num_tokens: int, | |
| num_reqs: int, | |
| num_scheduled_tokens_np: np.ndarray, | |
| max_num_scheduled_tokens: int, | |
| use_cascade_attn: bool, | |
| allow_microbatching: bool = True, | |
| force_eager: bool = False, | |
| # For cudagraph capture TODO(lucas): Refactor how we capture cudagraphs (will | |
| # be improved in model runner v2) | |
| force_uniform_decode: bool | None = None, | |
| force_has_lora: bool | None = None, | |
| force_num_active_loras: int | None = None, | |
| num_encoder_reqs: int = 0, | |
| ) -> tuple[ | |
| CUDAGraphMode, | |
| BatchDescriptor, | |
| bool, | |
| torch.Tensor | None, | |
| CUDAGraphStat | None, | |
| ]: | |
| uniform_decode = self._is_uniform_decode( | |
| max_num_scheduled_tokens=max_num_scheduled_tokens, | |
| uniform_decode_query_len=self.uniform_decode_query_len, | |
| num_tokens=num_tokens, | |
| num_reqs=num_reqs, | |
| force_uniform_decode=force_uniform_decode, | |
| ) | |
| # Encoder-decoder models only support CG for decoder_step > 0 (no enc_output | |
| # is present). Also, chunked-prefill is disabled, so batch are uniform. | |
| has_encoder_output = ( | |
| self.model_config.is_encoder_decoder and num_encoder_reqs > 0 | |
| ) | |
| # Compute LoRA state for cudagraph dispatch | |
| num_active_loras = ( | |
| force_num_active_loras | |
| if force_num_active_loras is not None | |
| else len(self.input_batch.lora_id_to_lora_request) | |
| ) | |
| has_lora = num_active_loras > 0 if force_has_lora is None else force_has_lora | |
| num_tokens_padded = self._pad_for_sequence_parallelism(num_tokens) | |
| def dispatch_cudagraph(num_tokens, disable_full=False, valid_modes=None): | |
| return self.cudagraph_dispatcher.dispatch( | |
| num_tokens=num_tokens, | |
| has_lora=has_lora, | |
| uniform_decode=uniform_decode, | |
| num_active_loras=num_active_loras, | |
| valid_modes={CUDAGraphMode.NONE} if force_eager else valid_modes, | |
| invalid_modes={CUDAGraphMode.FULL} if disable_full else None, | |
| ) | |
| cudagraph_mode, batch_descriptor = dispatch_cudagraph( | |
| num_tokens_padded, disable_full=use_cascade_attn or has_encoder_output | |
| ) | |
| num_tokens_padded = batch_descriptor.num_tokens | |
| if self.compilation_config.pass_config.enable_sp: | |
| assert ( | |
| batch_descriptor.num_tokens | |
| % self.vllm_config.parallel_config.tensor_parallel_size | |
| == 0 | |
| ), ( | |
| "Sequence parallelism requires num_tokens to be " | |
| "a multiple of tensor parallel size" | |
| ) | |
| # Extra coordination when running data-parallel since we need to coordinate | |
| # across ranks | |
| should_ubatch, num_tokens_across_dp = False, None | |
| if self.vllm_config.parallel_config.data_parallel_size > 1: | |
| should_ubatch, num_tokens_across_dp, synced_cudagraph_mode = ( | |
| coordinate_batch_across_dp( | |
| num_tokens_unpadded=num_tokens, | |
| parallel_config=self.parallel_config, | |
| allow_microbatching=allow_microbatching, | |
| num_tokens_padded=num_tokens_padded, | |
| uniform_decode=uniform_decode, | |
| cudagraph_mode=cudagraph_mode.value, | |
| ) | |
| ) | |
| # Extract DP-synced values | |
| if num_tokens_across_dp is not None: | |
| dp_rank = self.parallel_config.data_parallel_rank | |
| num_tokens_padded = int(num_tokens_across_dp[dp_rank].item()) | |
| # Re-dispatch with DP padding so we have the correct batch_descriptor | |
| cudagraph_mode, batch_descriptor = dispatch_cudagraph( | |
| num_tokens_padded, | |
| valid_modes={CUDAGraphMode(synced_cudagraph_mode)}, | |
| ) | |
| # Assert to make sure the agreed upon token count is correct otherwise | |
| # num_tokens_across_dp will no-longer be valid | |
| assert batch_descriptor.num_tokens == num_tokens_padded | |
| cudagraph_stats = None | |
| if self.vllm_config.observability_config.cudagraph_metrics: | |
| cudagraph_stats = CUDAGraphStat( | |
| num_unpadded_tokens=num_tokens, | |
| num_padded_tokens=batch_descriptor.num_tokens, | |
| num_paddings=batch_descriptor.num_tokens - num_tokens, | |
| runtime_mode=str(cudagraph_mode), | |
| ) | |
| return ( | |
| cudagraph_mode, | |
| batch_descriptor, | |
| should_ubatch, | |
| num_tokens_across_dp, | |
| cudagraph_stats, | |
| ) | |
| def _register_layerwise_nvtx_hooks(self) -> None: | |
| """ | |
| Register layerwise NVTX hooks if --enable-layerwise-nvtx-tracing is enabled | |
| to trace detailed information of each layer or module in the model. | |
| """ | |
| if ( | |
| self.vllm_config.observability_config.enable_layerwise_nvtx_tracing | |
| and not self.layerwise_nvtx_hooks_registered | |
| ): | |
| if self.compilation_config.cudagraph_mode != CUDAGraphMode.NONE: | |
| logger.debug_once( | |
| "layerwise NVTX tracing is not supported when CUDA graph is " | |
| "turned off; you may observe part or all of the model " | |
| "missing NVTX markers" | |
| ) | |
| # In STOCK_TORCH_COMPILE mode, after registering hooks here, | |
| # the __call__ function of nn.module will be recompiled with | |
| # fullgraph=True. Since nvtx.range_push/pop are not traceable | |
| # by torch dynamo, we can't register hook functions here | |
| # because hook functions will also be traced by torch dynamo. | |
| if ( | |
| self.vllm_config.compilation_config.mode | |
| == CompilationMode.STOCK_TORCH_COMPILE | |
| ): | |
| logger.debug_once( | |
| "layerwise NVTX tracing is not supported when " | |
| "CompilationMode is STOCK_TORCH_COMPILE, skipping " | |
| "function hooks registration" | |
| ) | |
| else: | |
| pyt_hooks = PytHooks() | |
| pyt_hooks.register_hooks(self.model, self.model.__class__.__name__) | |
| self.layerwise_nvtx_hooks_registered = True | |
| def _get_slot_mappings( | |
| self, | |
| num_tokens_padded: int, | |
| num_reqs_padded: int, | |
| num_tokens_unpadded: int, | |
| ubatch_slices: "UBatchSlices | None" = None, | |
| ) -> tuple[ | |
| dict[int, torch.Tensor] | None, | |
| dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None, | |
| ]: | |
| """ | |
| Build slot mappings in both formats needed by the system. | |
| Args: | |
| num_tokens_padded: Total number of tokens (padded) | |
| num_reqs_padded: Total number of requests (padded) | |
| num_tokens_unpadded: Actual number of tokens (unpadded) | |
| ubatch_slices: Optional ubatch slicing info for DBO | |
| Returns: | |
| A tuple of: | |
| - slot_mappings_by_gid: dict[int, torch.Tensor] for attention metadata | |
| - slot_mappings_by_layer: dict[str, torch.Tensor] or list for ForwardContext | |
| """ | |
| if not ( | |
| hasattr(self, "kv_cache_config") | |
| and self.kv_cache_config is not None | |
| and len(self.kv_cache_config.kv_cache_groups) > 0 | |
| ): | |
| return None, None | |
| def _get_slot_mapping(kv_cache_gid: int): | |
| assert num_reqs_padded is not None and num_tokens_padded is not None | |
| kv_cache_spec = self.kv_cache_config.kv_cache_groups[ | |
| kv_cache_gid | |
| ].kv_cache_spec | |
| if isinstance(kv_cache_spec, EncoderOnlyAttentionSpec): | |
| slot_mapping = torch.zeros( | |
| (num_tokens_padded,), | |
| dtype=torch.int64, | |
| device=self.device, | |
| ) | |
| else: | |
| blk_table = self.input_batch.block_table[kv_cache_gid] | |
| slot_mapping = blk_table.slot_mapping.gpu[:num_tokens_padded] | |
| # Fill unused with -1. Needed for reshape_and_cache in full cuda | |
| # graph mode. `blk_table_tensor` -1 to match mamba PAD_SLOT_ID | |
| slot_mapping[num_tokens_unpadded:num_tokens_padded].fill_(-1) | |
| return slot_mapping | |
| slot_mappings_by_gid = { | |
| gid: _get_slot_mapping(gid) | |
| for gid, _ in enumerate(self.kv_cache_config.kv_cache_groups) | |
| } | |
| slot_mappings_by_layer: dict[str, torch.Tensor] = {} | |
| for gid, kv_cache_group in enumerate(self.kv_cache_config.kv_cache_groups): | |
| slot_mapping = slot_mappings_by_gid[gid] | |
| for layer_name in kv_cache_group.layer_names: | |
| slot_mappings_by_layer[layer_name] = slot_mapping | |
| if ubatch_slices is not None: | |
| result: list[dict[str, torch.Tensor]] = [] | |
| for ubatch in ubatch_slices: | |
| sliced_mappings: dict[str, torch.Tensor] = {} | |
| for layer_name, slot_mapping in slot_mappings_by_layer.items(): | |
| sliced_mappings[layer_name] = slot_mapping[ubatch.token_slice] | |
| result.append(sliced_mappings) | |
| return slot_mappings_by_gid, result | |
| return slot_mappings_by_gid, slot_mappings_by_layer | |
| def _is_all_reqs_chunked_prefill(self) -> bool: | |
| """Check if all scheduled requests are marked to discard sampled tokens. | |
| This is true when `discard_request_mask` is set for every scheduled | |
| request (e.g., for chunked prefill requests that are not the last | |
| prefill chunk).""" | |
| num_reqs = self.input_batch.num_reqs | |
| return bool(self.discard_request_mask.np[:num_reqs].all()) | |
| def execute_model( | |
| self, | |
| scheduler_output: "SchedulerOutput", | |
| intermediate_tensors: IntermediateTensors | None = None, | |
| ) -> ModelRunnerOutput | AsyncModelRunnerOutput | IntermediateTensors | None: | |
| if self.execute_model_state is not None: | |
| raise RuntimeError( | |
| "State error: sample_tokens() must be called " | |
| "after execute_model() returns None." | |
| ) | |
| if self.routed_experts_initialized: | |
| self.routed_experts_capturer.clear_buffer() | |
| # If ngram_gpu is used, we need to copy the scheduler_output to avoid | |
| # the modification has influence on the scheduler_output in engine core process. | |
| # The replace is much faster than deepcopy. | |
| if ( | |
| self.speculative_config is not None | |
| and self.speculative_config.use_ngram_gpu() | |
| ): | |
| num_scheduled_tokens_copy = scheduler_output.num_scheduled_tokens.copy() | |
| spec_decode_tokens_copy = ( | |
| scheduler_output.scheduled_spec_decode_tokens.copy() | |
| ) | |
| scheduler_output = replace( | |
| scheduler_output, | |
| num_scheduled_tokens=num_scheduled_tokens_copy, | |
| scheduled_spec_decode_tokens=spec_decode_tokens_copy, | |
| ) | |
| if has_kv_transfer_group(): | |
| kv_connector_metadata = scheduler_output.kv_connector_metadata | |
| assert kv_connector_metadata is not None | |
| get_kv_transfer_group().handle_preemptions(kv_connector_metadata) | |
| num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens | |
| with ( | |
| record_function_or_nullcontext("gpu_model_runner: preprocess"), | |
| self.synchronize_input_prep(), | |
| ): | |
| # Update persistent batch states. | |
| deferred_state_corrections_fn = self._update_states(scheduler_output) | |
| if has_ec_transfer() and not get_ec_transfer().is_consumer: | |
| with self.maybe_get_ec_connector_output( | |
| scheduler_output, | |
| encoder_cache=self.encoder_cache, | |
| ) as ec_connector_output: | |
| self._execute_mm_encoder(scheduler_output) | |
| return make_empty_encoder_model_runner_output(scheduler_output) | |
| if not num_scheduled_tokens: | |
| if ( | |
| self.parallel_config.distributed_executor_backend | |
| == "external_launcher" | |
| and self.parallel_config.data_parallel_size > 1 | |
| ): | |
| # this is a corner case when both external launcher | |
| # and DP are enabled, num_scheduled_tokens could be | |
| # 0, and has_unfinished_requests in the outer loop | |
| # returns True. before returning early here we call | |
| # dummy run to ensure coordinate_batch_across_dp | |
| # is called into to avoid out of sync issues. | |
| self._dummy_run(1) | |
| if not has_kv_transfer_group(): | |
| # Return empty ModelRunnerOutput if no work to do. | |
| return EMPTY_MODEL_RUNNER_OUTPUT | |
| return self.kv_connector_no_forward(scheduler_output, self.vllm_config) | |
| if self.cache_config.kv_sharing_fast_prefill: | |
| assert not self.num_prompt_logprobs, ( | |
| "--kv-sharing-fast-prefill produces incorrect " | |
| "logprobs for prompt tokens, tokens, please disable " | |
| "it when the requests need prompt logprobs" | |
| ) | |
| num_reqs = self.input_batch.num_reqs | |
| req_ids = self.input_batch.req_ids | |
| tokens = [scheduler_output.num_scheduled_tokens[i] for i in req_ids] | |
| num_scheduled_tokens_np = np.array(tokens, dtype=np.int32) | |
| max_num_scheduled_tokens = int(num_scheduled_tokens_np.max()) | |
| num_tokens_unpadded = scheduler_output.total_num_scheduled_tokens | |
| logits_indices, spec_decode_metadata = self._prepare_inputs( | |
| scheduler_output, | |
| num_scheduled_tokens_np, | |
| ) | |
| cascade_attn_prefix_lens = None | |
| # Disable cascade attention when using microbatching (DBO) | |
| if self.cascade_attn_enabled and not self.parallel_config.use_ubatching: | |
| # Pre-compute cascade attention prefix lengths | |
| cascade_attn_prefix_lens = self._compute_cascade_attn_prefix_lens( | |
| num_scheduled_tokens_np, | |
| self.input_batch.num_computed_tokens_cpu[:num_reqs], | |
| scheduler_output.num_common_prefix_blocks, | |
| ) | |
| ( | |
| cudagraph_mode, | |
| batch_desc, | |
| should_ubatch, | |
| num_tokens_across_dp, | |
| cudagraph_stats, | |
| ) = self._determine_batch_execution_and_padding( | |
| num_tokens=num_tokens_unpadded, | |
| num_reqs=num_reqs, | |
| num_scheduled_tokens_np=num_scheduled_tokens_np, | |
| max_num_scheduled_tokens=max_num_scheduled_tokens, | |
| use_cascade_attn=cascade_attn_prefix_lens is not None, | |
| num_encoder_reqs=len(scheduler_output.scheduled_encoder_inputs), | |
| ) | |
| logger.debug( | |
| "Running batch with cudagraph_mode: %s, batch_descriptor: %s, " | |
| "should_ubatch: %s, num_tokens_across_dp: %s", | |
| cudagraph_mode, | |
| batch_desc, | |
| should_ubatch, | |
| num_tokens_across_dp, | |
| ) | |
| num_tokens_padded = batch_desc.num_tokens | |
| num_reqs_padded = ( | |
| batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs | |
| ) | |
| ubatch_slices, ubatch_slices_padded = maybe_create_ubatch_slices( | |
| should_ubatch, | |
| num_scheduled_tokens_np, | |
| num_tokens_padded, | |
| num_reqs_padded, | |
| self.parallel_config.num_ubatches, | |
| ) | |
| logger.debug( | |
| "ubatch_slices: %s, ubatch_slices_padded: %s", | |
| ubatch_slices, | |
| ubatch_slices_padded, | |
| ) | |
| # True if any attention backend handles KV cache update separately | |
| # from forward() (i.e., forward_includes_kv_cache_update=False). When true, | |
| # slot_mappings must use padded dimensions to match the key/value tensors. | |
| has_separate_kv_update = not all( | |
| all( | |
| g.backend.forward_includes_kv_cache_update | |
| for g in self.attn_groups[id] | |
| ) | |
| for id, spec in enumerate(self.kv_cache_config.kv_cache_groups) | |
| if not isinstance(spec.kv_cache_spec, EncoderOnlyAttentionSpec) | |
| ) | |
| pad_attn = cudagraph_mode == CUDAGraphMode.FULL | |
| if self.cache_config.mamba_cache_mode == "align": | |
| # preprocess_mamba reads req_state.num_computed_tokens (CPU) | |
| # to decide copy operations, so we must apply deferred | |
| # corrections before it runs. | |
| if deferred_state_corrections_fn: | |
| deferred_state_corrections_fn() | |
| deferred_state_corrections_fn = None | |
| mamba_bufs = self._get_mamba_bufs() | |
| mamba_utils.preprocess_mamba( | |
| scheduler_output, | |
| self.kv_cache_config, | |
| self.cache_config, | |
| self.mamba_state_idx, | |
| self.input_batch, | |
| self.requests, | |
| self.compilation_config.static_forward_context, | |
| self.model.get_mamba_state_copy_func(), | |
| mamba_bufs.preprocess, | |
| align_ctx=mamba_bufs.postprocess_align, | |
| ) | |
| # preprocess_mamba resets num_accepted_tokens_cpu to 1 | |
| # for requests whose state was copied to a new block. | |
| # Re-sync to GPU so the mamba kernel reads from the | |
| # correct initial state slot (init_token_idx = 0). | |
| self.num_accepted_tokens.np[:num_reqs] = ( | |
| self.input_batch.num_accepted_tokens_cpu[:num_reqs] | |
| ) | |
| self.num_accepted_tokens.copy_to_gpu(num_reqs) | |
| # Stage per-request inputs for the fused postprocess kernel | |
| # only when that kernel will actually run. The kernel is | |
| # gated on spec-decode + hybrid (see MambaBuffers.create); | |
| # without it, ``mamba_bufs.postprocess_align`` is None and | |
| # the staging buffers don't exist. | |
| if mamba_bufs.postprocess_align is not None: | |
| mamba_utils.stage_postprocess_inputs_to_gpu( | |
| mamba_bufs.postprocess_align, | |
| scheduler_output, | |
| self.input_batch.req_ids, | |
| num_reqs, | |
| self.requests, | |
| self.mamba_state_idx, | |
| ) | |
| use_spec_decode = len(scheduler_output.scheduled_spec_decode_tokens) > 0 | |
| ubatch_slices_attn = ubatch_slices_padded if pad_attn else ubatch_slices | |
| slot_mappings_by_group, slot_mappings = self._get_slot_mappings( | |
| num_tokens_padded=num_tokens_padded | |
| if pad_attn or has_separate_kv_update | |
| else num_tokens_unpadded, | |
| num_reqs_padded=( | |
| num_reqs_padded if pad_attn or has_separate_kv_update else num_reqs | |
| ), | |
| num_tokens_unpadded=num_tokens_unpadded, | |
| ubatch_slices=ubatch_slices_padded, | |
| ) | |
| attn_metadata, spec_decode_common_attn_metadata = ( | |
| self._build_attention_metadata( | |
| num_tokens=num_tokens_unpadded, | |
| num_tokens_padded=num_tokens_padded if pad_attn else None, | |
| num_reqs=num_reqs, | |
| num_reqs_padded=num_reqs_padded if pad_attn else None, | |
| max_query_len=max_num_scheduled_tokens, | |
| ubatch_slices=ubatch_slices_attn, | |
| logits_indices=logits_indices, | |
| use_spec_decode=use_spec_decode, | |
| num_scheduled_tokens=scheduler_output.num_scheduled_tokens, | |
| cascade_attn_prefix_lens=cascade_attn_prefix_lens, | |
| slot_mappings=slot_mappings_by_group, | |
| ) | |
| ) | |
| ( | |
| input_ids, | |
| inputs_embeds, | |
| positions, | |
| intermediate_tensors, | |
| model_kwargs, | |
| ec_connector_output, | |
| ) = self._preprocess( | |
| scheduler_output, num_tokens_padded, intermediate_tensors | |
| ) | |
| # Set cudagraph mode to none if calc_kv_scales is true. | |
| # KV scales calculation involves dynamic operations that are incompatible | |
| # with CUDA graph capture. | |
| if self.calculate_kv_scales: | |
| cudagraph_mode = CUDAGraphMode.NONE | |
| # Mark KV scales as calculated after the first forward pass | |
| self.calculate_kv_scales = False | |
| # Encoder-decoder models can only compile the pure decode steps where no | |
| # encoder inputs are present. Use eager for the first pass. | |
| num_encoder_reqs = len(scheduler_output.scheduled_encoder_inputs) | |
| has_encoder_input = ( | |
| self.model_config.is_encoder_decoder and num_encoder_reqs > 0 | |
| ) | |
| # Run the model. | |
| # Use persistent buffers for CUDA graphs. | |
| # When spec decode is enabled, defer connector finalization | |
| # (wait_for_save + clear metadata) until after draft model runs. | |
| defer_kv_connector_finalize = self.speculative_config is not None | |
| # Update the EPLB meta. | |
| if self.eplb_state is not None: | |
| self.eplb_state.prepare_forward( | |
| self.model_config, | |
| num_tokens_unpadded, | |
| ubatch_slices_padded, | |
| ) | |
| with ( | |
| set_forward_context( | |
| attn_metadata, | |
| self.vllm_config, | |
| num_tokens=num_tokens_padded, | |
| num_tokens_across_dp=num_tokens_across_dp, | |
| cudagraph_runtime_mode=cudagraph_mode, | |
| batch_descriptor=batch_desc, | |
| ubatch_slices=ubatch_slices_padded, | |
| slot_mapping=slot_mappings, | |
| skip_compiled=has_encoder_input, | |
| ), | |
| record_function_or_nullcontext("gpu_model_runner: forward"), | |
| self.maybe_get_kv_connector_output( | |
| scheduler_output, | |
| defer_finalize=defer_kv_connector_finalize, | |
| ) as kv_connector_output, | |
| ): | |
| model_output = self._model_forward( | |
| input_ids=input_ids, | |
| positions=positions, | |
| intermediate_tensors=intermediate_tensors, | |
| inputs_embeds=inputs_embeds, | |
| **model_kwargs, | |
| ) | |
| with record_function_or_nullcontext("gpu_model_runner: postprocess"): | |
| if self.use_aux_hidden_state_outputs: | |
| # True when EAGLE 3 is used. | |
| hidden_states, aux_hidden_states = model_output | |
| else: | |
| # Common case. | |
| hidden_states = model_output | |
| aux_hidden_states = None | |
| if not self.broadcast_pp_output: | |
| # Common case. | |
| if not get_pp_group().is_last_rank: | |
| # Return the intermediate tensors. | |
| assert isinstance(hidden_states, IntermediateTensors) | |
| self.kv_connector_output = kv_connector_output | |
| return hidden_states | |
| if self.is_pooling_model: | |
| # Return the pooling output. | |
| return self._pool( | |
| hidden_states, | |
| num_scheduled_tokens, | |
| num_scheduled_tokens_np, | |
| kv_connector_output, | |
| ) | |
| sample_hidden_states = hidden_states[logits_indices] | |
| logits = self.model.compute_logits(sample_hidden_states) | |
| else: | |
| # Rare case. | |
| assert not self.is_pooling_model | |
| sample_hidden_states = hidden_states[logits_indices] | |
| if not get_pp_group().is_last_rank: | |
| all_gather_tensors = { | |
| "residual": not is_residual_scattered_for_sp( | |
| self.vllm_config, num_tokens_padded | |
| ) | |
| } | |
| get_pp_group().send_tensor_dict( | |
| hidden_states.tensors, | |
| all_gather_group=get_tp_group(), | |
| all_gather_tensors=all_gather_tensors, | |
| ) | |
| logits = None | |
| else: | |
| logits = self.model.compute_logits(sample_hidden_states) | |
| model_output_broadcast_data: dict[str, Any] = {} | |
| if logits is not None: | |
| model_output_broadcast_data["logits"] = logits.contiguous() | |
| broadcasted = get_pp_group().broadcast_tensor_dict( | |
| model_output_broadcast_data, src=len(get_pp_group().ranks) - 1 | |
| ) | |
| assert broadcasted is not None | |
| logits = broadcasted["logits"] | |
| self.execute_model_state = ExecuteModelState( | |
| scheduler_output, | |
| logits, | |
| spec_decode_metadata, | |
| spec_decode_common_attn_metadata, | |
| hidden_states, | |
| sample_hidden_states, | |
| aux_hidden_states, | |
| ec_connector_output, | |
| cudagraph_stats, | |
| slot_mappings, | |
| ) | |
| self.kv_connector_output = kv_connector_output | |
| # Now the batch has been launched we can wait for corrections from the | |
| # previous model forward without breaking async scheduling. | |
| if deferred_state_corrections_fn: | |
| deferred_state_corrections_fn() | |
| return None | |
| def _input_fits_in_drafter( | |
| self, common_attn_metadata: CommonAttentionMetadata | None | |
| ) -> bool: | |
| if common_attn_metadata is None: | |
| return False | |
| assert self.speculative_config is not None | |
| # DFlash queries one extra token (the bonus token) beyond num_spec_tokens | |
| num_drafter_query_tokens = self.num_spec_tokens + ( | |
| 1 if self.speculative_config.use_dflash() else 0 | |
| ) | |
| return ( | |
| common_attn_metadata.max_seq_len + num_drafter_query_tokens | |
| <= self.effective_drafter_max_model_len | |
| ) | |
| def sample_tokens( | |
| self, grammar_output: "GrammarOutput | None" | |
| ) -> ModelRunnerOutput | AsyncModelRunnerOutput | IntermediateTensors: | |
| if self.execute_model_state is None: | |
| kv_connector_output = self.kv_connector_output | |
| self.kv_connector_output = None | |
| # receive sampled token ids from the last PP rank. | |
| if self.use_async_scheduling and not get_pp_group().is_last_rank: | |
| self._pp_receive_prev_sampled_token_ids_to_input_batch() | |
| # In case of PP with kv transfer, we need to pass through the | |
| # kv_connector_output | |
| return ModelRunnerOutput.with_kv_conn_output_only(kv_connector_output) | |
| # Unpack ephemeral state. | |
| ( | |
| scheduler_output, | |
| logits, | |
| spec_decode_metadata, | |
| spec_decode_common_attn_metadata, | |
| hidden_states, | |
| sample_hidden_states, | |
| aux_hidden_states, | |
| ec_connector_output, | |
| cudagraph_stats, | |
| slot_mappings, | |
| ) = self.execute_model_state | |
| # Clear ephemeral state. | |
| self.execute_model_state = None | |
| # Apply structured output bitmasks if present. | |
| if grammar_output is not None: | |
| apply_grammar_bitmask( | |
| scheduler_output, grammar_output, self.input_batch, logits | |
| ) | |
| with record_function_or_nullcontext("gpu_model_runner: sample"): | |
| sampler_output = self._sample(logits, spec_decode_metadata) | |
| self._update_states_after_model_execute( | |
| sampler_output.sampled_token_ids, scheduler_output | |
| ) | |
| if self.use_async_scheduling: | |
| pp = get_pp_group() | |
| # For torchrun external_launcher PP mode with broadcast_pp_output=True, | |
| # PP outputs have been broadcasted to all ranks at logits computation. | |
| # Therefore, here is no need to send sampled token ids again in this case. | |
| if not self.broadcast_pp_output and pp.world_size > 1 and pp.is_last_rank: | |
| self._pp_broadcast_prev_sampled_token_ids( | |
| sampler_output.sampled_token_ids | |
| ) | |
| self._draft_token_ids = None | |
| self._draft_probs = None | |
| self._draft_prob_req_ids = None | |
| self._draft_token_req_ids = None | |
| self.valid_sampled_token_count_gpu = None | |
| self.input_batch.prev_sampled_token_ids = None | |
| def propose_draft_token_ids(sampled_token_ids): | |
| assert spec_decode_common_attn_metadata is not None | |
| with record_function_or_nullcontext("gpu_model_runner: draft"): | |
| self._draft_token_ids = self.propose_draft_token_ids( | |
| scheduler_output, | |
| sampled_token_ids, | |
| self.input_batch.sampling_metadata, | |
| hidden_states, | |
| sample_hidden_states, | |
| aux_hidden_states, | |
| spec_decode_metadata, | |
| spec_decode_common_attn_metadata, | |
| slot_mappings, | |
| ) | |
| self._copy_draft_token_ids_to_cpu(scheduler_output) | |
| spec_config = self.speculative_config | |
| draft_after_bookkeeping = False | |
| if spec_config is not None: | |
| # Decide whether to run the drafter or zero out draft tokens. | |
| input_fits_in_drafter = self._input_fits_in_drafter( | |
| spec_decode_common_attn_metadata | |
| ) | |
| # Whether the drafter runs a GPU model forward (and thus carries | |
| # TP/EP/DP collectives), independent of padded-batch timing. | |
| drafter_runs_model_forward = ( | |
| spec_config.use_eagle() | |
| or spec_config.uses_draft_model() | |
| or spec_config.uses_extract_hidden_states() | |
| ) | |
| use_gpu_toks = ( | |
| drafter_runs_model_forward | |
| and not spec_config.disable_padded_drafter_batch | |
| ) | |
| if use_gpu_toks: | |
| # EAGLE/DraftModel speculative decoding can use the GPU sampled tokens | |
| # as inputs, and does not need to wait for bookkeeping to finish. | |
| assert isinstance( | |
| self.drafter, | |
| EagleProposer | |
| | DFlashProposer | |
| | DraftModelProposer | |
| | ExtractHiddenStatesProposer | |
| | Gemma4Proposer, | |
| ) | |
| sampled_token_ids = sampler_output.sampled_token_ids | |
| if input_fits_in_drafter: | |
| propose_draft_token_ids(sampled_token_ids) | |
| else: | |
| if self.valid_sampled_token_count_event is not None: | |
| assert spec_decode_common_attn_metadata is not None | |
| next_token_ids, valid_sampled_tokens_count = ( | |
| self.drafter.prepare_next_token_ids_padded( | |
| sampled_token_ids, | |
| self.requests, | |
| self.input_batch, | |
| self.discard_request_mask.gpu, | |
| ) | |
| ) | |
| self._copy_valid_sampled_token_count( | |
| next_token_ids, valid_sampled_tokens_count | |
| ) | |
| if self.parallel_config.data_parallel_size > 1: | |
| # Prevent hang when DP ranks disagree on input_fits_in_drafter | |
| self.drafter.dummy_run(num_tokens=1) | |
| elif ( | |
| spec_config.use_ngram_gpu() | |
| and not spec_config.disable_padded_drafter_batch | |
| ): | |
| assert isinstance(self.drafter, NgramProposerGPU) | |
| sampled_token_ids = sampler_output.sampled_token_ids | |
| if input_fits_in_drafter: | |
| propose_draft_token_ids(sampled_token_ids) | |
| elif self.valid_sampled_token_count_event is not None: | |
| assert spec_decode_common_attn_metadata is not None | |
| next_token_ids, valid_sampled_tokens_count, _ = ( | |
| self.drafter.update_token_ids_ngram( | |
| sampled_token_ids, | |
| self.input_batch, | |
| self.token_ids_gpu_tensor, | |
| self.num_tokens_no_spec_gpu, | |
| self.discard_request_mask.gpu, | |
| ) | |
| ) | |
| self._copy_valid_sampled_token_count( | |
| next_token_ids, valid_sampled_tokens_count | |
| ) | |
| else: | |
| # These drafters consume CPU sampled tokens, so they run | |
| # after bookkeeping. | |
| draft_after_bookkeeping = True | |
| if not input_fits_in_drafter: | |
| # Zero out draft tokens so the scheduler doesn't schedule | |
| # stale drafts from the previous step. | |
| # For Nemotron-H: it is necessary to zero out the draft tokens, | |
| # otherwise the stale tokens will corrupt Mamba recurrent | |
| # state and logprobs for sequences near max_model_len. | |
| self._draft_token_ids = torch.zeros( | |
| 1, device=self.device, dtype=torch.int32 | |
| ).expand(len(self.input_batch.req_ids), self.num_spec_tokens) | |
| self._draft_probs = None | |
| self._draft_prob_req_ids = None | |
| self._copy_draft_token_ids_to_cpu(scheduler_output, zeros_only=True) | |
| with record_function_or_nullcontext("gpu_model_runner: bookkeep"): | |
| ( | |
| num_nans_in_logits, | |
| logprobs_lists, | |
| valid_sampled_token_ids, | |
| prompt_logprobs_dict, | |
| req_ids_output_copy, | |
| req_id_to_index_output_copy, | |
| invalid_req_indices, | |
| ) = self._bookkeeping_sync( | |
| scheduler_output, | |
| sampler_output, | |
| logits, | |
| hidden_states, | |
| scheduler_output.total_num_scheduled_tokens, | |
| ) | |
| if draft_after_bookkeeping: | |
| # ngram and other speculative decoding methods use the sampled | |
| # tokens on the CPU, so they are run after bookkeeping. | |
| if input_fits_in_drafter: | |
| propose_draft_token_ids(valid_sampled_token_ids) | |
| elif ( | |
| drafter_runs_model_forward | |
| and self.parallel_config.data_parallel_size > 1 | |
| ): | |
| # Prevent hang when DP ranks disagree on input_fits_in_drafter | |
| assert isinstance( | |
| self.drafter, | |
| EagleProposer | |
| | DFlashProposer | |
| | DraftModelProposer | |
| | ExtractHiddenStatesProposer | |
| | Gemma4Proposer, | |
| ) | |
| self.drafter.dummy_run(num_tokens=1) | |
| # Finalize KV connector (wait_for_save + clear metadata) after | |
| # draft model runs. Deferred from target model forward to allow | |
| # draft model to also save its KV cache. | |
| if spec_config is not None: | |
| self.finalize_kv_connector() | |
| with record_function_or_nullcontext("gpu_model_runner: eplb"): | |
| self.eplb_step() | |
| # self.kv_connector_output may be modified during drafting | |
| kv_connector_output = self.kv_connector_output | |
| self.kv_connector_output = None | |
| with record_function_or_nullcontext("gpu_model_runner: ModelRunnerOutput"): | |
| output = ModelRunnerOutput( | |
| req_ids=req_ids_output_copy, | |
| req_id_to_index=req_id_to_index_output_copy, | |
| sampled_token_ids=valid_sampled_token_ids, | |
| logprobs=logprobs_lists, | |
| prompt_logprobs_dict=prompt_logprobs_dict, | |
| kv_connector_output=kv_connector_output, | |
| ec_connector_output=ec_connector_output | |
| if self.supports_mm_inputs | |
| else None, | |
| num_nans_in_logits=num_nans_in_logits, | |
| cudagraph_stats=cudagraph_stats, | |
| routed_experts=None, | |
| ) | |
| if not self.use_async_scheduling: | |
| if self.routed_experts_initialized: | |
| # Sync path: D2H was issued in ``_bookkeeping_sync`` and | |
| # synchronized by ``_to_list``'s event.synchronize(), so | |
| # the pinned buffers are ready to be wrapped as numpy. | |
| total = scheduler_output.total_num_scheduled_tokens | |
| output.routed_experts = RoutedExpertsLists( | |
| routing_data=self.routed_experts_cpu[:total].numpy(), | |
| slot_mapping=self.routed_experts_slot_mapping_cpu[:total].numpy(), | |
| ) | |
| return output | |
| with record_function_or_nullcontext( | |
| "gpu_model_runner: AsyncGPUModelRunnerOutput" | |
| ): | |
| # Async path: produce a device-side snapshot that the async | |
| # copy stream can D2H later. Both tensors must be private | |
| # clones because: | |
| # - ``routing_data`` source is the shared capturer buffer, | |
| # which is ``clear_buffer()``-ed at the start of the | |
| # next step on the default stream. | |
| # - ``slot_mapping`` source is our own | |
| # ``routed_experts_slot_mapping_device``, which the | |
| # next ``_prepare_inputs`` overwrites on the default | |
| # stream while the D2H is still pending on the copy | |
| # stream. | |
| # Without clones, the copy stream would read torn data. | |
| routed_experts_snapshot = None | |
| if self.routed_experts_initialized: | |
| buf = self.routed_experts_capturer.get_device_buffer() | |
| total = scheduler_output.total_num_scheduled_tokens | |
| routed_experts_snapshot = RoutedExpertsTensors( | |
| routing_data=buf[:total].clone(), | |
| slot_mapping=self.routed_experts_slot_mapping_device[ | |
| :total | |
| ].clone(), | |
| ) | |
| async_output = AsyncGPUModelRunnerOutput( | |
| model_runner_output=output, | |
| sampled_token_ids=sampler_output.sampled_token_ids, | |
| logprobs_tensors=sampler_output.logprobs_tensors, | |
| invalid_req_indices=invalid_req_indices, | |
| async_output_copy_stream=self._get_or_create_async_output_copy_stream(), | |
| vocab_size=self.input_batch.vocab_size, | |
| routed_experts=routed_experts_snapshot, | |
| check_ep_fault=self.check_ep_fault, | |
| ) | |
| with record_function_or_nullcontext( | |
| "gpu_model_runner: set_async_sampled_token_ids" | |
| ): | |
| # Save ref of sampled_token_ids CPU tensor if the batch contains | |
| # any requests with sampling params that require output ids. | |
| self.input_batch.set_async_sampled_token_ids( | |
| async_output.sampled_token_ids_cpu, | |
| async_output.async_copy_ready_event, | |
| ) | |
| return async_output | |
| def _pp_broadcast_prev_sampled_token_ids( | |
| self, sampled_token_ids: torch.Tensor | |
| ) -> None: | |
| """Broadcast sampled token ids (GPU) from last PP stage""" | |
| pp = get_pp_group() | |
| assert pp.is_last_rank | |
| # `prev_sampled_token_ids` is expected to have shape [num_reqs, 1]. | |
| assert sampled_token_ids.dim() == 2 and sampled_token_ids.shape[-1] == 1, ( | |
| "PP+async expects sampled_token_ids to have shape [num_reqs, 1]" | |
| ) | |
| # Skip for chunked prefill: sampled tokens are dummy | |
| # and will be discarded, no need to broadcast. | |
| if not self._is_all_reqs_chunked_prefill(): | |
| torch.distributed.broadcast( | |
| sampled_token_ids, src=pp.rank, group=pp.device_group | |
| ) | |
| def _pp_receive_prev_sampled_token_ids_to_input_batch(self) -> None: | |
| """Receive sampled token ids broadcast from last PP stage""" | |
| pp = get_pp_group() | |
| assert not pp.is_last_rank | |
| num_reqs = self.input_batch.num_reqs | |
| # `prev_sampled_token_ids` is expected to have shape [num_reqs, 1]. | |
| recv = torch.empty((num_reqs, 1), dtype=torch.int32, device=self.device) | |
| # skip for chunked prefill. | |
| if not self._is_all_reqs_chunked_prefill(): | |
| torch.distributed.broadcast(recv, src=pp.last_rank, group=pp.device_group) | |
| self.input_batch.prev_sampled_token_ids = recv | |
| # construct `prev_req_id_to_index` here so `_prepare_input_ids` | |
| # can map req_id -> previous batch row | |
| discard_req_indices = np.nonzero(self.discard_request_mask.np[:num_reqs])[0] | |
| discard_req_indices_set = set(discard_req_indices) | |
| prev_req_id_to_index: dict[str, int] = {} | |
| for i, req_id in enumerate(self.input_batch.req_ids): | |
| if i in discard_req_indices_set: | |
| continue | |
| prev_req_id_to_index[req_id] = i | |
| # PP+async scheduling: advance per-request local cached output length by | |
| # appending a placeholder (-1) token id. | |
| if (req_state := self.requests.get(req_id)) is not None: | |
| req_state.output_token_ids.append(-1) | |
| pos = self.input_batch.num_tokens_no_spec[i] | |
| self.input_batch.is_token_ids[i, pos] = True | |
| self.input_batch.num_tokens_no_spec[i] = pos + 1 | |
| self.input_batch.prev_req_id_to_index = prev_req_id_to_index | |
| def take_draft_token_ids(self) -> DraftTokenIds | None: | |
| if not self.num_spec_tokens or not self._draft_token_req_ids: | |
| return None | |
| draft_token_ids, req_ids = self._get_draft_token_ids_cpu() | |
| return DraftTokenIds(req_ids, draft_token_ids) | |
| def _copy_draft_token_ids_to_cpu( | |
| self, scheduler_output: "SchedulerOutput", zeros_only: bool = False | |
| ) -> None: | |
| if torch.is_tensor(self._draft_token_ids): | |
| assert isinstance(self._draft_token_ids, torch.Tensor) | |
| self.prev_num_spec_tokens = self._draft_token_ids.shape[1] | |
| # Check if we need to copy draft tokens to CPU. In async scheduling, | |
| # we only copy when needed for structured output, penalties or bad_words. | |
| if self.use_async_scheduling and not ( | |
| scheduler_output.has_structured_output_requests | |
| or self.input_batch.sampling_metadata.output_token_ids | |
| ): | |
| return | |
| # We must also set the corresponding request ids. | |
| self._draft_token_req_ids = self.input_batch.req_ids.copy() | |
| draft_token_ids: torch.Tensor = self._draft_token_ids | |
| if not torch.is_tensor(draft_token_ids): | |
| return | |
| assert self.draft_token_ids_event is not None | |
| assert self.draft_token_ids_copy_stream is not None | |
| assert self.draft_token_ids_cpu is not None | |
| default_stream = torch.cuda.current_stream() | |
| num_reqs = draft_token_ids.shape[0] | |
| num_spec_tokens = draft_token_ids.shape[1] | |
| with torch.cuda.stream(self.draft_token_ids_copy_stream): | |
| if not zeros_only: | |
| # Trigger async copy of draft token ids to cpu. | |
| self.draft_token_ids_copy_stream.wait_stream(default_stream) | |
| self.draft_token_ids_cpu[:num_reqs, :num_spec_tokens].copy_( | |
| draft_token_ids, non_blocking=True | |
| ) | |
| else: | |
| # No copy needed, just zero-out cpu tensor. | |
| self.draft_token_ids_cpu[:num_reqs, :num_spec_tokens] = 0 | |
| self.draft_token_ids_event.record() | |
| def _get_draft_token_ids_cpu(self) -> tuple[list[list[int]], list[str]]: | |
| if isinstance(self._draft_token_ids, list): | |
| return self._draft_token_ids, self.input_batch.req_ids | |
| req_ids = self._draft_token_req_ids | |
| if req_ids is None: | |
| return [], [] | |
| assert self.draft_token_ids_event is not None | |
| assert self.draft_token_ids_cpu is not None | |
| self.draft_token_ids_event.synchronize() | |
| assert isinstance(self._draft_token_ids, torch.Tensor) | |
| num_spec_tokens = self._draft_token_ids.shape[1] | |
| return self.draft_token_ids_cpu[ | |
| : len(req_ids), :num_spec_tokens | |
| ].tolist(), req_ids | |
| def _copy_valid_sampled_token_count( | |
| self, next_token_ids: torch.Tensor, valid_sampled_tokens_count: torch.Tensor | |
| ) -> None: | |
| if self.valid_sampled_token_count_event is None: | |
| return | |
| default_stream = torch.cuda.current_stream() | |
| # Initialize a new stream to overlap the copy operation with | |
| # prepare_input of draft model. | |
| with torch.cuda.stream(self.valid_sampled_token_count_copy_stream): | |
| self.valid_sampled_token_count_copy_stream.wait_stream(default_stream) # type: ignore | |
| counts = valid_sampled_tokens_count | |
| counts_cpu = self.valid_sampled_token_count_cpu | |
| assert counts_cpu is not None | |
| counts_cpu[: counts.shape[0]].copy_(counts, non_blocking=True) | |
| self.valid_sampled_token_count_event.record() | |
| if self.use_async_spec_decode: | |
| # Stash for GPU-side correction in _prepare_inputs. | |
| self.valid_sampled_token_count_gpu = valid_sampled_tokens_count | |
| self.input_batch.prev_sampled_token_ids = next_token_ids.unsqueeze(1) | |
| def _get_valid_sampled_token_count(self) -> list[int]: | |
| # Wait until valid_sampled_tokens_count is copied to cpu, | |
| prev_sampled_token_ids = self.input_batch.prev_sampled_token_ids | |
| sampled_count_event = self.valid_sampled_token_count_event | |
| if sampled_count_event is None or prev_sampled_token_ids is None: | |
| return [] | |
| counts_cpu = self.valid_sampled_token_count_cpu | |
| assert counts_cpu is not None | |
| sampled_count_event.synchronize() | |
| return counts_cpu[: prev_sampled_token_ids.shape[0]].tolist() | |
| def _get_spec_decode_draft_probs( | |
| self, spec_decode_metadata: SpecDecodeMetadata | |
| ) -> torch.Tensor | None: | |
| if self._draft_probs is None or self._draft_prob_req_ids is None: | |
| return None | |
| row_by_req_id = { | |
| req_id: idx for idx, req_id in enumerate(self._draft_prob_req_ids) | |
| } | |
| draft_probs_rows: list[torch.Tensor] = [] | |
| for req_id, num_draft in zip( | |
| self.input_batch.req_ids, spec_decode_metadata.num_draft_tokens | |
| ): | |
| if num_draft == 0: | |
| continue | |
| row_idx = row_by_req_id.get(req_id) | |
| if row_idx is None: | |
| logger.warning( | |
| "Missing cached draft probabilities for request %s; " | |
| "falling back to legacy speculative rejection behavior.", | |
| req_id, | |
| ) | |
| return None | |
| draft_probs_rows.append(self._draft_probs[row_idx, :num_draft]) | |
| if not draft_probs_rows: | |
| return None | |
| return torch.cat(draft_probs_rows, dim=0).contiguous() | |
| def propose_draft_token_ids( | |
| self, | |
| scheduler_output: "SchedulerOutput", | |
| sampled_token_ids: torch.Tensor | list[list[int]], | |
| sampling_metadata: SamplingMetadata, | |
| hidden_states: torch.Tensor, | |
| sample_hidden_states: torch.Tensor, | |
| aux_hidden_states: list[torch.Tensor] | None, | |
| spec_decode_metadata: SpecDecodeMetadata | None, | |
| common_attn_metadata: CommonAttentionMetadata, | |
| slot_mappings: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None, | |
| ) -> list[list[int]] | torch.Tensor: | |
| num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens | |
| spec_config = self.speculative_config | |
| assert spec_config is not None | |
| num_spec_tokens_to_schedule = scheduler_output.num_spec_tokens_to_schedule | |
| self._draft_probs = None | |
| self._draft_prob_req_ids = None | |
| if spec_config.method == "ngram": | |
| from vllm.v1.spec_decode.ngram_proposer import NgramProposer | |
| assert isinstance(sampled_token_ids, list) | |
| assert isinstance(self.drafter, NgramProposer) | |
| draft_token_ids = self.drafter.propose( | |
| num_spec_tokens_to_schedule, | |
| sampled_token_ids, | |
| self.input_batch.num_tokens_no_spec, | |
| self.input_batch.token_ids_cpu, | |
| slot_mappings=slot_mappings, | |
| ) | |
| elif spec_config.method == "custom_class": | |
| assert isinstance(sampled_token_ids, list) | |
| draft_token_ids = cast(Any, self.drafter).propose( | |
| sampled_token_ids, | |
| self.input_batch.num_tokens_no_spec, | |
| self.input_batch.token_ids_cpu, | |
| slot_mappings=slot_mappings, | |
| ) | |
| elif spec_config.use_ngram_gpu(): | |
| assert isinstance(self.drafter, NgramProposerGPU) | |
| ( | |
| next_token_ids, | |
| valid_sampled_tokens_count, | |
| valid_sampled_token_ids_gpu, | |
| ) = self.drafter.update_token_ids_ngram( | |
| sampled_token_ids, | |
| self.input_batch, | |
| self.token_ids_gpu_tensor, | |
| self.num_tokens_no_spec_gpu, | |
| self.discard_request_mask.gpu, | |
| ) | |
| self._copy_valid_sampled_token_count( | |
| next_token_ids, valid_sampled_tokens_count | |
| ) | |
| batch_size = next_token_ids.shape[0] | |
| draft_token_ids, num_valid_draft_tokens = self.drafter.propose( | |
| num_spec_tokens_to_schedule, | |
| self.num_tokens_no_spec_gpu[:batch_size], | |
| self.token_ids_gpu_tensor[:batch_size], | |
| valid_sampled_token_ids_gpu, | |
| valid_sampled_tokens_count, | |
| ) | |
| # Cache valid draft counts for scheduler-side trimming. | |
| self._num_valid_draft_tokens = num_valid_draft_tokens | |
| # Async D2H copy on a dedicated stream. | |
| copy_num_valid_draft_tokens( | |
| self._num_valid_draft_tokens_cpu, | |
| self._num_valid_draft_tokens_copy_stream, | |
| self._num_valid_draft_tokens_event, | |
| self._num_valid_draft_tokens, | |
| self.input_batch.num_reqs, | |
| ) | |
| elif spec_config.method == "suffix": | |
| assert isinstance(sampled_token_ids, list) | |
| assert isinstance(self.drafter, SuffixDecodingProposer) | |
| draft_token_ids = self.drafter.propose( | |
| num_spec_tokens_to_schedule, | |
| self.input_batch, | |
| sampled_token_ids, | |
| slot_mappings=slot_mappings, | |
| ) | |
| elif spec_config.method == "medusa": | |
| assert isinstance(sampled_token_ids, list) | |
| assert isinstance(self.drafter, MedusaProposer) | |
| if sample_hidden_states.shape[0] == len(sampled_token_ids): | |
| # The input to the target model does not include draft tokens. | |
| hidden_states = sample_hidden_states | |
| else: | |
| indices = [] | |
| offset = 0 | |
| assert spec_decode_metadata is not None, ( | |
| "No spec decode metadata for medusa" | |
| ) | |
| for num_draft, tokens in zip( | |
| spec_decode_metadata.num_draft_tokens, sampled_token_ids | |
| ): | |
| indices.append(offset + len(tokens) - 1) | |
| offset += num_draft + 1 | |
| indices = async_tensor_h2d(indices, device=self.device) | |
| hidden_states = sample_hidden_states[indices] | |
| draft_token_ids = self.drafter.propose( | |
| num_speculative_tokens=num_spec_tokens_to_schedule, | |
| target_hidden_states=hidden_states, | |
| sampling_metadata=sampling_metadata, | |
| slot_mappings=slot_mappings, | |
| ) | |
| elif spec_config.uses_extract_hidden_states(): | |
| assert isinstance(self.drafter, ExtractHiddenStatesProposer) | |
| assert isinstance(sampled_token_ids, torch.Tensor), ( | |
| "sampled_token_ids should be a torch.Tensor for " | |
| "extract_hidden_states method." | |
| ) | |
| if not self.use_aux_hidden_state_outputs or aux_hidden_states is None: | |
| raise ValueError( | |
| "aux_hidden_states are required when using `extract_hidden_states`" | |
| ) | |
| target_hidden_states = [h[:num_scheduled_tokens] for h in aux_hidden_states] | |
| draft_token_ids = self.drafter.propose( | |
| num_speculative_tokens=num_spec_tokens_to_schedule, | |
| sampled_token_ids=sampled_token_ids, | |
| target_hidden_states=target_hidden_states, | |
| common_attn_metadata=common_attn_metadata, | |
| slot_mappings=slot_mappings, | |
| ) | |
| next_token_ids, valid_sampled_tokens_count = ( | |
| self.drafter.prepare_next_token_ids_padded( | |
| sampled_token_ids, | |
| self.requests, | |
| self.input_batch, | |
| self.discard_request_mask.gpu, | |
| ) | |
| ) | |
| self._copy_valid_sampled_token_count( | |
| next_token_ids, valid_sampled_tokens_count | |
| ) | |
| elif ( | |
| spec_config.use_eagle() | |
| or spec_config.use_dflash() | |
| or spec_config.uses_draft_model() | |
| ): | |
| assert isinstance( | |
| self.drafter, | |
| EagleProposer | DFlashProposer | DraftModelProposer | Gemma4Proposer, | |
| ) | |
| if spec_config.disable_padded_drafter_batch: | |
| # When padded-batch is disabled, the sampled_token_ids should be | |
| # the cpu-side list[list[int]] of valid sampled tokens for each | |
| # request, with invalid requests having empty lists. | |
| assert isinstance(sampled_token_ids, list), ( | |
| "sampled_token_ids should be a python list when" | |
| "padded-batch is disabled." | |
| ) | |
| next_token_ids = self.drafter.prepare_next_token_ids_cpu( | |
| sampled_token_ids, | |
| self.requests, | |
| self.input_batch, | |
| scheduler_output.num_scheduled_tokens, | |
| ) | |
| else: | |
| # When using padded-batch, the sampled_token_ids should be | |
| # the gpu tensor of sampled tokens for each request, of shape | |
| # (num_reqs, num_spec_tokens + 1) with rejected tokens having | |
| # value -1. | |
| assert isinstance(sampled_token_ids, torch.Tensor), ( | |
| "sampled_token_ids should be a torch.Tensor when" | |
| "padded-batch is enabled." | |
| ) | |
| next_token_ids, valid_sampled_tokens_count = ( | |
| self.drafter.prepare_next_token_ids_padded( | |
| sampled_token_ids, | |
| self.requests, | |
| self.input_batch, | |
| self.discard_request_mask.gpu, | |
| ) | |
| ) | |
| self._copy_valid_sampled_token_count( | |
| next_token_ids, valid_sampled_tokens_count | |
| ) | |
| # Let the target override the hidden state fed to the drafter | |
| # (e.g. DeepSeek V4 MTP needs the pre-hc_head residual). Safe to | |
| # rebind here: hidden_states was already consumed for sampling | |
| # above and is not used again in this branch. | |
| alt = getattr( | |
| self.get_model(), "get_mtp_target_hidden_states", lambda: None | |
| )() | |
| if alt is not None: | |
| hidden_states = alt | |
| num_rejected_tokens_gpu = None | |
| if spec_decode_metadata is None: | |
| token_indices_to_sample = None | |
| # input_ids can be None for multimodal models. | |
| target_token_ids = self.input_ids.gpu[:num_scheduled_tokens] | |
| target_positions = self._get_positions(num_scheduled_tokens) | |
| if self.use_aux_hidden_state_outputs: | |
| assert aux_hidden_states is not None | |
| target_hidden_states = torch.cat( | |
| [h[:num_scheduled_tokens] for h in aux_hidden_states], dim=-1 | |
| ) | |
| else: | |
| target_hidden_states = hidden_states[:num_scheduled_tokens] | |
| else: | |
| if spec_config.disable_padded_drafter_batch: | |
| token_indices_to_sample = None | |
| common_attn_metadata, token_indices = self.drafter.prepare_inputs( | |
| common_attn_metadata, | |
| sampled_token_ids, | |
| spec_decode_metadata.num_draft_tokens, | |
| ) | |
| target_token_ids = self.input_ids.gpu[token_indices] | |
| target_positions = self._get_positions(token_indices) | |
| if self.use_aux_hidden_state_outputs: | |
| assert aux_hidden_states is not None | |
| target_hidden_states = torch.cat( | |
| [h[token_indices] for h in aux_hidden_states], dim=-1 | |
| ) | |
| else: | |
| target_hidden_states = hidden_states[token_indices] | |
| else: | |
| ( | |
| common_attn_metadata, | |
| token_indices_to_sample, | |
| num_rejected_tokens_gpu, | |
| ) = self.drafter.prepare_inputs_padded( | |
| common_attn_metadata, | |
| spec_decode_metadata, | |
| valid_sampled_tokens_count, | |
| ) | |
| total_num_tokens = common_attn_metadata.num_actual_tokens | |
| # When padding the batch, token_indices is just a range | |
| target_token_ids = self.input_ids.gpu[:total_num_tokens] | |
| target_positions = self._get_positions(total_num_tokens) | |
| if self.use_aux_hidden_state_outputs: | |
| assert aux_hidden_states is not None | |
| target_hidden_states = torch.cat( | |
| [h[:total_num_tokens] for h in aux_hidden_states], dim=-1 | |
| ) | |
| else: | |
| target_hidden_states = hidden_states[:total_num_tokens] | |
| if self.supports_mm_inputs and self.drafter.supports_mm_inputs: | |
| if os.environ.get("VLLM_SPEC_DRAFT_BLIND_MM", ""): | |
| # hetero-hidden-dim draft cannot consume target mm embeds; | |
| # fall back to an image-blind draft (text embeds for all ids) | |
| mm_embed_inputs = None | |
| else: | |
| mm_embed_inputs = self._gather_mm_embeddings( | |
| scheduler_output, | |
| shift_computed_tokens=(1 if self.drafter.pass_hidden_states_to_model else 0), | |
| ) | |
| else: | |
| mm_embed_inputs = None | |
| draft_token_ids = self.drafter.propose( | |
| num_speculative_tokens=num_spec_tokens_to_schedule, | |
| target_token_ids=target_token_ids, | |
| target_positions=target_positions, | |
| target_hidden_states=target_hidden_states, | |
| next_token_ids=next_token_ids, | |
| token_indices_to_sample=token_indices_to_sample, | |
| sampling_metadata=sampling_metadata, | |
| common_attn_metadata=common_attn_metadata, | |
| mm_embed_inputs=mm_embed_inputs, | |
| num_rejected_tokens_gpu=num_rejected_tokens_gpu, | |
| slot_mappings=slot_mappings, | |
| ) | |
| if hasattr(self.drafter, "take_last_draft_probs"): | |
| draft_probs = self.drafter.take_last_draft_probs() | |
| if draft_probs is not None: | |
| self._draft_probs = draft_probs | |
| self._draft_prob_req_ids = self.input_batch.req_ids.copy() | |
| return draft_token_ids | |
| def update_config(self, overrides: dict[str, Any]) -> None: | |
| allowed_config_names = {"load_config", "model_config"} | |
| for config_name, config_overrides in overrides.items(): | |
| if config_name not in allowed_config_names: | |
| allowed = ", ".join(sorted(allowed_config_names)) | |
| raise ValueError( | |
| f"Config override '{config_name}' is not supported. " | |
| f"Supported configs: {allowed}" | |
| ) | |
| config = getattr(self, config_name) | |
| new_config = update_config(config, config_overrides) | |
| setattr(self, config_name, new_config) | |
| def load_model(self, load_dummy_weights: bool = False) -> None: | |
| """ | |
| Args: | |
| load_dummy_weights: load dummy weights instead of real weights. | |
| """ | |
| logger.info_once( | |
| "Starting to load model %s...", | |
| self.model_config.model, | |
| scope="global", | |
| ) | |
| if self.parallel_config.enable_eplb: | |
| self.eplb_state = EplbState(self.parallel_config, self.device) | |
| eplb_models = 0 | |
| try: | |
| with DeviceMemoryProfiler() as m: | |
| time_before_load = time.perf_counter() | |
| if load_dummy_weights: | |
| self.load_config.load_format = "dummy" | |
| model_loader = get_model_loader(self.load_config) | |
| self.model = model_loader.load_model( | |
| vllm_config=self.vllm_config, model_config=self.model_config | |
| ) | |
| if self.lora_config: | |
| self.model = self.load_lora_model( | |
| self.model, self.vllm_config, self.device | |
| ) | |
| if hasattr(self, "drafter"): | |
| logger.info_once("Loading drafter model...") | |
| if hasattr(self.drafter, "load_model"): | |
| self.drafter.load_model(self.model) | |
| if ( | |
| hasattr(self.drafter, "model") | |
| and is_mixture_of_experts(self.drafter.model) | |
| and self.parallel_config.enable_eplb | |
| ): | |
| assert not self.parallel_config.enable_elastic_ep, ( | |
| "Elastic EP is not supported with drafter model." | |
| ) | |
| spec_config = self.vllm_config.speculative_config | |
| assert spec_config is not None | |
| assert spec_config.draft_model_config is not None | |
| logger.info_once( | |
| "EPLB is enabled for drafter model %s.", | |
| spec_config.draft_model_config.model, | |
| ) | |
| if self.eplb_state is None: | |
| self.eplb_state = EplbState( | |
| self.parallel_config, self.device | |
| ) | |
| self.eplb_state.add_model( | |
| self.drafter.model, | |
| spec_config.draft_model_config, | |
| ) | |
| assert hasattr(self.drafter, "set_eplb_state") | |
| self.drafter.set_eplb_state(self.eplb_state) | |
| eplb_models += 1 | |
| self._setup_eagle3_aux_hidden_state_outputs() | |
| # Resolve the MoE model, unwrapping VLM wrappers if needed. | |
| # VLM models (e.g. KimiK25ForConditionalGeneration) wrap the | |
| # actual MoE language model but don't implement | |
| # MixtureOfExperts themselves. | |
| moe_candidate = self.model | |
| if not is_mixture_of_experts(moe_candidate) and isinstance( | |
| moe_candidate, SupportsMultiModal | |
| ): | |
| moe_candidate = moe_candidate.get_language_model() | |
| if is_mixture_of_experts(moe_candidate): | |
| self._moe_model = moe_candidate | |
| if ( | |
| self._moe_model is not None | |
| and self.parallel_config.enable_eplb | |
| and not load_dummy_weights | |
| ): | |
| logger.info_once( | |
| "EPLB is enabled for model %s.", | |
| self.model_config.model, | |
| ) | |
| assert self.eplb_state is not None | |
| self.eplb_state.add_model( | |
| self._moe_model, | |
| self.model_config, | |
| ) | |
| eplb_models += 1 | |
| time_after_load = time.perf_counter() | |
| self.model_memory_usage = m.consumed_memory | |
| except torch.cuda.OutOfMemoryError as e: | |
| msg = ( | |
| "Failed to load model - not enough GPU memory. " | |
| "Try lowering --gpu-memory-utilization to free memory for weights, " | |
| "increasing --tensor-parallel-size, or using --quantization. " | |
| "See https://docs.vllm.ai/en/latest/configuration/conserving_memory/ " | |
| "for more tips." | |
| ) | |
| combined_msg = f"{msg} (original error: {e})" | |
| logger.error(combined_msg) | |
| raise e | |
| logger.info_once( | |
| "Model loading took %s GiB memory and %.6f seconds", | |
| format_gib(self.model_memory_usage), | |
| time_after_load - time_before_load, | |
| ) | |
| if not load_dummy_weights: | |
| prepare_communication_buffer_for_model(self.model) | |
| if (drafter := getattr(self, "drafter", None)) and ( | |
| drafter_model := getattr(drafter, "model", None) | |
| ): | |
| prepare_communication_buffer_for_model(drafter_model) | |
| mm_config = self.model_config.multimodal_config | |
| self.is_multimodal_pruning_enabled = ( | |
| supports_multimodal_pruning(self.get_model()) | |
| and mm_config is not None | |
| and mm_config.is_multimodal_pruning_enabled() | |
| ) | |
| self.requires_sequential_video_encoding = hasattr( | |
| self.get_model(), "requires_sequential_video_encoding" | |
| ) # Temporary hack for dynamic res video w/o support for bs>1 yet | |
| if ( | |
| self._moe_model is not None | |
| and self.parallel_config.enable_eplb | |
| and not load_dummy_weights | |
| and self.eplb_state is not None | |
| and self.eplb_state.is_async | |
| ): | |
| self.eplb_state.start_async_loop() | |
| if ( | |
| self.vllm_config.compilation_config.mode | |
| == CompilationMode.STOCK_TORCH_COMPILE | |
| ): | |
| from vllm.env_override import _apply_constrain_to_fx_strides_patch | |
| _apply_constrain_to_fx_strides_patch() | |
| backend = self.vllm_config.compilation_config.init_backend(self.vllm_config) | |
| compilation_counter.stock_torch_compile_count += 1 | |
| self.model.compile(fullgraph=True, backend=backend) | |
| return | |
| # for other compilation modes, cudagraph behavior is controlled by | |
| # CudagraphWrapper and CudagraphDispatcher of vllm. | |
| # wrap the model with full cudagraph wrapper if needed. | |
| cudagraph_mode = self.compilation_config.cudagraph_mode | |
| assert cudagraph_mode is not None | |
| if ( | |
| is_breakable_cudagraph_enabled() | |
| and cudagraph_mode != CUDAGraphMode.NONE | |
| and not self.parallel_config.use_ubatching | |
| ): | |
| self.model = BreakableCUDAGraphWrapper(self.model, self.vllm_config) | |
| drafter = getattr(self, "drafter", None) | |
| if drafter is not None and hasattr(drafter, "model"): | |
| drafter.model = BreakableCUDAGraphWrapper( | |
| drafter.model, self.vllm_config | |
| ) | |
| elif ( | |
| cudagraph_mode.has_full_cudagraphs() | |
| and not self.parallel_config.use_ubatching | |
| ): | |
| self.model = CUDAGraphWrapper( | |
| self.model, self.vllm_config, runtime_mode=CUDAGraphMode.FULL | |
| ) | |
| elif self.parallel_config.use_ubatching: | |
| if cudagraph_mode.has_full_cudagraphs(): | |
| self.model = UBatchWrapper( | |
| self.model, self.vllm_config, CUDAGraphMode.FULL, self.device | |
| ) | |
| else: | |
| self.model = UBatchWrapper( | |
| self.model, self.vllm_config, CUDAGraphMode.NONE, self.device | |
| ) | |
| get_offloader().post_init() | |
| def _setup_eagle3_aux_hidden_state_outputs(self) -> None: | |
| if not self.use_aux_hidden_state_outputs: | |
| return | |
| if not supports_eagle3(self.get_model()): | |
| raise RuntimeError( | |
| "Model does not support EAGLE3 interface but " | |
| "aux_hidden_state_outputs was requested" | |
| ) | |
| # Try to get auxiliary layers from speculative config, | |
| # otherwise use model's default layers | |
| aux_layers = self._get_eagle3_aux_layers_from_config() | |
| if aux_layers: | |
| logger.info( | |
| "Using auxiliary layers from speculative config: %s", aux_layers | |
| ) | |
| else: | |
| aux_layers = self.model.get_eagle3_default_aux_hidden_state_layers() | |
| self.model.set_aux_hidden_state_layers(aux_layers) | |
| def _get_eagle3_aux_layers_from_config(self) -> tuple[int, ...] | None: | |
| """Extract Eagle3 auxiliary layer indices from speculative config. | |
| These indices specify which hidden states from the base model should | |
| be used as auxiliary inputs for the Eagle3 drafter model during | |
| speculative decoding. | |
| Returns: | |
| Tuple of layer indices if found in draft model config, | |
| None otherwise. | |
| """ | |
| if not (self.speculative_config and self.speculative_config.draft_model_config): | |
| return None | |
| hf_config = self.speculative_config.draft_model_config.hf_config | |
| layer_ids = getattr(hf_config, "eagle_aux_hidden_state_layer_ids", None) | |
| if not layer_ids: | |
| dflash_config = getattr(hf_config, "dflash_config", None) | |
| eagle_config = getattr(hf_config, "eagle_config", None) | |
| if dflash_config and isinstance(dflash_config, dict): | |
| # Add 1 to convert DFlash's aux layer id semantics | |
| layer_ids = [ | |
| i + 1 for i in (dflash_config.get("target_layer_ids") or []) | |
| ] | |
| if eagle_config and isinstance(eagle_config, dict): | |
| layer_ids = eagle_config.get("eagle_aux_hidden_state_layer_ids") | |
| if layer_ids and isinstance(layer_ids, (list, tuple)): | |
| return tuple(layer_ids) | |
| return None | |
| def reload_weights( | |
| self, | |
| weights_iterator: Iterable[tuple[str, torch.Tensor]] | None = None, | |
| weights_path: str | None = None, | |
| is_checkpoint_format: bool = True, | |
| ) -> None: | |
| """ | |
| Reload weights from a weights iterator or from disk | |
| Args: | |
| weights_iterator: weights to load into model | |
| weights_path: path to load weights from if weights_iterator is not | |
| provided. Use path of original model if neither is provided. | |
| is_checkpoint_format: set to False if weights have already been | |
| processed into kernel format (repacking, renaming, etc.) | |
| """ | |
| # TODO(@kylesayrs): generalize to all runners and loaders | |
| # argument validation | |
| if weights_iterator is None and not is_checkpoint_format: | |
| logger.warning( | |
| "Reloading from disk means that weights will be in checkpoint format. " | |
| "Please use `is_checkpoint_format=True` " | |
| "to avoid weight reloading errors" | |
| ) | |
| model = self.get_model() | |
| weights_to_load = {name for name, _ in model.named_parameters()} | |
| counter_before_reloading = time.perf_counter() | |
| # load weights from disk if none are provided | |
| if weights_iterator is None: | |
| model_loader = get_model_loader(self.load_config) | |
| if not hasattr(model_loader, "get_all_weights"): | |
| raise NotImplementedError( | |
| f"Model reloading with `{self.load_config.load_format}` format" | |
| ) | |
| if weights_path is not None: | |
| self.model_config.model = weights_path | |
| weights_iterator = model_loader.get_all_weights(self.model_config, model) | |
| weights_iterator = cast( | |
| Iterable[tuple[str, torch.Tensor]], weights_iterator | |
| ) | |
| # begin loading weights | |
| logger.info_once("Reloading weights inplace...") | |
| if is_checkpoint_format: | |
| # load weights from checkpoint/ original model format | |
| initialize_layerwise_reload(model) | |
| loaded_weights = model.load_weights(weights_iterator) | |
| finalize_layerwise_reload(model, self.model_config) | |
| else: | |
| # load weights from kernel format | |
| logger.warning_once( | |
| "Reloading with `is_checkpoint_format=True` requires that " | |
| "weights be in kernel format and already sharded", | |
| ) | |
| loaded_weights = set() | |
| for name, loaded_weight in weights_iterator: | |
| param = model.get_parameter(name) # TODO: buffers? | |
| param.copy_(loaded_weight) | |
| loaded_weights.add(name) | |
| # logging and validation | |
| counter_after_reloading = time.perf_counter() | |
| diff_seconds = counter_after_reloading - counter_before_reloading | |
| logger.info_once( | |
| "Reloading and processing weights took %.2f seconds", | |
| diff_seconds, | |
| ) | |
| if self.model_config.quantization is None and loaded_weights is not None: | |
| weights_not_loaded = weights_to_load - loaded_weights | |
| if weights_not_loaded: | |
| logger.warning( | |
| "Following weights were not loaded from checkpoint: %s", | |
| weights_not_loaded, | |
| ) | |
| self.reset_encoder_cache() | |
| self.reset_mm_cache() | |
| def _get_prompt_logprobs_dict( | |
| self, | |
| hidden_states: torch.Tensor, | |
| num_scheduled_tokens: dict[str, int], | |
| ) -> dict[str, LogprobsTensors | None]: | |
| num_prompt_logprobs_dict = self.num_prompt_logprobs | |
| if not num_prompt_logprobs_dict: | |
| return {} | |
| prompt_logprobs_dict: dict[str, LogprobsTensors | None] = {} | |
| # Since prompt logprobs are a rare feature, prioritize simple, | |
| # maintainable loop over optimal performance. | |
| completed_prefill_reqs = [] | |
| for req_id, num_prompt_logprobs in num_prompt_logprobs_dict.items(): | |
| num_tokens = num_scheduled_tokens.get(req_id) | |
| if num_tokens is None: | |
| # This can happen if the request was preempted in prefill stage. | |
| continue | |
| # Get metadata for this request. | |
| request = self.requests[req_id] | |
| if request.prompt_token_ids is None: | |
| # Prompt logprobs is incompatible with prompt embeddings | |
| continue | |
| num_prompt_tokens = len(request.prompt_token_ids) | |
| prompt_token_ids = async_tensor_h2d( | |
| request.prompt_token_ids, device=self.device | |
| ) | |
| # Set up target LogprobsTensors object. | |
| logprobs_tensors = request.in_progress_prompt_logprobs_cpu | |
| if logprobs_tensors is None: | |
| # Create empty logprobs CPU tensors for the entire prompt. | |
| # If chunked, we'll copy in slice by slice. | |
| logprobs_tensors = LogprobsTensors.empty_cpu( | |
| num_prompt_tokens - 1, num_prompt_logprobs + 1 | |
| ) | |
| request.in_progress_prompt_logprobs_cpu = logprobs_tensors | |
| # Determine number of logits to retrieve. | |
| start_idx = request.num_computed_tokens | |
| start_tok = start_idx + 1 | |
| num_remaining_tokens = num_prompt_tokens - start_tok | |
| if num_tokens <= num_remaining_tokens: | |
| # This is a chunk, more tokens remain. | |
| # In the == case, there are no more prompt logprobs to produce | |
| # but we want to defer returning them to the next step where we | |
| # have new generated tokens to return. | |
| num_logits = num_tokens | |
| else: | |
| # This is the last chunk of prompt tokens to return. | |
| num_logits = num_remaining_tokens | |
| completed_prefill_reqs.append(req_id) | |
| prompt_logprobs_dict[req_id] = logprobs_tensors | |
| if num_logits <= 0: | |
| # This can happen for the final chunk if we prefilled exactly | |
| # (num_prompt_tokens - 1) tokens for this request in the prior | |
| # step. There are no more prompt logprobs to produce. | |
| continue | |
| # Get the logits corresponding to this req's prompt tokens. | |
| # If this is a partial request (i.e. chunked prefill), | |
| # then there is prompt logprob generated for each index. | |
| req_idx = self.input_batch.req_id_to_index[req_id] | |
| offset = self.query_start_loc.np[req_idx].item() | |
| prompt_hidden_states = hidden_states[offset : offset + num_logits] | |
| logits = self.model.compute_logits(prompt_hidden_states) | |
| # Get the "target" tokens for each index. For prompt at index i, | |
| # the token at prompt index i+1 is the "sampled" token we want | |
| # to gather the logprob for. | |
| tgt_token_ids = prompt_token_ids[start_tok : start_tok + num_logits] | |
| # Compute prompt scores respecting logprobs_mode. | |
| # NOTE: prompt tokens skip sampling processors, so | |
| # processed_* and raw_* yield the same scores here. | |
| if self.model_config.logprobs_mode in ("raw_logits", "processed_logits"): | |
| scores = logits.to(torch.float32) | |
| else: | |
| scores = self.sampler.compute_logprobs(logits) | |
| token_ids, logprobs, ranks, _ = self.sampler.gather_logprobs( | |
| scores, num_prompt_logprobs, tgt_token_ids | |
| ) | |
| # Transfer GPU->CPU async. | |
| chunk_slice = slice(start_idx, start_idx + num_logits) | |
| logprobs_tensors.logprob_token_ids[chunk_slice].copy_( | |
| token_ids, non_blocking=True | |
| ) | |
| logprobs_tensors.logprobs[chunk_slice].copy_(logprobs, non_blocking=True) | |
| logprobs_tensors.selected_token_ranks[chunk_slice].copy_( | |
| ranks, non_blocking=True | |
| ) | |
| # Remove requests that have completed prefill from the batch | |
| # num_prompt_logprobs_dict. | |
| for req_id in completed_prefill_reqs: | |
| del num_prompt_logprobs_dict[req_id] | |
| self.requests[req_id].in_progress_prompt_logprobs_cpu = None | |
| # Must synchronize the non-blocking GPU->CPU transfers. | |
| if prompt_logprobs_dict: | |
| self._sync_device() | |
| return prompt_logprobs_dict | |
| def _get_nans_in_logits( | |
| self, | |
| logits: torch.Tensor | None, | |
| ) -> dict[str, int]: | |
| try: | |
| if logits is None: | |
| return {req_id: 0 for req_id in self.input_batch.req_ids} | |
| num_nans_in_logits = {} | |
| num_nans_for_index = logits.isnan().sum(dim=-1).cpu().numpy() | |
| for req_id in self.input_batch.req_ids: | |
| req_index = self.input_batch.req_id_to_index[req_id] | |
| num_nans_in_logits[req_id] = ( | |
| int(num_nans_for_index[req_index]) | |
| if num_nans_for_index is not None and req_index < logits.shape[0] | |
| else 0 | |
| ) | |
| return num_nans_in_logits | |
| except IndexError: | |
| return {} | |
| def maybe_randomize_inputs( | |
| self, | |
| input_ids: torch.Tensor | None, | |
| inputs_embeds: torch.Tensor | None, | |
| randomize_inputs: bool = False, | |
| ): | |
| """ | |
| Randomize input_ids if VLLM_RANDOMIZE_DP_DUMMY_INPUTS is set. | |
| This is to help balance expert-selection | |
| - during profile_run | |
| - during DP rank dummy run | |
| """ | |
| dp_size = self.vllm_config.parallel_config.data_parallel_size | |
| randomize_inputs = randomize_inputs or ( | |
| envs.VLLM_RANDOMIZE_DP_DUMMY_INPUTS and dp_size > 1 | |
| ) | |
| if not randomize_inputs: | |
| yield | |
| elif input_ids is not None: | |
| def rand_input_ids() -> torch.Tensor: | |
| return torch.randint_like( | |
| self.input_ids.gpu, | |
| low=0, | |
| high=self.model_config.get_vocab_size(), | |
| ) | |
| logger.debug_once("Randomizing dummy input_ids for DP Rank") | |
| input_ids.copy_(rand_input_ids()[: input_ids.size(0)], non_blocking=True) | |
| yield | |
| input_ids.fill_(0) | |
| else: | |
| def rand_inputs_embeds() -> torch.Tensor: | |
| return torch.randn_like( | |
| self.inputs_embeds.gpu, | |
| ) | |
| assert inputs_embeds is not None | |
| logger.debug_once("Randomizing dummy inputs_embeds for DP Rank") | |
| inputs_embeds.copy_( | |
| rand_inputs_embeds()[: inputs_embeds.size(0)], non_blocking=True | |
| ) | |
| yield | |
| inputs_embeds.fill_(0) | |
| def _get_mm_dummy_batch( | |
| self, | |
| modality: str, | |
| max_items_per_batch: int, | |
| ) -> BatchedTensorInputs: | |
| """Dummy data for profiling and precompiling multimodal models.""" | |
| assert self.mm_budget is not None | |
| # Don't use `max_items_per_batch` here to avoid redundant computation | |
| dummy_mm_inputs = self.mm_registry.get_dummy_mm_inputs( | |
| self.model_config, | |
| mm_counts={modality: 1}, | |
| cache=self.mm_budget.cache, | |
| ) | |
| dummy_mm_item = dummy_mm_inputs["mm_kwargs"][modality][0] | |
| # We use the cache so that the item is saved to the cache, | |
| # but not read from the cache | |
| assert dummy_mm_item is not None, "Item should not already be cached" | |
| return next( | |
| mm_kwargs_batch | |
| for _, _, mm_kwargs_batch in group_and_batch_mm_kwargs( | |
| [(modality, dummy_mm_item)] * max_items_per_batch, | |
| device=self.device, | |
| pin_memory=PIN_MEMORY, | |
| ) | |
| ) | |
| def _dummy_run( | |
| self, | |
| num_tokens: int, | |
| cudagraph_runtime_mode: CUDAGraphMode | None = None, | |
| force_attention: bool = False, | |
| uniform_decode: bool = False, | |
| allow_microbatching: bool = True, | |
| skip_eplb: bool = False, | |
| is_profile: bool = False, | |
| create_mixed_batch: bool = False, | |
| remove_lora: bool = True, | |
| is_graph_capturing: bool = False, | |
| num_active_loras: int = 0, | |
| profile_seq_lens: int | None = None, | |
| randomize_inputs: bool = False, | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """ | |
| Run a dummy forward pass to warm up/profile run or capture the | |
| CUDA graph for the model. | |
| Args: | |
| num_tokens: Number of tokens to run the dummy forward pass. | |
| cudagraph_runtime_mode: used to control the behavior. | |
| - if not set will determine the cudagraph mode based on using | |
| the self.cudagraph_dispatcher. | |
| - CUDAGraphMode.NONE: No cudagraph, for warm up and profile run | |
| - CUDAGraphMode.PIECEWISE: Piecewise cudagraph. | |
| - CUDAGraphMode.FULL: Full cudagraph, attention metadata is | |
| needed. | |
| force_attention: If True, always create attention metadata. Used to | |
| warm up attention backend when mode is NONE. | |
| uniform_decode: If True, the batch is a uniform decode batch. | |
| skip_eplb: If True, skip EPLB state update. | |
| is_profile: If True, this is a profile run. | |
| create_mixed_batch: If True, create a mixed batch with both decode | |
| (1 token) and prefill (multiple tokens) requests. | |
| remove_lora: If False, dummy LoRAs are not destroyed after the run | |
| num_active_loras: Number of distinct active LoRAs to capture for. | |
| LoRA is activated when num_active_loras > 0. | |
| profile_seq_lens: If provided, use this value for seq_lens instead | |
| of max_query_len. Used to profile attention workspace that | |
| scales with context length. | |
| """ | |
| mm_config = self.vllm_config.model_config.multimodal_config | |
| if mm_config and mm_config.mm_encoder_only: | |
| # The current dummy run only covers LM execution, so we can skip it. | |
| # mm encoder dummy run may need to add in the future. | |
| return torch.tensor([]), torch.tensor([]) | |
| assert ( | |
| cudagraph_runtime_mode is None | |
| or cudagraph_runtime_mode.is_valid_runtime_mode() | |
| ) | |
| # If cudagraph_mode.decode_mode() == FULL and | |
| # cudagraph_mode.separate_routine(). This means that we are using | |
| # different graphs and/or modes for mixed prefill-decode batches vs. | |
| # uniform decode batches. A uniform decode batch means that all | |
| # requests have identical query length, except a potential virtual | |
| # request (shorter) in the batch account for padding. | |
| # Uniform decode batch could either be common pure decode, where | |
| # max_query_len == 1, or speculative decode, where | |
| # max_query_len == 1 + num_spec_decode_tokens. | |
| # When setting max_query_len = 1, we switch to and capture the optimized | |
| # routine of FA2 for pure decode, i.e., Flashdecode + an optimization | |
| # for GQA/MQA. | |
| max_query_len = self.uniform_decode_query_len if uniform_decode else num_tokens | |
| # Set num_scheduled_tokens based on num_tokens and max_num_seqs | |
| # for dummy run with LoRA so that the num_reqs collectively | |
| # has num_tokens in total. | |
| assert num_tokens <= self.max_num_tokens | |
| max_num_reqs = self.scheduler_config.max_num_seqs | |
| if create_mixed_batch: | |
| assert not uniform_decode | |
| # Create mixed batch: | |
| # first half decode tokens, second half one prefill | |
| num_decode_tokens = min(max_num_reqs - 1, num_tokens // 2) | |
| num_prefill_tokens = num_tokens - num_decode_tokens | |
| num_reqs = num_decode_tokens + 1 | |
| # Create decode requests (1 token each) followed by prefill request | |
| num_scheduled_tokens_list = [1] * num_decode_tokens + [num_prefill_tokens] | |
| # Note: Overriding max_query_len to be the prefill tokens | |
| max_query_len = num_prefill_tokens | |
| elif uniform_decode: | |
| assert not create_mixed_batch | |
| num_reqs = min(max_num_reqs, cdiv(num_tokens, max_query_len)) | |
| num_scheduled_tokens_list = [max_query_len] * num_reqs | |
| if num_tokens % max_query_len != 0: | |
| num_scheduled_tokens_list[-1] = num_tokens % max_query_len | |
| else: | |
| num_reqs = min(num_tokens, max_num_reqs) | |
| min_tokens_per_req = num_tokens // num_reqs | |
| num_scheduled_tokens_list = [min_tokens_per_req] * num_reqs | |
| num_scheduled_tokens_list[-1] += num_tokens % num_reqs | |
| assert sum(num_scheduled_tokens_list) == num_tokens | |
| assert len(num_scheduled_tokens_list) == num_reqs | |
| num_scheduled_tokens = np.array(num_scheduled_tokens_list, dtype=np.int32) | |
| num_tokens_unpadded = int(num_scheduled_tokens.sum()) | |
| num_sampled_tokens = np.ones(num_reqs, dtype=np.int32) | |
| _cudagraph_mode, batch_desc, should_ubatch, num_tokens_across_dp, _ = ( | |
| self._determine_batch_execution_and_padding( | |
| num_tokens=num_tokens_unpadded, | |
| num_reqs=num_reqs, | |
| num_scheduled_tokens_np=num_scheduled_tokens, | |
| max_num_scheduled_tokens=max_query_len, | |
| use_cascade_attn=False, | |
| allow_microbatching=allow_microbatching, | |
| force_eager=is_profile | |
| or (cudagraph_runtime_mode == CUDAGraphMode.NONE), | |
| # `force_uniform_decode` is used for cudagraph capture; because for | |
| # capturing mixed prefill-decode batches, we sometimes use | |
| # num_tokens == num_reqs which looks like a uniform decode batch to the | |
| # dispatcher; but we actually want to capture a piecewise cudagraph | |
| force_uniform_decode=uniform_decode, | |
| # `force_has_lora` is used for cudagraph capture; because LoRA is | |
| # activated later in the context manager, but we need to know the | |
| # LoRA state when determining the batch descriptor for capture | |
| force_has_lora=num_active_loras > 0, | |
| # `force_num_active_loras` is used for cudagraph capture; because we | |
| # need to capture graphs for specific num_active_loras counts | |
| force_num_active_loras=num_active_loras, | |
| ) | |
| ) | |
| if cudagraph_runtime_mode is None: | |
| cudagraph_runtime_mode = _cudagraph_mode | |
| else: | |
| assert cudagraph_runtime_mode == _cudagraph_mode, ( | |
| f"Cudagraph runtime mode mismatch in dummy_run. " | |
| f"Expected {_cudagraph_mode}, but got {cudagraph_runtime_mode}." | |
| ) | |
| num_tokens_padded = batch_desc.num_tokens | |
| num_reqs_padded = ( | |
| batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs | |
| ) | |
| dcp_dummy_context_len = get_dcp_dummy_context_len( | |
| self.dcp_world_size, | |
| self.parallel_config.cp_kv_cache_interleave_size, | |
| hasattr(self, "kv_cache_config"), | |
| create_mixed_batch, | |
| is_graph_capturing, | |
| uniform_decode, | |
| ) | |
| ubatch_slices, ubatch_slices_padded = maybe_create_ubatch_slices( | |
| should_ubatch, | |
| num_scheduled_tokens, | |
| num_tokens_padded, | |
| num_reqs_padded, | |
| self.vllm_config.parallel_config.num_ubatches, | |
| ) | |
| logger.debug( | |
| "ubatch_slices: %s, ubatch_slices_padded: %s", | |
| ubatch_slices, | |
| ubatch_slices_padded, | |
| ) | |
| attn_metadata: PerLayerAttnMetadata | None = None | |
| slot_mappings_by_group, slot_mappings = self._get_slot_mappings( | |
| num_tokens_padded=num_tokens_padded, | |
| num_reqs_padded=num_reqs_padded, | |
| num_tokens_unpadded=num_tokens_unpadded, | |
| ubatch_slices=ubatch_slices_padded, | |
| ) | |
| # Dummy runs have no real slot assignments — fill with -1 so | |
| # concat_and_cache kernels skip the KV write. | |
| if slot_mappings_by_group is not None: | |
| for sm in slot_mappings_by_group.values(): | |
| sm.fill_(-1) | |
| # _dummy_run shares pinned CPU buffers (seq_lens, query_start_loc, | |
| # etc.) with execute_model. It must participate in the same event | |
| # protocol so that back-to-back dummy/real steps don't overwrite | |
| # pinned memory while a prior non_blocking H2D DMA is still reading. | |
| with self.synchronize_input_prep(): | |
| # If force_attention is True, we always capture attention. | |
| # Otherwise, it only happens for cudagraph_runtime_mode=FULL. | |
| if force_attention or cudagraph_runtime_mode == CUDAGraphMode.FULL: | |
| if profile_seq_lens is not None: | |
| seq_lens = profile_seq_lens # type: ignore[assignment] | |
| elif create_mixed_batch: | |
| # In the mixed batch mode (used for FI warmup), we use | |
| # shorter sequence lengths to run faster. | |
| # TODO(luka) better system for describing dummy batches | |
| if dcp_dummy_context_len > 0: | |
| seq_lens = torch.tensor( # type: ignore[assignment] | |
| [1 + dcp_dummy_context_len] * num_decode_tokens | |
| + [num_prefill_tokens + dcp_dummy_context_len], | |
| dtype=torch.int, | |
| ) | |
| else: | |
| seq_lens = torch.tensor( # type: ignore[assignment] | |
| [1] * num_decode_tokens + [num_prefill_tokens + 1], | |
| dtype=torch.int, | |
| ) | |
| elif dcp_dummy_context_len > 0: | |
| seq_lens = max_query_len + dcp_dummy_context_len # type: ignore[assignment] | |
| else: | |
| seq_lens = max_query_len # type: ignore[assignment] | |
| self.optimistic_seq_lens_cpu[:num_reqs] = seq_lens | |
| self.optimistic_seq_lens_cpu[num_reqs:].fill_(0) | |
| self.seq_lens.copy_(self.optimistic_seq_lens_cpu, non_blocking=True) | |
| cum_num_tokens = self._get_cumsum_and_arange( | |
| num_scheduled_tokens, self.query_pos.np | |
| ) | |
| self.query_start_loc.np[1 : num_reqs + 1] = cum_num_tokens | |
| self.query_start_loc.np[num_reqs + 1 : num_reqs_padded + 1].fill( | |
| cum_num_tokens[-1] | |
| ) | |
| self.query_start_loc.copy_to_gpu() | |
| prepare_dcp_dummy_context_metadata( | |
| input_batch=self.input_batch, | |
| kv_cache_config=getattr(self, "kv_cache_config", None), | |
| query_pos=self.query_pos, | |
| positions=self.positions, | |
| query_start_loc=self.query_start_loc, | |
| num_reqs=num_reqs, | |
| num_tokens_unpadded=num_tokens_unpadded, | |
| dcp_dummy_context_len=dcp_dummy_context_len, | |
| ) | |
| # Sync block table CPU->GPU so cleared rows from | |
| # remove_request() are visible to the attention metadata | |
| # builder. Without this, stale block IDs from finished | |
| # requests can corrupt Mamba state. | |
| self.input_batch.block_table.commit_block_table(num_reqs_padded) | |
| pad_attn = cudagraph_runtime_mode == CUDAGraphMode.FULL | |
| attn_metadata, _ = self._build_attention_metadata( | |
| num_tokens=num_tokens_unpadded, | |
| num_tokens_padded=num_tokens_padded if pad_attn else None, | |
| num_reqs=num_reqs_padded, | |
| max_query_len=max_query_len, | |
| ubatch_slices=(ubatch_slices_padded if pad_attn else ubatch_slices), | |
| # FULL replay reads capture-time metadata buffers. Re-stage them | |
| # from the zeroed dummy block tables instead of retaining state | |
| # indices from the previous real batch. | |
| for_cudagraph_capture=( | |
| is_graph_capturing | |
| or cudagraph_runtime_mode == CUDAGraphMode.FULL | |
| ), | |
| slot_mappings=slot_mappings_by_group, | |
| use_spec_decode=self.speculative_config is not None, | |
| ) | |
| with self.maybe_dummy_run_with_lora( | |
| self.lora_config, | |
| num_scheduled_tokens, | |
| num_sampled_tokens, | |
| remove_lora, | |
| num_active_loras, | |
| ): | |
| # Make sure padding doesn't exceed max_num_tokens | |
| assert num_tokens_padded <= self.max_num_tokens | |
| model_kwargs = self._init_model_kwargs() | |
| if self.supports_mm_inputs and not self.model_config.is_encoder_decoder: | |
| input_ids, inputs_embeds = self._prepare_mm_inputs(num_tokens_padded) | |
| model_kwargs = { | |
| **model_kwargs, | |
| **self._dummy_mm_kwargs(num_reqs), | |
| } | |
| elif self.enable_prompt_embeds: | |
| input_ids = None | |
| inputs_embeds = self.inputs_embeds.gpu[:num_tokens_padded] | |
| model_kwargs = self._init_model_kwargs() | |
| else: | |
| input_ids = self.input_ids.gpu[:num_tokens_padded] | |
| inputs_embeds = None | |
| if self.uses_mrope: | |
| positions = self.mrope_positions.gpu[:, :num_tokens_padded] | |
| elif self.uses_xdrope_dim > 0: | |
| positions = self.xdrope_positions.gpu[:, :num_tokens_padded] | |
| else: | |
| positions = self.positions[:num_tokens_padded] | |
| if get_pp_group().is_first_rank: | |
| intermediate_tensors = None | |
| else: | |
| if self.intermediate_tensors is None: | |
| self.intermediate_tensors = ( | |
| self.model.make_empty_intermediate_tensors( | |
| batch_size=self.max_num_tokens, | |
| dtype=self.model_config.dtype, | |
| device=self.device, | |
| ) | |
| ) | |
| intermediate_tensors = self.sync_and_gather_intermediate_tensors( | |
| num_tokens_padded, None, False | |
| ) | |
| if ubatch_slices_padded is not None: | |
| # Adjust values to reflect a single ubatch. | |
| # TODO(sage,lucas): this is cruft that should be addressed in | |
| # the padding refactor. | |
| num_tokens_padded = ubatch_slices_padded[0].num_tokens | |
| if num_tokens_across_dp is not None: | |
| num_tokens_across_dp[:] = num_tokens_padded | |
| with ( | |
| self.maybe_randomize_inputs( | |
| input_ids, inputs_embeds, randomize_inputs=randomize_inputs | |
| ), | |
| set_forward_context( | |
| attn_metadata, | |
| self.vllm_config, | |
| num_tokens=num_tokens_padded, | |
| num_tokens_across_dp=num_tokens_across_dp, | |
| cudagraph_runtime_mode=cudagraph_runtime_mode, | |
| batch_descriptor=batch_desc, | |
| ubatch_slices=ubatch_slices_padded, | |
| slot_mapping=slot_mappings, | |
| ), | |
| ): | |
| outputs = self.model( | |
| input_ids=input_ids, | |
| positions=positions, | |
| intermediate_tensors=intermediate_tensors, | |
| inputs_embeds=inputs_embeds, | |
| **model_kwargs, | |
| ) | |
| if self.use_aux_hidden_state_outputs: | |
| hidden_states, _ = outputs | |
| else: | |
| hidden_states = outputs | |
| if self.speculative_config and ( | |
| self.speculative_config.use_eagle() | |
| or self.speculative_config.uses_draft_model() | |
| or self.speculative_config.uses_extract_hidden_states() | |
| ): | |
| assert isinstance( | |
| self.drafter, | |
| EagleProposer | |
| | DFlashProposer | |
| | DraftModelProposer | |
| | ExtractHiddenStatesProposer | |
| | Gemma4Proposer, | |
| ) | |
| assert self.speculative_config is not None | |
| # Eagle currently only supports PIECEWISE cudagraphs. | |
| # Therefore only use cudagraphs if the main model uses PIECEWISE | |
| # NOTE(lucas): this is a hack, need to clean up. | |
| use_cudagraphs = ( | |
| ( | |
| is_graph_capturing | |
| and cudagraph_runtime_mode == CUDAGraphMode.PIECEWISE | |
| ) | |
| or ( | |
| not is_graph_capturing | |
| and cudagraph_runtime_mode != CUDAGraphMode.NONE | |
| ) | |
| ) and not self.speculative_config.enforce_eager | |
| # Note(gnovack) - We need to disable cudagraphs for one of the two | |
| # lora cases when cudagraph_specialize_lora is enabled. This is a | |
| # short term mitigation for issue mentioned in | |
| # https://github.com/vllm-project/vllm/issues/28334 | |
| if ( | |
| self.compilation_config.cudagraph_specialize_lora | |
| and num_active_loras > 0 | |
| ): | |
| use_cudagraphs = False | |
| self.drafter.dummy_run( | |
| num_tokens, | |
| use_cudagraphs=use_cudagraphs, | |
| is_graph_capturing=is_graph_capturing, | |
| slot_mappings=slot_mappings, | |
| ) | |
| # We register layerwise NVTX hooks here after the first dynamo tracing is | |
| # done to avoid nvtx operations in hook functions being traced by | |
| # torch dynamo and causing graph breaks. | |
| # Note that for DYNAMO_ONCE and VLLM_COMPILE mode, | |
| # compiled model's dynamo tracing is only done once and the compiled model's | |
| # __call__ function is replaced by calling the compiled function. | |
| # So it's safe to register hooks here. Hooks will be registered to | |
| # both compiled and uncompiled models but they will never | |
| # be called on the compiled model execution path. | |
| self._register_layerwise_nvtx_hooks() | |
| # This is necessary to avoid blocking DP. | |
| # For dummy runs, we typically skip EPLB since we don't have any real | |
| # requests to process. | |
| # However, in DP settings, there may be cases when some DP ranks do | |
| # not have any requests to process, so they're executing dummy batches. | |
| # In such cases, we still have to trigger EPLB to make sure | |
| # ranks execute the rearrangement in synchronization. | |
| if not skip_eplb: | |
| self.eplb_step(is_dummy=True, is_profile=is_profile) | |
| logit_indices = np.cumsum(num_scheduled_tokens) - 1 | |
| logit_indices_device = torch.from_numpy(logit_indices).to( | |
| self.device, non_blocking=True | |
| ) | |
| return hidden_states, hidden_states[logit_indices_device] | |
| def _dummy_sampler_run( | |
| self, | |
| hidden_states: torch.Tensor, | |
| ) -> torch.Tensor: | |
| # The dummy hidden states may contain special values, | |
| # like `inf` or `nan`. | |
| # To avoid breaking the sampler, we use a random tensor here instead. | |
| mm_config = self.vllm_config.model_config.multimodal_config | |
| if mm_config and mm_config.mm_encoder_only: | |
| # MM Encoder only model no need to run sampler. | |
| return torch.tensor([]) | |
| hidden_states = torch.rand_like(hidden_states) | |
| logits = self.model.compute_logits(hidden_states) | |
| num_reqs = logits.size(0) | |
| dummy_tensors = lambda v: torch.full((num_reqs,), v, device=self.device) | |
| dummy_metadata = SamplingMetadata( | |
| temperature=dummy_tensors(0.5), | |
| all_greedy=False, | |
| all_random=False, | |
| top_p=dummy_tensors(0.9), | |
| top_k=dummy_tensors(logits.size(1) - 1), | |
| generators={}, | |
| max_num_logprobs=None, | |
| logprob_token_ids=None, | |
| no_penalties=True, | |
| prompt_token_ids=None, | |
| frequency_penalties=dummy_tensors(0.1), | |
| presence_penalties=dummy_tensors(0.1), | |
| repetition_penalties=dummy_tensors(0.1), | |
| output_token_ids=[[] for _ in range(num_reqs)], | |
| spec_token_ids=[[] for _ in range(num_reqs)], | |
| allowed_token_ids_mask=None, | |
| bad_words_token_ids={}, | |
| logitsprocs=LogitsProcessors(), | |
| ) | |
| try: | |
| sampler_output = self.sampler( | |
| logits=logits, sampling_metadata=dummy_metadata | |
| ) | |
| # Also warm forward_native (taken when generators dict is non-empty), | |
| # but skip the extra call in 'processed_logits' / 'processed_logprobs' | |
| # modes — there TopKTopPSampler binds forward = forward_native at | |
| # init time, so the warmup call is redundant and only inflates peak | |
| # memory during profile_run. | |
| # No .clone() of logits: warmup output is discarded, so any in-place | |
| # mutation by forward_native does not affect correctness. | |
| if self.sampler.logprobs_mode not in PROCESSED_LOGPROBS_MODES: | |
| self.sampler( | |
| logits=logits, | |
| sampling_metadata=replace( | |
| dummy_metadata, | |
| generators={ | |
| 0: torch.Generator(device=self.device).manual_seed(0) | |
| }, | |
| ), | |
| ) | |
| except RuntimeError as e: | |
| if "out of memory" in str(e): | |
| raise RuntimeError( | |
| "CUDA out of memory occurred when warming up sampler with " | |
| f"{num_reqs} dummy requests. Please try lowering " | |
| "`max_num_seqs` or `gpu_memory_utilization` when " | |
| "initializing the engine." | |
| ) from e | |
| else: | |
| raise e | |
| if self.speculative_config: | |
| draft_token_ids = [[0] for _ in range(num_reqs)] | |
| dummy_spec_decode_metadata = SpecDecodeMetadata.make_dummy( | |
| draft_token_ids, self.device | |
| ) | |
| num_tokens = sum(len(ids) for ids in draft_token_ids) | |
| draft_probs = None | |
| if ( | |
| self.speculative_config.rejection_sample_method == "standard" | |
| and self.speculative_config.draft_sample_method == "probabilistic" | |
| ): | |
| draft_probs = torch.rand( | |
| num_tokens, | |
| logits.shape[-1], | |
| device=self.device, | |
| dtype=torch.float32, | |
| ) | |
| draft_probs = torch.softmax(draft_probs, dim=-1) | |
| logits = torch.randn( | |
| num_tokens + num_reqs, | |
| logits.shape[-1], | |
| device=self.device, | |
| dtype=logits.dtype, | |
| ) | |
| self.rejection_sampler( | |
| dummy_spec_decode_metadata, | |
| draft_probs, | |
| logits, | |
| dummy_metadata, | |
| ) | |
| return sampler_output | |
| def _dummy_pooler_run_task( | |
| self, | |
| hidden_states: torch.Tensor, | |
| task: PoolingTask, | |
| ) -> PoolerOutput: | |
| num_tokens = hidden_states.shape[0] | |
| max_num_reqs = self.scheduler_config.max_num_seqs | |
| num_reqs = min(num_tokens, max_num_reqs) | |
| min_tokens_per_req = num_tokens // num_reqs | |
| num_scheduled_tokens_np = np.full(num_reqs, min_tokens_per_req) | |
| num_scheduled_tokens_np[-1] += num_tokens % num_reqs | |
| assert np.sum(num_scheduled_tokens_np) == num_tokens | |
| assert len(num_scheduled_tokens_np) == num_reqs | |
| req_num_tokens = num_tokens // num_reqs | |
| dummy_prompt_lens = torch.from_numpy(num_scheduled_tokens_np) | |
| dummy_token_ids = torch.zeros( | |
| (num_reqs, req_num_tokens), dtype=torch.int32, device=self.device | |
| ) | |
| model = cast(VllmModelForPooling, self.get_model()) | |
| dummy_pooling_params = PoolingParams(task=task) | |
| dummy_pooling_params.verify(self.model_config) | |
| to_update = model.pooler.get_pooling_updates(task) | |
| to_update.apply(dummy_pooling_params) | |
| dummy_metadata = PoolingMetadata( | |
| prompt_lens=dummy_prompt_lens, | |
| prompt_token_ids=dummy_token_ids, | |
| prompt_token_ids_cpu=dummy_token_ids.cpu(), | |
| pooling_params=[dummy_pooling_params] * num_reqs, | |
| pooling_states=[PoolingStates() for i in range(num_reqs)], | |
| ) | |
| dummy_metadata.build_pooling_cursor( | |
| num_scheduled_tokens_np, | |
| seq_lens_cpu=dummy_prompt_lens, | |
| device=hidden_states.device, | |
| ) | |
| try: | |
| return model.pooler( | |
| hidden_states=hidden_states, pooling_metadata=dummy_metadata | |
| ) | |
| except RuntimeError as e: | |
| if "out of memory" in str(e): | |
| raise RuntimeError( | |
| "CUDA out of memory occurred when warming up pooler " | |
| f"({task=}) with {num_reqs} dummy requests. Please try " | |
| "lowering `max_num_seqs` or `gpu_memory_utilization` when " | |
| "initializing the engine." | |
| ) from e | |
| else: | |
| raise e | |
| def _dummy_pooler_run( | |
| self, | |
| hidden_states: torch.Tensor, | |
| ) -> PoolerOutput: | |
| mm_config = self.vllm_config.model_config.multimodal_config | |
| if mm_config and mm_config.mm_encoder_only: | |
| # MM Encoder only model not need to run pooler. | |
| return torch.tensor([]) | |
| # Find the task that has the largest output for subsequent steps | |
| supported_pooling_tasks = self.get_supported_pooling_tasks() | |
| if not supported_pooling_tasks: | |
| raise RuntimeError( | |
| f"Model {self.model_config.model} does not support " | |
| "any pooling tasks. See " | |
| "https://docs.vllm.ai/en/latest/models/pooling_models.html " | |
| "to learn more." | |
| ) | |
| output_size = dict[PoolingTask, float]() | |
| for task in supported_pooling_tasks: | |
| # Run a full batch with each task to ensure none of them OOMs | |
| output = self._dummy_pooler_run_task(hidden_states, task) | |
| output_size[task] = sum(o.nbytes for o in output if o is not None) | |
| del output # Allow GC | |
| max_task = max(output_size.items(), key=lambda x: x[1])[0] | |
| return self._dummy_pooler_run_task(hidden_states, max_task) | |
| def profile_run(self) -> None: | |
| # Profile with multimodal encoder & encoder cache. | |
| if self.supports_mm_inputs: | |
| mm_config = self.model_config.multimodal_config | |
| if mm_config is not None and mm_config.skip_mm_profiling: | |
| logger.info( | |
| "Skipping memory profiling for multimodal encoder and " | |
| "encoder cache." | |
| ) | |
| else: | |
| mm_budget = self.mm_budget | |
| assert mm_budget is not None | |
| if (encoder_budget := mm_budget.get_encoder_budget()) > 0: | |
| if not mm_budget.mm_max_toks_per_item: | |
| # All modality limits are 0 — embedding-only mode. | |
| # Budget is non-zero for embedding storage, but | |
| # there's no encoder to profile. | |
| logger.info( | |
| "Skipping encoder profiling for embedding-only " | |
| "mode (all modality limits=0 with " | |
| "enable_mm_embeds=True).", | |
| ) | |
| else: | |
| # NOTE: Currently model is profiled with a single | |
| # non-text modality with the max possible input | |
| # tokens even when it supports multiple. | |
| dummy_modality = mm_budget.get_modality_with_max_tokens() | |
| max_mm_items_per_batch = mm_budget.mm_max_items_per_batch[ | |
| dummy_modality | |
| ] | |
| logger.info_once( | |
| "Encoder cache will be initialized with a " | |
| "budget of %s tokens, and profiled with " | |
| "%s %s items of the maximum feature size.", | |
| encoder_budget, | |
| max_mm_items_per_batch, | |
| dummy_modality, | |
| ) | |
| # Create dummy batch of multimodal inputs. | |
| batched_dummy_mm_inputs = self._get_mm_dummy_batch( | |
| dummy_modality, | |
| max_mm_items_per_batch, | |
| ) | |
| # Run multimodal encoder. | |
| dummy_encoder_outputs = self.model.embed_multimodal( | |
| **batched_dummy_mm_inputs | |
| ) | |
| sanity_check_mm_encoder_outputs( | |
| dummy_encoder_outputs, | |
| expected_num_items=max_mm_items_per_batch, | |
| ) | |
| for i, output in enumerate(dummy_encoder_outputs): | |
| self.encoder_cache[f"tmp_{i}"] = output | |
| # Add `is_profile` here to pre-allocate communication buffers | |
| hidden_states, last_hidden_states = self._dummy_run( | |
| self.max_num_tokens, is_profile=True | |
| ) | |
| if get_pp_group().is_last_rank: | |
| if self.is_pooling_model: | |
| output = self._dummy_pooler_run(hidden_states) | |
| else: | |
| output = self._dummy_sampler_run(last_hidden_states) | |
| else: | |
| output = None | |
| self._sync_device() | |
| del hidden_states, output | |
| self.encoder_cache.clear() | |
| gc.collect() | |
| def _init_minimal_kv_cache_for_profiling(self) -> None: | |
| from vllm.v1.core.kv_cache_utils import ( | |
| get_kv_cache_config_from_groups, | |
| get_kv_cache_groups, | |
| ) | |
| kv_cache_spec = self.get_kv_cache_spec() | |
| KVCacheSpecRegistry.check_kv_cache_spec_registry(kv_cache_spec) | |
| kv_cache_groups = get_kv_cache_groups(self.vllm_config, kv_cache_spec) | |
| # the minimum number of blocks required is 1 block *per sequence* | |
| min_blocks = ( | |
| min(self.max_num_reqs, self.compilation_config.max_cudagraph_capture_size) | |
| or 1 | |
| ) | |
| # Temporarily change num_gpu_blocks_override to allocate a minimal KV cache | |
| saved_override = self.cache_config.num_gpu_blocks_override | |
| self.cache_config.num_gpu_blocks_override = min_blocks | |
| minimal_config = get_kv_cache_config_from_groups( | |
| self.vllm_config, kv_cache_groups, available_memory=0 | |
| ) | |
| self.cache_config.num_gpu_blocks_override = saved_override | |
| self.initialize_kv_cache(minimal_config, is_profiling=True) | |
| self.cache_config.num_gpu_blocks = minimal_config.num_blocks | |
| logger.debug("Initialized minimal KV cache for CUDA graph profiling") | |
| def _freeze_gc(): | |
| gc.collect() | |
| should_freeze = not envs.VLLM_ENABLE_CUDAGRAPH_GC | |
| if should_freeze: | |
| gc.freeze() | |
| try: | |
| yield | |
| finally: | |
| if should_freeze: | |
| gc.unfreeze() | |
| gc.collect() | |
| def shutdown(self) -> None: | |
| """Release GPU tensors (model weights, KV caches, workspace) so that | |
| memory is reclaimable when running in the same process.""" | |
| from vllm.model_executor.layers.rotary_embedding import _ROPE_DICT | |
| from vllm.v1.worker.workspace import reset_workspace_manager | |
| # Calls torch.accelerator.synchronize() | |
| self._cleanup_profiling_kv_cache() | |
| if current_platform.is_rocm(): | |
| # Drop captured graphs before distributed teardown. On ROCm, delayed | |
| # graph destruction can surface HSA faults in the next engine startup. | |
| CUDAGraphWrapper.clear_all_graphs() | |
| BreakableCUDAGraphWrapper.clear_all_graphs() | |
| self.encoder_cudagraph_manager = None | |
| self.compilation_config.static_forward_context.clear() | |
| self.model = None # type: ignore[assignment] | |
| _ROPE_DICT.clear() | |
| reset_workspace_manager() | |
| if current_platform.is_rocm() or current_platform.is_xpu(): | |
| gc.collect() | |
| torch.accelerator.empty_cache() | |
| torch.accelerator.synchronize() | |
| def _cleanup_profiling_kv_cache(self) -> None: | |
| torch.accelerator.synchronize() | |
| if hasattr(self, "kv_caches") and self.kv_caches: | |
| for i in range(len(self.kv_caches)): | |
| self.kv_caches[i] = None # type: ignore | |
| self.kv_caches.clear() | |
| if hasattr(self, "cross_layers_kv_cache"): | |
| self.cross_layers_kv_cache = None | |
| self.cross_layers_attn_backend = None | |
| if hasattr(self, "attn_groups"): | |
| self.attn_groups.clear() | |
| if hasattr(self, "kv_cache_config"): | |
| delattr(self, "kv_cache_config") | |
| self.cache_config.num_gpu_blocks = None | |
| for layer in self.compilation_config.static_forward_context.values(): | |
| if hasattr(layer, "kv_cache"): | |
| kv_cache = layer.kv_cache | |
| layer.kv_cache = ( | |
| torch.tensor([]) if isinstance(kv_cache, torch.Tensor) else [] | |
| ) | |
| # Clean up quantized KV cache scale views | |
| # (int8_per_token_head, fp8_per_token_head) | |
| if hasattr(layer, "impl"): | |
| if hasattr(layer.impl, "_k_scale_cache"): | |
| layer.impl._k_scale_cache = None | |
| if hasattr(layer.impl, "_v_scale_cache"): | |
| layer.impl._v_scale_cache = None | |
| gc.collect() | |
| torch.accelerator.empty_cache() | |
| logger.debug("Cleaned up profiling KV cache and CUDA graphs") | |
| def _create_encoder_cudagraph_manager(self) -> "EncoderCudaGraphManager | None": | |
| if not ( | |
| self.compilation_config.cudagraph_mm_encoder and self.supports_mm_inputs | |
| ): | |
| return None | |
| # Use get_model() to unwrap CUDAGraphWrapper/UBatchWrapper, because | |
| # @runtime_checkable Protocol isinstance() checks do not work through | |
| # __getattr__ forwarding. | |
| from vllm.model_executor.models.interfaces import ( | |
| SupportsEncoderCudaGraph, | |
| supports_encoder_cudagraph, | |
| ) | |
| from vllm.v1.worker.encoder_cudagraph import ( | |
| EncoderCudaGraphManager, | |
| ) | |
| raw_model = self.get_model() | |
| if not supports_encoder_cudagraph(raw_model): | |
| return None | |
| return EncoderCudaGraphManager( | |
| vllm_config=self.vllm_config, | |
| device=self.device, | |
| dtype=self.dtype, | |
| model=cast(SupportsEncoderCudaGraph, raw_model), | |
| ) | |
| def _maybe_init_encoder_cudagraph_manager(self) -> None: | |
| if self.encoder_cudagraph_manager is None: | |
| self.encoder_cudagraph_manager = self._create_encoder_cudagraph_manager() | |
| if self.encoder_cudagraph_manager is not None: | |
| logger.info("Initialized EncoderCudaGraphManager for vision encoder") | |
| def profile_cudagraph_memory(self) -> int: | |
| with set_current_vllm_config(self.vllm_config): | |
| self._init_minimal_kv_cache_for_profiling() | |
| saved_num_cudagraph_captured = compilation_counter.num_cudagraph_captured | |
| capture_descs = self.cudagraph_dispatcher.get_capture_descs() | |
| # Use a temporary manager for memory profiling. The persistent manager | |
| # is initialized later so it does not keep profiling-only graph state. | |
| encoder_cudagraph_manager = self._create_encoder_cudagraph_manager() | |
| decoder_graphs = sum(len(descs) for _, descs in capture_descs) | |
| encoder_graphs = ( | |
| encoder_cudagraph_manager.get_num_graphs_to_capture() | |
| if encoder_cudagraph_manager is not None | |
| else 0 | |
| ) | |
| total_graphs = decoder_graphs + encoder_graphs | |
| if total_graphs == 0: | |
| logger.debug("No CUDA graphs will be captured, skipping profiling") | |
| self._cleanup_profiling_kv_cache() | |
| return 0 | |
| graph_groups = [ | |
| *( | |
| f"{mode.name}={len(descs)} (largest={descs[0].num_tokens})" | |
| for mode, descs in capture_descs | |
| if descs | |
| ), | |
| ] | |
| if encoder_graphs > 0: | |
| graph_groups.append( | |
| f"ENCODER={encoder_graphs} " | |
| f"(largest={encoder_cudagraph_manager.token_budgets[-1]})" | |
| ) | |
| logger.info("Profiling CUDA graph memory: %s", ", ".join(graph_groups)) | |
| # Use a temporary pool for profiling to avoid fragmentation in the main pool. | |
| profiling_pool = current_platform.graph_pool_handle() | |
| encoder_profiling_pool = current_platform.graph_pool_handle() | |
| original_pools: dict[int, Any] = {} | |
| all_wrappers = list(CUDAGraphWrapper._all_instances) + list( | |
| BreakableCUDAGraphWrapper._all_instances | |
| ) | |
| for instance in all_wrappers: | |
| original_pools[id(instance)] = instance.graph_pool | |
| instance.graph_pool = profiling_pool | |
| shared_memory_estimate = {} | |
| per_graph_estimate = {} | |
| encoder_memory_estimate = 0 | |
| # On ROCm, capture these throwaway profiling graphs on vLLM's dedicated | |
| # compute stream instead of the fresh side stream graph_capture() | |
| # allocates by default. torch's allocator pools free blocks per stream, | |
| # so a side-stream forward strands a persistent aiter scratch buffer in | |
| # a separate pool, shifting the physical placement of the real KV cache | |
| # allocated afterward and slowing bandwidth-bound decode ~20%. The | |
| # graphs are discarded, so a side stream is unnecessary here. | |
| # Use current_stream(), not torch.cuda.current_stream(): before vLLM | |
| # initializes its dedicated stream, torch returns the per-thread default | |
| # stream (cuda_stream=0), which cannot be used for cudagraph capture. | |
| # cap_ctx=None keeps the side-stream path on CUDA. | |
| cap_ctx = ( | |
| GraphCaptureContext(current_stream()) | |
| if current_platform.is_rocm() | |
| else None | |
| ) | |
| # Cleanup-only guard: CUDA graph capture errors should still propagate | |
| # because encoder graph capture is opt-in. | |
| try: | |
| set_cudagraph_capturing_enabled(True) | |
| with ( | |
| self._freeze_gc(), | |
| graph_capture(device=self.device, graph_capture_context=cap_ctx), | |
| ): | |
| torch.accelerator.synchronize() | |
| torch.accelerator.empty_cache() | |
| for mode, descs in capture_descs: | |
| profile_descs = descs[:2] | |
| mem_samples: list[int] = [] | |
| for i, desc in enumerate(profile_descs): | |
| mem_before = torch.accelerator.get_memory_info()[0] | |
| self._warmup_and_capture( | |
| desc, | |
| cudagraph_runtime_mode=mode, | |
| profile_seq_lens=( | |
| min( | |
| self.max_model_len, | |
| self.max_num_tokens // desc.num_tokens, | |
| ) | |
| if mode == CUDAGraphMode.FULL and i == 0 | |
| else None | |
| ), | |
| ) | |
| torch.accelerator.synchronize() | |
| free_after = torch.accelerator.get_memory_info()[0] | |
| mem_samples.append(mem_before - free_after) | |
| first_capture = mem_samples[0] | |
| # Use at least 1 MiB per graph for driver overhead | |
| per_graph = max( | |
| mem_samples[1] if len(mem_samples) > 1 else 0, 1 << 20 | |
| ) | |
| shared_memory_estimate[mode] = first_capture | |
| per_graph_estimate[mode] = per_graph * (len(descs) - 1) | |
| logger.debug( | |
| "Estimated %s CUDA graph memory: " | |
| "%.2f MiB first-capture + (%d-1) × %.2f MiB per-graph", | |
| mode.name, | |
| first_capture / (1 << 20), | |
| len(descs), | |
| per_graph / (1 << 20), | |
| ) | |
| if encoder_cudagraph_manager is not None: | |
| mem_before = torch.accelerator.get_memory_info()[0] | |
| encoder_cudagraph_manager.capture(graph_pool=encoder_profiling_pool) | |
| torch.accelerator.synchronize() | |
| free_after = torch.accelerator.get_memory_info()[0] | |
| encoder_memory_estimate = max(mem_before - free_after, 0) | |
| logger.debug( | |
| "Estimated encoder CUDA graph memory: %.2f MiB for %d graphs", | |
| encoder_memory_estimate / (1 << 20), | |
| encoder_graphs, | |
| ) | |
| finally: | |
| set_cudagraph_capturing_enabled(False) | |
| CUDAGraphWrapper.clear_all_graphs() | |
| BreakableCUDAGraphWrapper.clear_all_graphs() | |
| if encoder_cudagraph_manager is not None: | |
| encoder_cudagraph_manager.clear() | |
| all_wrappers = list(CUDAGraphWrapper._all_instances) + list( | |
| BreakableCUDAGraphWrapper._all_instances | |
| ) | |
| for instance in all_wrappers: | |
| if id(instance) in original_pools: | |
| instance.graph_pool = original_pools[id(instance)] | |
| for key_set in self.cudagraph_dispatcher.cudagraph_keys.values(): | |
| key_set.clear() | |
| self.cudagraph_dispatcher.keys_initialized = False | |
| self.maybe_remove_all_loras(self.lora_config) | |
| self._cleanup_profiling_kv_cache() | |
| compilation_counter.num_cudagraph_captured = saved_num_cudagraph_captured | |
| # FULL and PIECEWISE graphs share the global pool at runtime and are | |
| # never replayed concurrently, so the pool overlays their memory. | |
| # Take the max to avoid double-counting the overlap. | |
| decoder_estimate = max(shared_memory_estimate.values(), default=0) + sum( | |
| per_graph_estimate.values() | |
| ) | |
| # Encoder graphs use a manager-local pool at runtime, separate from the | |
| # decoder pool, so add their estimate instead of overlaying it. | |
| total_estimate = decoder_estimate + encoder_memory_estimate | |
| logger.info( | |
| "Estimated CUDA graph memory: %.2f GiB total", | |
| total_estimate / (1 << 30), | |
| ) | |
| return int(total_estimate) | |
| def capture_model(self) -> int: | |
| if self.compilation_config.cudagraph_mode == CUDAGraphMode.NONE: | |
| logger.warning( | |
| "Skipping CUDA graph capture. To turn on CUDA graph capture, " | |
| "ensure `cudagraph_mode` was not manually set to `NONE`" | |
| ) | |
| return 0 | |
| # Initialize encoder CUDA graph manager if enabled. | |
| self._maybe_init_encoder_cudagraph_manager() | |
| compilation_counter.num_gpu_runner_capture_triggers += 1 | |
| start_time = time.perf_counter() | |
| # Trigger CUDA graph capture for specific shapes. | |
| # Capture the large shapes first so that the smaller shapes | |
| # can reuse the memory pool allocated for the large shapes. | |
| set_cudagraph_capturing_enabled(True) | |
| # Setup torch profiler for graph capture traces (conditional) | |
| from vllm.distributed.parallel_state import get_world_group | |
| local_rank = get_world_group().local_rank | |
| enable_profiler = ( | |
| local_rank == 0 | |
| ) and self.vllm_config.profiler_config.capture_torch_profiler | |
| if enable_profiler: | |
| trace_dir = ( | |
| self.vllm_config.profiler_config.torch_profiler_dir + "/capture_traces" | |
| ) | |
| profiler = torch.profiler.profile( | |
| activities=[ | |
| torch.profiler.ProfilerActivity.CPU, | |
| torch.profiler.ProfilerActivity.CUDA, | |
| ], | |
| record_shapes=True, | |
| profile_memory=True, | |
| with_stack=True, | |
| on_trace_ready=torch.profiler.tensorboard_trace_handler( | |
| trace_dir, | |
| worker_name=f"graph_capture_rank_{local_rank}", | |
| use_gzip=True, | |
| ), | |
| ) | |
| logger.info_once( | |
| "Rank %d: Torch profiler enabled for CUDA graph capture, " | |
| "traces will be saved to: %s", | |
| local_rank, | |
| trace_dir, | |
| ) | |
| else: | |
| profiler = nullcontext() | |
| logger.info_once( | |
| "Rank %d: Torch profiler disabled for CUDA graph capture", local_rank | |
| ) | |
| with self._freeze_gc(), graph_capture(device=self.device): | |
| torch.accelerator.synchronize() | |
| torch.accelerator.empty_cache() | |
| start_free_gpu_memory = torch.accelerator.get_memory_info()[0] | |
| for ( | |
| runtime_mode, | |
| batch_descs, | |
| ) in self.cudagraph_dispatcher.get_capture_descs(): | |
| self._capture_cudagraphs( | |
| batch_descriptors=batch_descs, | |
| cudagraph_runtime_mode=runtime_mode, | |
| profiler=profiler, | |
| ) | |
| torch.accelerator.synchronize() | |
| # Capture the drafter's piecewise CUDA graphs as well. Upstream | |
| # only captures the target model here, so the draft loop silently | |
| # runs uncaptured at eager-like speed. | |
| if ( | |
| self.speculative_config is not None | |
| and hasattr(self, "drafter") | |
| and hasattr(self.drafter, "cudagraph_dispatcher") | |
| and hasattr(self.drafter, "dummy_run") | |
| and not os.environ.get("VLLM_SPEC_DISABLE_DRAFT_CUDAGRAPH", "") | |
| ): | |
| for ( | |
| drafter_runtime_mode, | |
| drafter_batch_descs, | |
| ) in self.drafter.cudagraph_dispatcher.get_capture_descs(): | |
| if drafter_runtime_mode == CUDAGraphMode.NONE: | |
| continue | |
| for drafter_desc in sorted( | |
| drafter_batch_descs, key=lambda d: -d.num_tokens | |
| ): | |
| self.drafter.dummy_run( | |
| num_tokens=drafter_desc.num_tokens, | |
| use_cudagraphs=True, | |
| is_graph_capturing=True, | |
| ) | |
| torch.accelerator.synchronize() | |
| logger.info("Captured drafter CUDA graphs") | |
| # Capture encoder CUDA graphs if enabled | |
| if self.encoder_cudagraph_manager is not None: | |
| encoder_graph_pool = current_platform.graph_pool_handle() | |
| self.encoder_cudagraph_manager.capture(graph_pool=encoder_graph_pool) | |
| torch.accelerator.synchronize() | |
| end_free_gpu_memory = torch.accelerator.get_memory_info()[0] | |
| # Disable cudagraph capturing globally, so any unexpected cudagraph | |
| # capturing will be detected and raise an error after here. | |
| # Note: We don't put it into graph_capture context manager because | |
| # we may do lazy capturing in future that still allows capturing | |
| # after here. | |
| set_cudagraph_capturing_enabled(False) | |
| torch.accelerator.synchronize() | |
| torch.accelerator.empty_cache() | |
| # Lock workspace to prevent resizing during execution. | |
| # Max workspace sizes should have been captured during warmup/profiling. | |
| lock_workspace() | |
| end_time = time.perf_counter() | |
| elapsed_time = end_time - start_time | |
| cuda_graph_size = start_free_gpu_memory - end_free_gpu_memory | |
| # This usually takes 5~20 seconds. | |
| logger.info_once( | |
| "Graph capturing finished in %.0f secs, took %.2f GiB", | |
| elapsed_time, | |
| cuda_graph_size / (1 << 30), | |
| ) | |
| return cuda_graph_size | |
| def _warmup_and_capture( | |
| self, | |
| desc: BatchDescriptor, | |
| cudagraph_runtime_mode: CUDAGraphMode, | |
| profile_seq_lens: int | None = None, | |
| allow_microbatching: bool = False, | |
| num_warmups: int | None = None, | |
| profiler: AbstractContextManager[Any] | None = None, | |
| ): | |
| if profiler is None: | |
| profiler = nullcontext() | |
| if num_warmups is None: | |
| num_warmups = self.compilation_config.cudagraph_num_of_warmups | |
| force_attention = cudagraph_runtime_mode == CUDAGraphMode.FULL | |
| for _ in range(num_warmups): | |
| self._dummy_run( | |
| desc.num_tokens, | |
| cudagraph_runtime_mode=CUDAGraphMode.NONE, | |
| force_attention=force_attention, | |
| uniform_decode=desc.uniform, | |
| allow_microbatching=allow_microbatching, | |
| skip_eplb=True, | |
| remove_lora=False, | |
| num_active_loras=desc.num_active_loras, | |
| profile_seq_lens=profile_seq_lens, | |
| ) | |
| if num_warmups > 0: | |
| # Warmups may use auxiliary streams. Ensure all of their work has | |
| # completed before beginning CUDA graph capture. | |
| torch.accelerator.synchronize() | |
| with ( | |
| profiler, | |
| torch.profiler.record_function( | |
| f"capture_{desc.num_tokens}_{cudagraph_runtime_mode.name}" | |
| ), | |
| ): | |
| self._dummy_run( | |
| desc.num_tokens, | |
| cudagraph_runtime_mode=cudagraph_runtime_mode, | |
| uniform_decode=desc.uniform, | |
| allow_microbatching=allow_microbatching, | |
| skip_eplb=True, | |
| remove_lora=False, | |
| num_active_loras=desc.num_active_loras, | |
| is_graph_capturing=True, | |
| profile_seq_lens=profile_seq_lens, | |
| ) | |
| def _capture_cudagraphs( | |
| self, | |
| batch_descriptors: list[BatchDescriptor], | |
| cudagraph_runtime_mode: CUDAGraphMode, | |
| profiler: AbstractContextManager[Any] | None = None, | |
| ): | |
| assert ( | |
| cudagraph_runtime_mode != CUDAGraphMode.NONE | |
| and cudagraph_runtime_mode.is_valid_runtime_mode() | |
| ), f"Invalid cudagraph runtime mode: {cudagraph_runtime_mode}" | |
| if not batch_descriptors: | |
| return | |
| uniform_decode = batch_descriptors[0].uniform | |
| # Only rank 0 should print progress bar during capture | |
| if is_global_first_rank(): | |
| batch_descriptors = tqdm( | |
| batch_descriptors, | |
| disable=not self.load_config.use_tqdm_on_load, | |
| desc="Capturing CUDA graphs ({}, {})".format( | |
| "decode" if uniform_decode else "mixed prefill-decode", | |
| cudagraph_runtime_mode.name, | |
| ), | |
| ) | |
| # We skip EPLB here since we don't want to record dummy metrics | |
| for batch_desc in batch_descriptors: | |
| # We currently only capture ubatched graphs when its a FULL | |
| # cudagraph, a uniform decode batch, and the number of tokens | |
| # is above the threshold. Otherwise we just capture a non-ubatched | |
| # version of the graph | |
| allow_microbatching = ( | |
| self.parallel_config.use_ubatching | |
| and cudagraph_runtime_mode == CUDAGraphMode.FULL | |
| and uniform_decode | |
| and check_ubatch_thresholds( | |
| config=self.vllm_config.parallel_config, | |
| num_tokens=batch_desc.num_tokens, | |
| uniform_decode=uniform_decode, | |
| ) | |
| ) | |
| self._warmup_and_capture( | |
| batch_desc, | |
| cudagraph_runtime_mode=cudagraph_runtime_mode, | |
| allow_microbatching=allow_microbatching, | |
| profiler=profiler, | |
| ) | |
| torch.accelerator.synchronize() | |
| self.maybe_remove_all_loras(self.lora_config) | |
| def initialize_attn_backend( | |
| self, | |
| kv_cache_config: KVCacheConfig, | |
| is_profiling: bool = False, | |
| ) -> None: | |
| """ | |
| Initialize the attention backends and attention metadata builders. | |
| """ | |
| assert len(self.attn_groups) == 0, "Attention backends are already initialized" | |
| class AttentionGroupKey(NamedTuple): | |
| """Deduplication key for attention groups within a KV cache group. | |
| Splits on per-rank ``num_heads_q`` in addition to backend + spec | |
| so layers with different Q-head counts (e.g. a spec-decode draft | |
| with fewer attention heads than its target) get separate metadata | |
| builders. The builders' scratch (e.g. ``softmax_segm_*`` in | |
| ``triton_attn``, ``num_qo_heads`` in FlashInfer) is sized by | |
| ``num_heads_q`` and assumes uniformity within the group; see | |
| ``get_num_attention_heads_from_layers`` in | |
| ``vllm/v1/attention/backends/utils.py``. | |
| """ | |
| attn_backend: type[AttentionBackend] | |
| kv_cache_spec: KVCacheSpec | |
| num_heads_q: int | |
| def get_attn_backends_for_group( | |
| kv_cache_group_spec: KVCacheGroupSpec, | |
| ) -> tuple[dict[AttentionGroupKey, list[str]], set[type[AttentionBackend]]]: | |
| layer_type = cast(type[Any], AttentionLayerBase) | |
| layers = get_layers_from_vllm_config( | |
| self.vllm_config, layer_type, kv_cache_group_spec.layer_names | |
| ) | |
| attn_backends = {} | |
| attn_backend_layers = defaultdict(list) | |
| # Dedupe based on full class name; this is a bit safer than | |
| # using the class itself as the key because when we create dynamic | |
| # attention backend subclasses (e.g. ChunkedLocalAttention) unless | |
| # they are cached correctly, there will be different objects per | |
| # layer. | |
| for layer_name in kv_cache_group_spec.layer_names: | |
| attn_backend = layers[layer_name].get_attn_backend() | |
| if layer_name in self.kv_sharing_fast_prefill_eligible_layers: | |
| attn_backend = create_fast_prefill_custom_backend( | |
| "FastPrefill", | |
| attn_backend, # type: ignore[arg-type] | |
| ) | |
| full_cls_name = attn_backend.full_cls_name() | |
| layer_kv_cache_spec = kv_cache_group_spec.kv_cache_spec | |
| if isinstance(layer_kv_cache_spec, UniformTypeKVCacheSpecs): | |
| layer_kv_cache_spec = layer_kv_cache_spec.kv_cache_specs[layer_name] | |
| # Non-Attention layer types (e.g. Mamba1, ShortConv) do not | |
| # expose ``num_heads``; fall back to 0 so they cluster as | |
| # before. Such layers never coexist with Attention in a | |
| # single KV cache group (different KVCacheSpec), so the | |
| # fallback can never spuriously merge them with attention | |
| # layers. | |
| num_heads_q = getattr(layers[layer_name], "num_heads", 0) | |
| key = (full_cls_name, layer_kv_cache_spec, num_heads_q) | |
| attn_backends[key] = AttentionGroupKey( | |
| attn_backend, layer_kv_cache_spec, num_heads_q | |
| ) | |
| attn_backend_layers[key].append(layer_name) | |
| return ( | |
| {attn_backends[k]: v for k, v in attn_backend_layers.items()}, | |
| set(group_key.attn_backend for group_key in attn_backends.values()), | |
| ) | |
| def create_attn_groups( | |
| attn_backends_map: dict[AttentionGroupKey, list[str]], | |
| kv_cache_group_id: int, | |
| ) -> list[AttentionGroup]: | |
| attn_groups: list[AttentionGroup] = [] | |
| for key, layer_names in attn_backends_map.items(): | |
| attn_group = AttentionGroup( | |
| key.attn_backend, | |
| layer_names, | |
| key.kv_cache_spec, | |
| kv_cache_group_id, | |
| ) | |
| attn_groups.append(attn_group) | |
| return attn_groups | |
| attention_backend_maps = [] | |
| attention_backend_list = [] | |
| for kv_cache_group_spec in kv_cache_config.kv_cache_groups: | |
| attn_backends = get_attn_backends_for_group(kv_cache_group_spec) | |
| attention_backend_maps.append(attn_backends[0]) | |
| attention_backend_list.append(attn_backends[1]) | |
| # Resolve cudagraph_mode before actually initialize metadata_builders | |
| self._check_and_update_cudagraph_mode( | |
| attention_backend_list, | |
| kv_cache_config.kv_cache_groups, | |
| is_profiling=is_profiling, | |
| ) | |
| # Check if attention backend supports PCP&DCP and related features. | |
| check_attention_cp_compatibility(self.vllm_config) | |
| for i, attn_backend_map in enumerate(attention_backend_maps): | |
| self.attn_groups.append(create_attn_groups(attn_backend_map, i)) | |
| def initialize_metadata_builders( | |
| self, kv_cache_config: KVCacheConfig, kernel_block_sizes: list[int] | |
| ) -> None: | |
| """ | |
| Create the metadata builders for all KV cache groups and attn groups. | |
| """ | |
| for kv_cache_group_id in range(len(kv_cache_config.kv_cache_groups)): | |
| for attn_group in self.attn_groups[kv_cache_group_id]: | |
| attn_group.create_metadata_builders( | |
| self.vllm_config, | |
| self.device, | |
| kernel_block_sizes[kv_cache_group_id] | |
| if kv_cache_group_id < len(kernel_block_sizes) | |
| else None, | |
| num_metadata_builders=1 | |
| if not self.parallel_config.use_ubatching | |
| else self.parallel_config.num_ubatches, | |
| ) | |
| # Calculate reorder batch threshold (if needed) | |
| # Note (tdoublep): do this *after* constructing builders, | |
| # because some of them change the threshold at init time. | |
| self.calculate_reorder_batch_threshold() | |
| # Initialize drafter attention backend | |
| if self.speculative_config and ( | |
| self.speculative_config.use_eagle() | |
| or self.speculative_config.uses_draft_model() | |
| ): | |
| assert isinstance( | |
| self.drafter, | |
| EagleProposer | DFlashProposer | DraftModelProposer | Gemma4Proposer, | |
| ) | |
| self.drafter.initialize_attn_backend(kv_cache_config, kernel_block_sizes) | |
| def _check_and_update_cudagraph_mode( | |
| self, | |
| attention_backends: list[set[type[AttentionBackend]]], | |
| kv_cache_groups: list[KVCacheGroupSpec], | |
| is_profiling: bool = False, | |
| ) -> None: | |
| """ | |
| Resolve the cudagraph_mode when there are multiple attention | |
| groups with potential conflicting CUDA graph support. | |
| Then initialize the cudagraph_dispatcher based on the resolved | |
| cudagraph_mode. | |
| """ | |
| min_cg_support = AttentionCGSupport.ALWAYS | |
| min_cg_attn_backend = None | |
| for attn_backend_set, kv_cache_group in zip( | |
| attention_backends, kv_cache_groups | |
| ): | |
| for attn_backend in attn_backend_set: | |
| builder_cls = attn_backend.get_builder_cls() | |
| cg_support = builder_cls.get_cudagraph_support( | |
| self.vllm_config, kv_cache_group.kv_cache_spec | |
| ) | |
| if cg_support.value < min_cg_support.value: | |
| min_cg_support = cg_support | |
| min_cg_attn_backend = attn_backend.__name__ | |
| cudagraph_mode = self.compilation_config.resolve_cudagraph_mode_and_sizes( | |
| min_cg_support, | |
| min_cg_attn_backend, | |
| self.uniform_decode_query_len, | |
| use_v2_model_runner=False, | |
| tensor_parallel_size=self.parallel_config.tensor_parallel_size, | |
| kv_cache_config=self.kv_cache_config, | |
| max_num_reqs=self.max_num_reqs, | |
| is_profiling=is_profiling, | |
| ) | |
| # Trigger cudagraph dispatching keys initialization after | |
| # resolved cudagraph mode. | |
| self.cudagraph_dispatcher.initialize_cudagraph_keys( | |
| cudagraph_mode, self.uniform_decode_query_len | |
| ) | |
| # Initialize drafter's cudagraph dispatcher if using spec decode. | |
| if self.speculative_config and ( | |
| self.speculative_config.use_eagle() | |
| or self.speculative_config.uses_draft_model() | |
| or self.speculative_config.uses_extract_hidden_states() | |
| ): | |
| assert isinstance( | |
| self.drafter, | |
| EagleProposer | |
| | DFlashProposer | |
| | DraftModelProposer | |
| | ExtractHiddenStatesProposer | |
| | Gemma4Proposer, | |
| ) | |
| self.drafter.initialize_cudagraph_keys(cudagraph_mode) | |
| def calculate_reorder_batch_threshold(self) -> None: | |
| """ | |
| Choose the minimum reorder batch threshold from all attention groups. | |
| Backends should be able to support lower threshold then what they request | |
| just may have a performance penalty due to that backend treating decodes | |
| as prefills. | |
| """ | |
| min_none_high = lambda a, b: a if b is None else b if a is None else min(a, b) | |
| reorder_batch_thresholds: list[int | None] = [ | |
| group.get_metadata_builder().reorder_batch_threshold | |
| for group in self._attn_group_iterator() | |
| ] | |
| # If there are no attention groups (attention-free model) or no backend | |
| # reports a threshold, leave reordering disabled. | |
| if len(reorder_batch_thresholds) == 0: | |
| self.reorder_batch_threshold = None | |
| return | |
| self.reorder_batch_threshold = reduce(min_none_high, reorder_batch_thresholds) # type: ignore[assignment] | |
| def may_reinitialize_input_batch( | |
| self, kv_cache_config: KVCacheConfig, kernel_block_sizes: list[int] | |
| ) -> None: | |
| """ | |
| Re-initialize the input batch if the block sizes are different from | |
| what it was originally created with. This happens when the final | |
| block size (determined after model loading) differs from the | |
| placeholder used during __init__, or when there are multiple | |
| KV cache groups. | |
| Args: | |
| kv_cache_config: The KV cache configuration. | |
| kernel_block_sizes: The kernel block sizes for each KV cache group. | |
| """ | |
| block_sizes = [] | |
| max_num_blocks = [] | |
| slot_mapping_modes = [] | |
| max_model_len = max(self.max_model_len, self.max_encoder_len) | |
| for kv_cache_group in kv_cache_config.kv_cache_groups: | |
| kv_cache_spec = kv_cache_group.kv_cache_spec | |
| kv_cache_spec_kind = get_kv_cache_spec_kind(kv_cache_spec) | |
| if kv_cache_spec_kind == KVCacheSpecKind.ENCODER_ONLY_ATTENTION: | |
| continue | |
| block_size = kv_cache_spec.block_size | |
| block_sizes.append(block_size) | |
| if kv_cache_spec_kind == KVCacheSpecKind.MAMBA: | |
| slot_mapping_modes.append(SlotMappingMode.NONE) | |
| else: | |
| slot_mapping_modes.append(SlotMappingMode.TOKEN_TO_KV_SLOT) | |
| max_num_blocks_per_req = kv_cache_spec.max_num_blocks_per_req( | |
| self.vllm_config, max_model_len | |
| ) | |
| max_num_blocks.append(max_num_blocks_per_req) | |
| if ( | |
| block_sizes != self._init_block_sizes | |
| or kernel_block_sizes != self._init_kernel_block_sizes | |
| or max_num_blocks != self._init_max_num_blocks | |
| or slot_mapping_modes != self._init_slot_mapping_modes | |
| ): | |
| self._init_block_sizes = block_sizes | |
| self._init_kernel_block_sizes = kernel_block_sizes | |
| self._init_max_num_blocks = max_num_blocks | |
| self._init_slot_mapping_modes = slot_mapping_modes | |
| self.input_batch = InputBatch( | |
| max_num_reqs=self.max_num_reqs, | |
| max_model_len=max_model_len, | |
| max_num_batched_tokens=self.max_num_tokens, | |
| device=self.device, | |
| vocab_size=self.model_config.get_vocab_size(), | |
| block_sizes=block_sizes, | |
| kernel_block_sizes=kernel_block_sizes, | |
| max_num_blocks_per_req=max_num_blocks, | |
| num_spec_tokens=self.num_spec_tokens, | |
| logitsprocs=self.input_batch.logitsprocs, | |
| logitsprocs_need_output_token_ids=self.input_batch.logitsprocs_need_output_token_ids, | |
| is_pooling_model=self.is_pooling_model, | |
| cp_kv_cache_interleave_size=self.parallel_config.cp_kv_cache_interleave_size, | |
| reasoning_config=self.vllm_config.reasoning_config, | |
| use_replayssm=self.cache_config.use_replayssm, | |
| slot_mapping_modes=slot_mapping_modes, | |
| ) | |
| assert self._init_block_sizes == block_sizes, ( | |
| f"InputBatch block_sizes {self._init_block_sizes} != " | |
| f"kv_cache block_sizes {block_sizes}" | |
| ) | |
| assert self._init_kernel_block_sizes == kernel_block_sizes, ( | |
| f"InputBatch kernel_block_sizes {self._init_kernel_block_sizes} " | |
| f"!= kv_cache kernel_block_sizes {kernel_block_sizes}" | |
| ) | |
| def _allocate_kv_cache_tensors( | |
| self, kv_cache_config: KVCacheConfig | |
| ) -> dict[str, torch.Tensor]: | |
| """ | |
| Initializes the KV cache buffer with the correct size. The buffer needs | |
| to be reshaped to the desired shape before being used by the models. | |
| Args: | |
| kv_cache_config: The KV cache config | |
| Returns: | |
| dict[str, torch.Tensor]: A map between layer names to their | |
| corresponding memory buffer for KV cache. | |
| """ | |
| kv_cache_raw_tensors: dict[str, torch.Tensor] = {} | |
| packed_backing: torch.Tensor | None = None | |
| for kv_cache_tensor in kv_cache_config.kv_cache_tensors: | |
| if kv_cache_tensor.block_stride > 0: | |
| # Allocate once; all packed tensors alias the same backing. | |
| if packed_backing is None: | |
| packed_backing = torch.zeros( | |
| kv_cache_tensor.size, | |
| dtype=torch.int8, | |
| device=self.device, | |
| ) | |
| tensor = packed_backing | |
| else: | |
| tensor = torch.zeros( | |
| kv_cache_tensor.size, dtype=torch.int8, device=self.device | |
| ) | |
| for layer_name in kv_cache_tensor.shared_by: | |
| kv_cache_raw_tensors[layer_name] = tensor | |
| layer_names = set() | |
| for group in kv_cache_config.kv_cache_groups: | |
| for layer_name in group.layer_names: | |
| if layer_name in self.runner_only_attn_layers: | |
| continue | |
| layer_names.add(layer_name) | |
| assert layer_names == set(kv_cache_raw_tensors.keys()), ( | |
| "Some layers are not correctly initialized" | |
| ) | |
| return kv_cache_raw_tensors | |
| def _attn_group_iterator(self) -> Iterator[AttentionGroup]: | |
| return itertools.chain.from_iterable(self.attn_groups) | |
| def _kv_cache_spec_attn_group_iterator(self) -> Iterator[AttentionGroup]: | |
| if not self.kv_cache_config.kv_cache_groups: | |
| return | |
| for attn_groups in self.attn_groups: | |
| yield from attn_groups | |
| def _reshape_kv_cache_tensors( | |
| self, | |
| kv_cache_raw_tensors: dict[str, torch.Tensor], | |
| kernel_block_sizes: list[int], | |
| ) -> dict[str, torch.Tensor]: | |
| """ | |
| Reshape the KV cache tensors to the desired shape and dtype. | |
| Args: | |
| kv_cache_raw_tensors: The KV cache buffer of each layer, with | |
| correct size but uninitialized shape. | |
| kernel_block_sizes: The kernel block sizes for each KV cache group. | |
| Returns: | |
| Dict[str, torch.Tensor]: A map between layer names to their | |
| corresponding memory buffer for KV cache. | |
| """ | |
| kv_caches: dict[str, torch.Tensor] = {} | |
| has_attn, has_mamba = False, False | |
| # Map layer names to (offset, block_stride) within the packed | |
| # backing tensor so we can create strided views per layer. | |
| layer_packing: dict[str, tuple[int, int]] = {} | |
| for kv_tensor in self.kv_cache_config.kv_cache_tensors: | |
| if kv_tensor.block_stride > 0: | |
| for ln in kv_tensor.shared_by: | |
| layer_packing[ln] = (kv_tensor.offset, kv_tensor.block_stride) | |
| for group in self._kv_cache_spec_attn_group_iterator(): | |
| kv_cache_spec = group.kv_cache_spec | |
| attn_backend = group.backend | |
| if group.kv_cache_group_id == len(kernel_block_sizes): | |
| # There may be a last group for layers without kv cache. | |
| continue | |
| kernel_block_size = kernel_block_sizes[group.kv_cache_group_id] | |
| for layer_name in group.layer_names: | |
| if layer_name in self.runner_only_attn_layers: | |
| continue | |
| raw_tensor = kv_cache_raw_tensors[layer_name] | |
| packing = layer_packing.get(layer_name) | |
| if packing is not None: | |
| _, blk_stride = packing | |
| num_blocks = raw_tensor.numel() // blk_stride | |
| else: | |
| assert raw_tensor.numel() % kv_cache_spec.page_size_bytes == 0 | |
| num_blocks = raw_tensor.numel() // kv_cache_spec.page_size_bytes | |
| if isinstance(kv_cache_spec, AttentionSpec): | |
| has_attn = True | |
| num_blocks_per_kv_block = ( | |
| kv_cache_spec.block_size // kernel_block_size | |
| ) | |
| kernel_num_blocks = num_blocks * num_blocks_per_kv_block | |
| # For MLA with compression, storage_block_size != block_size | |
| if kv_cache_spec.storage_block_size != kv_cache_spec.block_size: | |
| shape_block_size = kv_cache_spec.storage_block_size | |
| else: | |
| shape_block_size = kernel_block_size | |
| # Skipped layers (--kv-cache-dtype-skip-layers) need | |
| # the unquantized shape. | |
| layer_cache_dtype_str = ( | |
| "auto" | |
| if kv_cache_spec.kv_quant_mode == KVQuantMode.NONE | |
| else getattr( | |
| kv_cache_spec, | |
| "cache_dtype_str", | |
| None, | |
| ) | |
| or self.cache_config.cache_dtype | |
| ) | |
| kv_cache_shape = attn_backend.get_kv_cache_shape( | |
| kernel_num_blocks, | |
| shape_block_size, | |
| kv_cache_spec.num_kv_heads, | |
| kv_cache_spec.head_size, | |
| cache_dtype_str=layer_cache_dtype_str, | |
| ) | |
| try: | |
| kv_cache_stride_order = attn_backend.get_kv_cache_stride_order() | |
| assert len(kv_cache_stride_order) == len(kv_cache_shape) | |
| except (AttributeError, NotImplementedError): | |
| kv_cache_stride_order = tuple(range(len(kv_cache_shape))) | |
| raw_tensor = kv_cache_raw_tensors[layer_name] | |
| kv_caches[layer_name] = _reshape_attention_kv_cache( | |
| raw_tensor, | |
| kv_cache_spec, | |
| kv_cache_shape, | |
| kv_cache_stride_order, | |
| kernel_num_blocks, | |
| packing, | |
| ) | |
| elif isinstance(kv_cache_spec, MambaSpec): | |
| has_mamba = True | |
| raw_tensor = kv_cache_raw_tensors[layer_name] | |
| page_size_bytes = kv_cache_spec.page_size_bytes | |
| # Hold a single contiguous [num_blocks, 1, 1, page_size_bytes] | |
| # int8 page view per layer; the layer's bind_kv_cache unpacks | |
| # each block's bytes into its conv/ssm state views. Keeping | |
| # one tensor per layer lets the KV connector register it | |
| # without special-casing Mamba. | |
| kv_caches[layer_name] = raw_tensor[ | |
| : num_blocks * page_size_bytes | |
| ].view(num_blocks, 1, 1, page_size_bytes) | |
| else: | |
| raise NotImplementedError | |
| # Reconcile divergent KV layouts to blocks-first. Triggered by hybrid | |
| # attention/mamba models, and by encoder-decoder models whose shared | |
| # decoder/cross-attention allocation mixes K/V-first and blocks-first | |
| # backends (see _has_mixed_attention_kv_layout). | |
| if has_attn and ( | |
| has_mamba or self._has_mixed_attention_kv_layout(kernel_block_sizes) | |
| ): | |
| self._update_hybrid_attention_mamba_layout(kv_caches, kernel_block_sizes) | |
| return kv_caches | |
| def _has_mixed_attention_kv_layout(self, kernel_block_sizes: list[int]) -> bool: | |
| """Whether attention groups disagree on the physical KV cache layout. | |
| Encoder-decoder models (e.g. Whisper) share one raw KV allocation | |
| between a decoder self-attention layer (K/V-first ROCM_ATTN, block dim | |
| 1) and a cross-attention layer (blocks-first, block dim 0). Mixed block | |
| dims mean a block ID maps to different bytes per layer, so the shared | |
| buffer must be normalized to a single (blocks-first) layout. | |
| """ | |
| block_dims: set[int] = set() | |
| for group in self._kv_cache_spec_attn_group_iterator(): | |
| kv_cache_spec = group.kv_cache_spec | |
| if not isinstance(kv_cache_spec, AttentionSpec): | |
| continue | |
| if group.kv_cache_group_id == len(kernel_block_sizes): | |
| continue | |
| block_dims.add( | |
| group.backend.get_kv_cache_block_dim( | |
| kernel_block_sizes[group.kv_cache_group_id], | |
| kv_cache_spec.num_kv_heads, | |
| kv_cache_spec.head_size, | |
| cache_dtype_str=self.cache_config.cache_dtype, | |
| ) | |
| ) | |
| return len(block_dims) > 1 | |
| def _update_hybrid_attention_mamba_layout( | |
| self, kv_caches: dict[str, torch.Tensor], kernel_block_sizes: list[int] | |
| ) -> None: | |
| """ | |
| Update the layout of attention layers from (2, num_blocks, ...) to | |
| (num_blocks, 2, ...). | |
| Args: | |
| kv_caches: The KV cache buffer of each layer. | |
| kernel_block_sizes: The kernel block sizes for each KV cache group. | |
| """ | |
| for group in self._kv_cache_spec_attn_group_iterator(): | |
| kv_cache_spec = group.kv_cache_spec | |
| if not isinstance(kv_cache_spec, AttentionSpec): | |
| continue | |
| block_dim = group.backend.get_kv_cache_block_dim( | |
| kernel_block_sizes[group.kv_cache_group_id], | |
| kv_cache_spec.num_kv_heads, | |
| kv_cache_spec.head_size, | |
| cache_dtype_str=self.cache_config.cache_dtype, | |
| ) | |
| # block_dim: 0 means (num_blocks, 2, ...); 1 means (2, num_blocks, ...). | |
| if block_dim == 0: | |
| continue | |
| assert block_dim == 1 | |
| for layer_name in group.layer_names: | |
| kv_cache = kv_caches[layer_name] | |
| hidden_size = kv_cache.shape[2:].numel() | |
| kv_cache.as_strided_( | |
| size=kv_cache.shape, | |
| stride=(hidden_size, 2 * hidden_size, *kv_cache.stride()[2:]), | |
| ) | |
| def initialize_kv_cache_tensors( | |
| self, kv_cache_config: KVCacheConfig, kernel_block_sizes: list[int] | |
| ) -> dict[str, torch.Tensor]: | |
| """ | |
| Initialize the memory buffer for KV cache. | |
| Args: | |
| kv_cache_config: The KV cache config | |
| kernel_block_sizes: The kernel block sizes for each KV cache group. | |
| Returns: | |
| Dict[str, torch.Tensor]: A map between layer names to their | |
| corresponding memory buffer for KV cache. | |
| """ | |
| # Try creating KV caches optimized for kv-connector transfers | |
| cache_dtype = self.cache_config.cache_dtype | |
| if self.use_uniform_kv_cache(self.attn_groups): | |
| kv_caches, cross_layers_kv_cache, attn_backend = ( | |
| self.allocate_uniform_kv_caches( | |
| kv_cache_config, | |
| self.attn_groups, | |
| cache_dtype, | |
| self.device, | |
| kernel_block_sizes, | |
| ) | |
| ) | |
| self.cross_layers_kv_cache = cross_layers_kv_cache | |
| self.cross_layers_attn_backend = attn_backend | |
| else: | |
| # Fallback to the general case | |
| # Initialize the memory buffer for KV cache | |
| kv_cache_raw_tensors = self._allocate_kv_cache_tensors(kv_cache_config) | |
| # Change the memory buffer to the desired shape | |
| kv_caches = self._reshape_kv_cache_tensors( | |
| kv_cache_raw_tensors, kernel_block_sizes | |
| ) | |
| # Set up cross-layer KV cache sharing | |
| for layer_name, target_layer_name in self.shared_kv_cache_layers.items(): | |
| logger.debug("%s reuses KV cache of %s", layer_name, target_layer_name) | |
| kv_caches[layer_name] = kv_caches[target_layer_name] | |
| num_attn_module = ( | |
| 2 if self.model_config.hf_config.model_type == "longcat_flash" else 1 | |
| ) | |
| bind_kv_cache( | |
| kv_caches, | |
| self.compilation_config.static_forward_context, | |
| self.kv_caches, | |
| num_attn_module, | |
| ) | |
| return kv_caches | |
| def maybe_add_kv_sharing_layers_to_kv_cache_groups( | |
| self, kv_cache_config: KVCacheConfig | |
| ) -> None: | |
| """ | |
| Add layers that re-use KV cache to KV cache group of its target layer. | |
| Mapping of KV cache tensors happens in `initialize_kv_cache_tensors()` | |
| """ | |
| if not self.shared_kv_cache_layers: | |
| # No cross-layer KV sharing, return | |
| return | |
| add_kv_sharing_layers_to_kv_cache_groups( | |
| self.shared_kv_cache_layers, | |
| kv_cache_config.kv_cache_groups, | |
| self.runner_only_attn_layers, | |
| ) | |
| if self.cache_config.kv_sharing_fast_prefill: | |
| # In You Only Cache Once (https://arxiv.org/abs/2405.05254) or other | |
| # similar KV sharing setups, only the layers that generate KV caches | |
| # are involved in the prefill phase, enabling prefill to early exit. | |
| attn_layers = get_layers_from_vllm_config(self.vllm_config, Attention) | |
| for layer_name in reversed(attn_layers): | |
| if layer_name in self.shared_kv_cache_layers: | |
| self.kv_sharing_fast_prefill_eligible_layers.add(layer_name) | |
| else: | |
| break | |
| def initialize_kv_cache( | |
| self, | |
| kv_cache_config: KVCacheConfig, | |
| is_profiling: bool = False, | |
| ) -> None: | |
| """ | |
| Initialize KV cache based on `kv_cache_config`. | |
| Args: | |
| kv_cache_config: Configuration for the KV cache, including the KV | |
| cache size of each layer | |
| """ | |
| kv_cache_config = deepcopy(kv_cache_config) | |
| self.kv_cache_config = kv_cache_config | |
| self._mamba_bufs = None | |
| self.may_add_encoder_only_layers_to_kv_cache_config() | |
| self.maybe_add_kv_sharing_layers_to_kv_cache_groups(kv_cache_config) | |
| self.initialize_attn_backend(kv_cache_config, is_profiling=is_profiling) | |
| initialize_mamba_ssu_backend( | |
| self.vllm_config.mamba_config, self.kv_cache_config | |
| ) | |
| # The kernel block size for all KV cache groups. For example, if | |
| # kv_cache_manager uses block_size 256 for a given group, but the attention | |
| # backends for that group only supports block_size 64, we will return | |
| # kernel_block_size 64 and split the 256-token-block to 4 blocks with 64 | |
| # tokens each. | |
| kernel_block_sizes = prepare_kernel_block_sizes( | |
| kv_cache_config, self.attn_groups | |
| ) | |
| self._kernel_block_sizes = kernel_block_sizes | |
| # create metadata builders | |
| self.initialize_metadata_builders(kv_cache_config, kernel_block_sizes) | |
| # Reinitialize need to after initialize_attn_backend | |
| self.may_reinitialize_input_batch(kv_cache_config, kernel_block_sizes) | |
| kv_caches = self.initialize_kv_cache_tensors( | |
| kv_cache_config, kernel_block_sizes | |
| ) | |
| if ( | |
| self.speculative_config | |
| and self.speculative_config.uses_extract_hidden_states() | |
| ): | |
| assert isinstance(self.drafter, ExtractHiddenStatesProposer) | |
| # validate all draft model layers belong to the same kv cache | |
| # group | |
| self.drafter.validate_same_kv_cache_group(kv_cache_config) | |
| if has_kv_transfer_group() and not is_profiling: | |
| kv_transfer_group = get_kv_transfer_group() | |
| if self.cross_layers_kv_cache is not None: | |
| assert self.cross_layers_attn_backend is not None | |
| kv_transfer_group.register_cross_layers_kv_cache( | |
| self.cross_layers_kv_cache, self.cross_layers_attn_backend | |
| ) | |
| else: | |
| kv_transfer_group.register_kv_caches(kv_caches) | |
| kv_transfer_group.set_host_xfer_buffer_ops(copy_kv_blocks) | |
| def _get_attention_kv_cache_gid(self) -> int: | |
| """Find the KV cache group index for attention layers. | |
| Must match :attr:`RoutedExpertsManager.attn_gid` in the scheduler: | |
| both pick the first ``FullAttentionSpec`` group so hybrid models | |
| (Mamba / linear-attention layers that use other AttentionSpec | |
| subclasses) end up indexing the same slot layout on both sides. | |
| Falls back to 0 only for legacy single-group configs. | |
| """ | |
| for gid, group in enumerate(self.kv_cache_config.kv_cache_groups): | |
| if isinstance(group.kv_cache_spec, FullAttentionSpec): | |
| return gid | |
| return 0 | |
| def init_routed_experts_capturer(self): | |
| logger.info( | |
| "Initializing routed experts capturer, enable_return_routed_experts: %s", | |
| self.model_config.enable_return_routed_experts, | |
| ) | |
| self.routed_experts_capturer = RoutedExpertsCapturer( | |
| max_num_batched_tokens=self.scheduler_config.max_num_batched_tokens, | |
| vllm_config=self.vllm_config, | |
| ) | |
| self.routed_experts_attn_gid = self._get_attention_kv_cache_gid() | |
| self._bind_routed_experts_capturer(self.routed_experts_capturer) | |
| # Pinned CPU buffer for non-blocking D2H of ``routing_data`` on | |
| # the sync scheduling path. Shape / dtype mirror the device | |
| # capturer exactly so ``copy_`` is a straight memcpy. | |
| self.routed_experts_cpu = torch.empty( | |
| self.routed_experts_capturer.device_buffer.shape, | |
| dtype=self.routed_experts_capturer.device_buffer.dtype, | |
| device="cpu", | |
| pin_memory=PIN_MEMORY, | |
| ) | |
| # ``slot_mapping`` dtype is fixed to int64 by | |
| # ``block_table.slot_mapping``; we mirror that here. | |
| max_tokens = self.scheduler_config.max_num_batched_tokens | |
| self.routed_experts_slot_mapping_cpu = torch.empty( | |
| (max_tokens,), | |
| dtype=torch.int64, | |
| device="cpu", | |
| pin_memory=PIN_MEMORY, | |
| ) | |
| # Private device buffer so the shared ``block_table.slot_mapping`` | |
| # can be overwritten by the next ``_prepare_inputs`` while the | |
| # D2H is still pending on the copy stream. Written in | |
| # ``_prepare_inputs``, read in ``_bookkeeping_sync`` (sync path) | |
| # or cloned into a snapshot (async path). | |
| self.routed_experts_slot_mapping_device = torch.empty( | |
| (max_tokens,), | |
| dtype=torch.int64, | |
| device=self.device, | |
| ) | |
| self.routed_experts_initialized = True | |
| def _bind_routed_experts_capturer(self, capturer: RoutedExpertsCapturer) -> None: | |
| from vllm.model_executor.layers.fused_moe.layer import MoERunner | |
| from vllm.model_executor.layers.fused_moe.modular_kernel import ( | |
| FusedMoEExpertsMonolithic, | |
| ) | |
| from vllm.model_executor.layers.fused_moe.router.base_router import ( | |
| BaseRouter, | |
| ) | |
| for module in self.model.modules(): | |
| if not isinstance(module, MoERunner): | |
| continue | |
| layer_id = module.layer_id | |
| def _capture_fn(topk_ids, _layer_id=layer_id, _capturer=capturer): | |
| _capturer.capture(_layer_id, topk_ids) | |
| quant_method = module._quant_method | |
| moe_kernel = getattr(quant_method, "moe_kernel", None) | |
| impl = getattr(moe_kernel, "impl", None) | |
| fused_experts = getattr(impl, "fused_experts", None) | |
| if quant_method.is_monolithic: | |
| if not ( | |
| isinstance(fused_experts, FusedMoEExpertsMonolithic) | |
| and fused_experts.supports_routing_replay_capture() | |
| ): | |
| raise ValueError( | |
| "--enable-return-routed-experts is not supported with " | |
| f"monolithic MoE kernel {type(fused_experts).__name__}; " | |
| "routed expert IDs would be silently all-zero." | |
| ) | |
| fused_experts.set_capture_fn(_capture_fn) | |
| elif isinstance(module.router, BaseRouter): | |
| module.router.set_capture_fn(_capture_fn) | |
| def may_add_encoder_only_layers_to_kv_cache_config(self) -> None: | |
| """ | |
| Add encoder-only layers to the KV cache config. | |
| """ | |
| block_size = self.vllm_config.cache_config.block_size | |
| encoder_only_attn_specs: dict[AttentionSpec, list[str]] = defaultdict(list) | |
| attn_layers = get_layers_from_vllm_config(self.vllm_config, Attention) | |
| for layer_name, attn_module in attn_layers.items(): | |
| if attn_module.attn_type == AttentionType.ENCODER_ONLY: | |
| attn_spec: AttentionSpec = EncoderOnlyAttentionSpec( | |
| block_size=block_size, | |
| num_kv_heads=attn_module.num_kv_heads, | |
| head_size=attn_module.head_size, | |
| dtype=self.kv_cache_dtype, | |
| ) | |
| encoder_only_attn_specs[attn_spec].append(layer_name) | |
| self.runner_only_attn_layers.add(layer_name) | |
| if len(encoder_only_attn_specs) > 0: | |
| assert len(encoder_only_attn_specs) == 1, ( | |
| "Only support one encoder-only attention spec now" | |
| ) | |
| spec, layer_names = encoder_only_attn_specs.popitem() | |
| self.kv_cache_config.kv_cache_groups.append( | |
| KVCacheGroupSpec(layer_names=layer_names, kv_cache_spec=spec) | |
| ) | |
| def get_kv_cache_spec(self) -> dict[str, KVCacheSpec]: | |
| """ | |
| Generates the KVCacheSpec by parsing the kv cache format from each | |
| Attention module in the static forward context. | |
| Returns: | |
| KVCacheSpec: A dictionary mapping layer names to their KV cache | |
| format. Layers that do not need KV cache are not included. | |
| """ | |
| if has_ec_transfer() and not get_ec_transfer().is_consumer: | |
| return {} | |
| kv_cache_spec: dict[str, KVCacheSpec] = {} | |
| layer_type = cast(type[Any], AttentionLayerBase) | |
| attn_layers = get_layers_from_vllm_config(self.vllm_config, layer_type) | |
| for layer_name, attn_module in attn_layers.items(): | |
| if isinstance(attn_module, Attention) and ( | |
| kv_tgt_layer := attn_module.kv_sharing_target_layer_name | |
| ): | |
| # The layer doesn't need its own KV cache and will use that of | |
| # the target layer. We skip creating a KVCacheSpec for it, so | |
| # that KV cache management logic will act as this layer does | |
| # not exist, and doesn't allocate KV cache for the layer. This | |
| # enables the memory saving of cross-layer kv sharing, allowing | |
| # a given amount of memory to accommodate longer context lengths | |
| # or enable more requests to be processed simultaneously. | |
| self.shared_kv_cache_layers[layer_name] = kv_tgt_layer | |
| continue | |
| # Skip modules that don't need KV cache (eg encoder-only attention) | |
| if spec := attn_module.get_kv_cache_spec(self.vllm_config): | |
| if isinstance(spec, AttentionSpec): | |
| backend = attn_module.get_attn_backend() | |
| # indexes_kv_by_block_stride() -> get_kv_cache_stride_order() | |
| # -> get_kv_cache_layout() needs the current vLLM config. | |
| with set_current_vllm_config(self.vllm_config): | |
| indexes = backend.indexes_kv_by_block_stride() | |
| spec = replace(spec, indexes_kv_by_block_stride=indexes) | |
| kv_cache_spec[layer_name] = spec | |
| return kv_cache_spec | |
| def _to_list(self, sampled_token_ids: torch.Tensor) -> list[list[int]]: | |
| # This is a short term mitigation for issue mentioned in | |
| # https://github.com/vllm-project/vllm/issues/22754. | |
| # `tolist` would trigger a cuda wise stream sync, which | |
| # would block other copy ops from other cuda streams. | |
| # A cuda event sync would avoid such a situation. Since | |
| # this is in the critical path of every single model | |
| # forward loop, this has caused perf issue for a disagg | |
| # setup. | |
| pinned = self.sampled_token_ids_pinned_cpu[: sampled_token_ids.shape[0]] | |
| pinned.copy_(sampled_token_ids, non_blocking=True) | |
| self.transfer_event.record() | |
| self.transfer_event.synchronize() | |
| return pinned.tolist() | |
| def get_encoder_timing_stats(self) -> dict[str, dict[str, float | int]]: | |
| """ | |
| Get encoder timing stats for all requests and clear the registry. | |
| Returns: | |
| Dictionary mapping request_id to stats dict. | |
| """ | |
| with self._encoder_timing_lock: | |
| stats = { | |
| req_id: stats_obj.to_dict() | |
| for req_id, stats_obj in self.encoder_timing_registry.items() | |
| } | |
| self.encoder_timing_registry.clear() | |
| return stats | |
| def timed_encoder_operation( | |
| self, | |
| should_time: bool, | |
| group_lora_refs: list[tuple[str, Any]], | |
| current_item_idx: int, | |
| num_items: int, | |
| ): | |
| """ | |
| Context manager to time encoder forward operations. | |
| Args: | |
| should_time: Whether timing is enabled | |
| group_lora_refs: Full list of (request_id, pos_info) tuples | |
| current_item_idx: Starting index for this group | |
| num_items: Number of items in this group | |
| """ | |
| if not should_time: | |
| yield | |
| return | |
| group_refs = group_lora_refs[current_item_idx : current_item_idx + num_items] | |
| group_request_ids = {req_id for req_id, _ in group_refs} | |
| torch.accelerator.synchronize() | |
| start_time = time.perf_counter() | |
| try: | |
| yield | |
| finally: | |
| torch.accelerator.synchronize() | |
| elapsed = time.perf_counter() - start_time | |
| per_request_time = elapsed / max(len(group_request_ids), 1) | |
| with self._encoder_timing_lock: | |
| for req_id in group_request_ids: | |
| if req_id not in self.encoder_timing_registry: | |
| self.encoder_timing_registry[req_id] = EncoderTimingStats() | |
| stats = self.encoder_timing_registry[req_id] | |
| stats.encoder_forward_secs += per_request_time | |
| stats.num_encoder_calls += 1 | |
| class EncoderTimingStats: | |
| """Per-request timing statistics for encoder forward pass.""" | |
| encoder_forward_secs: float = 0.0 | |
| """Time spent in vision encoder forward pass (seconds).""" | |
| num_encoder_calls: int = 0 | |
| """Number of times encoder was called for this request.""" | |
| def to_dict(self) -> dict[str, float | int]: | |
| return { | |
| "encoder_forward_secs": self.encoder_forward_secs, | |
| "num_encoder_calls": self.num_encoder_calls, | |
| } | |