7752 lines
336 KiB
Python
7752 lines
336 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||
|
||
import functools
|
||
import gc
|
||
import itertools
|
||
import threading
|
||
import time
|
||
from collections import defaultdict
|
||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||
from contextlib import contextmanager
|
||
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.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 (
|
||
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 get_mm_features_in_window, group_and_batch_mm_kwargs
|
||
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,
|
||
get_dtype_size,
|
||
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,
|
||
)
|
||
|
||
# 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()
|
||
|
||
@torch.inference_mode()
|
||
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_tensor in kv_caches:
|
||
if cache_tensor is not None:
|
||
cache_tensor.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,
|
||
pin_memory=PIN_MEMORY,
|
||
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,
|
||
max_concurrency=self.vllm_config.max_concurrent_batches,
|
||
)
|
||
|
||
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 _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:
|
||
self.requests.pop(req_id, None)
|
||
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.
|
||
for mm_hash in scheduler_output.free_encoder_mm_hashes:
|
||
self.encoder_cache.pop(mm_hash, None)
|
||
|
||
# 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
|
||
|
||
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,
|
||
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 _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.encoder_cache[mm_hashes[i]] = pe_tensor.to(self.device)
|
||
self.maybe_save_ec_to_connector(self.encoder_cache, mm_hashes[i])
|
||
# 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.encoder_cache[mm_hash] = output
|
||
logger.debug("Finish execute for mm hash %s", mm_hash)
|
||
self.maybe_save_ec_to_connector(self.encoder_cache, mm_hash)
|
||
|
||
return encoder_outputs
|
||
|
||
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.encoder_cache.get(mm_hash, None)
|
||
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
|
||
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
|
||
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,
|
||
)
|
||
)
|
||
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):
|
||
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,
|
||
)
|
||
|
||
@contextmanager
|
||
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,
|
||
)
|
||
|
||
@staticmethod
|
||
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())
|
||
|
||
@torch.inference_mode()
|
||
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,
|
||
)
|
||
# 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
|
||
)
|
||
|
||
@torch.inference_mode
|
||
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:
|
||
mm_embed_inputs = self._gather_mm_embeddings(
|
||
scheduler_output,
|
||
shift_computed_tokens=1,
|
||
)
|
||
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():
|
||
assert config_name in allowed_config_names, (
|
||
f"Config `{config_name}` not supported. "
|
||
f"Allowed configs: {allowed_config_names}"
|
||
)
|
||
config = getattr(self, config_name)
|
||
new_config = update_config(config, config_overrides)
|
||
setattr(self, config_name, new_config)
|
||
|
||
@instrument(span_name="Loading (GPU)")
|
||
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 logprobs.
|
||
logprobs = self.sampler.compute_logprobs(logits)
|
||
token_ids, logprobs, ranks, _ = self.sampler.gather_logprobs(
|
||
logprobs, 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 {}
|
||
|
||
@contextmanager
|
||
def maybe_randomize_inputs(
|
||
self, input_ids: torch.Tensor | None, inputs_embeds: torch.Tensor | None
|
||
):
|
||
"""
|
||
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 = envs.VLLM_RANDOMIZE_DP_DUMMY_INPUTS and dp_size > 1
|
||
if not randomize_inputs:
|
||
yield
|
||
elif input_ids is not None:
|
||
|
||
@functools.cache
|
||
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:
|
||
|
||
@functools.cache
|
||
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,
|
||
)
|
||
)
|
||
|
||
@torch.inference_mode()
|
||
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,
|
||
) -> 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),
|
||
for_cudagraph_capture=is_graph_capturing,
|
||
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),
|
||
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]
|
||
|
||
@torch.inference_mode()
|
||
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_logits",
|
||
"processed_logprobs",
|
||
):
|
||
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
|
||
|
||
@torch.inference_mode()
|
||
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)
|
||
min_blocks = 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")
|
||
|
||
@staticmethod
|
||
@contextmanager
|
||
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")
|
||
|
||
@torch.inference_mode()
|
||
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),
|
||
)
|
||
|
||
@torch.inference_mode()
|
||
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")
|
||
|
||
@torch.inference_mode()
|
||
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
|
||
|
||
# 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):
|
||
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)
|
||
|
||
@instrument(span_name="Capture model")
|
||
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)
|
||
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,
|
||
)
|
||
torch.accelerator.synchronize()
|
||
|
||
# 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,
|
||
):
|
||
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,
|
||
)
|
||
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,
|
||
):
|
||
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,
|
||
)
|
||
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_extract_hidden_states()
|
||
):
|
||
assert isinstance(
|
||
self.drafter,
|
||
EagleProposer
|
||
| DFlashProposer
|
||
| 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,
|
||
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]
|
||
state_tensors = []
|
||
storage_offset_bytes = 0
|
||
for shape, dtype in zip(kv_cache_spec.shapes, kv_cache_spec.dtypes):
|
||
dtype_size = get_dtype_size(dtype)
|
||
num_element_per_page = (
|
||
kv_cache_spec.page_size_bytes // dtype_size
|
||
)
|
||
target_shape = (num_blocks, *shape)
|
||
stride = torch.empty(target_shape).stride()
|
||
target_stride = (num_element_per_page, *stride[1:])
|
||
assert storage_offset_bytes % dtype_size == 0
|
||
tensor = torch.as_strided(
|
||
raw_tensor.view(dtype),
|
||
size=target_shape,
|
||
stride=target_stride,
|
||
storage_offset=storage_offset_bytes // dtype_size,
|
||
)
|
||
state_tensors.append(tensor)
|
||
storage_offset_bytes += stride[0] * dtype_size
|
||
|
||
kv_caches[layer_name] = state_tensors
|
||
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.router.base_router import (
|
||
BaseRouter,
|
||
)
|
||
|
||
for module in self.compilation_config.static_forward_context.values():
|
||
if isinstance(module, MoERunner) and isinstance(module.router, BaseRouter):
|
||
layer_id = module.layer_id
|
||
|
||
def _capture_fn(topk_ids, _layer_id=layer_id, _capturer=capturer):
|
||
_capturer.capture(_layer_id, topk_ids)
|
||
|
||
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
|
||
|
||
@contextmanager
|
||
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
|
||
|
||
|
||
@dataclass
|
||
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,
|
||
}
|