main
py 7,522 lines 327 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import functools
5 import gc
6 import itertools
7 import threading
8 import time
9 from collections import defaultdict
10 from collections.abc import Callable, Iterable, Iterator, Sequence
11 from contextlib import AbstractContextManager, contextmanager, nullcontext
12 from copy import copy, deepcopy
13 from dataclasses import replace
14 from functools import reduce
15 from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias, cast
16
17 import numpy as np
18 import torch
19 import torch.distributed
20 import torch.nn as nn
21 from tqdm import tqdm
22
23 import vllm.envs as envs
24 from vllm.compilation.breakable_cudagraph import (
25 BreakableCUDAGraphWrapper,
26 is_breakable_cudagraph_enabled,
27 )
28 from vllm.compilation.counter import compilation_counter
29 from vllm.compilation.cuda_graph import CUDAGraphStat, CUDAGraphWrapper
30 from vllm.compilation.monitor import set_cudagraph_capturing_enabled
31 from vllm.config import (
32 CompilationMode,
33 CUDAGraphMode,
34 VllmConfig,
35 get_layers_from_vllm_config,
36 set_current_vllm_config,
37 update_config,
38 )
39 from vllm.config.cache import CacheConfig
40 from vllm.config.ec_manager_config import EncoderCacheManagerMetadata
41 from vllm.config.model import PROCESSED_LOGPROBS_MODES
42 from vllm.distributed.ec_transfer import get_ec_transfer, has_ec_transfer
43 from vllm.distributed.eplb.eplb_state import EplbState
44 from vllm.distributed.kv_transfer import get_kv_transfer_group, has_kv_transfer_group
45 from vllm.distributed.kv_transfer.kv_connector.utils import (
46 copy_kv_blocks,
47 )
48 from vllm.distributed.parallel_state import (
49 get_dcp_group,
50 get_pp_group,
51 get_tp_group,
52 graph_capture,
53 is_global_first_rank,
54 )
55 from vllm.forward_context import (
56 BatchDescriptor,
57 set_forward_context,
58 )
59 from vllm.logger import init_logger
60 from vllm.lora.layers import BaseLayerWithLoRA, LoRAMapping, LoRAMappingType
61 from vllm.model_executor.layers.attention import Attention
62 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
63 from vllm.model_executor.layers.fused_moe.all2all_utils import get_ep_all2all_manager
64 from vllm.model_executor.layers.mamba.mamba_utils import MambaStateCopyFuncsByType
65 from vllm.model_executor.layers.mamba.ops.ssu_dispatch import (
66 initialize_mamba_ssu_backend,
67 )
68 from vllm.model_executor.layers.rotary_embedding import MRotaryEmbedding
69 from vllm.model_executor.model_loader import get_model_loader
70 from vllm.model_executor.model_loader.reload import (
71 finalize_layerwise_reload,
72 initialize_layerwise_reload,
73 )
74 from vllm.model_executor.models.interfaces import (
75 MixtureOfExperts,
76 MultiModalEmbeddings,
77 SupportsMRoPE,
78 SupportsMultiModal,
79 get_mixture_of_experts_model,
80 supports_eagle3,
81 supports_mrope,
82 supports_multimodal_pruning,
83 supports_realtime,
84 supports_transcription,
85 )
86 from vllm.model_executor.models.interfaces_base import (
87 VllmModelForPooling,
88 is_pooling_model,
89 is_text_generation_model,
90 )
91 from vllm.model_executor.offloader import (
92 create_offloader,
93 get_offloader,
94 set_offloader,
95 )
96 from vllm.model_executor.warmup.jit_warmup import JitWarmupRegistry
97 from vllm.multimodal import MULTIMODAL_REGISTRY
98 from vllm.multimodal.encoder_budget import MultiModalBudget
99 from vllm.multimodal.inputs import (
100 BatchedTensorInputs,
101 MultiModalKwargsItem,
102 PlaceholderRange,
103 )
104 from vllm.multimodal.utils import (
105 copy_mm_embedding_modality,
106 get_mm_features_in_window,
107 group_and_batch_mm_kwargs,
108 set_mm_embedding_modality,
109 )
110 from vllm.platforms import current_platform
111 from vllm.pooling_params import PoolingParams
112 from vllm.sampling_params import SamplingType
113 from vllm.sequence import IntermediateTensors
114 from vllm.tasks import GenerationTask, PoolingTask, SupportedTask
115 from vllm.tracing import instrument
116 from vllm.utils import length_from_prompt_token_ids_or_embeds
117 from vllm.utils.gc_utils import freeze_gc_for_cudagraph_capture
118 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
119 from vllm.utils.math_utils import cdiv, round_up
120 from vllm.utils.mem_utils import DeviceMemoryProfiler, format_gib
121 from vllm.utils.nvtx_pytorch_hooks import PytHooks
122 from vllm.utils.platform_utils import num_compute_units
123 from vllm.utils.torch_utils import (
124 PIN_MEMORY,
125 async_tensor_h2d,
126 kv_cache_dtype_str_to_dtype,
127 )
128 from vllm.v1.attention.backend import (
129 AttentionBackend,
130 AttentionCGSupport,
131 AttentionMetadata,
132 AttentionMetadataBuilder,
133 AttentionType,
134 CommonAttentionMetadata,
135 )
136 from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadataBuilder
137 from vllm.v1.attention.backends.linear_attn import (
138 BailingLinearAttentionMetadataBuilder,
139 )
140 from vllm.v1.attention.backends.mamba2_attn import Mamba2AttentionMetadataBuilder
141 from vllm.v1.attention.backends.short_conv_attn import ShortConvAttentionMetadataBuilder
142 from vllm.v1.attention.backends.utils import (
143 NULL_BLOCK_ID,
144 create_fast_prefill_custom_backend,
145 get_dcp_local_seq_lens,
146 reorder_batch_to_split_decodes_and_prefills,
147 )
148 from vllm.v1.core.sched.output import NewRequestData
149 from vllm.v1.cudagraph_dispatcher import CudagraphDispatcher
150 from vllm.v1.kv_cache_interface import (
151 AttentionSpec,
152 ChunkedLocalAttentionSpec,
153 CrossAttentionSpec,
154 EncoderOnlyAttentionSpec,
155 FullAttentionSpec,
156 KVCacheConfig,
157 KVCacheGroupSpec,
158 KVCacheSpec,
159 KVCacheSpecKind,
160 SlidingWindowSpec,
161 UniformTypeKVCacheSpecs,
162 get_kv_cache_spec_kind,
163 )
164 from vllm.v1.kv_cache_spec_registry import KVCacheSpecRegistry
165 from vllm.v1.outputs import (
166 EMPTY_MODEL_RUNNER_OUTPUT,
167 AsyncModelRunnerOutput,
168 DraftTokenIds,
169 ECConnectorOutput,
170 KVConnectorOutput,
171 LogprobsLists,
172 LogprobsTensors,
173 ModelRunnerOutput,
174 PoolerOutput,
175 SamplerOutput,
176 make_empty_encoder_model_runner_output,
177 )
178 from vllm.v1.pool.late_interaction_runner import LateInteractionRunner
179 from vllm.v1.pool.metadata import PoolingMetadata, PoolingStates
180 from vllm.v1.sample.logits_processor import LogitsProcessors, build_logitsprocs
181 from vllm.v1.sample.logits_processor.interface import LogitsProcessor
182 from vllm.v1.sample.metadata import SamplingMetadata
183 from vllm.v1.sample.rejection_sampler import RejectionSampler
184 from vllm.v1.sample.sampler import Sampler
185 from vllm.v1.spec_decode.custom_class_proposer import create_custom_proposer
186 from vllm.v1.spec_decode.dflash import DFlashProposer
187 from vllm.v1.spec_decode.draft_model import DraftModelProposer
188 from vllm.v1.spec_decode.eagle import EagleProposer
189 from vllm.v1.spec_decode.extract_hidden_states import ExtractHiddenStatesProposer
190 from vllm.v1.spec_decode.gemma4 import Gemma4Proposer
191 from vllm.v1.spec_decode.medusa import MedusaProposer
192 from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
193 from vllm.v1.spec_decode.ngram_proposer_gpu import (
194 NgramProposerGPU,
195 copy_num_valid_draft_tokens,
196 update_ngram_gpu_tensors_incremental,
197 update_scheduler_for_invalid_drafts,
198 )
199 from vllm.v1.spec_decode.step3p5 import Step3p5MTPProposer
200 from vllm.v1.spec_decode.suffix_decoding import SuffixDecodingProposer
201 from vllm.v1.spec_decode.utils import update_num_computed_tokens_for_batch_change
202 from vllm.v1.structured_output.utils import apply_grammar_bitmask
203 from vllm.v1.utils import CpuGpuBuffer, record_function_or_nullcontext
204 from vllm.v1.worker import mamba_utils
205 from vllm.v1.worker.block_table import SlotMappingMode
206 from vllm.v1.worker.cp_utils import (
207 check_attention_cp_compatibility,
208 get_dcp_dummy_context_len,
209 prepare_dcp_dummy_context_metadata,
210 )
211 from vllm.v1.worker.dp_utils import coordinate_batch_across_dp
212 from vllm.v1.worker.ec_connector_model_runner_mixin import ECConnectorModelRunnerMixin
213 from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
214 from vllm.v1.worker.gpu_ubatch_wrapper import UBatchWrapper
215 from vllm.v1.worker.kv_connector_model_runner_mixin import KVConnectorModelRunnerMixin
216 from vllm.v1.worker.lora_model_runner_mixin import LoRAModelRunnerMixin
217 from vllm.v1.worker.ubatch_utils import (
218 UBatchSlices,
219 check_ubatch_thresholds,
220 maybe_create_ubatch_slices,
221 split_attn_metadata,
222 )
223 from vllm.v1.worker.utils import (
224 EncoderTimingStats,
225 is_residual_scattered_for_sp,
226 raise_if_nan_logits,
227 )
228 from vllm.v1.worker.workspace import lock_workspace
229
230 from .utils import (
231 AttentionGroup,
232 KVBlockZeroer,
233 add_kv_sharing_layers_to_kv_cache_groups,
234 allocate_kv_cache,
235 bind_kv_cache,
236 copy_kv_cache_blocks_inplace,
237 prepare_kernel_block_sizes,
238 sanity_check_mm_encoder_outputs,
239 )
240
241 if TYPE_CHECKING:
242 from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
243 from vllm.v1.spec_decode.ngram_proposer import NgramProposer
244 from vllm.v1.worker.encoder_cudagraph import EncoderCudaGraphManager
245
246 logger = init_logger(__name__)
247
248
249 def _get_parameter_for_reload(model: nn.Module, name: str) -> nn.Parameter:
250 """Resolve checkpoint names without changing the model's module tree."""
251 module_name, _, parameter_name = name.rpartition(".")
252 module = model.get_submodule(module_name)
253 if isinstance(module, BaseLayerWithLoRA):
254 module = module.base_layer
255 return module.get_parameter(parameter_name)
256
257
258 AttnMetadataDict: TypeAlias = dict[str, AttentionMetadata]
259 # list when ubatching is enabled
260 PerLayerAttnMetadata: TypeAlias = list[AttnMetadataDict] | AttnMetadataDict
261
262
263 def count_nans_per_row(logits: torch.Tensor) -> torch.Tensor:
264 """Per-row NaN counts, left on device."""
265 return logits.isnan().sum(dim=-1, dtype=torch.int32)
266
267
268 def nans_to_dict(counts: list[int], req_id_to_index: dict[str, int]) -> dict[str, int]:
269 """Map per-row NaN counts onto request ids.
270
271 Rows and requests need not correspond one-to-one, so requests without a
272 row are reported as 0.
273 """
274 return {
275 req_id: int(counts[i]) if i < len(counts) else 0
276 for req_id, i in req_id_to_index.items()
277 }
278
279
280 # Wrapper for ModelRunnerOutput to support overlapped execution.
281 class AsyncGPUModelRunnerOutput(AsyncModelRunnerOutput):
282 def __init__(
283 self,
284 model_runner_output: ModelRunnerOutput,
285 sampled_token_ids: torch.Tensor,
286 logprobs_tensors: LogprobsTensors | None,
287 invalid_req_indices: list[int],
288 async_output_copy_stream: torch.cuda.Stream,
289 vocab_size: int,
290 check_ep_fault: bool = False,
291 num_nans: torch.Tensor | None = None,
292 ):
293 self._model_runner_output = model_runner_output
294 self._invalid_req_indices = invalid_req_indices
295
296 # Event on the copy stream so we can synchronize the non-blocking copy.
297 # Blocking (sleep) event to avoid busy-polling the CUDA driver lock.
298 self.async_copy_ready_event = torch.cuda.Event(blocking=True)
299
300 # Keep a reference to the device tensor to avoid it being
301 # deallocated until we finish copying it to the host.
302 self._sampled_token_ids = sampled_token_ids
303 self.vocab_size = vocab_size
304 self._logprobs_tensors = logprobs_tensors
305 self._num_nans = num_nans
306 self._has_fault: torch.Tensor | None = None
307
308 # Initiate the copy on a separate stream, but do not synchronize it.
309 default_stream = torch.cuda.current_stream()
310 with torch.cuda.stream(async_output_copy_stream):
311 async_output_copy_stream.wait_stream(default_stream)
312 self.sampled_token_ids_cpu = self._sampled_token_ids.to(
313 "cpu", non_blocking=True
314 )
315 self._logprobs_tensors_cpu = (
316 self._logprobs_tensors.to_cpu_nonblocking()
317 if self._logprobs_tensors
318 else None
319 )
320 self._num_nans_cpu = (
321 self._num_nans.to("cpu", non_blocking=True)
322 if self._num_nans is not None
323 else None
324 )
325 if check_ep_fault:
326 has_fault = get_ep_all2all_manager().query_fault()
327 self._has_fault = has_fault.to("cpu", non_blocking=True)
328 self.async_copy_ready_event.record()
329
330 def get_output(self) -> ModelRunnerOutput:
331 """Copy the device tensors to the host and return a ModelRunnerOutput.
332
333 This function blocks until the copy is finished.
334 """
335 max_gen_len = self.sampled_token_ids_cpu.shape[-1]
336 self.async_copy_ready_event.synchronize()
337
338 # Release the device tensors once the copy has completed.
339 del self._logprobs_tensors
340 del self._sampled_token_ids
341 if max_gen_len == 1:
342 valid_sampled_token_ids = self.sampled_token_ids_cpu.tolist()
343 for i in self._invalid_req_indices:
344 valid_sampled_token_ids[i].clear()
345 logprobs_lists = None
346 if self._logprobs_tensors_cpu is not None:
347 logprobs_lists = self._logprobs_tensors_cpu.tolists()
348 else:
349 valid_sampled_token_ids, logprobs_lists = RejectionSampler.parse_output(
350 self.sampled_token_ids_cpu,
351 self.vocab_size,
352 self._invalid_req_indices,
353 logprobs_tensors=self._logprobs_tensors_cpu,
354 )
355
356 output = self._model_runner_output
357 output.sampled_token_ids = valid_sampled_token_ids
358 output.logprobs = logprobs_lists
359
360 if self._num_nans_cpu is not None:
361 output.num_nans_in_logits = nans_to_dict(
362 self._num_nans_cpu.tolist(), output.req_id_to_index
363 )
364 if envs.VLLM_RAISE_ON_LOGIT_NANS:
365 raise_if_nan_logits(output.num_nans_in_logits)
366 del self._num_nans
367
368 if self._has_fault is not None and self._has_fault.item():
369 mask = get_ep_all2all_manager().query_active_mask()
370 raise RuntimeError(
371 "Fault detected in EP all2all communication: "
372 "one or more ranks timed out during dispatch/combine. "
373 f"Mask: {mask.cpu().tolist()}"
374 )
375
376 return output
377
378
379 def _copy_pooler_output_to_cpu(
380 raw_pooler_output: PoolerOutput, finished_mask: list[bool]
381 ) -> list[torch.Tensor | None]:
382 num_reqs = len(finished_mask)
383
384 if isinstance(raw_pooler_output, torch.Tensor):
385 if raw_pooler_output.shape[0] != num_reqs:
386 raise ValueError(
387 "Pooler output batch size does not match finished mask size: "
388 f"{raw_pooler_output.shape[0]} != {num_reqs}."
389 )
390
391 num_finished = sum(finished_mask)
392 if num_finished == 0:
393 return [None] * num_reqs
394 if num_finished == num_reqs:
395 return list(raw_pooler_output.to("cpu", non_blocking=True))
396
397 # partial finished
398 finished_indices = [i for i, include in enumerate(finished_mask) if include]
399 index_tensor = torch.tensor(
400 finished_indices, device=raw_pooler_output.device, dtype=torch.long
401 )
402 finished_outputs = raw_pooler_output.index_select(0, index_tensor).to(
403 "cpu", non_blocking=True
404 )
405 partial_pooler_output: list[torch.Tensor | None] = [None] * num_reqs
406 for i, out in zip(finished_indices, finished_outputs):
407 partial_pooler_output[i] = out
408 return partial_pooler_output
409
410 assert isinstance(raw_pooler_output, list)
411 if len(raw_pooler_output) != num_reqs:
412 raise ValueError(
413 "Pooler output batch size does not match finished mask size: "
414 f"{len(raw_pooler_output)} != {num_reqs}."
415 )
416
417 pooler_output: list[torch.Tensor | None] = [None] * num_reqs
418 for i, (out, include) in enumerate(zip(raw_pooler_output, finished_mask)):
419 if include and out is not None:
420 pooler_output[i] = out.to("cpu", non_blocking=True)
421 return pooler_output
422
423
424 class AsyncGPUPoolingModelRunnerOutput(AsyncModelRunnerOutput):
425 def __init__(
426 self,
427 model_runner_output: ModelRunnerOutput,
428 raw_pooler_output: PoolerOutput,
429 finished_mask: list[bool],
430 async_output_copy_stream: torch.cuda.Stream,
431 ):
432 self._model_runner_output = model_runner_output
433
434 # Event on the copy stream so we can synchronize the non-blocking copy.
435 # Blocking (sleep) event to avoid busy-polling the CUDA driver lock.
436 self.async_copy_ready_event = torch.cuda.Event(blocking=True)
437
438 # Keep a reference to the device tensors to avoid them being
439 # deallocated until we finish copying it to the host.
440 self._raw_pooler_output = raw_pooler_output
441
442 # Initiate the copy on a separate stream, but do not synchronize it.
443 default_stream = torch.cuda.current_stream()
444 with torch.cuda.stream(async_output_copy_stream):
445 async_output_copy_stream.wait_stream(default_stream)
446 self._model_runner_output.pooler_output = _copy_pooler_output_to_cpu(
447 raw_pooler_output=self._raw_pooler_output,
448 finished_mask=finished_mask,
449 )
450 self.async_copy_ready_event.record()
451
452 def get_output(self) -> ModelRunnerOutput:
453 """Copy the device tensors to the host and return a ModelRunnerOutput.
454 This function blocks until the copy is finished.
455 """
456 self.async_copy_ready_event.synchronize()
457
458 # Release the device tensors once the copy has completed.
459 del self._raw_pooler_output
460 return self._model_runner_output
461
462
463 class ExecuteModelState(NamedTuple):
464 """Ephemeral cached state transferred between execute_model() and
465 sample_tokens(), after execute_model() returns None."""
466
467 scheduler_output: "SchedulerOutput"
468 logits: torch.Tensor
469 spec_decode_metadata: SpecDecodeMetadata | None
470 spec_decode_common_attn_metadata: CommonAttentionMetadata | None
471 hidden_states: torch.Tensor
472 sample_hidden_states: torch.Tensor
473 aux_hidden_states: list[torch.Tensor] | None
474 ec_connector_output: ECConnectorOutput | None
475 cudagraph_stats: CUDAGraphStat | None
476 slot_mappings: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None
477
478
479 class GPUModelRunner(
480 LoRAModelRunnerMixin, KVConnectorModelRunnerMixin, ECConnectorModelRunnerMixin
481 ):
482 @JitWarmupRegistry.capture
483 def __init__(
484 self,
485 vllm_config: VllmConfig,
486 device: torch.device,
487 ):
488 self.vllm_config = vllm_config
489 self.model_config = vllm_config.model_config
490 self.cache_config = vllm_config.cache_config
491 self.offload_config = vllm_config.offload_config
492 self.compilation_config = vllm_config.compilation_config
493 self.lora_config = vllm_config.lora_config
494 self.load_config = vllm_config.load_config
495 self.parallel_config = vllm_config.parallel_config
496 self.scheduler_config = vllm_config.scheduler_config
497 self.speculative_config = vllm_config.speculative_config
498 self.observability_config = vllm_config.observability_config
499
500 model_config = self.model_config
501 cache_config = self.cache_config
502 scheduler_config = self.scheduler_config
503 parallel_config = self.parallel_config
504 self.device = device
505 self.dtype = self.model_config.dtype
506
507 self.check_ep_fault = False
508 if parallel_config.data_parallel_size > 1 and self.model_config.is_moe:
509 self.check_ep_fault = get_ep_all2all_manager().support_fault_tolerance
510
511 self.kv_cache_dtype = kv_cache_dtype_str_to_dtype(
512 cache_config.cache_dtype, self.model_config
513 )
514
515 self.is_pooling_model = model_config.runner_type == "pooling"
516 self.enable_prompt_embeds = model_config.enable_prompt_embeds
517 self.is_multimodal_raw_input_only_model = (
518 model_config.is_multimodal_raw_input_only_model
519 )
520 # These will be overridden in load_model()
521 self.is_multimodal_pruning_enabled = False
522 self.requires_sequential_video_encoding = False
523 self.max_model_len = model_config.max_model_len
524
525 # Always set to false after the first forward pass
526 self.dcp_world_size = self.parallel_config.decode_context_parallel_size
527 self.dcp_rank = 0 if self.dcp_world_size <= 1 else get_dcp_group().rank_in_group
528 self.max_num_tokens = scheduler_config.max_num_batched_tokens
529 self.max_num_reqs = scheduler_config.max_num_seqs
530
531 # Broadcast PP output for external_launcher (torchrun)
532 # to make sure we are synced across pp ranks
533 # TODO: Support overlapping micro-batches
534 # https://github.com/vllm-project/vllm/issues/18019
535 self.broadcast_pp_output = (
536 self.parallel_config.distributed_executor_backend == "external_launcher"
537 and len(get_pp_group().ranks) > 1
538 )
539
540 # Model-related.
541 self.num_query_heads = model_config.get_num_attention_heads(parallel_config)
542 self.inputs_embeds_size = model_config.get_inputs_embeds_size()
543 # Only relevant for models using ALiBi (e.g, MPT)
544 self.use_alibi = model_config.uses_alibi
545
546 self.cascade_attn_enabled = not self.model_config.disable_cascade_attn
547 self.is_mm_prefix_lm = self.model_config.is_mm_prefix_lm
548
549 # Multi-modal data support
550 self.mm_registry = MULTIMODAL_REGISTRY
551 self.uses_mrope = model_config.uses_mrope
552 self.mrope_num_dims = model_config.mrope_num_dims
553 self.supports_mm_inputs = model_config.supports_multimodal_inputs
554
555 if self.model_config.is_encoder_decoder:
556 # Maximum length of the encoder input, only for encoder-decoder
557 # models.
558 self.max_encoder_len = scheduler_config.max_num_encoder_input_tokens
559 else:
560 self.max_encoder_len = 0
561
562 # Async scheduling
563 self.use_async_scheduling = self.scheduler_config.async_scheduling
564
565 # Async PP broadcast of sampled token ids, waited on in _prepare_input_ids.
566 self._pp_recv_work: torch.distributed.Work | None = None
567
568 # Sampler
569 self.sampler = Sampler(
570 logprobs_mode=self.model_config.logprobs_mode,
571 use_fp64_gumbel=self.model_config.use_fp64_gumbel,
572 )
573
574 self.eplb_state: EplbState | None = None
575 self._moe_model: MixtureOfExperts | None = None
576 # NOTE(yongji): flag to temporarily disable EPLB during scaling up/down
577 self.eep_eplb_suppressed = False
578 """
579 State of the expert parallelism load balancer.
580
581 Will be lazily initialized when the model is loaded.
582 """
583
584 # Lazy initializations
585 # self.model: nn.Module # Set after load_model
586 # Initialize in initialize_kv_cache
587 self.kv_caches: list[torch.Tensor] = []
588 # indexes: [kv_cache_group_id][attn_group]
589 self.attn_groups: list[list[AttentionGroup]] = []
590 # self.kv_cache_config: KVCacheConfig
591
592 # mm_hash -> encoder_output
593 self.encoder_cache: dict[str, torch.Tensor] = {}
594 self.late_interaction_runner = LateInteractionRunner()
595
596 # Encoder CUDA graph manager (initialized after model load if enabled)
597 self.encoder_cudagraph_manager: EncoderCudaGraphManager | None = None
598
599 self.use_aux_hidden_state_outputs = False
600 # Set up speculative decoding.
601 # NOTE(Jiayi): currently we put the entire draft model on
602 # the last PP rank. This is not ideal if there are many
603 # layers in the draft model.
604 if self.speculative_config and get_pp_group().is_last_rank:
605 self.drafter: (
606 NgramProposer # noqa: F823
607 | NgramProposerGPU
608 | SuffixDecodingProposer
609 | EagleProposer
610 | DFlashProposer
611 | DraftModelProposer
612 | MedusaProposer
613 | ExtractHiddenStatesProposer
614 | Gemma4Proposer
615 | Step3p5MTPProposer
616 )
617 if self.speculative_config.method == "custom_class":
618 self.drafter = create_custom_proposer( # type: ignore[assignment]
619 self.vllm_config
620 )
621 elif self.speculative_config.method == "ngram":
622 from vllm.v1.spec_decode.ngram_proposer import NgramProposer
623
624 self.drafter = NgramProposer(self.vllm_config)
625 elif self.speculative_config.uses_draft_model():
626 self.drafter = DraftModelProposer(
627 vllm_config=self.vllm_config,
628 device=self.device,
629 runner=self,
630 )
631 elif self.speculative_config.use_ngram_gpu():
632 self.drafter = NgramProposerGPU(self.vllm_config, self.device, self)
633 self.num_tokens_no_spec_gpu = torch.zeros(
634 self.max_num_reqs, dtype=torch.int32, device=device
635 )
636 self.token_ids_gpu_tensor = torch.zeros(
637 self.max_num_reqs,
638 self.max_model_len,
639 dtype=torch.int32,
640 device=device,
641 )
642 self._ngram_pinned_idx_buf = torch.zeros(
643 self.max_num_reqs, dtype=torch.long, pin_memory=True
644 )
645 self._ngram_pinned_val_buf = torch.zeros(
646 self.max_num_reqs, dtype=torch.int32, pin_memory=True
647 )
648 elif self.speculative_config.use_gemma4_mtp():
649 self.drafter = Gemma4Proposer(self.vllm_config, self.device, self)
650 elif self.speculative_config.use_step3p5_mtp():
651 self.drafter = Step3p5MTPProposer(self.vllm_config, self.device, self)
652 elif self.speculative_config.use_dflash():
653 self.drafter = DFlashProposer(self.vllm_config, self.device, self)
654 self.use_aux_hidden_state_outputs = True
655 elif self.speculative_config.method == "suffix":
656 self.drafter = SuffixDecodingProposer(self.vllm_config)
657 elif self.speculative_config.use_eagle():
658 self.drafter = EagleProposer(self.vllm_config, self.device, self)
659 if self.speculative_config.method == "eagle3":
660 self.use_aux_hidden_state_outputs = (
661 self.drafter.eagle3_use_aux_hidden_state
662 )
663 elif self.speculative_config.method == "medusa":
664 self.drafter = MedusaProposer(
665 vllm_config=self.vllm_config, device=self.device
666 )
667 elif self.speculative_config.method == "extract_hidden_states":
668 self.drafter = ExtractHiddenStatesProposer(
669 vllm_config=self.vllm_config, device=self.device
670 )
671 self.use_aux_hidden_state_outputs = True
672 else:
673 raise ValueError(
674 "Unknown speculative decoding method: "
675 f"{self.speculative_config.method}"
676 )
677 self.rejection_sampler = RejectionSampler(
678 self.sampler, self.speculative_config, self.device
679 )
680
681 self.num_spec_tokens = 0
682 self.prev_num_spec_tokens = 0
683 self.valid_sampled_token_count_gpu: torch.Tensor | None = None
684 if self.speculative_config:
685 self.num_spec_tokens = self.speculative_config.num_speculative_tokens
686 self.prev_num_spec_tokens = self.num_spec_tokens
687 draft_config = self.speculative_config.draft_model_config
688 if draft_config is not None and draft_config.max_model_len is not None:
689 self.effective_drafter_max_model_len = draft_config.max_model_len
690 else:
691 self.effective_drafter_max_model_len = self.max_model_len
692 self.use_async_spec_decode = (
693 self.use_async_scheduling and self.num_spec_tokens > 0
694 )
695
696 # Request states.
697 self.requests: dict[str, CachedRequestState] = {}
698 # NOTE(rob): num_prompt_logprobs only includes reqs
699 # that are currently in the prefill phase.
700 self.num_prompt_logprobs: dict[str, int] = {}
701
702 # Input Batch
703 # NOTE(Chen): Ideally, we should initialize the input batch inside
704 # `initialize_kv_cache` based on the kv cache config. However, as in
705 # https://github.com/vllm-project/vllm/pull/18298, due to some unknown
706 # reasons, we have to initialize the input batch before `load_model`,
707 # quantization + weight offloading will fail otherwise. As a temporary
708 # solution, we initialize the input batch here, and re-initialize it
709 # in `initialize_kv_cache` if the block_sizes here is different from
710 # the block_sizes in the kv cache config.
711 logits_processors = model_config.logits_processors
712 custom_logitsprocs: Sequence[str | type[LogitsProcessor]] = (
713 tuple(logits_processors) if logits_processors is not None else ()
714 )
715 placeholder_block_size = (
716 self.cache_config.block_size or CacheConfig.DEFAULT_BLOCK_SIZE
717 )
718 placeholder_max_num_blocks = cdiv(
719 max(self.max_model_len, self.max_encoder_len), placeholder_block_size
720 )
721 self._init_block_sizes = [placeholder_block_size]
722 self._init_kernel_block_sizes = [placeholder_block_size]
723 self._init_max_num_blocks = [placeholder_max_num_blocks]
724 self._init_slot_mapping_modes = [SlotMappingMode.TOKEN_TO_KV_SLOT]
725 self.cp_kv_cache_interleave_size = (
726 self.parallel_config.cp_kv_cache_interleave_size
727 )
728 # Capture warmup providers registered by the initial placeholder InputBatch
729 self.input_batch = InputBatch(
730 max_num_reqs=self.max_num_reqs,
731 # We need to use the encoder length for encoder-decoder
732 # because of KV cache for cross-attention.
733 max_model_len=max(self.max_model_len, self.max_encoder_len),
734 max_num_batched_tokens=self.max_num_tokens,
735 device=self.device,
736 vocab_size=self.model_config.get_vocab_size(),
737 block_sizes=[placeholder_block_size],
738 kernel_block_sizes=[placeholder_block_size],
739 max_num_blocks_per_req=[placeholder_max_num_blocks],
740 num_spec_tokens=self.num_spec_tokens,
741 logitsprocs=build_logitsprocs(
742 self.vllm_config,
743 self.device,
744 PIN_MEMORY,
745 self.is_pooling_model,
746 custom_logitsprocs,
747 ),
748 # We currently don't know whether a particular custom logits processor
749 # uses output token ids so we set this conservatively. Thinking-budget
750 # tracking is requested dynamically when a budgeted request is in the
751 # batch.
752 logitsprocs_need_output_token_ids=bool(custom_logitsprocs),
753 is_pooling_model=self.is_pooling_model,
754 cp_kv_cache_interleave_size=self.parallel_config.cp_kv_cache_interleave_size,
755 reasoning_config=self.vllm_config.reasoning_config,
756 use_replayssm=self.cache_config.use_replayssm,
757 )
758
759 # Separate cuda stream for overlapping transfer of sampled token ids from
760 # GPU to CPU when async scheduling is enabled.
761 self.async_output_copy_stream: torch.cuda.Stream | None = None
762 # cuda event to synchronize use of reused CPU tensors between steps
763 # when async scheduling is enabled.
764 self.prepare_inputs_event: torch.Event | None = None
765 if self.use_async_scheduling:
766 self.async_output_copy_stream = torch.cuda.Stream()
767 # Blocking (sleep) event to avoid busy-polling the CUDA driver lock;
768 # under TP contention that spin can balloon and make the rank a straggler.
769 self.prepare_inputs_event = torch.cuda.Event(blocking=True)
770
771 # self.cudagraph_batch_sizes sorts in ascending order.
772 if (
773 self.compilation_config.cudagraph_capture_sizes
774 and self.compilation_config.cudagraph_mode != CUDAGraphMode.NONE
775 ):
776 self.cudagraph_batch_sizes = sorted(
777 self.compilation_config.cudagraph_capture_sizes
778 )
779 else:
780 self.cudagraph_batch_sizes = []
781
782 # Cache the device properties.
783 self._init_device_properties()
784
785 # Encoder timing registry for observability
786 self.encoder_timing_registry: dict[str, EncoderTimingStats] = {}
787 self._encoder_timing_lock = threading.Lock()
788
789 # Persistent buffers for CUDA graphs.
790 self.input_ids = self._make_buffer(self.max_num_tokens, dtype=torch.int32)
791 # [max_num_reqs, depth] prompt token ids preceding each request's chunk
792 # start, for models exposing `token_lookback_depth`. Sized after load.
793 self.lookback_token_ids: CpuGpuBuffer | None = None
794 self.positions = torch.zeros(
795 self.max_num_tokens, dtype=torch.int64, device=self.device
796 )
797 self.is_padding = torch.zeros(
798 self.max_num_tokens, dtype=torch.bool, device=self.device
799 )
800 self.query_start_loc = self._make_buffer(
801 self.max_num_reqs + 1, dtype=torch.int32
802 )
803 self.seq_lens = torch.zeros(
804 self.max_num_reqs, dtype=torch.int32, device=self.device
805 )
806 self.optimistic_seq_lens_cpu = torch.zeros(
807 self.max_num_reqs, dtype=torch.int32, pin_memory=PIN_MEMORY
808 )
809 self.num_computed_tokens = torch.zeros(
810 self.max_num_reqs, dtype=torch.int32, device=self.device
811 )
812 self.prev_num_draft_tokens = self._make_buffer(
813 self.max_num_reqs, dtype=torch.int32
814 )
815 self.req_indices = self._make_buffer(self.max_num_tokens, dtype=torch.int64)
816 # Maps current batch position -> previous batch position (-1 for new reqs)
817 self.prev_positions = self._make_buffer(self.max_num_reqs, dtype=torch.int64)
818 self.num_scheduled_tokens = self._make_buffer(
819 self.max_num_reqs, dtype=torch.int32
820 )
821
822 self.encoder_seq_lens = self._make_buffer(self.max_num_reqs, dtype=torch.int32)
823 if self.dcp_world_size > 1:
824 self.dcp_local_seq_lens = self._make_buffer(
825 self.max_num_reqs, dtype=torch.int32
826 )
827 # Because inputs_embeds may be bfloat16 and we don't need a numpy
828 # version of this tensor, avoid a RuntimeError by not creating a
829 # numpy buffer.
830 self.inputs_embeds = self._make_buffer(
831 self.max_num_tokens, self.inputs_embeds_size, dtype=self.dtype, numpy=False
832 )
833 self.is_token_ids = self._make_buffer(self.max_num_tokens, dtype=torch.bool)
834 self.discard_request_mask = self._make_buffer(
835 self.max_num_reqs, dtype=torch.bool
836 )
837 self.num_decode_draft_tokens = self._make_buffer(
838 self.max_num_reqs, dtype=torch.int32
839 )
840 self.num_accepted_tokens = self._make_buffer(
841 self.max_num_reqs, dtype=torch.int32
842 )
843
844 # Only relevant for models using M-RoPE (e.g, Qwen2-VL)
845 if self.uses_mrope:
846 # NOTE: `mrope_positions` is implemented with one additional dummy
847 # position on purpose to make it non-contiguous so that it can work
848 # with torch compile.
849 # See detailed explanation in https://github.com/vllm-project/vllm/pull/12128#discussion_r1926431923
850
851 # NOTE: For text-only inputs, each dimension has identical position
852 # IDs, making M-RoPE functionally equivalent to 1D-RoPE.
853 # See page 5 of https://arxiv.org/abs/2409.12191
854 self.mrope_positions = self._make_buffer(
855 (self.mrope_num_dims, self.max_num_tokens + 1), dtype=torch.int64
856 )
857
858 # None in the first PP rank. The rest are set after load_model.
859 self.intermediate_tensors: IntermediateTensors | None = None
860
861 # OPTIMIZATION: Cache the arange tensors rather than creating them
862 # every step. Keep in int64 to avoid overflow with long context.
863 # - arange_np: immutable [0, 1, 2, ...] used as source for batched computation
864 # - query_pos: CpuGpuBuffer for the computed batched arange result
865 arange_size = max(self.max_num_reqs + 1, self.max_num_tokens)
866 self.arange_np = np.arange(arange_size, dtype=np.int64)
867 self.query_pos = self._make_buffer(arange_size, dtype=torch.int64)
868 self._arange_scratch = np.empty(arange_size, dtype=np.int64)
869
870 # Layer pairings for cross-layer KV sharing.
871 # If an Attention layer `layer_name` is in the keys of this dict, it
872 # means this layer will perform attention using the keys and values
873 # from the KV cache of `shared_kv_cache_layers[layer_name]`.
874 self.shared_kv_cache_layers: dict[str, str] = {}
875 self.kv_sharing_fast_prefill_eligible_layers: set[str] = set()
876
877 self.kv_sharing_fast_prefill_logits_indices = None
878 if self.cache_config.kv_sharing_fast_prefill:
879 self.kv_sharing_fast_prefill_logits_indices = torch.zeros(
880 self.max_num_tokens, dtype=torch.int32, device=self.device
881 )
882
883 self.uniform_decode_query_len = 1 + self.num_spec_tokens
884
885 # Cudagraph dispatcher for runtime cudagraph dispatching.
886 self.cudagraph_dispatcher = CudagraphDispatcher(self.vllm_config)
887
888 self.mm_budget = (
889 MultiModalBudget(self.vllm_config, self.mm_registry)
890 if self.supports_mm_inputs
891 else None
892 )
893
894 self.reorder_batch_threshold: int | None = None
895
896 # Attention layers that are only in the KVCacheConfig of the runner
897 # (e.g., KV sharing, encoder-only attention), but not in the
898 # KVCacheConfig of the scheduler.
899 self.runner_only_attn_layers: set[str] = set()
900
901 # Cached outputs.
902 self._draft_token_ids: list[list[int]] | torch.Tensor | None = None
903 self._draft_probs: torch.Tensor | None = None
904 self._draft_prob_req_ids: list[str] | None = None
905 # N-gram GPU path: async D2H buffer/event for per-request valid draft counts.
906 self._num_valid_draft_tokens: torch.Tensor | None = None
907 self._num_valid_draft_tokens_cpu: torch.Tensor | None = None
908 self._num_valid_draft_tokens_event: torch.cuda.Event | None = None
909 self._num_valid_draft_tokens_copy_stream: torch.cuda.Stream | None = None
910 if (
911 self.speculative_config is not None
912 and self.speculative_config.use_ngram_gpu()
913 ):
914 self._num_valid_draft_tokens_cpu = torch.empty(
915 self.max_num_reqs, dtype=torch.int32, pin_memory=PIN_MEMORY
916 )
917 self._num_valid_draft_tokens_event = torch.cuda.Event()
918 self._num_valid_draft_tokens_copy_stream = torch.cuda.Stream()
919
920 self._draft_token_req_ids: list[str] | None = None
921 self.transfer_event = torch.Event()
922 self.sampled_token_ids_pinned_cpu = torch.empty(
923 (self.max_num_reqs, 1),
924 dtype=torch.int64,
925 device="cpu",
926 pin_memory=PIN_MEMORY,
927 )
928
929 # Pre-allocated tensor for copying valid sampled token counts to CPU,
930 # with dedicated stream for overlapping and event for coordination.
931 self.valid_sampled_token_count_event: torch.Event | None = None
932 self.valid_sampled_token_count_copy_stream: torch.cuda.Stream | None = None
933 # We also copy the drafted tokens to the CPU asynchronously,
934 # in case we need them for structured outputs.
935 self.draft_token_ids_event: torch.Event | None = None
936 self.draft_token_ids_copy_stream: torch.cuda.Stream | None = None
937 self.valid_sampled_token_count_cpu: torch.Tensor | None = None
938 self.draft_token_ids_cpu: torch.Tensor | None = None
939 self.num_accepted_tokens_event: torch.Event | None = None
940 if self.num_spec_tokens:
941 self.draft_token_ids_event = torch.Event()
942 self.num_accepted_tokens_event = torch.Event()
943 self.draft_token_ids_copy_stream = torch.cuda.Stream()
944 self.draft_token_ids_cpu = torch.empty(
945 (self.max_num_reqs, self.num_spec_tokens),
946 dtype=torch.int64,
947 device="cpu",
948 pin_memory=PIN_MEMORY,
949 )
950 if self.use_async_scheduling:
951 self.valid_sampled_token_count_event = torch.Event()
952 self.valid_sampled_token_count_copy_stream = torch.cuda.Stream()
953 self.valid_sampled_token_count_cpu = torch.empty(
954 self.max_num_reqs,
955 dtype=torch.int32,
956 device="cpu",
957 pin_memory=PIN_MEMORY,
958 )
959
960 # Model weight offloader
961 # Make sure this is called before any get_offloader call
962 set_offloader(create_offloader(self.offload_config))
963
964 # Ephemeral state transferred between execute_model() and sample_tokens().
965 self.execute_model_state: ExecuteModelState | None = None
966 self.kv_connector_output: KVConnectorOutput | None = None
967 self.mamba_state_idx: dict[str, int] = {}
968 self._mamba_bufs: mamba_utils.MambaBuffers | None = None
969 self._mamba_state_copy_funcs: MambaStateCopyFuncsByType | None = None
970 self.layerwise_nvtx_hooks_registered = False
971
972 def update_max_model_len(self, max_model_len: int) -> None:
973 self.max_model_len = max_model_len
974 if self.speculative_config:
975 draft_config = self.speculative_config.draft_model_config
976 if draft_config is None or draft_config.max_model_len is None:
977 self.effective_drafter_max_model_len = self.max_model_len
978
979 def reset_mm_cache(self) -> None:
980 """Clear the multi-modal cache that was used during profiling,
981 but no longer needed during inference.
982 """
983 if self.mm_budget:
984 self.mm_budget.reset_cache()
985 self.late_interaction_runner.clear()
986
987 def reset_encoder_cache(self) -> None:
988 """Clear the GPU-side encoder cache storing vision embeddings.
989
990 This should be called when model weights are updated to ensure
991 stale embeddings computed with old weights are not reused.
992 """
993 self.encoder_cache.clear()
994 self.late_interaction_runner.clear()
995
996 def release_late_interaction_query_cache(self, query_keys: list[str]) -> None:
997 self.late_interaction_runner.release_queries(query_keys)
998
999 def _get_positions(self, num_tokens: Any):
1000 if isinstance(num_tokens, int):
1001 if self.uses_mrope:
1002 return self.mrope_positions.gpu[:, :num_tokens]
1003 return self.positions[:num_tokens]
1004 else:
1005 if self.uses_mrope:
1006 return self.mrope_positions.gpu[:, num_tokens]
1007 return self.positions[num_tokens]
1008
1009 def _make_buffer(
1010 self, *size: int | torch.SymInt, dtype: torch.dtype, numpy: bool = True
1011 ) -> CpuGpuBuffer:
1012 return CpuGpuBuffer(
1013 *size,
1014 dtype=dtype,
1015 device=self.device,
1016 with_numpy=numpy,
1017 )
1018
1019 def _get_mamba_state_copy_funcs(self) -> MambaStateCopyFuncsByType:
1020 if self._mamba_state_copy_funcs is None:
1021 mamba_groups = mamba_utils.get_mamba_groups(self.kv_cache_config)
1022 mamba_types = {spec.mamba_type for spec in mamba_groups}
1023 copy_funcs = self.model.get_mamba_state_copy_funcs(mamba_types)
1024 mamba_utils.validate_mamba_state_copy_funcs(mamba_groups, copy_funcs)
1025 self._mamba_state_copy_funcs = copy_funcs
1026 return self._mamba_state_copy_funcs
1027
1028 def _get_mamba_bufs(self) -> mamba_utils.MambaBuffers:
1029 # Only reachable on the ``mamba_cache_mode == "align"`` path.
1030 # The postprocess sub-object is additionally gated on spec
1031 # decode + hybrid model.
1032 assert self.cache_config.mamba_cache_mode == "align"
1033 if self._mamba_bufs is None:
1034 self._mamba_bufs = mamba_utils.MambaBuffers.create(
1035 max_num_reqs=self.max_num_reqs,
1036 kv_cache_config=self.kv_cache_config,
1037 copy_funcs=self._get_mamba_state_copy_funcs(),
1038 make_buffer=self._make_buffer,
1039 device=self.device,
1040 with_postprocess_align=(
1041 self.speculative_config is not None and self.model_config.is_hybrid
1042 ),
1043 )
1044 return self._mamba_bufs
1045
1046 def _prepare_lookback_token_ids(self, num_reqs: int) -> torch.Tensor:
1047 """Gather, per request, the `depth` prompt token ids preceding its
1048 first scheduled token (column j is position start - 1 - j); -1 where
1049 the position is before the prompt or already past it. Generated
1050 positions are left to the model: under async scheduling the CPU token
1051 table holds placeholders for them."""
1052 buf = self.lookback_token_ids
1053 assert buf is not None
1054 buf.np.fill(-1)
1055 if num_reqs > 0:
1056 depth = buf.np.shape[1]
1057 starts = self.input_batch.num_computed_tokens_cpu[:num_reqs, None]
1058 pos = starts - np.arange(1, depth + 1)
1059 num_prompt = self.input_batch.num_prompt_tokens[:num_reqs, None]
1060 valid = (pos >= 0) & (pos < num_prompt)
1061 rows = np.arange(num_reqs)[:, None]
1062 ids = self.input_batch.token_ids_cpu[rows, np.clip(pos, 0, None)]
1063 buf.np[:num_reqs] = np.where(valid, ids, -1)
1064 return buf.copy_to_gpu()
1065
1066 def _init_model_kwargs(self, num_reqs: int | None = None):
1067 model_kwargs = dict[str, Any]()
1068
1069 if self.lookback_token_ids is not None:
1070 if num_reqs is None:
1071 num_reqs = self.input_batch.num_reqs
1072 model_kwargs["lookback_token_ids"] = self._prepare_lookback_token_ids(
1073 num_reqs
1074 )
1075
1076 if not self.is_pooling_model:
1077 return model_kwargs
1078
1079 num_reqs = self.input_batch.num_reqs
1080 pooling_params = self.input_batch.get_pooling_params()
1081
1082 token_type_id_requests = dict[int, Any]()
1083 for i, param in enumerate(pooling_params):
1084 if (
1085 param.extra_kwargs is not None
1086 and (token_types := param.extra_kwargs.get("compressed_token_type_ids"))
1087 is not None
1088 ):
1089 token_type_id_requests[i] = token_types
1090
1091 if len(token_type_id_requests) == 0:
1092 return model_kwargs
1093
1094 # Build ids on CPU using the CPU-resident upper bound for seq_lens;
1095 # `torch.arange(seq_lens[i])` with a GPU scalar would force a sync.
1096 seq_lens_cpu = self.optimistic_seq_lens_cpu[:num_reqs].tolist()
1097 token_type_ids = []
1098
1099 for i in range(num_reqs):
1100 seq_len_i = seq_lens_cpu[i]
1101 pos = token_type_id_requests.get(i, seq_len_i)
1102 ids = (torch.arange(seq_len_i) >= pos).int()
1103 token_type_ids.append(ids)
1104
1105 token_type_ids_cpu = torch.empty(
1106 sum(seq_lens_cpu), dtype=torch.int32, pin_memory=PIN_MEMORY
1107 )
1108 torch.cat(token_type_ids, out=token_type_ids_cpu)
1109 model_kwargs["token_type_ids"] = token_type_ids_cpu.to(
1110 device=self.device, non_blocking=True
1111 )
1112 return model_kwargs
1113
1114 def _may_reorder_batch(self, scheduler_output: "SchedulerOutput") -> None:
1115 """Update the order of requests in the batch based on the attention
1116 backend's needs. For example, some attention backends (namely MLA) may
1117 want to separate requests based on if the attention computation will be
1118 compute-bound or memory-bound.
1119
1120 Args:
1121 scheduler_output: The scheduler output.
1122
1123 """
1124 # Attention free models have zero kv_cache_groups, however models
1125 # like Mamba are also attention free but use the kv_cache for
1126 # keeping its internal state. This is why we check the number
1127 # of kv_cache groups instead of solely checking
1128 # for self.model_config.is_attention_free.
1129 if len(self.kv_cache_config.kv_cache_groups) == 0:
1130 return
1131
1132 if self.reorder_batch_threshold is not None:
1133 reorder_batch_to_split_decodes_and_prefills(
1134 self.input_batch,
1135 scheduler_output,
1136 decode_threshold=self.reorder_batch_threshold,
1137 )
1138
1139 def _init_kv_zero_meta(self) -> None:
1140 """One-time precomputation for _zero_block_ids.
1141
1142 Called from gpu_worker.py outside the CuMem pool context.
1143 """
1144 self._kv_block_zeroer = KVBlockZeroer(
1145 self.device,
1146 attn_groups_iter=self._kv_cache_spec_attn_group_iterator(),
1147 kernel_block_sizes=self._kernel_block_sizes,
1148 runner_only_attn_layers=self.runner_only_attn_layers,
1149 static_forward_context=self.compilation_config.static_forward_context,
1150 num_blocks=self.kv_cache_config.num_blocks,
1151 )
1152
1153 def _zero_block_ids(self, block_ids: list[int]) -> None:
1154 """Zero the KV cache memory for the given block IDs."""
1155 if hasattr(self, "_kv_block_zeroer"):
1156 self._kv_block_zeroer.zero_block_ids(block_ids)
1157
1158 # Note: used for model runner override.
1159 def _init_device_properties(self) -> None:
1160 """Initialize attributes from torch.cuda.get_device_properties"""
1161 self.num_sms = num_compute_units(self.device.index)
1162
1163 # Note: used for model runner override.
1164 def _sync_device(self) -> None:
1165 torch.accelerator.synchronize()
1166
1167 def _get_or_create_async_output_copy_stream(self) -> torch.cuda.Stream:
1168 stream = self.async_output_copy_stream
1169 if stream is None:
1170 stream = torch.cuda.Stream()
1171 self.async_output_copy_stream = stream
1172 return stream
1173
1174 def _on_request_state_removed(
1175 self,
1176 req_id: str,
1177 req_state: CachedRequestState | None,
1178 ) -> None:
1179 """Hook for platform runners to clean request-scoped side caches."""
1180 del req_id, req_state
1181
1182 def _process_encoder_cache_scheduler_output(
1183 self,
1184 scheduler_output: "SchedulerOutput",
1185 ) -> None:
1186 """Apply scheduler-side encoder cache lifecycle updates."""
1187 for mm_hash in scheduler_output.free_encoder_mm_hashes:
1188 self.encoder_cache.pop(mm_hash, None)
1189
1190 def _update_states(self, scheduler_output: "SchedulerOutput") -> Callable | None:
1191 """Update the cached states and the persistent batch with the scheduler
1192 output.
1193
1194 The updated states are used by the `_prepare_inputs` function to create
1195 the input GPU tensors for the model.
1196
1197 The SamplingMetadata is updated and copied to the GPU if there is a
1198 new/resumed/paused/finished request in the batch.
1199 """
1200 # Remove finished requests from the cached states.
1201 for req_id in scheduler_output.finished_req_ids:
1202 req_state = self.requests.pop(req_id, None)
1203 self._on_request_state_removed(req_id, req_state)
1204 self.num_prompt_logprobs.pop(req_id, None)
1205 self.late_interaction_runner.on_requests_finished(
1206 scheduler_output.finished_req_ids
1207 )
1208 # Remove the finished requests from the persistent batch.
1209 # NOTE(woosuk): There could be an edge case where finished_req_ids and
1210 # scheduled_req_ids overlap. This happens when a request is aborted and
1211 # then resubmitted with the same ID. In this case, we treat them as two
1212 # distinct requests - clearing the cached states for the first request
1213 # and handling the second as a new request.
1214 for req_id in scheduler_output.finished_req_ids:
1215 self.input_batch.remove_request(req_id)
1216
1217 # Zero GPU memory for freshly allocated cache blocks to prevent
1218 # stale NaN/data from corrupting attention or SSM computation.
1219 if scheduler_output.new_block_ids_to_zero:
1220 self._zero_block_ids(scheduler_output.new_block_ids_to_zero)
1221 if scheduler_output.kv_cache_block_copies:
1222 copy_kv_cache_blocks_inplace(
1223 self.kv_caches,
1224 self.kv_cache_config.num_blocks,
1225 scheduler_output.kv_cache_block_copies,
1226 )
1227
1228 # Free the cached encoder outputs.
1229 self._process_encoder_cache_scheduler_output(scheduler_output)
1230
1231 # Remove the unscheduled requests from the persistent batch.
1232 # NOTE(woosuk): The unscheduled requests are either preempted requests
1233 # or running requests that are not scheduled in this step. We remove
1234 # them from the persistent batch but keep their cached states since
1235 # they will be scheduled again sometime in the future.
1236 scheduled_req_ids = scheduler_output.num_scheduled_tokens.keys()
1237 cached_req_ids = self.input_batch.req_id_to_index.keys()
1238 resumed_req_ids = scheduler_output.scheduled_cached_reqs.resumed_req_ids
1239 # NOTE(zhuohan): cached_req_ids and resumed_req_ids are usually disjoint,
1240 # so `(scheduled_req_ids - resumed_req_ids) == scheduled_req_ids` holds
1241 # apart from the forced-preemption case in reset_prefix_cache. And in
1242 # that case we include the resumed_req_ids in the unscheduled set so
1243 # that they get cleared from the persistent batch before being re-scheduled
1244 # in the normal resumed request path.
1245 unscheduled_req_ids = cached_req_ids - (scheduled_req_ids - resumed_req_ids)
1246 # NOTE(woosuk): The persistent batch optimization assumes that
1247 # consecutive batches contain mostly the same requests. If batches
1248 # have low request overlap (e.g., alternating between two distinct
1249 # sets of requests), this optimization becomes very inefficient.
1250 for req_id in unscheduled_req_ids:
1251 self.input_batch.remove_request(req_id)
1252
1253 is_ngram_gpu = (
1254 self.speculative_config is not None
1255 and self.speculative_config.use_ngram_gpu()
1256 )
1257 if is_ngram_gpu:
1258 ngram_gpu_new_reqs: list[CachedRequestState] = []
1259
1260 reqs_to_add: list[CachedRequestState] = []
1261 deferred_spec_decode_corrections = []
1262
1263 # Add new requests to the cached states.
1264 for new_req_data in scheduler_output.scheduled_new_reqs:
1265 req_id = new_req_data.req_id
1266 if req_id in self.requests:
1267 # For streaming case only.
1268 req_state = self._update_streaming_request(req_id, new_req_data)
1269 reqs_to_add.append(req_state)
1270 continue
1271
1272 sampling_params = new_req_data.sampling_params
1273 pooling_params = new_req_data.pooling_params
1274
1275 if (
1276 sampling_params
1277 and sampling_params.sampling_type == SamplingType.RANDOM_SEED
1278 ):
1279 generator = torch.Generator(device=self.device)
1280 generator.manual_seed(sampling_params.seed)
1281 else:
1282 generator = None
1283
1284 if self.is_pooling_model:
1285 assert pooling_params is not None
1286 task = pooling_params.task
1287 assert task is not None, "You did not set `task` in the API"
1288
1289 model = cast(VllmModelForPooling, self.get_model())
1290 to_update = model.pooler.get_pooling_updates(task)
1291 to_update.apply(pooling_params)
1292
1293 req_state = CachedRequestState(
1294 req_id=req_id,
1295 prompt_token_ids=new_req_data.prompt_token_ids,
1296 prompt_embeds=new_req_data.prompt_embeds,
1297 prompt_is_token_ids=new_req_data.prompt_is_token_ids,
1298 mm_features=new_req_data.mm_features,
1299 sampling_params=sampling_params,
1300 pooling_params=pooling_params,
1301 generator=generator,
1302 block_ids=new_req_data.block_ids,
1303 num_computed_tokens=new_req_data.num_computed_tokens,
1304 output_token_ids=[],
1305 lora_request=new_req_data.lora_request,
1306 )
1307 self.requests[req_id] = req_state
1308 self.late_interaction_runner.register_request(req_id, pooling_params)
1309
1310 if sampling_params and sampling_params.prompt_logprobs is not None:
1311 self.num_prompt_logprobs[req_id] = (
1312 self.input_batch.vocab_size
1313 if sampling_params.prompt_logprobs == -1
1314 else sampling_params.prompt_logprobs
1315 )
1316
1317 # Only relevant for models using M-RoPE (e.g, Qwen2-VL)
1318 if self.uses_mrope:
1319 self._init_mrope_positions(req_state)
1320
1321 reqs_to_add.append(req_state)
1322 # Track new requests for ngram_gpu full tensor copy
1323 if is_ngram_gpu:
1324 ngram_gpu_new_reqs.append(req_state)
1325
1326 # Update the states of the running/resumed requests.
1327 is_last_rank = get_pp_group().is_last_rank
1328 req_data = scheduler_output.scheduled_cached_reqs
1329 scheduled_spec_tokens = scheduler_output.scheduled_spec_decode_tokens
1330
1331 # Save scheduler-allocated spec lengths before trimming so
1332 # prev_num_draft_len keeps the optimistic count for rejection correction.
1333 original_num_spec_per_req: dict[str, int] = {}
1334 if (
1335 self.speculative_config is not None
1336 and self.speculative_config.use_ngram_gpu()
1337 ):
1338 for req_id, toks in scheduled_spec_tokens.items():
1339 original_num_spec_per_req[req_id] = len(toks)
1340 update_scheduler_for_invalid_drafts(
1341 self._num_valid_draft_tokens_event,
1342 self._num_valid_draft_tokens_cpu,
1343 scheduler_output,
1344 self.input_batch.req_id_to_index,
1345 )
1346 if self.use_async_spec_decode:
1347 self.prev_num_draft_tokens.np.fill(0)
1348
1349 for i, req_id in enumerate(req_data.req_ids):
1350 req_state = self.requests[req_id]
1351 num_computed_tokens = req_data.num_computed_tokens[i]
1352 new_block_ids = req_data.new_block_ids[i]
1353 resumed_from_preemption = req_id in req_data.resumed_req_ids
1354 num_output_tokens = req_data.num_output_tokens[i]
1355 req_index = self.input_batch.req_id_to_index.get(req_id)
1356
1357 if req_state.prev_num_draft_len and self.use_async_scheduling:
1358 # prev_num_draft_len is used in async scheduling mode with
1359 # spec decode. it indicates if need to update num_computed_tokens
1360 # of the request. for example:
1361 # first step: num_computed_tokens = 0, spec_tokens = [],
1362 # prev_num_draft_len = 0.
1363 # second step: num_computed_tokens = 100(prompt length),
1364 # spec_tokens = [a,b], prev_num_draft_len = 0.
1365 # third step: num_computed_tokens = 100 + 2, spec_tokens = [c,d],
1366 # prev_num_draft_len = 2.
1367 # num_computed_tokens in first step and second step doesn't contain
1368 # the spec tokens length, but in third step it contains the
1369 # spec tokens length. we only need to update num_computed_tokens
1370 # when prev_num_draft_len > 0.
1371 if req_index is None:
1372 req_state.prev_num_draft_len = 0
1373 else:
1374 # Optimistically assume all accepted; queue up a correction
1375 # to be called after the model forward to preserve async
1376 # scheduling. Corrected on GPU in _prepare_inputs.
1377 optimistic_num_accepted = req_state.prev_num_draft_len
1378 req_state.output_token_ids.extend([-1] * optimistic_num_accepted)
1379
1380 deferred_spec_decode_corrections.append(
1381 (req_id, optimistic_num_accepted, req_state)
1382 )
1383
1384 prev_req_index = (
1385 self.input_batch.prev_req_id_to_index.get(req_id)
1386 if self.input_batch.prev_req_id_to_index
1387 else None
1388 )
1389 if prev_req_index is not None:
1390 self.prev_num_draft_tokens.np[prev_req_index] = (
1391 optimistic_num_accepted
1392 )
1393
1394 if is_ngram_gpu and optimistic_num_accepted > 0:
1395 self.input_batch.num_tokens_no_spec[req_index] += (
1396 optimistic_num_accepted
1397 )
1398
1399 # Update the cached states.
1400 req_state.num_computed_tokens = num_computed_tokens
1401
1402 if not is_last_rank:
1403 if not req_data.new_token_ids:
1404 # Async scheduled PP: Sampled tokens propagated via GPU broadcast.
1405 new_token_ids: list[int] = []
1406 else:
1407 # Non-async scheduling with PP: The scheduler sends
1408 # sampled token ids back because there's no direct communication
1409 # between the first-stage worker and the last-stage worker.
1410 new_token_ids = req_data.new_token_ids[i]
1411 # Add the sampled token(s) from the previous step (if any).
1412 # This doesn't include "unverified" tokens like spec tokens.
1413 num_new_tokens = (
1414 num_computed_tokens + len(new_token_ids) - req_state.num_tokens
1415 )
1416 if num_new_tokens == 1:
1417 # Avoid slicing list in most common case.
1418 req_state.output_token_ids.append(new_token_ids[-1])
1419 elif num_new_tokens > 0:
1420 req_state.output_token_ids.extend(
1421 new_token_ids[-num_new_tokens:]
1422 )
1423 elif num_output_tokens < len(req_state.output_token_ids):
1424 # Some output tokens were discarded due to a sync-KV-load
1425 # failure, or output_token_ids was inflated by the optimistic
1426 # extend above (async spec decode). Align the cached state.
1427 del req_state.output_token_ids[num_output_tokens:]
1428 if req_index is not None:
1429 end_idx = (
1430 self.input_batch.num_prompt_tokens[req_index]
1431 + num_output_tokens
1432 )
1433 self.input_batch.num_tokens_no_spec[req_index] = end_idx
1434
1435 # Update the block IDs.
1436 if not resumed_from_preemption:
1437 if new_block_ids is not None:
1438 # Append the new blocks to the existing block IDs.
1439 for block_ids, new_ids in zip(req_state.block_ids, new_block_ids):
1440 block_ids.extend(new_ids)
1441 else:
1442 assert req_index is None
1443 assert new_block_ids is not None
1444 # The request is resumed from preemption.
1445 # Replace the existing block IDs with the new ones.
1446 req_state.block_ids = new_block_ids
1447
1448 if req_index is None:
1449 # The request is not in the persistent batch.
1450 # The request was either preempted and resumed later, or was not
1451 # scheduled in the previous step and needs to be added again.
1452
1453 if self.use_async_scheduling and num_output_tokens > 0:
1454 # We must recover the output token ids for resumed requests in the
1455 # async scheduling case, so that correct input_ids are obtained.
1456 resumed_token_ids = req_data.all_token_ids[req_id]
1457 req_state.output_token_ids = resumed_token_ids[-num_output_tokens:]
1458
1459 reqs_to_add.append(req_state)
1460 # Track resumed requests for ngram_gpu full tensor copy
1461 if is_ngram_gpu:
1462 ngram_gpu_new_reqs.append(req_state)
1463 continue
1464
1465 # Update the persistent batch.
1466 self.input_batch.num_computed_tokens_cpu[req_index] = num_computed_tokens
1467 if new_block_ids is not None:
1468 self.input_batch.block_table.append_row(new_block_ids, req_index)
1469
1470 # For the last rank, we don't need to update the token_ids_cpu
1471 # because the sampled tokens are already cached.
1472 if not is_last_rank:
1473 start_token_index = self.input_batch.num_tokens_no_spec[req_index]
1474 # For chunked prefill, num_computed_tokens may less
1475 # than num_tokens_no_spec.
1476 # Async scheduled PP: no new_token_ids, advance num_tokens_no_spec
1477 # according to num_computed_tokens.
1478 end_token_index = max(
1479 start_token_index,
1480 num_computed_tokens + len(new_token_ids),
1481 )
1482 if end_token_index > start_token_index:
1483 if new_token_ids:
1484 # Add new_token_ids to token_ids_cpu.
1485 num_new_tokens = end_token_index - start_token_index
1486 tokens_to_append = new_token_ids[-num_new_tokens:]
1487 self.input_batch.token_ids_cpu[
1488 req_index, start_token_index:end_token_index
1489 ] = tokens_to_append
1490 self.input_batch.is_token_ids[
1491 req_index, start_token_index:end_token_index
1492 ] = True
1493 self.input_batch.num_tokens_no_spec[req_index] = end_token_index
1494
1495 # Add spec_token_ids to token_ids_cpu.
1496 self.input_batch.update_req_spec_token_ids(req_state, scheduled_spec_tokens)
1497 # Restore scheduler-side draft count after ngram trimming.
1498 if original_num_spec_per_req:
1499 orig = original_num_spec_per_req.get(req_id, 0)
1500 if orig != req_state.prev_num_draft_len:
1501 req_state.prev_num_draft_len = orig
1502
1503 # Add the new or resumed requests to the persistent batch.
1504 # The smaller empty indices are filled first.
1505 for request in reqs_to_add:
1506 self.input_batch.add_request(request)
1507 self.input_batch.update_req_spec_token_ids(request, scheduled_spec_tokens)
1508
1509 # Condense the batched states if there are gaps left by removed requests
1510 self.input_batch.condense()
1511 # Allow attention backend to reorder the batch, potentially
1512 self._may_reorder_batch(scheduler_output)
1513 # Refresh batch metadata with any pending updates.
1514 self.input_batch.refresh_metadata()
1515
1516 # Incrementally update ngram_gpu tensors after batch is stable
1517 if is_ngram_gpu:
1518 update_ngram_gpu_tensors_incremental(
1519 self.input_batch,
1520 self.token_ids_gpu_tensor,
1521 self.num_tokens_no_spec_gpu,
1522 ngram_gpu_new_reqs,
1523 self.device,
1524 _pinned_idx_buf=self._ngram_pinned_idx_buf,
1525 _pinned_val_buf=self._ngram_pinned_val_buf,
1526 )
1527
1528 if deferred_spec_decode_corrections:
1529
1530 def correct_spec_decode_token_counts():
1531 valid_sampled_token_count = self._get_valid_sampled_token_count()
1532 if not valid_sampled_token_count:
1533 return
1534 prev_req_id_to_index = self.input_batch.prev_req_id_to_index
1535 if not prev_req_id_to_index:
1536 return
1537 for (
1538 req_id,
1539 optimistic_num_accepted,
1540 req_state,
1541 ) in deferred_spec_decode_corrections:
1542 prev_req_index = prev_req_id_to_index.get(req_id)
1543 if prev_req_index is None:
1544 continue
1545 num_accepted = valid_sampled_token_count[prev_req_index] - 1
1546 correction = optimistic_num_accepted - num_accepted
1547 req_state.num_computed_tokens -= correction
1548 cur_req_index = self.input_batch.req_id_to_index.get(req_id)
1549 if cur_req_index is None:
1550 continue
1551 self.input_batch.num_computed_tokens_cpu[cur_req_index] -= (
1552 correction
1553 )
1554 if is_ngram_gpu and correction > 0:
1555 self.input_batch.num_tokens_no_spec[cur_req_index] -= correction
1556 self.num_tokens_no_spec_gpu[cur_req_index] -= correction
1557
1558 return correct_spec_decode_token_counts
1559 else:
1560 return None
1561
1562 def _update_states_after_model_execute(
1563 self, output_token_ids: torch.Tensor, scheduler_output: "SchedulerOutput"
1564 ) -> None:
1565 """Update the cached states after model execution.
1566
1567 This is used for MTP/EAGLE for hybrid models, as in linear attention,
1568 only the last token's state is kept. In MTP/EAGLE, for draft tokens
1569 the state are kept util we decide how many tokens are accepted for
1570 each sequence, and a shifting is done during the next iteration
1571 based on the number of accepted tokens.
1572 """
1573 if not self.speculative_config or not self.model_config.is_hybrid:
1574 return
1575
1576 # Count the number of accepted tokens for each sequence.
1577 # Valid tokens are contiguous from position 0, so counting non-(-1)
1578 # tokens gives us the first -1 position (i.e., number of accepted).
1579 num_reqs = output_token_ids.size(0)
1580 self.num_accepted_tokens.gpu[:num_reqs] = (output_token_ids != -1).sum(dim=1)
1581
1582 if self.cache_config.mamba_cache_mode == "align":
1583 # Fused GPU postprocess: state copies + per-request accepted-token
1584 # update without CPU-GPU sync. The metadata
1585 # (num_scheduled_tokens, num_draft_tokens, num_computed_tokens) is
1586 # pre-staged to GPU buffers in _prepare_inputs.
1587 mamba_utils.postprocess_mamba_align_gpu(
1588 bufs=self._get_mamba_bufs(),
1589 num_reqs=num_reqs,
1590 num_accepted_tokens_gpu=self.num_accepted_tokens.gpu,
1591 num_accepted_tokens_cpu_tensor=(
1592 self.input_batch.num_accepted_tokens_cpu_tensor
1593 ),
1594 input_batch=self.input_batch,
1595 kv_cache_config=self.kv_cache_config,
1596 forward_context=self.compilation_config.static_forward_context,
1597 mamba_state_copy_funcs=self._get_mamba_state_copy_funcs(),
1598 )
1599
1600 assert self.num_accepted_tokens_event is not None
1601 self.num_accepted_tokens_event.record()
1602 else:
1603 self.input_batch.num_accepted_tokens_cpu_tensor[:num_reqs].copy_(
1604 self.num_accepted_tokens.gpu[:num_reqs], non_blocking=True
1605 )
1606 assert self.num_accepted_tokens_event is not None
1607 self.num_accepted_tokens_event.record()
1608
1609 def _update_streaming_request(
1610 self, req_id: str, new_req_data: NewRequestData
1611 ) -> CachedRequestState:
1612 """Updates streaming session request from `scheduled_new_reqs`.
1613
1614 Removes the request from InputBatch (if present), updates the cached
1615 state, and prepares it for re-addition to the batch.
1616
1617 NOTE: prompt_token_ids includes intermediate output tokens - tokens
1618 previously generated but now are input context (part of the prompt).
1619 """
1620 self.input_batch.remove_request(req_id)
1621 req_state = self.requests[req_id]
1622
1623 req_state.prompt_token_ids = new_req_data.prompt_token_ids
1624 req_state.mm_features = new_req_data.mm_features
1625 req_state.prompt_embeds = new_req_data.prompt_embeds
1626 req_state.sampling_params = new_req_data.sampling_params
1627 req_state.pooling_params = new_req_data.pooling_params
1628 self.late_interaction_runner.register_request(req_id, req_state.pooling_params)
1629 req_state.block_ids = new_req_data.block_ids
1630 req_state.num_computed_tokens = new_req_data.num_computed_tokens
1631 req_state.num_prompt_tokens = length_from_prompt_token_ids_or_embeds(
1632 req_state.prompt_token_ids, req_state.prompt_embeds
1633 )
1634
1635 # Clear `output_token_ids` as previous output tokens are now part of
1636 # `prompt_token_ids`.
1637 req_state.output_token_ids.clear()
1638
1639 if self.uses_mrope:
1640 self._init_mrope_positions(req_state)
1641
1642 return req_state
1643
1644 def _init_mrope_positions(self, req_state: CachedRequestState):
1645 model = self.get_model()
1646 assert supports_mrope(model), "M-RoPE support is not implemented."
1647 mrope_model = cast(SupportsMRoPE, model)
1648
1649 # `prompt_embeds` is a passthrough modality (no grid_thw), models'
1650 # M-RoPE code assumes per-feature grid info, so filter it out. The
1651 # prompt_embeds positions are treated as text positions for M-RoPE.
1652 mrope_features = [
1653 f for f in req_state.mm_features if f.modality != "prompt_embeds"
1654 ]
1655
1656 if req_state.prompt_token_ids is not None:
1657 input_tokens = req_state.prompt_token_ids
1658 elif req_state.prompt_embeds is not None:
1659 # For embeddings-only inputs, get_mrope_input_positions only
1660 # needs the sequence length when mm_features is empty (which is
1661 # the case here since prompt_embeds are filtered out above).
1662 seq_len = req_state.prompt_embeds.shape[0]
1663 input_tokens = list(range(seq_len))
1664 else:
1665 raise ValueError(
1666 "M-RoPE requires either prompt_token_ids or prompt_embeds."
1667 )
1668
1669 req_state.mrope_positions, req_state.mrope_position_delta = (
1670 mrope_model.get_mrope_input_positions(
1671 input_tokens,
1672 mrope_features,
1673 )
1674 )
1675
1676 def _extract_mm_kwargs(
1677 self,
1678 scheduler_output: "SchedulerOutput",
1679 ) -> BatchedTensorInputs:
1680 if not scheduler_output or not self.is_multimodal_raw_input_only_model:
1681 return {}
1682
1683 mm_kwargs = list[tuple[str, MultiModalKwargsItem]]()
1684 for req in scheduler_output.scheduled_new_reqs:
1685 for feature in req.mm_features:
1686 if feature.data is not None:
1687 mm_kwargs.append((feature.modality, feature.data))
1688
1689 # Input all modalities at once
1690 mm_kwargs_combined: BatchedTensorInputs = {}
1691 for _, _, mm_kwargs_batch in group_and_batch_mm_kwargs(
1692 mm_kwargs,
1693 device=self.device,
1694 pin_memory=PIN_MEMORY,
1695 ):
1696 mm_kwargs_combined.update(mm_kwargs_batch)
1697
1698 return mm_kwargs_combined
1699
1700 def _dummy_mm_kwargs(self, num_seqs: int) -> BatchedTensorInputs:
1701 if not self.is_multimodal_raw_input_only_model:
1702 return {}
1703
1704 mm_budget = self.mm_budget
1705 assert mm_budget is not None
1706
1707 if not mm_budget.mm_max_toks_per_item:
1708 return {} # No tower modalities (embed-only mode)
1709
1710 dummy_modality = mm_budget.get_modality_with_max_tokens()
1711 return self._get_mm_dummy_batch(dummy_modality, num_seqs)
1712
1713 def _get_cumsum_and_arange(
1714 self,
1715 num_tokens: np.ndarray,
1716 arange_out: np.ndarray,
1717 cumsum_dtype: np.dtype | None = None,
1718 ) -> np.ndarray:
1719 """Get the cumulative sum and batched arange of the given array.
1720 E.g., [2, 5, 3] -> [2, 7, 10], arange written to
1721 arange_out[:10] as [0, 1, 0, 1, 2, 3, 4, 0, 1, 2].
1722 Equivalent to but faster than:
1723 np.concatenate([np.arange(n) for n in num_tokens])
1724 """
1725 # Step 1. [2, 5, 3] -> [2, 7, 10]
1726 cu_num_tokens = np.cumsum(num_tokens, dtype=cumsum_dtype)
1727 total_num_tokens = cu_num_tokens[-1]
1728 # Step 2. [2, 7, 10] -> [0, 0, 2, 2, 2, 2, 2, 7, 7, 7]
1729 cumsums_offsets = np.repeat(cu_num_tokens - num_tokens, num_tokens)
1730 # Step 3. [0, 1, 0, 1, 2, 3, 4, 0, 1, 2]
1731 np.subtract(
1732 self.arange_np[:total_num_tokens],
1733 cumsums_offsets,
1734 out=arange_out[:total_num_tokens],
1735 )
1736
1737 return cu_num_tokens
1738
1739 def _compute_prev_positions(self, num_reqs: int) -> None:
1740 """Build prev_positions mapping: current pos -> previous pos (-1 if new).
1741
1742 Populates self.prev_positions.np[:num_reqs] with the mapping.
1743 """
1744 prev_req_id_to_index = self.input_batch.prev_req_id_to_index
1745 prev_positions = self.prev_positions.np[:num_reqs]
1746
1747 if not prev_req_id_to_index:
1748 prev_positions.fill(-1)
1749 return
1750
1751 for i, req_id in enumerate(self.input_batch.req_ids[:num_reqs]):
1752 prev_positions[i] = prev_req_id_to_index.get(req_id, -1)
1753
1754 def _prepare_input_ids(
1755 self,
1756 scheduler_output: "SchedulerOutput",
1757 num_reqs: int,
1758 total_num_scheduled_tokens: int,
1759 cu_num_tokens: np.ndarray,
1760 ) -> None:
1761 """Prepare the input IDs for the current batch.
1762
1763 Carefully handles the `prev_sampled_token_ids` which can be cached
1764 from the previous engine iteration, in which case those tokens on the
1765 GPU need to be copied into the corresponding slots into input_ids.
1766
1767 Uses self.prev_positions[:num_reqs] which maps current pos -> prev pos
1768 (-1 for new requests).
1769 """
1770 # Sync the async PP broadcast before reading sampled tokens.
1771 if self._pp_recv_work is not None:
1772 self._pp_recv_work.wait()
1773 self._pp_recv_work = None
1774
1775 if self.input_batch.prev_sampled_token_ids is None:
1776 # Normal scheduling case
1777 self.input_ids.copy_to_gpu(total_num_scheduled_tokens)
1778 if self.enable_prompt_embeds:
1779 self.inputs_embeds.copy_to_gpu(total_num_scheduled_tokens)
1780 self.is_token_ids.copy_to_gpu(total_num_scheduled_tokens)
1781 return
1782
1783 # Async scheduling case, where some decode requests from the previous
1784 # iteration won't have entries in input_ids_cpu and need to be copied
1785 # on the GPU from prev_sampled_token_ids.
1786 prev_positions = self.prev_positions.np[:num_reqs]
1787 scheduled_spec_tokens = scheduler_output.scheduled_spec_decode_tokens
1788 sample_flattened_indices: list[int] = []
1789 spec_flattened_indices: list[int] = []
1790 prev_draft_token_indices: list[int] = []
1791 prev_indices: list[int] = []
1792 common_indices_match = True
1793 max_flattened_index = -1
1794 total_num_spec_tokens = 0
1795
1796 for cur_index in range(num_reqs):
1797 prev_index = prev_positions[cur_index]
1798 if prev_index < 0:
1799 continue
1800 prev_indices.append(prev_index)
1801 req_id = self.input_batch.req_ids[cur_index]
1802 # We need to compute the flattened input_ids index of the
1803 # last token in each common request.
1804 draft_len = len(scheduled_spec_tokens.get(req_id, ()))
1805 total_num_spec_tokens += draft_len
1806 flattened_index = cu_num_tokens[cur_index].item() - 1
1807 # example: cu_num_tokens = [2, 5, 8], draft_tokens = [1, 2, 2]
1808 # sample_flattened_indices = [0, 2, 5]
1809 # spec_flattened_indices = [1, 3, 4, 6, 7]
1810 sample_flattened_indices.append(flattened_index - draft_len)
1811 spec_flattened_indices.extend(
1812 range(flattened_index - draft_len + 1, flattened_index + 1)
1813 )
1814 start = prev_index * self.prev_num_spec_tokens
1815 # prev_draft_token_indices is used to find which draft_tokens_id
1816 # should be copied to input_ids
1817 # example: prev draft_tokens_id [[1,2], [3,4], [5, 6]]
1818 # flatten draft_tokens_id [1,2,3,4,5,6]
1819 # draft_len of each request [1, 2, 1]
1820 # then prev_draft_token_indices is [0, 2, 3, 4]
1821 prev_draft_token_indices.extend(range(start, start + draft_len))
1822 common_indices_match &= prev_index == flattened_index
1823 max_flattened_index = max(max_flattened_index, flattened_index)
1824
1825 num_common_tokens = len(sample_flattened_indices)
1826 total_without_spec = total_num_scheduled_tokens - total_num_spec_tokens
1827 if self.enable_prompt_embeds:
1828 # The multimodal embed path reads is_token_ids.gpu; its .cpu copy is
1829 # refreshed every step but the async fast path below replaces draft
1830 # tokens directly on the GPU. Mark those positions as token IDs before
1831 # refreshing the GPU mask so their embeddings are rebuilt. Sampled
1832 # tokens are already marked by bookkeeping.
1833 if spec_flattened_indices:
1834 self.is_token_ids.np[spec_flattened_indices] = True
1835 self.is_token_ids.copy_to_gpu(total_num_scheduled_tokens)
1836 if num_common_tokens < total_without_spec:
1837 # If not all requests are decodes from the last iteration,
1838 # we need to copy the input_ids_cpu to the GPU first.
1839 self.input_ids.copy_to_gpu(total_num_scheduled_tokens)
1840 if self.enable_prompt_embeds:
1841 self.inputs_embeds.copy_to_gpu(total_num_scheduled_tokens)
1842 if num_common_tokens == 0:
1843 # No requests in common with the previous iteration
1844 # So input_ids.cpu will have all the input ids.
1845 return
1846 if common_indices_match and max_flattened_index == (num_common_tokens - 1):
1847 # Common-case optimization: the batch is unchanged
1848 # and no reordering happened.
1849 # The indices are both the same permutation of 0..N-1 so
1850 # we can copy directly using a single slice.
1851 self.input_ids.gpu[:num_common_tokens].copy_(
1852 self.input_batch.prev_sampled_token_ids[:num_common_tokens, 0],
1853 non_blocking=True,
1854 )
1855 return
1856 # Upload the index tensors asynchronously so the scatter can be non-blocking.
1857 sampled_tokens_index_tensor = torch.tensor(
1858 sample_flattened_indices, dtype=torch.int64, pin_memory=PIN_MEMORY
1859 ).to(self.device, non_blocking=True)
1860 prev_common_req_indices_tensor = torch.tensor(
1861 prev_indices, dtype=torch.int64, pin_memory=PIN_MEMORY
1862 ).to(self.device, non_blocking=True)
1863 self.input_ids.gpu.scatter_(
1864 dim=0,
1865 index=sampled_tokens_index_tensor,
1866 src=self.input_batch.prev_sampled_token_ids[
1867 prev_common_req_indices_tensor, 0
1868 ],
1869 )
1870
1871 # Scatter the draft tokens after the sampled tokens are scattered.
1872 if self._draft_token_ids is None or not spec_flattened_indices:
1873 return
1874
1875 assert isinstance(self._draft_token_ids, torch.Tensor)
1876 draft_tokens_index_tensor = torch.tensor(
1877 spec_flattened_indices, dtype=torch.int64, pin_memory=PIN_MEMORY
1878 ).to(self.device, non_blocking=True)
1879 prev_draft_token_indices_tensor = torch.tensor(
1880 prev_draft_token_indices, dtype=torch.int64, pin_memory=PIN_MEMORY
1881 ).to(self.device, non_blocking=True)
1882
1883 # because input_ids dtype is torch.int32,
1884 # so convert draft_token_ids to torch.int32 here.
1885 draft_token_ids = self._draft_token_ids.to(dtype=torch.int32)
1886
1887 self.input_ids.gpu.scatter_(
1888 dim=0,
1889 index=draft_tokens_index_tensor,
1890 src=draft_token_ids.flatten()[prev_draft_token_indices_tensor],
1891 )
1892
1893 def _get_encoder_seq_lens(
1894 self,
1895 num_scheduled_tokens: dict[str, int],
1896 kv_cache_spec: KVCacheSpec,
1897 num_reqs: int,
1898 for_cudagraph_capture: bool = False,
1899 ) -> tuple[torch.Tensor | None, np.ndarray | None]:
1900 if not isinstance(kv_cache_spec, CrossAttentionSpec):
1901 return None, None
1902
1903 # Zero out buffer for padding requests that are not actually scheduled (CGs)
1904 self.encoder_seq_lens.np[:num_reqs] = 0
1905
1906 # Build encoder_seq_lens array mapping request indices to
1907 # encoder lengths for inputs scheduled in this batch
1908 for req_id in num_scheduled_tokens:
1909 req_index = self.input_batch.req_id_to_index[req_id]
1910 req_state = self.requests[req_id]
1911 if req_state.mm_features is None:
1912 self.encoder_seq_lens.np[req_index] = 0
1913 continue
1914
1915 # Get the total number of encoder input tokens for running encoder requests
1916 # whether encoding is finished or not so that cross-attention knows how
1917 # many encoder tokens to attend to.
1918 encoder_input_tokens = sum(
1919 feature.mm_position.length for feature in req_state.mm_features
1920 )
1921 self.encoder_seq_lens.np[req_index] = encoder_input_tokens
1922 if for_cudagraph_capture:
1923 # During CUDA graph capture, we need to use realistic encoder lengths
1924 # so that max_seqlen_k is captured with the correct value.
1925 max_encoder_len = getattr(
1926 self.model_config.hf_config,
1927 "max_source_positions",
1928 self.max_encoder_len,
1929 )
1930 self.encoder_seq_lens.np[:num_reqs] = max_encoder_len
1931
1932 self.encoder_seq_lens.copy_to_gpu(num_reqs)
1933 encoder_seq_lens = self.encoder_seq_lens.gpu[:num_reqs]
1934 encoder_seq_lens_cpu = self.encoder_seq_lens.np[:num_reqs]
1935
1936 return encoder_seq_lens, encoder_seq_lens_cpu
1937
1938 def _prepare_inputs(
1939 self,
1940 scheduler_output: "SchedulerOutput",
1941 num_scheduled_tokens: np.ndarray,
1942 ) -> tuple[
1943 torch.Tensor,
1944 SpecDecodeMetadata | None,
1945 int,
1946 ]:
1947 """Returns:
1948 tuple[logits_indices, spec_decode_metadata, max_num_sampled_tokens]
1949
1950 """
1951 total_num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
1952 assert total_num_scheduled_tokens > 0
1953 num_reqs = self.input_batch.num_reqs
1954 assert num_reqs > 0
1955
1956 # OPTIMIZATION: Start copying the block table first.
1957 # This way, we can overlap the copy with the following CPU operations.
1958 self.input_batch.block_table.commit_block_table(num_reqs)
1959
1960 # Get request indices.
1961 # E.g., [2, 5, 3] -> [0, 0, 1, 1, 1, 1, 1, 2, 2, 2]
1962 req_indices = np.repeat(self.arange_np[:num_reqs], num_scheduled_tokens)
1963
1964 # cu_num_tokens: [2, 5, 3] -> [2, 7, 10]
1965 # self.query_pos.np[:10]: [0, 1, 0, 1, 2, 3, 4, 0, 1, 2]
1966 cu_num_tokens = self._get_cumsum_and_arange(
1967 num_scheduled_tokens, self.query_pos.np
1968 )
1969
1970 # Get positions.
1971 positions_np = (
1972 self.input_batch.num_computed_tokens_cpu[req_indices]
1973 + self.query_pos.np[: cu_num_tokens[-1]]
1974 )
1975
1976 # Calculate M-RoPE positions.
1977 # Only relevant for models using M-RoPE (e.g, Qwen2-VL)
1978 if self.uses_mrope:
1979 self._calc_mrope_positions(scheduler_output)
1980
1981 # Get token indices.
1982 # E.g., [0, 1, 0, 1, 2, 3, 4, 0, 1, 2]
1983 # -> [0, 1, M, M + 1, M + 2, M + 3, M + 4, 2 * M, 2 * M + 1, 2 * M + 2]
1984 # where M is the max_model_len.
1985 token_indices = (
1986 positions_np + req_indices * self.input_batch.token_ids_cpu.shape[1]
1987 )
1988 token_indices_tensor = torch.from_numpy(token_indices)
1989
1990 # NOTE(woosuk): We use torch.index_select instead of np.take here
1991 # because torch.index_select is much faster than np.take for large
1992 # tensors.
1993 torch.index_select(
1994 self.input_batch.token_ids_cpu_tensor.flatten(),
1995 0,
1996 token_indices_tensor,
1997 out=self.input_ids.cpu[:total_num_scheduled_tokens],
1998 )
1999 if self.enable_prompt_embeds:
2000 is_token_ids = self.input_batch.is_token_ids_tensor.flatten()
2001 torch.index_select(
2002 is_token_ids,
2003 0,
2004 token_indices_tensor,
2005 out=self.is_token_ids.cpu[:total_num_scheduled_tokens],
2006 )
2007
2008 # Because we did not pre-allocate a massive prompt_embeds CPU tensor on
2009 # the InputBatch, we need to fill in the prompt embeds into the expected
2010 # spots in the GpuModelRunner's pre-allocated prompt_embeds tensor.
2011 if self.input_batch.req_prompt_embeds:
2012 output_idx = 0
2013 for req_idx in range(num_reqs):
2014 num_sched = num_scheduled_tokens[req_idx]
2015
2016 # Skip if this request doesn't have embeddings
2017 if req_idx not in self.input_batch.req_prompt_embeds:
2018 output_idx += num_sched
2019 continue
2020
2021 # Skip if no tokens scheduled
2022 if num_sched <= 0:
2023 output_idx += num_sched
2024 continue
2025
2026 req_embeds = self.input_batch.req_prompt_embeds[req_idx]
2027 start_pos = self.input_batch.num_computed_tokens_cpu[req_idx]
2028
2029 # Skip if trying to read beyond available embeddings
2030 if start_pos >= req_embeds.shape[0]:
2031 output_idx += num_sched
2032 continue
2033
2034 # Copy available embeddings
2035 end_pos = start_pos + num_sched
2036 actual_end = min(end_pos, req_embeds.shape[0])
2037 actual_num_sched = actual_end - start_pos
2038
2039 if actual_num_sched > 0:
2040 self.inputs_embeds.cpu[
2041 output_idx : output_idx + actual_num_sched
2042 ].copy_(req_embeds[start_pos:actual_end])
2043
2044 output_idx += num_sched
2045
2046 # Prepare the attention metadata.
2047 self.query_start_loc.np[0] = 0
2048 self.query_start_loc.np[1 : num_reqs + 1] = cu_num_tokens
2049 # Note: pad query_start_loc to be non-decreasing, as kernels
2050 # like FlashAttention requires that
2051 self.query_start_loc.np[num_reqs + 1 :].fill(cu_num_tokens[-1])
2052 self.query_start_loc.copy_to_gpu()
2053 query_start_loc = self.query_start_loc.gpu[: num_reqs + 1]
2054
2055 # Compute optimistic seq_lens (assumes all draft tokens from previous
2056 # iteration accepted). Store in optimistic_seq_lens_cpu for use by
2057 # _build_attention_metadata (max_seq_len) and discard_request_mask.
2058 # seq_lens (GPU) will be computed later using the same optimistic values.
2059 torch.add(
2060 self.input_batch.num_computed_tokens_cpu_tensor[:num_reqs],
2061 torch.from_numpy(num_scheduled_tokens),
2062 out=self.optimistic_seq_lens_cpu[:num_reqs],
2063 )
2064 self.optimistic_seq_lens_cpu[num_reqs:].fill_(0)
2065
2066 # Build prev_positions mapping: current pos -> prev pos (-1 if new).
2067 # Used for gathering from previous iteration's GPU tensors.
2068 prev_req_id_to_index = self.input_batch.prev_req_id_to_index
2069 self._compute_prev_positions(num_reqs)
2070
2071 num_tokens = [self.requests[r].num_tokens for r in self.input_batch.req_ids]
2072 num_tokens_np = np.array(num_tokens, dtype=np.int32)
2073
2074 # Record which requests should not be sampled,
2075 # so that we could clear the sampled tokens before returning
2076 self.discard_request_mask.np[:num_reqs] = (
2077 self.optimistic_seq_lens_cpu[:num_reqs].numpy() < num_tokens_np
2078 )
2079 self.discard_request_mask.copy_to_gpu(num_reqs)
2080
2081 # Sync num_accepted_tokens from CPU (set by
2082 # _update_states_after_model_execute for hybrid models).
2083 # Skipped under async scheduling (non-align): the CPU copy races with
2084 # the in-flight D2H copy and with input-batch row moves.
2085 needs_cpu_accepted_counts = self.num_accepted_tokens_event is not None and not (
2086 self.use_async_scheduling and self.cache_config.mamba_cache_mode != "align"
2087 )
2088 if needs_cpu_accepted_counts:
2089 assert self.num_accepted_tokens_event is not None
2090 self.num_accepted_tokens_event.synchronize()
2091 # Async mode: condense() reordered indices, use prev_positions mapping
2092 if self.use_async_scheduling and prev_req_id_to_index:
2093 prev_idx = self.prev_positions.np[:num_reqs]
2094 new_mask = prev_idx < 0
2095 self.num_accepted_tokens.np[:num_reqs] = (
2096 self.input_batch.num_accepted_tokens_cpu[
2097 np.where(new_mask, 0, prev_idx)
2098 ]
2099 )
2100 self.num_accepted_tokens.np[:num_reqs][new_mask] = 1
2101 self.input_batch.num_accepted_tokens_cpu[:num_reqs] = (
2102 self.num_accepted_tokens.np[:num_reqs]
2103 )
2104 else:
2105 # Non-async mode: use values directly
2106 self.num_accepted_tokens.np[:num_reqs] = (
2107 self.input_batch.num_accepted_tokens_cpu[:num_reqs]
2108 )
2109 self.num_accepted_tokens.np[num_reqs:].fill(1)
2110 self.num_accepted_tokens.copy_to_gpu()
2111 else:
2112 # Default to 1; update_num_computed_tokens_for_batch_change below
2113 # corrects rows that had drafts from valid_sampled_token_count.
2114 self.num_accepted_tokens.np.fill(1)
2115 self.num_accepted_tokens.gpu.fill_(1)
2116
2117 # Update num_computed_tokens on GPU. In async spec decode,
2118 # CPU values are optimistic (all drafts accepted). The kernel
2119 # corrects on GPU using the previous step's
2120 # valid_sampled_token_count_gpu. Otherwise, just copy from CPU.
2121 if (
2122 self.use_async_spec_decode
2123 and self.valid_sampled_token_count_gpu is not None
2124 and prev_req_id_to_index
2125 ):
2126 self.prev_positions.copy_to_gpu(num_reqs)
2127 self.prev_num_draft_tokens.copy_to_gpu()
2128 cpu_values = self.input_batch.num_computed_tokens_cpu_tensor[:num_reqs].to(
2129 device=self.device, non_blocking=True
2130 )
2131 update_num_computed_tokens_for_batch_change(
2132 self.num_computed_tokens,
2133 self.num_accepted_tokens.gpu[:num_reqs],
2134 self.prev_positions.gpu[:num_reqs],
2135 self.valid_sampled_token_count_gpu,
2136 self.prev_num_draft_tokens.gpu,
2137 cpu_values,
2138 )
2139 else:
2140 self.num_computed_tokens[:num_reqs].copy_(
2141 self.input_batch.num_computed_tokens_cpu_tensor[:num_reqs],
2142 non_blocking=True,
2143 )
2144
2145 self.req_indices.np[:total_num_scheduled_tokens] = req_indices
2146 self.req_indices.copy_to_gpu(total_num_scheduled_tokens)
2147 req_indices_gpu = self.req_indices.gpu[:total_num_scheduled_tokens]
2148
2149 self.query_pos.copy_to_gpu(total_num_scheduled_tokens)
2150 self.num_scheduled_tokens.np[:num_reqs] = num_scheduled_tokens
2151 self.num_scheduled_tokens.copy_to_gpu(num_reqs)
2152 num_scheduled_tokens_gpu = self.num_scheduled_tokens.gpu[:num_reqs]
2153 self.positions[:total_num_scheduled_tokens] = (
2154 self.num_computed_tokens[req_indices_gpu].to(torch.int64)
2155 + self.query_pos.gpu[:total_num_scheduled_tokens]
2156 )
2157 self.seq_lens[:num_reqs] = (
2158 self.num_computed_tokens[:num_reqs] + num_scheduled_tokens_gpu
2159 )
2160 self.seq_lens[num_reqs:].fill_(0)
2161
2162 self.input_batch.block_table.compute_slot_mapping(
2163 num_reqs,
2164 self.query_start_loc.gpu[: num_reqs + 1],
2165 self.positions[:total_num_scheduled_tokens],
2166 )
2167
2168 # Copy the tensors to the GPU.
2169 self._prepare_input_ids(
2170 scheduler_output,
2171 num_reqs,
2172 total_num_scheduled_tokens,
2173 cu_num_tokens,
2174 )
2175
2176 if self.uses_mrope:
2177 # Only relevant for models using M-RoPE (e.g, Qwen2-VL)
2178 # Copy one row at a time. mrope_positions is allocated as
2179 # [num_dims, max_num_tokens + 1] with a dummy trailing column to keep it
2180 # non-contiguous for torch.compile, so cpu[:, :N] is a strided view.
2181 # copy_() cannot express a strided source as a single
2182 # cudaMemcpyAsync, so it first gathers into a contiguous *pageable*
2183 # temporary, and a pageable H2D ignores non_blocking=True and
2184 # synchronizes the stream before the transfer starts. Each row is
2185 # contiguous within the pinned allocation, so per-row copies stay on
2186 # the pinned path and are genuinely asynchronous.
2187 for row in range(self.mrope_positions.gpu.shape[0]):
2188 self.mrope_positions.gpu[row, :total_num_scheduled_tokens].copy_(
2189 self.mrope_positions.cpu[row, :total_num_scheduled_tokens],
2190 non_blocking=True,
2191 )
2192 if self.use_async_spec_decode and self.uses_mrope:
2193 drift = self.num_computed_tokens[req_indices_gpu].to(
2194 torch.int64
2195 ) - async_tensor_h2d(
2196 self.input_batch.num_computed_tokens_cpu_tensor[req_indices],
2197 device=self.device,
2198 dtype=torch.int64,
2199 )
2200 self.mrope_positions.gpu[:, :total_num_scheduled_tokens] += drift
2201
2202 use_spec_decode = len(scheduler_output.scheduled_spec_decode_tokens) > 0
2203 if not use_spec_decode:
2204 # NOTE(woosuk): Due to chunked prefills, the batch may contain
2205 # partial requests. While we should not sample any token
2206 # from these partial requests, we do so for simplicity.
2207 # We will ignore the sampled tokens from the partial requests.
2208 # TODO: Support prompt logprobs.
2209 logits_indices = query_start_loc[1:] - 1
2210 spec_decode_metadata = None
2211 num_sampled_tokens = np.ones(num_reqs, dtype=np.int32)
2212 else:
2213 # Get the number of draft tokens for each request.
2214 # Iterate over the dictionary rather than all requests since not all
2215 # requests have draft tokens.
2216 num_draft_tokens = np.zeros(num_reqs, dtype=np.int32)
2217 # For chunked prefills, use -1 as mask rather than 0, as guided
2218 # decoding may rollback speculative tokens.
2219 num_decode_draft_tokens = np.full(num_reqs, -1, dtype=np.int32)
2220 for (
2221 req_id,
2222 draft_token_ids,
2223 ) in scheduler_output.scheduled_spec_decode_tokens.items():
2224 req_idx = self.input_batch.req_id_to_index[req_id]
2225 draft_len = len(draft_token_ids)
2226 num_draft_tokens[req_idx] = draft_len
2227 if num_scheduled_tokens[req_idx] == draft_len + 1:
2228 num_decode_draft_tokens[req_idx] = draft_len
2229 spec_decode_metadata = self._calc_spec_decode_metadata(
2230 num_draft_tokens, cu_num_tokens
2231 )
2232 logits_indices = spec_decode_metadata.logits_indices
2233 num_sampled_tokens = num_draft_tokens + 1
2234 # For DECODE only cuda graph of some attention backends (e.g., GDN).
2235 self.num_decode_draft_tokens.np[:num_reqs] = num_decode_draft_tokens
2236 self.num_decode_draft_tokens.np[num_reqs:].fill(-1)
2237 self.num_decode_draft_tokens.copy_to_gpu()
2238
2239 # Hot-Swap lora model
2240 if self.lora_config:
2241 assert (
2242 np.sum(num_sampled_tokens)
2243 <= self.vllm_config.scheduler_config.max_num_batched_tokens
2244 )
2245 self.set_active_loras(
2246 self.input_batch, num_scheduled_tokens, num_sampled_tokens
2247 )
2248
2249 return (
2250 logits_indices,
2251 spec_decode_metadata,
2252 int(num_sampled_tokens.max()),
2253 )
2254
2255 def _build_attention_metadata(
2256 self,
2257 num_tokens: int,
2258 num_reqs: int,
2259 max_query_len: int,
2260 num_tokens_padded: int | None = None,
2261 num_reqs_padded: int | None = None,
2262 ubatch_slices: UBatchSlices | None = None,
2263 logits_indices: torch.Tensor | None = None,
2264 max_num_sampled_tokens: int | None = None,
2265 use_spec_decode: bool = False,
2266 for_cudagraph_capture: bool = False,
2267 num_scheduled_tokens: dict[str, int] | None = None,
2268 cascade_attn_prefix_lens: list[list[int]] | None = None,
2269 slot_mappings: dict[int, torch.Tensor] | None = None,
2270 ) -> tuple[PerLayerAttnMetadata, CommonAttentionMetadata | None]:
2271 """Returns:
2272 tuple[attn_metadata, spec_decode_common_attn_metadata]
2273
2274 """
2275 # Attention metadata is not needed for attention free models
2276 if len(self.kv_cache_config.kv_cache_groups) == 0:
2277 return {}, None
2278
2279 num_tokens_padded = num_tokens_padded or num_tokens
2280 num_reqs_padded = num_reqs_padded or num_reqs
2281 assert num_reqs_padded is not None and num_tokens_padded is not None
2282
2283 attn_metadata: PerLayerAttnMetadata = {}
2284 if ubatch_slices is not None:
2285 attn_metadata = [dict() for _ in range(len(ubatch_slices))]
2286
2287 if for_cudagraph_capture:
2288 # For some attention backends (e.g. FA) with sliding window models we need
2289 # to make sure the backend see a max_seq_len that is larger to the sliding
2290 # window size when capturing to make sure the correct kernel is selected.
2291 max_seq_len = self.max_model_len
2292 else:
2293 max_seq_len = self.optimistic_seq_lens_cpu.numpy()[:num_reqs].max().item()
2294
2295 kv_cache_groups = self.kv_cache_config.kv_cache_groups
2296
2297 def _get_block_table(kv_cache_gid: int):
2298 assert num_reqs_padded is not None and num_tokens_padded is not None
2299 kv_cache_spec = kv_cache_groups[kv_cache_gid].kv_cache_spec
2300 if isinstance(kv_cache_spec, EncoderOnlyAttentionSpec):
2301 blk_table_tensor = torch.zeros(
2302 (num_reqs_padded, 1),
2303 dtype=torch.int32,
2304 device=self.device,
2305 )
2306 else:
2307 blk_table = self.input_batch.block_table[kv_cache_gid]
2308 blk_table_tensor = blk_table.get_device_tensor(num_reqs_padded)
2309
2310 # Fill unused block table entries with NULL_BLOCK_ID (null block)
2311 # for CUDAGraph padding. Block 0 is reserved for padding.
2312 blk_table_tensor[num_reqs:num_reqs_padded].fill_(NULL_BLOCK_ID)
2313 return blk_table_tensor
2314
2315 assert slot_mappings is not None
2316 block_table_gid_0 = _get_block_table(0)
2317 slot_mapping_gid_0 = slot_mappings[0]
2318
2319 num_computed_tokens_cpu = self.input_batch.num_computed_tokens_cpu_tensor[
2320 :num_reqs_padded
2321 ]
2322 num_prompt_tokens_cpu = self.input_batch.num_prompt_tokens_cpu_tensor[
2323 :num_reqs_padded
2324 ]
2325 seq_lens_cpu_upper_bound = self.optimistic_seq_lens_cpu[:num_reqs_padded]
2326
2327 # is_prefilling: True if request is still in prefill phase.
2328 # Used by mamba backends to distinguish actual decodes from
2329 # short extends.
2330 is_prefilling = num_computed_tokens_cpu < num_prompt_tokens_cpu
2331 # Zero out padded rows so stale data from condense() doesn't
2332 # misclassify padding as prefill in CUDA graph mode.
2333 is_prefilling[num_reqs:] = False
2334
2335 # Compute mm_prefix bidirectional ranges before building
2336 # attention metadata so builders handle them during build().
2337 # By default, ranges exceeding sliding_window are skipped to prevent
2338 # early tokens from attending across the entire image span. Models that
2339 # clamp mm_prefix to the sliding window *in-kernel* (e.g. Gemma4, which
2340 # needs HF's (causal OR blockwise) AND sliding_window on sliding layers)
2341 # opt out of the skip so the bidirectional range survives for images
2342 # larger than the window; the kernel then bounds it per-query.
2343 req_doc_ranges: dict[int, list[tuple[int, int]]] | None = None
2344 if self.is_mm_prefix_lm:
2345 req_doc_ranges = {}
2346 hf_text_config = self.model_config.hf_text_config
2347 _bidi_sw = getattr(hf_text_config, "sliding_window", None)
2348 _clamps_in_kernel = getattr(
2349 self.model, "mm_prefix_clamp_sliding_window", False
2350 ) or getattr(hf_text_config, "mm_prefix_clamp_sliding_window", False)
2351 # Some models (DeepSeek-V4 vision) define the bidirectional span
2352 # over the whole sentinel block ([IMAGE_START, IMAGE_END]) rather
2353 # than the embed tokens, and prepend a position-dependent
2354 # alignment pad before the first sentinel. For those, derive the
2355 # span from the full placeholder range and strip the pad.
2356 # TODO(Isotr0py): Refactor mm_prefix_lm implementation
2357 # for better readability and maintainability.
2358 _span_pad_modulus = getattr(
2359 hf_text_config, "mm_prefix_span_leading_pad_modulus", 0
2360 )
2361 for req_id in self.input_batch.req_ids:
2362 image_doc_ranges = []
2363 req_state = self.requests[req_id]
2364 for mm_feature in req_state.mm_features:
2365 if mm_feature.modality == "audio":
2366 continue
2367 pos_info = mm_feature.mm_position
2368 if _span_pad_modulus:
2369 pad = (
2370 _span_pad_modulus - 1 - pos_info.offset % _span_pad_modulus
2371 )
2372 img_doc_range = [
2373 (
2374 pos_info.offset + pad,
2375 pos_info.offset + pos_info.length - 1,
2376 )
2377 ]
2378 else:
2379 img_doc_range = pos_info.extract_embeds_range()
2380 for r in img_doc_range:
2381 if (
2382 not _clamps_in_kernel
2383 and _bidi_sw is not None
2384 and (r[1] - r[0] + 1) > _bidi_sw
2385 ):
2386 continue
2387 image_doc_ranges.append(r)
2388 req_idx = self.input_batch.req_id_to_index[req_id]
2389 req_doc_ranges[req_idx] = image_doc_ranges
2390
2391 # Reference Sliding Window Attention (R-SWA): pass per-request prompt
2392 # lengths so the attention backend can keep the prefix globally visible.
2393 # The backend owns the persistent CUDA-graph-safe GPU buffer.
2394 rswa_prefix_lens = None
2395 if self.model_config.rswa_window is not None:
2396 rswa_prefix_lens = num_prompt_tokens_cpu
2397
2398 replayssm_decode_base_cpu = None
2399 if self.cache_config.use_replayssm:
2400 replayssm_decode_base_cpu = (
2401 self.input_batch.replayssm_decode_base_cpu_tensor[:num_reqs_padded]
2402 )
2403
2404 cm_base = CommonAttentionMetadata(
2405 query_start_loc=self.query_start_loc.gpu[: num_reqs_padded + 1],
2406 query_start_loc_cpu=self.query_start_loc.cpu[: num_reqs_padded + 1],
2407 seq_lens=self.seq_lens[:num_reqs_padded],
2408 seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound,
2409 replayssm_decode_base_cpu=replayssm_decode_base_cpu,
2410 num_reqs=num_reqs_padded,
2411 num_actual_tokens=num_tokens_padded,
2412 max_query_len=max_query_len,
2413 max_seq_len=max_seq_len,
2414 block_table_tensor=block_table_gid_0,
2415 slot_mapping=slot_mapping_gid_0,
2416 causal=True,
2417 is_prefilling=is_prefilling,
2418 positions=self.positions[:num_tokens_padded],
2419 mm_req_doc_ranges=req_doc_ranges,
2420 rswa_prefix_lens=rswa_prefix_lens,
2421 )
2422
2423 if self.dcp_world_size > 1:
2424 self.dcp_local_seq_lens.cpu[:num_reqs] = get_dcp_local_seq_lens(
2425 self.optimistic_seq_lens_cpu[:num_reqs],
2426 self.dcp_world_size,
2427 self.dcp_rank,
2428 self.parallel_config.cp_kv_cache_interleave_size,
2429 )
2430 self.dcp_local_seq_lens.cpu[num_reqs:].fill_(0)
2431 self.dcp_local_seq_lens.copy_to_gpu(num_reqs_padded)
2432
2433 cm_base.dcp_local_seq_lens = self.dcp_local_seq_lens.gpu[:num_reqs_padded]
2434 cm_base.dcp_local_seq_lens_cpu_upper_bound = self.dcp_local_seq_lens.cpu[
2435 :num_reqs_padded
2436 ]
2437
2438 if logits_indices is not None and self.cache_config.kv_sharing_fast_prefill:
2439 cm_base.num_logits_indices = logits_indices.size(0)
2440 cm_base.max_logits_per_req = max_num_sampled_tokens
2441 cm_base.logits_indices_padded = self._prepare_kv_sharing_fast_prefill(
2442 logits_indices
2443 )
2444
2445 # Cache attention metadata builds across hybrid KV-cache groups
2446 # The only thing that changes between different hybrid KV-cache groups when the
2447 # same metadata builder and KVCacheSpec is the same is the block table, so we
2448 # can cache the attention metadata builds and just update the block table using
2449 # `builder.update_block_table` if the builder supports it.
2450 cached_attn_metadata: dict[
2451 tuple[KVCacheSpec, type[AttentionMetadataBuilder]], AttentionMetadata
2452 ] = {}
2453
2454 def _build_attn_group_metadata(
2455 kv_cache_gid: int,
2456 attn_gid: int,
2457 common_attn_metadata: CommonAttentionMetadata,
2458 ubid: int | None = None,
2459 ) -> None:
2460 attn_group = self.attn_groups[kv_cache_gid][attn_gid]
2461 builder = attn_group.get_metadata_builder(ubid or 0)
2462 kv_cache_spec = kv_cache_groups[kv_cache_gid].kv_cache_spec
2463 if isinstance(kv_cache_spec, UniformTypeKVCacheSpecs):
2464 kv_cache_spec = kv_cache_spec.kv_cache_specs[attn_group.layer_names[0]]
2465 cache_key = (kv_cache_spec, type(builder))
2466
2467 cascade_attn_prefix_len = (
2468 cascade_attn_prefix_lens[kv_cache_gid][attn_gid]
2469 if cascade_attn_prefix_lens
2470 else 0
2471 )
2472
2473 extra_attn_metadata_args = {}
2474 if use_spec_decode and isinstance(
2475 builder,
2476 (
2477 Mamba2AttentionMetadataBuilder,
2478 GDNAttentionMetadataBuilder,
2479 BailingLinearAttentionMetadataBuilder,
2480 ShortConvAttentionMetadataBuilder,
2481 ),
2482 ):
2483 assert ubid is None, (
2484 "UBatching not supported with GDN or linear attn yet"
2485 )
2486 extra_attn_metadata_args = dict(
2487 num_accepted_tokens=self.num_accepted_tokens.gpu[:num_reqs_padded],
2488 num_decode_draft_tokens_cpu=self.num_decode_draft_tokens.cpu[
2489 :num_reqs_padded
2490 ],
2491 )
2492
2493 if for_cudagraph_capture:
2494 attn_metadata_i = builder.build_for_cudagraph_capture(
2495 common_attn_metadata
2496 )
2497 elif (
2498 cache_key in cached_attn_metadata
2499 and builder.supports_update_block_table
2500 ):
2501 attn_metadata_i = builder.update_block_table(
2502 cached_attn_metadata[cache_key],
2503 common_attn_metadata.block_table_tensor,
2504 common_attn_metadata.slot_mapping,
2505 )
2506 else:
2507 attn_metadata_i = builder.build(
2508 common_prefix_len=cascade_attn_prefix_len,
2509 common_attn_metadata=common_attn_metadata,
2510 **extra_attn_metadata_args,
2511 )
2512 if builder.supports_update_block_table:
2513 cached_attn_metadata[cache_key] = attn_metadata_i
2514
2515 if ubid is None:
2516 assert isinstance(attn_metadata, dict)
2517 attn_metadata_dict = attn_metadata
2518 else:
2519 assert isinstance(attn_metadata, list)
2520 attn_metadata_dict = attn_metadata[ubid]
2521
2522 for layer_name in attn_group.layer_names:
2523 attn_metadata_dict[layer_name] = attn_metadata_i
2524
2525 # Prepare the attention metadata for each KV cache group and make layers
2526 # in the same group share the same metadata.
2527 spec_decode_common_attn_metadata = None
2528 for kv_cache_gid, kv_cache_group in enumerate(kv_cache_groups):
2529 cm = copy(cm_base) # shallow copy
2530
2531 # Basically only the encoder seq_lens, block_table and slot_mapping change
2532 # for each kv_cache_group.
2533 cm.encoder_seq_lens, cm.encoder_seq_lens_cpu = self._get_encoder_seq_lens(
2534 num_scheduled_tokens or {},
2535 kv_cache_group.kv_cache_spec,
2536 num_reqs_padded,
2537 for_cudagraph_capture=for_cudagraph_capture,
2538 )
2539 if kv_cache_gid > 0:
2540 cm.block_table_tensor = _get_block_table(kv_cache_gid)
2541 cm.slot_mapping = slot_mappings[kv_cache_gid]
2542
2543 if self.speculative_config and spec_decode_common_attn_metadata is None:
2544 if isinstance(
2545 self.drafter,
2546 (
2547 EagleProposer,
2548 DFlashProposer,
2549 Gemma4Proposer,
2550 ExtractHiddenStatesProposer,
2551 ),
2552 ):
2553 if self.drafter.kv_cache_gid == kv_cache_gid:
2554 spec_decode_common_attn_metadata = cm
2555 else:
2556 spec_decode_common_attn_metadata = cm
2557 # Capture per-group block tables for multi-group proposers.
2558 if self.speculative_config and isinstance(self.drafter, Step3p5MTPProposer):
2559 self.drafter.set_per_group_attn_metadata(
2560 kv_cache_gid, cm.block_table_tensor, cm.slot_mapping
2561 )
2562 elif self.speculative_config and isinstance(self.drafter, Gemma4Proposer):
2563 self.drafter.set_per_group_block_table(
2564 kv_cache_gid, cm.block_table_tensor
2565 )
2566
2567 for attn_gid in range(len(self.attn_groups[kv_cache_gid])):
2568 if ubatch_slices is not None:
2569 for ubid, _cm in enumerate(split_attn_metadata(ubatch_slices, cm)):
2570 _build_attn_group_metadata(kv_cache_gid, attn_gid, _cm, ubid)
2571
2572 else:
2573 _build_attn_group_metadata(kv_cache_gid, attn_gid, cm)
2574
2575 if spec_decode_common_attn_metadata is not None and (
2576 num_reqs != num_reqs_padded or num_tokens != num_tokens_padded
2577 ):
2578 # Currently the drafter still only uses piecewise cudagraphs (and modifies
2579 # the attention metadata in directly), and therefore does not want to use
2580 # padded attention metadata.
2581 spec_decode_common_attn_metadata = (
2582 spec_decode_common_attn_metadata.unpadded(num_tokens, num_reqs)
2583 )
2584
2585 return attn_metadata, spec_decode_common_attn_metadata
2586
2587 def _compute_cascade_attn_prefix_lens(
2588 self,
2589 num_scheduled_tokens: np.ndarray,
2590 num_computed_tokens: np.ndarray,
2591 num_common_prefix_blocks: list[int],
2592 ) -> list[list[int]] | None:
2593 """Returns:
2594 Optional[cascade_attn_prefix_lens]
2595 cascade_attn_prefix_lens is 2D:
2596 ``[kv_cache_group_id][attn_group_idx]``,
2597 None if we should not use cascade attention
2598
2599 """
2600 use_cascade_attn = False
2601 num_kv_cache_groups = len(self.kv_cache_config.kv_cache_groups)
2602 cascade_attn_prefix_lens: list[list[int]] = [
2603 [] for _ in range(num_kv_cache_groups)
2604 ]
2605
2606 for kv_cache_gid in range(num_kv_cache_groups):
2607 for attn_group in self.attn_groups[kv_cache_gid]:
2608 if isinstance(attn_group.kv_cache_spec, EncoderOnlyAttentionSpec):
2609 cascade_attn_prefix_len = 0
2610 else:
2611 # 0 if cascade attention should not be used
2612 cascade_attn_prefix_len = self._compute_cascade_attn_prefix_len(
2613 num_scheduled_tokens,
2614 num_computed_tokens,
2615 num_common_prefix_blocks[kv_cache_gid],
2616 attn_group.kv_cache_spec,
2617 attn_group.get_metadata_builder(),
2618 )
2619 cascade_attn_prefix_lens[kv_cache_gid].append(cascade_attn_prefix_len)
2620 use_cascade_attn |= cascade_attn_prefix_len > 0
2621
2622 return cascade_attn_prefix_lens if use_cascade_attn else None
2623
2624 def _compute_cascade_attn_prefix_len(
2625 self,
2626 num_scheduled_tokens: np.ndarray,
2627 num_computed_tokens: np.ndarray,
2628 num_common_prefix_blocks: int,
2629 kv_cache_spec: KVCacheSpec,
2630 attn_metadata_builder: AttentionMetadataBuilder,
2631 ) -> int:
2632 """Compute the length of the common prefix for cascade attention.
2633
2634 NOTE(woosuk): The common prefix length returned by this function
2635 represents the length used specifically for cascade attention, not the
2636 actual number of tokens shared between requests. When cascade attention
2637 is disabled (use_cascade=False), this function returns 0 even if
2638 requests share common tokens. Additionally, the common prefix length is
2639 truncated to a multiple of the block size and may be further truncated
2640 due to implementation details explained below.
2641
2642 Args:
2643 num_scheduled_tokens: Number of tokens scheduled per request.
2644 num_computed_tokens: Number of tokens already computed per request.
2645 num_common_prefix_blocks: Number of shared KV cache blocks.
2646 kv_cache_spec: The KV cache spec of the group being considered.
2647 attn_metadata_builder: Builder used to decide whether cascade
2648 attention can be used.
2649
2650 Returns:
2651 int: Length of common prefix in tokens.
2652
2653 """
2654 common_prefix_len = num_common_prefix_blocks * kv_cache_spec.block_size
2655 if common_prefix_len == 0:
2656 # Common case.
2657 return 0
2658
2659 # NOTE(woosuk): Cascade attention uses two attention kernels: one
2660 # for the common prefix and the other for the rest. For the first
2661 # kernel, we concatenate all the query tokens (possibly from
2662 # different requests) and treat them as if they are from the same
2663 # request. Then, we use bi-directional attention to process the
2664 # common prefix in the KV cache. Importantly, this means that the
2665 # first kernel does not do any masking.
2666
2667 # Consider the following example:
2668 # Request 1's input query: [D, E, X]
2669 # Request 1's kv cache: [A, B, C, D, E, X]
2670 # Request 1's num_computed_tokens: 3 (i.e., [A, B, C])
2671 # Request 2's input query: [E, Y]
2672 # Request 2's kv cache: [A, B, C, D, E, Y]
2673 # Request 2's num_computed_tokens: 4 (i.e., [A, B, C, D])
2674
2675 # If we use [A, B, C, D, E] as the common prefix, then the
2676 # first kernel will compute the bi-directional attention between
2677 # input query [D, E, X, E, Y] and common prefix [A, B, C, D, E].
2678 # However, this is wrong because D in Request 1 should not attend to
2679 # E in the common prefix (i.e., we need masking).
2680 # To avoid this, [A, B, C, D] should be the common prefix.
2681 # That is, the common prefix should be capped by the minimum
2682 # num_computed_tokens among the requests, and plus one to include
2683 # the first token of the query.
2684
2685 # In practice, we use [A, B, C] as the common prefix, instead of
2686 # [A, B, C, D] (i.e., the common prefix is capped by the minimum
2687 # num_computed_tokens, without plus one).
2688 # This is because of an implementation detail: We want to always
2689 # use two kernels for cascade attention. Let's imagine:
2690 # Request 3's input query: [D]
2691 # Request 3's kv cache: [A, B, C, D]
2692 # Request 3's num_computed_tokens: 3 (i.e., [A, B, C])
2693 # If we use [A, B, C, D] as the common prefix for Request 1-3,
2694 # then Request 3 will be processed only by the first kernel,
2695 # and the second kernel will get an empty input. While this is not
2696 # a fundamental problem, our current implementation does not support
2697 # this case.
2698 common_prefix_len = min(common_prefix_len, num_computed_tokens.min())
2699 # common_prefix_len should be a multiple of the block size.
2700 common_prefix_len = (
2701 common_prefix_len // kv_cache_spec.block_size * kv_cache_spec.block_size
2702 )
2703 use_sliding_window = isinstance(kv_cache_spec, SlidingWindowSpec) or (
2704 isinstance(kv_cache_spec, FullAttentionSpec)
2705 and kv_cache_spec.sliding_window is not None
2706 )
2707 use_local_attention = isinstance(kv_cache_spec, ChunkedLocalAttentionSpec) or (
2708 isinstance(kv_cache_spec, FullAttentionSpec)
2709 and kv_cache_spec.attention_chunk_size is not None
2710 )
2711 assert isinstance(kv_cache_spec, AttentionSpec)
2712 use_cascade = attn_metadata_builder.use_cascade_attention(
2713 common_prefix_len=common_prefix_len,
2714 query_lens=num_scheduled_tokens,
2715 num_query_heads=self.num_query_heads,
2716 num_kv_heads=kv_cache_spec.num_kv_heads,
2717 use_alibi=self.use_alibi,
2718 use_sliding_window=use_sliding_window,
2719 use_local_attention=use_local_attention,
2720 num_sms=self.num_sms,
2721 dcp_world_size=self.dcp_world_size,
2722 )
2723 return common_prefix_len if use_cascade else 0
2724
2725 def _calc_mrope_positions(self, scheduler_output: "SchedulerOutput"):
2726 mrope_pos_ptr = 0
2727 for index, req_id in enumerate(self.input_batch.req_ids):
2728 req = self.requests[req_id]
2729 assert req.mrope_positions is not None
2730
2731 num_computed_tokens = self.input_batch.num_computed_tokens_cpu[index]
2732 num_scheduled_tokens = scheduler_output.num_scheduled_tokens[req_id]
2733 num_prompt_tokens = length_from_prompt_token_ids_or_embeds(
2734 req.prompt_token_ids, req.prompt_embeds
2735 )
2736
2737 if num_computed_tokens + num_scheduled_tokens > num_prompt_tokens:
2738 prompt_part_len = max(0, num_prompt_tokens - num_computed_tokens)
2739 completion_part_len = max(0, num_scheduled_tokens - prompt_part_len)
2740 else:
2741 prompt_part_len = num_scheduled_tokens
2742 completion_part_len = 0
2743
2744 assert num_scheduled_tokens == prompt_part_len + completion_part_len
2745
2746 if prompt_part_len > 0:
2747 # prompt's mrope_positions are pre-computed
2748 dst_start = mrope_pos_ptr
2749 dst_end = mrope_pos_ptr + prompt_part_len
2750 src_start = num_computed_tokens
2751 src_end = num_computed_tokens + prompt_part_len
2752
2753 self.mrope_positions.cpu[:, dst_start:dst_end] = req.mrope_positions[
2754 :, src_start:src_end
2755 ]
2756 mrope_pos_ptr += prompt_part_len
2757
2758 if completion_part_len > 0:
2759 # compute completion's mrope_positions on-the-fly
2760 dst_start = mrope_pos_ptr
2761 dst_end = mrope_pos_ptr + completion_part_len
2762
2763 assert req.mrope_position_delta is not None
2764 MRotaryEmbedding.get_next_input_positions_tensor(
2765 out=self.mrope_positions.np,
2766 out_offset=dst_start,
2767 mrope_position_delta=req.mrope_position_delta,
2768 context_len=num_computed_tokens + prompt_part_len,
2769 num_new_tokens=completion_part_len,
2770 )
2771
2772 mrope_pos_ptr += completion_part_len
2773
2774 def _calc_spec_decode_metadata(
2775 self,
2776 num_draft_tokens: np.ndarray,
2777 cu_num_scheduled_tokens: np.ndarray,
2778 ) -> SpecDecodeMetadata:
2779 # Inputs:
2780 # cu_num_scheduled_tokens: [ 4, 104, 107, 207, 209]
2781 # num_draft_tokens: [ 3, 0, 2, 0, 1]
2782 # Outputs:
2783 # cu_num_draft_tokens: [ 3, 3, 5, 5, 6]
2784 # logits_indices: [ 0, 1, 2, 3, 103, 104, 105, 106,
2785 # 206, 207, 208]
2786 # target_logits_indices: [ 0, 1, 2, 5, 6, 9]
2787 # bonus_logits_indices: [ 3, 4, 7, 8, 10]
2788
2789 # Compute the logits indices.
2790 # [4, 1, 3, 1, 2]
2791 num_sampled_tokens = num_draft_tokens + 1
2792
2793 # Step 1.
2794 # cu_num_sampled_tokens: [4, 5, 8, 9, 11]
2795 # _arange_scratch[:11]: [0, 1, 2, 3, 0, 0, 1, 2, 0, 0, 1]
2796 cu_num_sampled_tokens = self._get_cumsum_and_arange(
2797 num_sampled_tokens, self._arange_scratch, cumsum_dtype=np.int32
2798 )
2799 # Step 2. [0, 0, 0, 0, 103, 104, 104, 104, 206, 207, 207]
2800 logits_indices = np.repeat(
2801 cu_num_scheduled_tokens - num_sampled_tokens, num_sampled_tokens
2802 )
2803 # Step 3. [0, 1, 2, 3, 103, 104, 105, 106, 206, 207, 208]
2804 logits_indices += self._arange_scratch[: cu_num_sampled_tokens[-1]]
2805
2806 # Compute the bonus logits indices.
2807 bonus_logits_indices = cu_num_sampled_tokens - 1
2808
2809 # Compute the draft logits indices.
2810 # cu_num_draft_tokens: [3, 3, 5, 5, 6]
2811 # _arange_scratch[:6]: [0, 1, 2, 0, 1, 0]
2812 cu_num_draft_tokens = self._get_cumsum_and_arange(
2813 num_draft_tokens, self._arange_scratch, cumsum_dtype=np.int32
2814 )
2815 # [0, 0, 0, 5, 5, 9]
2816 target_logits_indices = np.repeat(
2817 cu_num_sampled_tokens - num_sampled_tokens, num_draft_tokens
2818 )
2819 # [0, 1, 2, 5, 6, 9]
2820 target_logits_indices += self._arange_scratch[: cu_num_draft_tokens[-1]]
2821
2822 cu_num_draft_tokens = async_tensor_h2d(cu_num_draft_tokens, device=self.device)
2823 cu_num_sampled_tokens = async_tensor_h2d(
2824 cu_num_sampled_tokens, device=self.device
2825 )
2826 logits_indices = async_tensor_h2d(logits_indices, device=self.device)
2827 target_logits_indices = async_tensor_h2d(
2828 target_logits_indices, device=self.device
2829 )
2830 bonus_logits_indices = async_tensor_h2d(
2831 bonus_logits_indices, device=self.device
2832 )
2833
2834 # Compute the draft token ids.
2835 # draft_token_indices: [ 1, 2, 3, 105, 106, 208]
2836 draft_token_ids = self.input_ids.gpu[logits_indices]
2837 draft_token_ids = draft_token_ids[target_logits_indices + 1]
2838
2839 return SpecDecodeMetadata(
2840 draft_token_ids=draft_token_ids,
2841 num_draft_tokens=num_draft_tokens.tolist(),
2842 cu_num_draft_tokens=cu_num_draft_tokens,
2843 cu_num_sampled_tokens=cu_num_sampled_tokens,
2844 target_logits_indices=target_logits_indices,
2845 bonus_logits_indices=bonus_logits_indices,
2846 logits_indices=logits_indices,
2847 )
2848
2849 def _prepare_kv_sharing_fast_prefill(
2850 self,
2851 logits_indices: torch.Tensor,
2852 ) -> torch.Tensor:
2853 assert self.kv_sharing_fast_prefill_logits_indices is not None
2854 num_logits = logits_indices.shape[0]
2855 assert num_logits > 0
2856 self.kv_sharing_fast_prefill_logits_indices[:num_logits].copy_(logits_indices)
2857 # There might have leftover indices in logits_indices[num_logits:]
2858 # from previous iterations, whose values may be greater than the
2859 # batch size in the current iteration. To ensure indices are always
2860 # valid, fill the padded indices with the last index. Broadcast the
2861 # scalar GPU-side to avoid a D2H sync on `.item()`.
2862 self.kv_sharing_fast_prefill_logits_indices[num_logits:] = logits_indices[-1]
2863 # Dispatch for the decoder portion of the model.
2864 _, batch_desc = self.cudagraph_dispatcher.dispatch(
2865 num_logits, invalid_modes={CUDAGraphMode.FULL}
2866 )
2867 num_logits_padded = batch_desc.num_tokens
2868 logits_indices_padded = self.kv_sharing_fast_prefill_logits_indices[
2869 :num_logits_padded
2870 ]
2871 return logits_indices_padded
2872
2873 def _batch_mm_inputs_from_scheduler(
2874 self,
2875 scheduler_output: "SchedulerOutput",
2876 ) -> tuple[
2877 list[str],
2878 list[tuple[str, MultiModalKwargsItem]],
2879 list[tuple[str, PlaceholderRange]],
2880 ]:
2881 """Batch multimodal inputs from scheduled encoder inputs.
2882
2883 Args:
2884 scheduler_output: The scheduler output containing scheduled encoder
2885 inputs.
2886
2887 Returns:
2888 A tuple of (mm_hashes, mm_kwargs, mm_lora_refs) where:
2889 - mm_hashes: List of multimodal hashes for each item
2890 - mm_kwargs: List of multimodal kwargs for each item
2891 - mm_lora_refs: List of (req_id, placeholder_range) for each item
2892
2893 """
2894 scheduled_encoder_inputs = scheduler_output.scheduled_encoder_inputs
2895 if not scheduled_encoder_inputs:
2896 return [], [], []
2897
2898 mm_hashes = list[str]()
2899 mm_kwargs = list[tuple[str, MultiModalKwargsItem]]()
2900 # Multimodal LoRA reference info to map each multimodal item
2901 # back to its request & position
2902 mm_lora_refs = list[tuple[str, PlaceholderRange]]()
2903 for req_id, encoder_input_ids in scheduled_encoder_inputs.items():
2904 req_state = self.requests[req_id]
2905
2906 for mm_input_id in encoder_input_ids:
2907 mm_feature = req_state.mm_features[mm_input_id]
2908 if mm_feature.data is None:
2909 continue
2910
2911 mm_hashes.append(mm_feature.identifier)
2912 mm_kwargs.append((mm_feature.modality, mm_feature.data))
2913 mm_lora_refs.append((req_id, mm_feature.mm_position))
2914
2915 return mm_hashes, mm_kwargs, mm_lora_refs
2916
2917 def _cache_encoder_output(
2918 self,
2919 mm_hash: str,
2920 output: torch.Tensor,
2921 ec_manager_metadata: "EncoderCacheManagerMetadata | None",
2922 free_encoder_mm_hashes: list[str],
2923 ) -> None:
2924 """Store an encoder output for later multimodal embedding gather."""
2925 del ec_manager_metadata, free_encoder_mm_hashes
2926 self.encoder_cache[mm_hash] = output
2927 self.maybe_save_ec_to_connector(self.encoder_cache, mm_hash)
2928
2929 def _execute_mm_encoder(
2930 self, scheduler_output: "SchedulerOutput"
2931 ) -> list[torch.Tensor]:
2932 mm_hashes, mm_kwargs, mm_lora_refs = self._batch_mm_inputs_from_scheduler(
2933 scheduler_output
2934 )
2935
2936 if not mm_kwargs:
2937 return []
2938
2939 # `prompt_embeds` is a passthrough modality, the tensor is already in
2940 # the model embedding space, so no encoder runs. Inject each
2941 # `prompt_embeds` tensor directly into the encoder cache here so that
2942 # `_gather_mm_embeddings` can splice it via the standard `is_mm_embed`
2943 # path.
2944 pe_indices = [
2945 i
2946 for i, (modality, _) in enumerate(mm_kwargs)
2947 if modality == "prompt_embeds"
2948 ]
2949 if pe_indices:
2950 for i in pe_indices:
2951 pe_tensor = mm_kwargs[i][1]["embedding"].data
2952 assert isinstance(pe_tensor, torch.Tensor)
2953
2954 self._cache_encoder_output(
2955 mm_hashes[i],
2956 async_tensor_h2d(pe_tensor, device=self.device),
2957 scheduler_output.ec_manager_metadata,
2958 scheduler_output.free_encoder_mm_hashes,
2959 )
2960 # Filter out `prompt_embeds` items from mm_kwargs/mm_hashes/mm_lora_refs
2961 # since they don't require further encoder processing.
2962 mm_hashes = [h for i, h in enumerate(mm_hashes) if i not in pe_indices]
2963 mm_kwargs = [k for i, k in enumerate(mm_kwargs) if i not in pe_indices]
2964 mm_lora_refs = [
2965 r for i, r in enumerate(mm_lora_refs) if i not in pe_indices
2966 ]
2967 if not mm_kwargs:
2968 return [] # nothing left to encode after filtering out `prompt_embeds`
2969
2970 should_time = bool(
2971 self.observability_config
2972 and self.observability_config.enable_mm_processor_stats
2973 and scheduler_output.scheduled_encoder_inputs
2974 )
2975
2976 # Batch mm inputs as much as we can: if a request in the batch has
2977 # multiple modalities or a different modality than the previous one,
2978 # we process it separately to preserve item order.
2979 # FIXME(ywang96): This is a hacky way to deal with multiple modalities
2980 # in the same batch while still being able to benefit from batching
2981 # multimodal inputs. The proper solution should be reordering the
2982 # encoder outputs.
2983 model = cast(SupportsMultiModal, self.model)
2984
2985 if self.lora_config and self.lora_manager.supports_tower_connector_lora():
2986 # Build LoRA mappings independently for encoder inputs
2987 # (encoder batch structure is different from main batch)
2988 prompt_lora_mapping = []
2989 token_lora_mapping = []
2990 lora_requests = set()
2991 encoder_token_counts = []
2992 connector_token_counts = []
2993
2994 for (req_id, pos_info), (modality, mm_item) in zip(
2995 mm_lora_refs,
2996 mm_kwargs,
2997 ):
2998 req_idx = self.input_batch.req_id_to_index[req_id]
2999 lora_id = int(self.input_batch.request_lora_mapping[req_idx])
3000
3001 tower_tokens, connector_tokens = self.model.get_mm_lora_token_counts( # type: ignore[attr-defined]
3002 modality=modality,
3003 mm_kwargs=mm_item,
3004 num_mm_embeds=pos_info.get_num_embeds(),
3005 )
3006 prompt_lora_mapping.append(lora_id)
3007 token_lora_mapping.extend([lora_id] * tower_tokens)
3008 encoder_token_counts.append(tower_tokens)
3009 connector_token_counts.append(connector_tokens)
3010
3011 if lora_id > 0:
3012 lora_request = self.input_batch.lora_id_to_lora_request.get(lora_id)
3013 if lora_request is not None:
3014 lora_requests.add(lora_request)
3015
3016 # Set tower adapter mapping
3017 tower_mapping = LoRAMapping(
3018 tuple(token_lora_mapping),
3019 tuple(prompt_lora_mapping),
3020 is_prefill=True,
3021 type=LoRAMappingType.TOWER,
3022 )
3023 self.lora_manager.set_active_adapters(lora_requests, tower_mapping)
3024
3025 # Only set connector mapping if the model actually has a connector.
3026 # Some multimodal models inherit a stub `get_num_mm_connector_tokens`
3027 # from `SupportsMultiModal`, which returns None and should not be
3028 # treated as a signal that connector LoRA is supported.
3029 mm_mapping = (
3030 self.model.get_mm_mapping() # type: ignore[attr-defined]
3031 if hasattr(self.model, "get_mm_mapping")
3032 else None
3033 )
3034 if (
3035 mm_mapping is not None
3036 and mm_mapping.connector
3037 and all(count is not None for count in connector_token_counts)
3038 ):
3039 connector_token_mapping = np.repeat(
3040 np.array(prompt_lora_mapping, dtype=np.int32),
3041 np.array(connector_token_counts, dtype=np.int32),
3042 )
3043 connector_mapping = LoRAMapping(
3044 index_mapping=tuple(connector_token_mapping.tolist()),
3045 prompt_mapping=tuple(prompt_lora_mapping),
3046 is_prefill=True,
3047 type=LoRAMappingType.CONNECTOR,
3048 )
3049
3050 self.lora_manager.set_active_adapters(
3051 lora_requests,
3052 connector_mapping,
3053 )
3054
3055 encoder_outputs: list[torch.Tensor] = []
3056 # Track the current index in mm_kwargs/mm_lora_refs to map groups to request IDs
3057 current_item_idx = 0
3058 for modality, num_items, mm_kwargs_batch in group_and_batch_mm_kwargs(
3059 mm_kwargs, device=self.device, pin_memory=PIN_MEMORY
3060 ):
3061 batch_outputs: MultiModalEmbeddings
3062
3063 # EVS and dynamic res video related change.
3064 # (ekhvedchenia): Temporary hack to limit peak memory usage when
3065 # processing multimodal data. This solves the issue with scheduler
3066 # putting too many video samples into a single batch. Scheduler
3067 # uses pruned vision tokens count to compare it versus compute
3068 # budget which is incorrect (Either input media size or non-pruned
3069 # output vision tokens count should be considered)
3070 # dynamic res video for nemotron temporarily uses this hack via
3071 # requires_sequential_video_encoding
3072 # because it doesn't yet support video batching.
3073 # TODO(ywang96): Fix memory profiling to take EVS into account and
3074 # remove this hack.
3075 if (
3076 (
3077 self.is_multimodal_pruning_enabled
3078 or self.requires_sequential_video_encoding
3079 )
3080 and modality == "video"
3081 and num_items > 1
3082 ):
3083 batch_outputs_lst = list[torch.Tensor]()
3084 for video_idx in range(num_items):
3085 video_mm_kwargs_item = mm_kwargs[current_item_idx + video_idx]
3086 with self.timed_encoder_operation(
3087 should_time, mm_lora_refs, current_item_idx + video_idx, 1
3088 ):
3089 _, _, micro_batch_mm_inputs = next(
3090 group_and_batch_mm_kwargs(
3091 [video_mm_kwargs_item],
3092 device=self.device,
3093 pin_memory=PIN_MEMORY,
3094 )
3095 )
3096
3097 micro_batch_outputs = model.embed_multimodal(
3098 **micro_batch_mm_inputs
3099 )
3100
3101 batch_outputs_lst.extend(micro_batch_outputs)
3102
3103 batch_outputs = batch_outputs_lst
3104 else:
3105 # Run the encoder.
3106 # `batch_outputs` is either of the following:
3107 # 1. A tensor of shape (num_items, feature_size, hidden_size)
3108 # in case feature_size is fixed across all multimodal items.
3109 # 2. A list or tuple (length: num_items) of tensors,
3110 # each of shape (feature_size, hidden_size) in case the feature
3111 # size is dynamic depending on the input multimodal items.
3112
3113 with self.timed_encoder_operation(
3114 should_time, mm_lora_refs, current_item_idx, num_items
3115 ):
3116 cudagraph_output = None
3117 if (
3118 self.encoder_cudagraph_manager is not None
3119 and self.encoder_cudagraph_manager.supports_modality(modality)
3120 ):
3121 cudagraph_output = self.encoder_cudagraph_manager.execute(
3122 mm_kwargs_batch,
3123 )
3124
3125 if cudagraph_output is not None:
3126 batch_outputs = cudagraph_output
3127 else:
3128 batch_outputs = model.embed_multimodal(**mm_kwargs_batch)
3129
3130 sanity_check_mm_encoder_outputs(batch_outputs, expected_num_items=num_items)
3131 encoder_outputs.extend(batch_outputs)
3132
3133 current_item_idx += num_items
3134
3135 # Cache the encoder outputs by mm_hash
3136 for mm_hash, output in zip(mm_hashes, encoder_outputs):
3137 self._cache_encoder_output(
3138 mm_hash,
3139 output,
3140 scheduler_output.ec_manager_metadata,
3141 scheduler_output.free_encoder_mm_hashes,
3142 )
3143 logger.debug("Finish execute for mm hash %s", mm_hash)
3144
3145 return encoder_outputs
3146
3147 def _get_encoder_output_from_cache(self, mm_hash: str) -> torch.Tensor | None:
3148 """Return a cached encoder output for multimodal
3149 embedding gather."""
3150 return self.encoder_cache.get(mm_hash, None)
3151
3152 def _gather_mm_embeddings(
3153 self,
3154 scheduler_output: "SchedulerOutput",
3155 shift_computed_tokens: int = 0,
3156 ) -> tuple[list[torch.Tensor], torch.Tensor]:
3157 total_num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
3158
3159 mm_embeds = list[torch.Tensor]()
3160 is_mm_embed = torch.zeros(
3161 total_num_scheduled_tokens,
3162 dtype=torch.bool,
3163 device="cpu",
3164 pin_memory=PIN_MEMORY,
3165 )
3166
3167 req_start_idx = 0
3168 should_sync_mrope_positions = False
3169
3170 for req_id in self.input_batch.req_ids:
3171 mm_embeds_req: list[torch.Tensor] = []
3172
3173 num_scheduled_tokens = scheduler_output.num_scheduled_tokens[req_id]
3174 req_state = self.requests[req_id]
3175 num_computed_tokens = req_state.num_computed_tokens + shift_computed_tokens
3176
3177 mm_features = req_state.mm_features
3178 lo, hi = get_mm_features_in_window(
3179 mm_features,
3180 start=num_computed_tokens,
3181 end=num_computed_tokens + num_scheduled_tokens,
3182 )
3183 for i in range(lo, hi):
3184 mm_feature = mm_features[i]
3185 pos_info = mm_feature.mm_position
3186 start_pos = pos_info.offset
3187 num_encoder_tokens = pos_info.length
3188
3189 start_idx = max(num_computed_tokens - start_pos, 0)
3190 end_idx = min(
3191 num_computed_tokens - start_pos + num_scheduled_tokens,
3192 num_encoder_tokens,
3193 )
3194 assert start_idx < end_idx
3195 curr_embeds_start, curr_embeds_end = (
3196 pos_info.get_embeds_indices_in_range(start_idx, end_idx)
3197 )
3198 # If there are no embeddings in the current range, we skip
3199 # gathering the embeddings.
3200 if curr_embeds_start == curr_embeds_end:
3201 continue
3202
3203 mm_hash = mm_feature.identifier
3204 encoder_output = self._get_encoder_output_from_cache(mm_hash)
3205 if encoder_output is None:
3206 # A feature starting at/after the processed boundary is only
3207 # reached via the drafter's +1 look-ahead and might not be
3208 # encoded yet; fall back to the token embedding for drafting.
3209 if (
3210 start_pos
3211 >= req_state.num_computed_tokens + num_scheduled_tokens
3212 ):
3213 continue
3214 raise RuntimeError(f"Encoder cache miss for {mm_hash}.")
3215
3216 if (is_embed := pos_info.is_embed) is not None:
3217 is_embed = is_embed[start_idx:end_idx]
3218 mm_embeds_item = encoder_output[curr_embeds_start:curr_embeds_end]
3219 else:
3220 mm_embeds_item = encoder_output[start_idx:end_idx]
3221
3222 req_start_pos = req_start_idx + start_pos - num_computed_tokens
3223 # OR mask for overlapping mm_features (use_audio_in_video)
3224 if is_embed is None:
3225 is_mm_embed[req_start_pos + start_idx : req_start_pos + end_idx] = (
3226 True
3227 )
3228 else:
3229 is_mm_embed[
3230 req_start_pos + start_idx : req_start_pos + end_idx
3231 ] |= is_embed
3232 set_mm_embedding_modality(mm_embeds_item, mm_feature.modality)
3233 mm_embeds_req.append(mm_embeds_item)
3234
3235 if self.is_multimodal_pruning_enabled and self.uses_mrope:
3236 assert req_state.mrope_positions is not None
3237 should_sync_mrope_positions = True
3238 old_mm_embeds_req = mm_embeds_req
3239 mm_embeds_req, new_mrope_positions, new_delta = (
3240 self.model.recompute_mrope_positions(
3241 input_ids=req_state.prompt_token_ids,
3242 multimodal_embeddings=mm_embeds_req,
3243 mrope_positions=req_state.mrope_positions,
3244 num_computed_tokens=req_state.num_computed_tokens,
3245 )
3246 )
3247 mm_embeds_req = [
3248 copy_mm_embedding_modality(src, dst)
3249 for src, dst in zip(old_mm_embeds_req, mm_embeds_req)
3250 ]
3251 req_state.mrope_positions.copy_(new_mrope_positions)
3252 req_state.mrope_position_delta = new_delta
3253
3254 mm_embeds.extend(mm_embeds_req)
3255 req_start_idx += num_scheduled_tokens
3256
3257 if should_sync_mrope_positions:
3258 self._calc_mrope_positions(scheduler_output)
3259 self.mrope_positions.copy_to_gpu(total_num_scheduled_tokens)
3260
3261 return mm_embeds, is_mm_embed
3262
3263 def get_model(self) -> nn.Module:
3264 if not hasattr(self, "model"):
3265 raise ValueError("Cannot get model before model has been initialized")
3266 if isinstance(
3267 self.model, (CUDAGraphWrapper, UBatchWrapper, BreakableCUDAGraphWrapper)
3268 ):
3269 # get raw model out of the cudagraph wrapper.
3270 return self.model.unwrap()
3271 return self.model
3272
3273 def get_draft_model(self) -> nn.Module | None:
3274 drafter = getattr(self, "drafter", None)
3275 if drafter is None:
3276 return None
3277 model = getattr(drafter, "model", None)
3278 if isinstance(
3279 model, (CUDAGraphWrapper, UBatchWrapper, BreakableCUDAGraphWrapper)
3280 ):
3281 return cast(nn.Module, model.unwrap())
3282 return cast(nn.Module | None, model)
3283
3284 def get_supported_generation_tasks(self) -> list[GenerationTask]:
3285 model = self.get_model()
3286 supported_tasks = list[GenerationTask]()
3287
3288 if is_text_generation_model(model):
3289 supported_tasks.append("generate")
3290
3291 if supports_transcription(model):
3292 if model.supports_transcription_only:
3293 return ["transcription"]
3294
3295 supported_tasks.append("transcription")
3296
3297 if supports_realtime(model):
3298 supported_tasks.append("realtime")
3299
3300 return supported_tasks
3301
3302 def get_supported_pooling_tasks(self) -> list[PoolingTask]:
3303 model = self.get_model()
3304 if not is_pooling_model(model):
3305 return []
3306
3307 return list(model.pooler.get_supported_tasks())
3308
3309 def get_supported_tasks(self) -> tuple[SupportedTask, ...]:
3310 tasks = list[SupportedTask]()
3311
3312 if self.model_config.runner_type == "generate":
3313 tasks.extend(self.get_supported_generation_tasks())
3314 if self.model_config.runner_type == "pooling":
3315 tasks.extend(self.get_supported_pooling_tasks())
3316
3317 return tuple(tasks)
3318
3319 def sync_and_gather_intermediate_tensors(
3320 self,
3321 num_tokens: int,
3322 intermediate_tensors: IntermediateTensors | None,
3323 sync_self: bool,
3324 ) -> IntermediateTensors:
3325 assert self.intermediate_tensors is not None
3326
3327 tp = self.vllm_config.parallel_config.tensor_parallel_size
3328 is_rs = is_residual_scattered_for_sp(self.vllm_config, num_tokens)
3329
3330 # When sequence parallelism is enabled, the "residual" tensor is
3331 # sharded across TP ranks. All-gather it here because downstream
3332 # QKV + Attention needs the full residual before the SP split point.
3333 if sync_self:
3334 assert intermediate_tensors is not None
3335 for k, v in intermediate_tensors.items():
3336 is_scattered = k == "residual" and is_rs
3337 if is_scattered:
3338 local_len = num_tokens // tp
3339 v = get_tp_group().all_gather(v[:local_len], dim=0)
3340
3341 self.intermediate_tensors[k][:num_tokens].copy_(
3342 v[:num_tokens], non_blocking=True
3343 )
3344
3345 return IntermediateTensors(
3346 {k: v[:num_tokens] for k, v in self.intermediate_tensors.items()}
3347 )
3348
3349 def eplb_step(self, is_dummy: bool = False, is_profile: bool = False) -> None:
3350 """Step for the EPLB (Expert Parallelism Load Balancing) state."""
3351 if not self.parallel_config.enable_eplb or self.eep_eplb_suppressed:
3352 return
3353
3354 assert self.eplb_state is not None
3355 assert self._moe_model is not None
3356 self.eplb_state.step(
3357 is_dummy,
3358 is_profile,
3359 log_stats=self.parallel_config.eplb_config.log_balancedness,
3360 )
3361
3362 def setup_eplb_from_mapping(
3363 self,
3364 expanded_physical_to_logical: torch.Tensor,
3365 ) -> None:
3366 assert self.eplb_state is not None
3367 self.eplb_state.update_mapping(
3368 self.model_config,
3369 expanded_physical_to_logical,
3370 )
3371
3372 def _pool(
3373 self,
3374 hidden_states: torch.Tensor,
3375 num_scheduled_tokens: int,
3376 num_scheduled_tokens_np: np.ndarray,
3377 kv_connector_output: KVConnectorOutput | None,
3378 ) -> ModelRunnerOutput | AsyncModelRunnerOutput:
3379 num_reqs = self.input_batch.num_reqs
3380 assert num_reqs == len(self.input_batch.pooling_params), (
3381 "Either all or none of the requests in a batch must be pooling request"
3382 )
3383
3384 hidden_states = hidden_states[:num_scheduled_tokens]
3385 seq_lens_cpu = self.optimistic_seq_lens_cpu[:num_reqs]
3386
3387 pooling_metadata = self.input_batch.get_pooling_metadata()
3388 pooling_metadata.build_pooling_cursor(
3389 num_scheduled_tokens_np,
3390 seq_lens_cpu,
3391 device=hidden_states.device,
3392 query_start_loc_gpu=self.query_start_loc.gpu[: num_reqs + 1],
3393 )
3394
3395 model = cast(VllmModelForPooling, self.model)
3396 raw_pooler_output: PoolerOutput = model.pooler(
3397 hidden_states=hidden_states, pooling_metadata=pooling_metadata
3398 )
3399
3400 finished_mask = pooling_metadata.get_pooling_cursor().get_finished_mask()
3401 raw_pooler_output = self.late_interaction_runner.postprocess_pooler_output(
3402 raw_pooler_output=raw_pooler_output,
3403 pooling_params=pooling_metadata.pooling_params,
3404 req_ids=self.input_batch.req_ids,
3405 finished_mask=finished_mask,
3406 )
3407
3408 model_runner_output = ModelRunnerOutput(
3409 req_ids=self.input_batch.req_ids.copy(),
3410 req_id_to_index=self.input_batch.req_id_to_index.copy(),
3411 kv_connector_output=kv_connector_output,
3412 )
3413
3414 if raw_pooler_output is None or not any(finished_mask):
3415 self._sync_device()
3416 model_runner_output.pooler_output = [None] * num_reqs
3417 return model_runner_output
3418
3419 if not current_platform.is_cuda_alike():
3420 # cpu/xpu runners cannot use the CUDA stream/event-based wrapper.
3421 model_runner_output.pooler_output = _copy_pooler_output_to_cpu(
3422 raw_pooler_output=raw_pooler_output,
3423 finished_mask=finished_mask,
3424 )
3425 self._sync_device()
3426 return model_runner_output
3427
3428 return AsyncGPUPoolingModelRunnerOutput(
3429 model_runner_output=model_runner_output,
3430 raw_pooler_output=raw_pooler_output,
3431 finished_mask=finished_mask,
3432 async_output_copy_stream=self._get_or_create_async_output_copy_stream(),
3433 )
3434
3435 def _pad_for_sequence_parallelism(self, num_scheduled_tokens: int) -> int:
3436 # Pad tokens to multiple of tensor_parallel_size when
3437 # enabled collective fusion for SP
3438 tp_size = self.vllm_config.parallel_config.tensor_parallel_size
3439 if self.compilation_config.pass_config.enable_sp and tp_size > 1:
3440 return round_up(num_scheduled_tokens, tp_size)
3441 return num_scheduled_tokens
3442
3443 def _prepare_padding_mask(
3444 self, num_tokens_unpadded: int, num_tokens_padded: int
3445 ) -> torch.Tensor:
3446 padding_mask = self.is_padding[:num_tokens_padded]
3447 padding_mask[:num_tokens_unpadded].fill_(False)
3448 padding_mask[num_tokens_unpadded:].fill_(True)
3449 return padding_mask
3450
3451 def _prepare_mm_inputs(
3452 self, num_tokens: int
3453 ) -> tuple[torch.Tensor | None, torch.Tensor]:
3454 if self.model.requires_raw_input_tokens:
3455 input_ids = self.input_ids.gpu[:num_tokens]
3456 else:
3457 input_ids = None
3458
3459 inputs_embeds = self.inputs_embeds.gpu[:num_tokens]
3460 return input_ids, inputs_embeds
3461
3462 def _preprocess(
3463 self,
3464 scheduler_output: "SchedulerOutput",
3465 num_input_tokens: int, # Padded
3466 intermediate_tensors: IntermediateTensors | None = None,
3467 ) -> tuple[
3468 torch.Tensor | None,
3469 torch.Tensor | None,
3470 torch.Tensor,
3471 IntermediateTensors | None,
3472 dict[str, Any],
3473 ECConnectorOutput | None,
3474 ]:
3475 num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
3476 is_first_rank = get_pp_group().is_first_rank
3477 is_encoder_decoder = self.model_config.is_encoder_decoder
3478
3479 # Clamp speculative scheduler placeholders (-1) before embedding lookup.
3480 if self.speculative_config is not None:
3481 self.input_ids.gpu[:num_input_tokens].clamp_(min=0)
3482
3483 # _prepare_inputs may reorder the batch, so we must gather multi
3484 # modal outputs after that to ensure the correct order
3485 ec_connector_output = None
3486
3487 if self.supports_mm_inputs and is_first_rank and not is_encoder_decoder:
3488 # Run the multimodal encoder if any.
3489 with self.maybe_get_ec_connector_output(
3490 scheduler_output,
3491 encoder_cache=self.encoder_cache,
3492 ) as ec_connector_output:
3493 self._execute_mm_encoder(scheduler_output)
3494 mm_embeds, is_mm_embed = self._gather_mm_embeddings(scheduler_output)
3495
3496 # NOTE(woosuk): To unify token ids and soft tokens (vision
3497 # embeddings), we always use embeddings (rather than token ids)
3498 # as input to the multimodal model, even when the input is text.
3499 if self.enable_prompt_embeds and self.input_batch.req_prompt_embeds:
3500 # Some positions carry precomputed prompt_embeds: they are
3501 # already in self.inputs_embeds and marked is_token_ids=False.
3502 # Embed only the token-id positions (zeroing the placeholder ids
3503 # at prompt_embeds positions so the embedding gather cannot read
3504 # out-of-range ids), and write them back without clobbering the
3505 # prompt_embeds positions.
3506 is_token_ids = self.is_token_ids.gpu[:num_scheduled_tokens]
3507 safe_input_ids = torch.where(
3508 is_token_ids,
3509 self.input_ids.gpu[:num_scheduled_tokens],
3510 0,
3511 )
3512 inputs_embeds_scheduled = self.model.embed_input_ids(
3513 safe_input_ids,
3514 multimodal_embeddings=mm_embeds,
3515 is_multimodal=is_mm_embed,
3516 )
3517 target = self.inputs_embeds.gpu[:num_scheduled_tokens]
3518 self.inputs_embeds.gpu[:num_scheduled_tokens] = torch.where(
3519 is_token_ids.unsqueeze(-1),
3520 inputs_embeds_scheduled,
3521 target,
3522 )
3523 else:
3524 inputs_embeds_scheduled = self.model.embed_input_ids(
3525 self.input_ids.gpu[:num_scheduled_tokens],
3526 multimodal_embeddings=mm_embeds,
3527 is_multimodal=is_mm_embed,
3528 )
3529
3530 # TODO(woosuk): Avoid the copy. Optimize.
3531 self.inputs_embeds.gpu[:num_scheduled_tokens].copy_(
3532 inputs_embeds_scheduled
3533 )
3534
3535 input_ids, inputs_embeds = self._prepare_mm_inputs(num_input_tokens)
3536 model_kwargs = {
3537 **self._init_model_kwargs(),
3538 **self._extract_mm_kwargs(scheduler_output),
3539 }
3540 elif self.enable_prompt_embeds and is_first_rank:
3541 # Get the input embeddings for the tokens that are not input embeds,
3542 # then put them into the appropriate positions.
3543 # TODO(qthequartermasterman): Since even when prompt embeds are
3544 # enabled, (a) not all requests will use prompt embeds, and (b)
3545 # after the initial prompt is processed, the rest of the generated
3546 # tokens will be token ids, it is not desirable to have the
3547 # embedding layer outside of the CUDA graph all the time. The v0
3548 # engine avoids this by "double compiling" the CUDA graph, once
3549 # with input_ids and again with inputs_embeds, for all num_tokens.
3550 # If a batch only has token ids, then including the embedding layer
3551 # in the CUDA graph will be more performant (like in the else case
3552 # below).
3553 is_token_ids = self.is_token_ids.np[:num_scheduled_tokens]
3554 token_ids_idx_np = np.nonzero(is_token_ids)[0]
3555 # Some tokens ids may need to become embeds
3556 if token_ids_idx_np.size > 0:
3557 token_ids_idx = async_tensor_h2d(token_ids_idx_np, device=self.device)
3558 token_ids = self.input_ids.gpu[token_ids_idx]
3559 tokens_to_embeds = self.model.embed_input_ids(input_ids=token_ids)
3560 self.inputs_embeds.gpu[token_ids_idx] = tokens_to_embeds
3561
3562 inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens]
3563 model_kwargs = self._init_model_kwargs()
3564 input_ids = None
3565 else:
3566 # For text-only models, we use token ids as input.
3567 # While it is possible to use embeddings as input just like the
3568 # multimodal models, it is not desirable for performance since
3569 # then the embedding layer is not included in the CUDA graph.
3570 input_ids = self.input_ids.gpu[:num_input_tokens]
3571 inputs_embeds = None
3572 model_kwargs = self._init_model_kwargs()
3573
3574 if self.uses_mrope:
3575 positions = self.mrope_positions.gpu[:, :num_input_tokens]
3576 else:
3577 positions = self.positions[:num_input_tokens]
3578 if num_input_tokens > num_scheduled_tokens:
3579 self.positions[num_scheduled_tokens:num_input_tokens].zero_()
3580
3581 if is_first_rank:
3582 intermediate_tensors = None
3583 else:
3584 assert intermediate_tensors is not None
3585 intermediate_tensors = self.sync_and_gather_intermediate_tensors(
3586 num_input_tokens, intermediate_tensors, True
3587 )
3588
3589 if is_encoder_decoder and scheduler_output.scheduled_encoder_inputs:
3590 # Run the encoder, just like we do with other multimodal inputs.
3591 # For an encoder-decoder model, our processing here is a bit
3592 # simpler, because the outputs are just passed to the decoder.
3593 # We are not doing any prompt replacement. We also will only
3594 # ever have a single encoder input.
3595 encoder_outputs = self._execute_mm_encoder(scheduler_output)
3596 model_kwargs.update({"encoder_outputs": encoder_outputs})
3597
3598 return (
3599 input_ids,
3600 inputs_embeds,
3601 positions,
3602 intermediate_tensors,
3603 model_kwargs,
3604 ec_connector_output,
3605 )
3606
3607 def _sample(
3608 self,
3609 logits: torch.Tensor | None,
3610 spec_decode_metadata: SpecDecodeMetadata | None,
3611 ) -> SamplerOutput:
3612 # Sample the next token and get logprobs if needed.
3613 sampling_metadata = self.input_batch.sampling_metadata
3614 # Update output token ids with tokens sampled in last step
3615 # if async scheduling and required by current sampling params.
3616 self.input_batch.update_async_output_token_ids()
3617 if spec_decode_metadata is None:
3618 return self.sampler(
3619 logits=logits,
3620 sampling_metadata=sampling_metadata,
3621 )
3622
3623 # Update spec_token_ids with real draft tokens from pre step only when
3624 # output_token_ids is needed (penalties or bad_words are in use).
3625 if self.use_async_scheduling and self._draft_token_req_ids is not None:
3626 draft_token_ids_cpu, _ = self._get_draft_token_ids_cpu()
3627 self.input_batch.update_async_spec_token_ids(draft_token_ids_cpu)
3628
3629 draft_probs = self._get_spec_decode_draft_probs(spec_decode_metadata)
3630 sampler_output = self.rejection_sampler(
3631 spec_decode_metadata,
3632 draft_probs,
3633 logits,
3634 sampling_metadata,
3635 )
3636 return sampler_output
3637
3638 def _bookkeeping_sync(
3639 self,
3640 scheduler_output: "SchedulerOutput",
3641 sampler_output: SamplerOutput,
3642 logits: torch.Tensor | None,
3643 hidden_states: torch.Tensor,
3644 num_scheduled_tokens: int,
3645 ) -> tuple[
3646 dict[str, int],
3647 torch.Tensor | None,
3648 LogprobsLists | None,
3649 list[list[int]],
3650 dict[str, LogprobsTensors | None],
3651 list[str],
3652 dict[str, int],
3653 list[int],
3654 ]:
3655 num_nans: torch.Tensor | None = None
3656 num_nans_in_logits: dict[str, int] = {}
3657 if envs.VLLM_COMPUTE_NANS_IN_LOGITS:
3658 if self.use_async_scheduling:
3659 # Keep the counts on device; they ride the async output copy
3660 # stream rather than blocking here.
3661 num_nans = None if logits is None else count_nans_per_row(logits)
3662 else:
3663 num_nans_in_logits = self._get_nans_in_logits(logits)
3664
3665 num_reqs = self.input_batch.num_reqs
3666 discard_sampled_tokens_req_indices = np.nonzero(
3667 self.discard_request_mask.np[:num_reqs]
3668 )[0]
3669 for i in discard_sampled_tokens_req_indices:
3670 gen = self.input_batch.generators.get(int(i))
3671 if gen is not None:
3672 gen.set_offset(gen.get_offset() - 4)
3673
3674 # Copy some objects so they don't get modified after returning.
3675 # This is important when using async scheduling.
3676 req_ids_output_copy = self.input_batch.req_ids.copy()
3677 req_id_to_index_output_copy = self.input_batch.req_id_to_index.copy()
3678
3679 num_sampled_tokens = sampler_output.sampled_token_ids.shape[0]
3680 sampled_token_ids = sampler_output.sampled_token_ids
3681 logprobs_tensors = sampler_output.logprobs_tensors
3682 invalid_req_indices = []
3683 logprobs_lists = None
3684 if not self.use_async_scheduling:
3685 with gpu_sync_allowed():
3686 # Get the valid generated tokens.
3687 max_gen_len = sampled_token_ids.shape[-1]
3688 if max_gen_len == 1:
3689 # No spec decode tokens.
3690 valid_sampled_token_ids = self._to_list(sampled_token_ids)
3691 # Mask out the sampled tokens that should not be sampled.
3692 for i in discard_sampled_tokens_req_indices:
3693 valid_sampled_token_ids[int(i)].clear()
3694
3695 if logprobs_tensors is not None:
3696 logprobs_lists = logprobs_tensors.tolists()
3697 else:
3698 # Includes spec decode tokens.
3699 valid_sampled_token_ids, logprobs_lists = (
3700 RejectionSampler.parse_output(
3701 sampled_token_ids,
3702 self.input_batch.vocab_size,
3703 discard_sampled_tokens_req_indices,
3704 logprobs_tensors=logprobs_tensors,
3705 )
3706 )
3707 else:
3708 valid_sampled_token_ids = []
3709 invalid_req_indices = discard_sampled_tokens_req_indices.tolist()
3710 invalid_req_indices_set = set(invalid_req_indices)
3711
3712 # Cache the sampled tokens on the GPU and avoid CPU sync.
3713 # These will be copied into input_ids in the next step
3714 # when preparing inputs.
3715 # With spec decoding, this is done in propose_draft_token_ids().
3716 if self.input_batch.prev_sampled_token_ids is None:
3717 assert sampled_token_ids.shape[-1] == 1
3718 self.input_batch.prev_sampled_token_ids = sampled_token_ids
3719 self.input_batch.prev_req_id_to_index = {
3720 req_id: i
3721 for i, req_id in enumerate(self.input_batch.req_ids)
3722 if i not in invalid_req_indices_set
3723 }
3724
3725 # Cache the sampled tokens in the model runner, so that the scheduler
3726 # doesn't need to send them back.
3727 # NOTE(woosuk): As an exception, when using PP, the scheduler sends
3728 # the sampled tokens back, because there's no direct communication
3729 # between the first-stage worker and the last-stage worker.
3730 req_ids = self.input_batch.req_ids
3731 for req_idx in range(num_sampled_tokens):
3732 if self.use_async_scheduling:
3733 sampled_ids = [-1] if req_idx not in invalid_req_indices_set else None
3734 else:
3735 sampled_ids = valid_sampled_token_ids[req_idx]
3736
3737 num_sampled_ids: int = len(sampled_ids) if sampled_ids else 0
3738
3739 if not sampled_ids:
3740 continue
3741
3742 start_idx = self.input_batch.num_tokens_no_spec[req_idx]
3743 end_idx = start_idx + num_sampled_ids
3744 assert end_idx <= self.max_model_len, (
3745 "Sampled token IDs exceed the max model length. "
3746 f"Total number of tokens: {end_idx} > max_model_len: "
3747 f"{self.max_model_len}"
3748 )
3749
3750 self.input_batch.token_ids_cpu[req_idx, start_idx:end_idx] = sampled_ids
3751 self.input_batch.is_token_ids[req_idx, start_idx:end_idx] = True
3752 self.input_batch.num_tokens_no_spec[req_idx] = end_idx
3753
3754 req_id = req_ids[req_idx]
3755 req_state = self.requests[req_id]
3756 req_state.output_token_ids.extend(sampled_ids)
3757
3758 # Compute prompt logprobs if needed.
3759 prompt_logprobs_dict = self._get_prompt_logprobs_dict(
3760 hidden_states[:num_scheduled_tokens],
3761 scheduler_output.num_scheduled_tokens,
3762 )
3763
3764 return (
3765 num_nans_in_logits,
3766 num_nans,
3767 logprobs_lists,
3768 valid_sampled_token_ids,
3769 prompt_logprobs_dict,
3770 req_ids_output_copy,
3771 req_id_to_index_output_copy,
3772 invalid_req_indices,
3773 )
3774
3775 @contextmanager
3776 def synchronize_input_prep(self):
3777 if self.prepare_inputs_event is None:
3778 yield
3779 return
3780
3781 # Ensure prior step has finished with reused CPU tensors.
3782 # This is required in the async scheduling case because
3783 # the CPU->GPU transfer happens async.
3784 self.prepare_inputs_event.synchronize()
3785 try:
3786 yield
3787 finally:
3788 self.prepare_inputs_event.record()
3789
3790 def _model_forward(
3791 self,
3792 input_ids: torch.Tensor | None = None,
3793 positions: torch.Tensor | None = None,
3794 intermediate_tensors: IntermediateTensors | None = None,
3795 inputs_embeds: torch.Tensor | None = None,
3796 **model_kwargs: dict[str, Any],
3797 ) -> Any:
3798 """Helper method to call the model forward pass.
3799
3800 This method can be overridden by subclasses for model execution.
3801 Motivation: We can inspect only this method versus
3802 the whole execute_model, which has additional logic.
3803
3804 Args:
3805 input_ids: Input token IDs
3806 positions: Token positions
3807 intermediate_tensors: Tensors from previous pipeline stages
3808 inputs_embeds: Input embeddings (alternative to input_ids)
3809 **model_kwargs: Additional model arguments
3810
3811 Returns:
3812 Model output tensor
3813
3814 """
3815 return self.model(
3816 input_ids=input_ids,
3817 positions=positions,
3818 intermediate_tensors=intermediate_tensors,
3819 inputs_embeds=inputs_embeds,
3820 **model_kwargs,
3821 )
3822
3823 @staticmethod
3824 def _is_uniform_decode(
3825 max_num_scheduled_tokens: int,
3826 uniform_decode_query_len: int,
3827 num_tokens: int,
3828 num_reqs: int,
3829 force_uniform_decode: bool | None = None,
3830 ) -> bool:
3831 """Checks if it's a decode batch with same amount scheduled tokens
3832 across all requests.
3833 """
3834 return (
3835 (
3836 (max_num_scheduled_tokens == uniform_decode_query_len)
3837 and (num_tokens == max_num_scheduled_tokens * num_reqs)
3838 )
3839 if force_uniform_decode is None
3840 else force_uniform_decode
3841 )
3842
3843 def _allow_microbatching(
3844 self, num_reqs: int, num_scheduled_tokens_np: np.ndarray
3845 ) -> bool:
3846 """Refuse to microbatch a step that splits a prefix from its writer.
3847
3848 A request can be admitted on a prefix cache hit against blocks another
3849 request in the same batch is only computing now. Run whole, the step
3850 issues every KV cache write before any attention read and the hit
3851 holds; split, a reader in the first half would attend over blocks the
3852 writer in the second half has not filled in yet. Vetoing on one rank
3853 settles it for all, since ranks agree on microbatching collectively.
3854 """
3855 if not self.parallel_config.use_ubatching or num_reqs < 2:
3856 return True
3857 computed = self.input_batch.num_computed_tokens_cpu[:num_reqs]
3858 query_lens = num_scheduled_tokens_np[:num_reqs]
3859 # A decode reads nothing it was not already given in an earlier step.
3860 readers = np.flatnonzero(
3861 (query_lens > (self.reorder_batch_threshold or 1)) & (computed > 0)
3862 )
3863 if readers.size == 0:
3864 return True
3865
3866 for block_table in self.input_batch.block_table.block_tables:
3867 block_size = block_table.block_size
3868 table = block_table.get_numpy_array()
3869 start = computed // block_size
3870 stop = (computed + query_lens + block_size - 1) // block_size
3871 span = int((stop - start).max())
3872 columns = start[:, None] + np.arange(span)[None, :]
3873 written = np.unique(
3874 np.take_along_axis(
3875 table[:num_reqs],
3876 np.minimum(columns, table.shape[1] - 1),
3877 axis=1,
3878 )[columns < stop[:, None]]
3879 )
3880 for reader in readers:
3881 # Whole blocks only: a reader ends inside a partly filled one,
3882 # which is private to it and which it fills before reading.
3883 whole = computed[reader] // block_size
3884 if np.isin(table[reader, :whole], written).any():
3885 return False
3886 return True
3887
3888 def _determine_batch_execution_and_padding(
3889 self,
3890 num_tokens: int,
3891 num_reqs: int,
3892 num_scheduled_tokens_np: np.ndarray,
3893 max_num_scheduled_tokens: int,
3894 use_cascade_attn: bool,
3895 allow_microbatching: bool = True,
3896 force_eager: bool = False,
3897 # For cudagraph capture TODO(lucas): Refactor how we capture cudagraphs (will
3898 # be improved in model runner v2)
3899 force_uniform_decode: bool | None = None,
3900 force_has_lora: bool | None = None,
3901 force_num_active_loras: int | None = None,
3902 num_encoder_reqs: int = 0,
3903 ) -> tuple[
3904 CUDAGraphMode,
3905 BatchDescriptor,
3906 bool,
3907 torch.Tensor | None,
3908 CUDAGraphStat | None,
3909 ]:
3910 uniform_decode = self._is_uniform_decode(
3911 max_num_scheduled_tokens=max_num_scheduled_tokens,
3912 uniform_decode_query_len=self.uniform_decode_query_len,
3913 num_tokens=num_tokens,
3914 num_reqs=num_reqs,
3915 force_uniform_decode=force_uniform_decode,
3916 )
3917 # Encoder-decoder models only support CG for decoder_step > 0 (no enc_output
3918 # is present). Also, chunked-prefill is disabled, so batch are uniform.
3919 has_encoder_output = (
3920 self.model_config.is_encoder_decoder and num_encoder_reqs > 0
3921 )
3922
3923 # Compute LoRA state for cudagraph dispatch
3924 num_active_loras = (
3925 force_num_active_loras
3926 if force_num_active_loras is not None
3927 else len(self.input_batch.lora_id_to_lora_request)
3928 )
3929 has_lora = num_active_loras > 0 if force_has_lora is None else force_has_lora
3930
3931 num_tokens_padded = self._pad_for_sequence_parallelism(num_tokens)
3932
3933 def dispatch_cudagraph(num_tokens, disable_full=False, valid_modes=None):
3934 return self.cudagraph_dispatcher.dispatch(
3935 num_tokens=num_tokens,
3936 has_lora=has_lora,
3937 uniform_decode=uniform_decode,
3938 num_active_loras=num_active_loras,
3939 valid_modes={CUDAGraphMode.NONE} if force_eager else valid_modes,
3940 invalid_modes={CUDAGraphMode.FULL} if disable_full else None,
3941 )
3942
3943 cudagraph_mode, batch_descriptor = dispatch_cudagraph(
3944 num_tokens_padded, disable_full=use_cascade_attn or has_encoder_output
3945 )
3946 num_tokens_padded = batch_descriptor.num_tokens
3947 if self.compilation_config.pass_config.enable_sp:
3948 assert (
3949 batch_descriptor.num_tokens
3950 % self.vllm_config.parallel_config.tensor_parallel_size
3951 == 0
3952 ), (
3953 "Sequence parallelism requires num_tokens to be "
3954 "a multiple of tensor parallel size"
3955 )
3956
3957 # Extra coordination when running data-parallel since we need to coordinate
3958 # across ranks
3959 should_ubatch, num_tokens_across_dp = False, None
3960 if self.vllm_config.parallel_config.data_parallel_size > 1:
3961 should_ubatch, num_tokens_across_dp, synced_cudagraph_mode = (
3962 coordinate_batch_across_dp(
3963 num_tokens_unpadded=num_tokens,
3964 parallel_config=self.parallel_config,
3965 allow_microbatching=allow_microbatching,
3966 num_tokens_padded=num_tokens_padded,
3967 uniform_decode=uniform_decode,
3968 cudagraph_mode=cudagraph_mode.value,
3969 )
3970 )
3971
3972 # Extract DP-synced values
3973 if num_tokens_across_dp is not None:
3974 dp_rank = self.parallel_config.data_parallel_rank
3975 num_tokens_padded = int(num_tokens_across_dp[dp_rank].item())
3976 # Re-dispatch with DP padding so we have the correct batch_descriptor
3977 cudagraph_mode, batch_descriptor = dispatch_cudagraph(
3978 num_tokens_padded,
3979 valid_modes={CUDAGraphMode(synced_cudagraph_mode)},
3980 )
3981 # Assert to make sure the agreed upon token count is correct otherwise
3982 # num_tokens_across_dp will no-longer be valid
3983 assert batch_descriptor.num_tokens == num_tokens_padded
3984
3985 cudagraph_stats = None
3986 if self.vllm_config.observability_config.cudagraph_metrics:
3987 cudagraph_stats = CUDAGraphStat(
3988 num_unpadded_tokens=num_tokens,
3989 num_padded_tokens=batch_descriptor.num_tokens,
3990 num_paddings=batch_descriptor.num_tokens - num_tokens,
3991 runtime_mode=str(cudagraph_mode),
3992 )
3993
3994 return (
3995 cudagraph_mode,
3996 batch_descriptor,
3997 should_ubatch,
3998 num_tokens_across_dp,
3999 cudagraph_stats,
4000 )
4001
4002 def _register_layerwise_nvtx_hooks(self) -> None:
4003 """Register layerwise NVTX hooks if --enable-layerwise-nvtx-tracing is enabled
4004 to trace detailed information of each layer or module in the model.
4005 """
4006 if (
4007 self.vllm_config.observability_config.enable_layerwise_nvtx_tracing
4008 and not self.layerwise_nvtx_hooks_registered
4009 ):
4010 if self.compilation_config.cudagraph_mode != CUDAGraphMode.NONE:
4011 logger.debug_once(
4012 "layerwise NVTX tracing is not supported when CUDA graph is "
4013 "turned off; you may observe part or all of the model "
4014 "missing NVTX markers"
4015 )
4016
4017 # In STOCK_TORCH_COMPILE mode, after registering hooks here,
4018 # the __call__ function of nn.module will be recompiled with
4019 # fullgraph=True. Since nvtx.range_push/pop are not traceable
4020 # by torch dynamo, we can't register hook functions here
4021 # because hook functions will also be traced by torch dynamo.
4022 if (
4023 self.vllm_config.compilation_config.mode
4024 == CompilationMode.STOCK_TORCH_COMPILE
4025 ):
4026 logger.debug_once(
4027 "layerwise NVTX tracing is not supported when "
4028 "CompilationMode is STOCK_TORCH_COMPILE, skipping "
4029 "function hooks registration"
4030 )
4031 else:
4032 pyt_hooks = PytHooks()
4033 pyt_hooks.register_hooks(self.model, self.model.__class__.__name__)
4034 self.layerwise_nvtx_hooks_registered = True
4035
4036 def _get_slot_mappings(
4037 self,
4038 num_tokens_padded: int,
4039 num_reqs_padded: int,
4040 num_tokens_unpadded: int,
4041 ubatch_slices: "UBatchSlices | None" = None,
4042 ) -> tuple[
4043 dict[int, torch.Tensor] | None,
4044 dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None,
4045 ]:
4046 """Build slot mappings in both formats needed by the system.
4047
4048 Args:
4049 num_tokens_padded: Total number of tokens (padded)
4050 num_reqs_padded: Total number of requests (padded)
4051 num_tokens_unpadded: Actual number of tokens (unpadded)
4052 ubatch_slices: Optional ubatch slicing info for DBO
4053
4054 Returns:
4055 A tuple of:
4056 - slot_mappings_by_gid: dict[int, torch.Tensor] for attention metadata
4057 - slot_mappings_by_layer: dict[str, torch.Tensor] or list for ForwardContext
4058
4059 """
4060 if not (
4061 hasattr(self, "kv_cache_config")
4062 and self.kv_cache_config is not None
4063 and len(self.kv_cache_config.kv_cache_groups) > 0
4064 ):
4065 return None, None
4066
4067 def _get_slot_mapping(kv_cache_gid: int):
4068 assert num_reqs_padded is not None and num_tokens_padded is not None
4069 kv_cache_spec = self.kv_cache_config.kv_cache_groups[
4070 kv_cache_gid
4071 ].kv_cache_spec
4072 if isinstance(kv_cache_spec, EncoderOnlyAttentionSpec):
4073 slot_mapping = torch.zeros(
4074 (num_tokens_padded,),
4075 dtype=torch.int64,
4076 device=self.device,
4077 )
4078 else:
4079 blk_table = self.input_batch.block_table[kv_cache_gid]
4080 slot_mapping = blk_table.slot_mapping.gpu[:num_tokens_padded]
4081
4082 # Fill unused with -1. Needed for reshape_and_cache in full cuda
4083 # graph mode. `blk_table_tensor` -1 to match mamba PAD_SLOT_ID
4084 slot_mapping[num_tokens_unpadded:num_tokens_padded].fill_(-1)
4085
4086 return slot_mapping
4087
4088 slot_mappings_by_gid = {
4089 gid: _get_slot_mapping(gid)
4090 for gid, _ in enumerate(self.kv_cache_config.kv_cache_groups)
4091 }
4092
4093 slot_mappings_by_layer: dict[str, torch.Tensor] = {}
4094 for gid, kv_cache_group in enumerate(self.kv_cache_config.kv_cache_groups):
4095 slot_mapping = slot_mappings_by_gid[gid]
4096 for layer_name in kv_cache_group.layer_names:
4097 slot_mappings_by_layer[layer_name] = slot_mapping
4098
4099 if ubatch_slices is not None:
4100 result: list[dict[str, torch.Tensor]] = []
4101 for ubatch in ubatch_slices:
4102 sliced_mappings: dict[str, torch.Tensor] = {}
4103 for layer_name, slot_mapping in slot_mappings_by_layer.items():
4104 sliced_mappings[layer_name] = slot_mapping[ubatch.token_slice]
4105 result.append(sliced_mappings)
4106 return slot_mappings_by_gid, result
4107
4108 return slot_mappings_by_gid, slot_mappings_by_layer
4109
4110 def _is_all_reqs_chunked_prefill(self) -> bool:
4111 """Check if all scheduled requests are marked to discard sampled tokens.
4112
4113 This is true when `discard_request_mask` is set for every scheduled
4114 request (e.g., for chunked prefill requests that are not the last
4115 prefill chunk)."""
4116 num_reqs = self.input_batch.num_reqs
4117 return bool(self.discard_request_mask.np[:num_reqs].all())
4118
4119 @torch.inference_mode()
4120 def execute_model(
4121 self,
4122 scheduler_output: "SchedulerOutput",
4123 intermediate_tensors: IntermediateTensors | None = None,
4124 ) -> ModelRunnerOutput | AsyncModelRunnerOutput | IntermediateTensors | None:
4125 if self.execute_model_state is not None:
4126 raise RuntimeError(
4127 "State error: sample_tokens() must be called "
4128 "after execute_model() returns None."
4129 )
4130
4131 # If ngram_gpu is used, we need to copy the scheduler_output to avoid
4132 # the modification has influence on the scheduler_output in engine core process.
4133 # The replace is much faster than deepcopy.
4134 if (
4135 self.speculative_config is not None
4136 and self.speculative_config.use_ngram_gpu()
4137 ):
4138 num_scheduled_tokens_copy = scheduler_output.num_scheduled_tokens.copy()
4139 spec_decode_tokens_copy = (
4140 scheduler_output.scheduled_spec_decode_tokens.copy()
4141 )
4142 scheduler_output = replace(
4143 scheduler_output,
4144 num_scheduled_tokens=num_scheduled_tokens_copy,
4145 scheduled_spec_decode_tokens=spec_decode_tokens_copy,
4146 )
4147
4148 if has_kv_transfer_group():
4149 kv_connector_metadata = scheduler_output.kv_connector_metadata
4150 assert kv_connector_metadata is not None
4151 get_kv_transfer_group().handle_preemptions(kv_connector_metadata)
4152
4153 num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
4154 with (
4155 record_function_or_nullcontext("gpu_model_runner: preprocess"),
4156 self.synchronize_input_prep(),
4157 ):
4158 # Update persistent batch states.
4159 deferred_state_corrections_fn = self._update_states(scheduler_output)
4160
4161 if has_ec_transfer() and not get_ec_transfer().is_consumer:
4162 with self.maybe_get_ec_connector_output(
4163 scheduler_output,
4164 encoder_cache=self.encoder_cache,
4165 ) as ec_connector_output:
4166 self._execute_mm_encoder(scheduler_output)
4167 return make_empty_encoder_model_runner_output(scheduler_output)
4168
4169 if not num_scheduled_tokens:
4170 if (
4171 self.parallel_config.distributed_executor_backend
4172 == "external_launcher"
4173 and self.parallel_config.data_parallel_size > 1
4174 ):
4175 # this is a corner case when both external launcher
4176 # and DP are enabled, num_scheduled_tokens could be
4177 # 0, and has_unfinished_requests in the outer loop
4178 # returns True. before returning early here we call
4179 # dummy run to ensure coordinate_batch_across_dp
4180 # is called into to avoid out of sync issues.
4181 self._dummy_run(1)
4182 if not has_kv_transfer_group():
4183 # Return empty ModelRunnerOutput if no work to do.
4184 return EMPTY_MODEL_RUNNER_OUTPUT
4185 return self.kv_connector_no_forward(scheduler_output, self.vllm_config)
4186
4187 if self.cache_config.kv_sharing_fast_prefill:
4188 assert not self.num_prompt_logprobs, (
4189 "--kv-sharing-fast-prefill produces incorrect "
4190 "logprobs for prompt tokens, tokens, please disable "
4191 "it when the requests need prompt logprobs"
4192 )
4193
4194 num_reqs = self.input_batch.num_reqs
4195 req_ids = self.input_batch.req_ids
4196 tokens = [scheduler_output.num_scheduled_tokens[i] for i in req_ids]
4197 num_scheduled_tokens_np = np.array(tokens, dtype=np.int32)
4198 max_num_scheduled_tokens = int(num_scheduled_tokens_np.max())
4199 num_tokens_unpadded = scheduler_output.total_num_scheduled_tokens
4200
4201 logits_indices, spec_decode_metadata, max_num_sampled_tokens = (
4202 self._prepare_inputs(scheduler_output, num_scheduled_tokens_np)
4203 )
4204
4205 cascade_attn_prefix_lens = None
4206 # Disable cascade attention when using microbatching (DBO)
4207 if self.cascade_attn_enabled and not self.parallel_config.use_ubatching:
4208 # Pre-compute cascade attention prefix lengths
4209 cascade_attn_prefix_lens = self._compute_cascade_attn_prefix_lens(
4210 num_scheduled_tokens_np,
4211 self.input_batch.num_computed_tokens_cpu[:num_reqs],
4212 scheduler_output.num_common_prefix_blocks,
4213 )
4214
4215 (
4216 cudagraph_mode,
4217 batch_desc,
4218 should_ubatch,
4219 num_tokens_across_dp,
4220 cudagraph_stats,
4221 ) = self._determine_batch_execution_and_padding(
4222 num_tokens=num_tokens_unpadded,
4223 num_reqs=num_reqs,
4224 num_scheduled_tokens_np=num_scheduled_tokens_np,
4225 max_num_scheduled_tokens=max_num_scheduled_tokens,
4226 use_cascade_attn=cascade_attn_prefix_lens is not None,
4227 num_encoder_reqs=len(scheduler_output.scheduled_encoder_inputs),
4228 allow_microbatching=self._allow_microbatching(
4229 num_reqs, num_scheduled_tokens_np
4230 ),
4231 )
4232
4233 logger.debug(
4234 "Running batch with cudagraph_mode: %s, batch_descriptor: %s, "
4235 "should_ubatch: %s, num_tokens_across_dp: %s",
4236 cudagraph_mode,
4237 batch_desc,
4238 should_ubatch,
4239 num_tokens_across_dp,
4240 )
4241
4242 num_tokens_padded = batch_desc.num_tokens
4243 num_reqs_padded = (
4244 batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs
4245 )
4246 ubatch_slices, ubatch_slices_padded = maybe_create_ubatch_slices(
4247 should_ubatch,
4248 num_scheduled_tokens_np,
4249 num_tokens_padded,
4250 num_reqs_padded,
4251 self.parallel_config.num_ubatches,
4252 )
4253
4254 logger.debug(
4255 "ubatch_slices: %s, ubatch_slices_padded: %s",
4256 ubatch_slices,
4257 ubatch_slices_padded,
4258 )
4259
4260 # True if any attention backend handles KV cache update separately
4261 # from forward() (i.e., forward_includes_kv_cache_update=False). When true,
4262 # slot_mappings must use padded dimensions to match the key/value tensors.
4263 has_separate_kv_update = not all(
4264 all(
4265 g.backend.forward_includes_kv_cache_update
4266 for g in self.attn_groups[id]
4267 )
4268 for id, spec in enumerate(self.kv_cache_config.kv_cache_groups)
4269 if not isinstance(spec.kv_cache_spec, EncoderOnlyAttentionSpec)
4270 )
4271 pad_attn = cudagraph_mode == CUDAGraphMode.FULL
4272
4273 if self.cache_config.mamba_cache_mode == "align":
4274 # preprocess_mamba reads req_state.num_computed_tokens (CPU)
4275 # to decide copy operations, so we must apply deferred
4276 # corrections before it runs.
4277 if deferred_state_corrections_fn:
4278 deferred_state_corrections_fn()
4279 deferred_state_corrections_fn = None
4280 mamba_bufs = self._get_mamba_bufs()
4281 mamba_utils.preprocess_mamba(
4282 scheduler_output,
4283 self.kv_cache_config,
4284 self.cache_config,
4285 self.mamba_state_idx,
4286 self.input_batch,
4287 self.requests,
4288 self.compilation_config.static_forward_context,
4289 self._get_mamba_state_copy_funcs(),
4290 mamba_bufs.preprocess,
4291 align_ctx=mamba_bufs.postprocess_align,
4292 )
4293 # preprocess_mamba resets num_accepted_tokens_cpu to 1
4294 # for requests whose state was copied to a new block.
4295 # Re-sync to GPU so the mamba kernel reads from the
4296 # correct initial state slot (init_token_idx = 0).
4297 self.num_accepted_tokens.np[:num_reqs] = (
4298 self.input_batch.num_accepted_tokens_cpu[:num_reqs]
4299 )
4300 self.num_accepted_tokens.copy_to_gpu(num_reqs)
4301
4302 # Stage per-request inputs for the fused postprocess kernel
4303 # only when that kernel will actually run. The kernel is
4304 # gated on spec-decode + hybrid (see MambaBuffers.create);
4305 # without it, ``mamba_bufs.postprocess_align`` is None and
4306 # the staging buffers don't exist.
4307 if mamba_bufs.postprocess_align is not None:
4308 mamba_utils.stage_postprocess_inputs_to_gpu(
4309 mamba_bufs.postprocess_align,
4310 scheduler_output,
4311 self.input_batch.req_ids,
4312 num_reqs,
4313 self.requests,
4314 self.mamba_state_idx,
4315 )
4316
4317 use_spec_decode = len(scheduler_output.scheduled_spec_decode_tokens) > 0
4318 ubatch_slices_attn = ubatch_slices_padded if pad_attn else ubatch_slices
4319
4320 slot_mappings_by_group, slot_mappings = self._get_slot_mappings(
4321 num_tokens_padded=num_tokens_padded
4322 if pad_attn or has_separate_kv_update
4323 else num_tokens_unpadded,
4324 num_reqs_padded=(
4325 num_reqs_padded if pad_attn or has_separate_kv_update else num_reqs
4326 ),
4327 num_tokens_unpadded=num_tokens_unpadded,
4328 ubatch_slices=ubatch_slices_padded,
4329 )
4330
4331 attn_metadata, spec_decode_common_attn_metadata = (
4332 self._build_attention_metadata(
4333 num_tokens=num_tokens_unpadded,
4334 num_tokens_padded=num_tokens_padded if pad_attn else None,
4335 num_reqs=num_reqs,
4336 num_reqs_padded=num_reqs_padded if pad_attn else None,
4337 max_query_len=max_num_scheduled_tokens,
4338 ubatch_slices=ubatch_slices_attn,
4339 logits_indices=logits_indices,
4340 max_num_sampled_tokens=max_num_sampled_tokens,
4341 use_spec_decode=use_spec_decode,
4342 num_scheduled_tokens=scheduler_output.num_scheduled_tokens,
4343 cascade_attn_prefix_lens=cascade_attn_prefix_lens,
4344 slot_mappings=slot_mappings_by_group,
4345 )
4346 )
4347
4348 (
4349 input_ids,
4350 inputs_embeds,
4351 positions,
4352 intermediate_tensors,
4353 model_kwargs,
4354 ec_connector_output,
4355 ) = self._preprocess(
4356 scheduler_output, num_tokens_padded, intermediate_tensors
4357 )
4358
4359 # Encoder-decoder models can only compile the pure decode steps where no
4360 # encoder inputs are present. Use eager for the first pass.
4361 num_encoder_reqs = len(scheduler_output.scheduled_encoder_inputs)
4362 has_encoder_input = (
4363 self.model_config.is_encoder_decoder and num_encoder_reqs > 0
4364 )
4365
4366 # Run the model.
4367 # Use persistent buffers for CUDA graphs.
4368 # When spec decode is enabled, defer connector finalization
4369 # (wait_for_save + clear metadata) until after draft model runs.
4370 defer_kv_connector_finalize = self.speculative_config is not None
4371 # Update the EPLB meta.
4372 if self.eplb_state is not None:
4373 self.eplb_state.prepare_forward(
4374 self.model_config,
4375 num_tokens_unpadded,
4376 ubatch_slices_padded,
4377 )
4378 is_padding = self._prepare_padding_mask(num_tokens_unpadded, num_tokens_padded)
4379 with (
4380 set_forward_context(
4381 attn_metadata,
4382 self.vllm_config,
4383 num_tokens=num_tokens_padded,
4384 num_tokens_across_dp=num_tokens_across_dp,
4385 cudagraph_runtime_mode=cudagraph_mode,
4386 batch_descriptor=batch_desc,
4387 ubatch_slices=ubatch_slices_padded,
4388 slot_mapping=slot_mappings,
4389 skip_compiled=has_encoder_input,
4390 is_padding=is_padding,
4391 ),
4392 record_function_or_nullcontext("gpu_model_runner: forward"),
4393 self.maybe_get_kv_connector_output(
4394 scheduler_output,
4395 defer_finalize=defer_kv_connector_finalize,
4396 ) as kv_connector_output,
4397 ):
4398 model_output = self._model_forward(
4399 input_ids=input_ids,
4400 positions=positions,
4401 intermediate_tensors=intermediate_tensors,
4402 inputs_embeds=inputs_embeds,
4403 **model_kwargs,
4404 )
4405
4406 with record_function_or_nullcontext("gpu_model_runner: postprocess"):
4407 if self.use_aux_hidden_state_outputs:
4408 # True when EAGLE 3 is used.
4409 hidden_states, aux_hidden_states = model_output
4410 else:
4411 # Common case.
4412 hidden_states = model_output
4413 aux_hidden_states = None
4414
4415 if not self.broadcast_pp_output:
4416 # Common case.
4417 if not get_pp_group().is_last_rank:
4418 # Return the intermediate tensors.
4419 assert isinstance(hidden_states, IntermediateTensors)
4420 self.kv_connector_output = kv_connector_output
4421 return hidden_states
4422
4423 if self.is_pooling_model:
4424 # Return the pooling output.
4425 return self._pool(
4426 hidden_states,
4427 num_scheduled_tokens,
4428 num_scheduled_tokens_np,
4429 kv_connector_output,
4430 )
4431
4432 sample_hidden_states = hidden_states[logits_indices]
4433 logits = self.model.compute_logits(sample_hidden_states)
4434 else:
4435 # Rare case.
4436 assert not self.is_pooling_model
4437
4438 sample_hidden_states = hidden_states[logits_indices]
4439 if not get_pp_group().is_last_rank:
4440 all_gather_tensors = {
4441 "residual": not is_residual_scattered_for_sp(
4442 self.vllm_config, num_tokens_padded
4443 )
4444 }
4445 get_pp_group().send_tensor_dict(
4446 hidden_states.tensors,
4447 all_gather_group=get_tp_group(),
4448 all_gather_tensors=all_gather_tensors,
4449 )
4450 logits = None
4451 else:
4452 logits = self.model.compute_logits(sample_hidden_states)
4453
4454 model_output_broadcast_data: dict[str, Any] = {}
4455 if logits is not None:
4456 model_output_broadcast_data["logits"] = logits.contiguous()
4457
4458 broadcasted = get_pp_group().broadcast_tensor_dict(
4459 model_output_broadcast_data, src=len(get_pp_group().ranks) - 1
4460 )
4461 assert broadcasted is not None
4462 logits = broadcasted["logits"]
4463
4464 self.execute_model_state = ExecuteModelState(
4465 scheduler_output,
4466 logits,
4467 spec_decode_metadata,
4468 spec_decode_common_attn_metadata,
4469 hidden_states,
4470 sample_hidden_states,
4471 aux_hidden_states,
4472 ec_connector_output,
4473 cudagraph_stats,
4474 slot_mappings,
4475 )
4476 self.kv_connector_output = kv_connector_output
4477
4478 # Now the batch has been launched we can wait for corrections from the
4479 # previous model forward without breaking async scheduling.
4480 if deferred_state_corrections_fn:
4481 deferred_state_corrections_fn()
4482
4483 return None
4484
4485 def _input_fits_in_drafter(
4486 self, common_attn_metadata: CommonAttentionMetadata | None
4487 ) -> bool:
4488 if common_attn_metadata is None:
4489 return False
4490 assert self.speculative_config is not None
4491 # DFlash queries one extra token (the bonus token) beyond num_spec_tokens
4492 num_drafter_query_tokens = self.num_spec_tokens + (
4493 1 if self.speculative_config.use_dflash() else 0
4494 )
4495 return (
4496 common_attn_metadata.max_seq_len + num_drafter_query_tokens
4497 <= self.effective_drafter_max_model_len
4498 )
4499
4500 @torch.inference_mode
4501 def sample_tokens(
4502 self, grammar_output: "GrammarOutput | None"
4503 ) -> ModelRunnerOutput | AsyncModelRunnerOutput | IntermediateTensors:
4504 if self.execute_model_state is None:
4505 kv_connector_output = self.kv_connector_output
4506 self.kv_connector_output = None
4507 # receive sampled token ids from the last PP rank.
4508 if self.use_async_scheduling and not get_pp_group().is_last_rank:
4509 self._pp_receive_prev_sampled_token_ids_to_input_batch()
4510 # In case of PP with kv transfer, we need to pass through the
4511 # kv_connector_output
4512 return ModelRunnerOutput.with_kv_conn_output_only(kv_connector_output)
4513
4514 # Unpack ephemeral state.
4515 (
4516 scheduler_output,
4517 logits,
4518 spec_decode_metadata,
4519 spec_decode_common_attn_metadata,
4520 hidden_states,
4521 sample_hidden_states,
4522 aux_hidden_states,
4523 ec_connector_output,
4524 cudagraph_stats,
4525 slot_mappings,
4526 ) = self.execute_model_state
4527 # Clear ephemeral state.
4528 self.execute_model_state = None
4529
4530 # Apply structured output bitmasks if present.
4531 if grammar_output is not None:
4532 apply_grammar_bitmask(
4533 scheduler_output, grammar_output, self.input_batch, logits
4534 )
4535
4536 with record_function_or_nullcontext("gpu_model_runner: sample"):
4537 sampler_output = self._sample(logits, spec_decode_metadata)
4538
4539 self._update_states_after_model_execute(
4540 sampler_output.sampled_token_ids, scheduler_output
4541 )
4542 if self.use_async_scheduling:
4543 pp = get_pp_group()
4544 # For torchrun external_launcher PP mode with broadcast_pp_output=True,
4545 # PP outputs have been broadcasted to all ranks at logits computation.
4546 # Therefore, here is no need to send sampled token ids again in this case.
4547 if not self.broadcast_pp_output and pp.world_size > 1 and pp.is_last_rank:
4548 self._pp_broadcast_prev_sampled_token_ids(
4549 sampler_output.sampled_token_ids
4550 )
4551
4552 self._draft_token_ids = None
4553 self._draft_probs = None
4554 self._draft_prob_req_ids = None
4555 self._draft_token_req_ids = None
4556 self.valid_sampled_token_count_gpu = None
4557 self.input_batch.prev_sampled_token_ids = None
4558
4559 def propose_draft_token_ids(sampled_token_ids):
4560 assert spec_decode_common_attn_metadata is not None
4561 with record_function_or_nullcontext("gpu_model_runner: draft"):
4562 self._draft_token_ids = self.propose_draft_token_ids(
4563 scheduler_output,
4564 sampled_token_ids,
4565 self.input_batch.sampling_metadata,
4566 hidden_states,
4567 sample_hidden_states,
4568 aux_hidden_states,
4569 spec_decode_metadata,
4570 spec_decode_common_attn_metadata,
4571 slot_mappings,
4572 )
4573 self._copy_draft_token_ids_to_cpu(scheduler_output)
4574
4575 spec_config = self.speculative_config
4576 draft_after_bookkeeping = False
4577 if spec_config is not None:
4578 # Decide whether to run the drafter or zero out draft tokens.
4579 input_fits_in_drafter = self._input_fits_in_drafter(
4580 spec_decode_common_attn_metadata
4581 )
4582 # Whether the drafter runs a GPU model forward (and thus carries
4583 # TP/EP/DP collectives), independent of padded-batch timing.
4584 drafter_runs_model_forward = (
4585 spec_config.use_eagle()
4586 or spec_config.uses_draft_model()
4587 or spec_config.uses_extract_hidden_states()
4588 )
4589 use_gpu_toks = (
4590 drafter_runs_model_forward
4591 and not spec_config.disable_padded_drafter_batch
4592 )
4593 if use_gpu_toks:
4594 # EAGLE/DraftModel speculative decoding can use the GPU sampled tokens
4595 # as inputs, and does not need to wait for bookkeeping to finish.
4596 assert isinstance(
4597 self.drafter,
4598 EagleProposer
4599 | DFlashProposer
4600 | DraftModelProposer
4601 | ExtractHiddenStatesProposer
4602 | Gemma4Proposer,
4603 )
4604 sampled_token_ids = sampler_output.sampled_token_ids
4605 if input_fits_in_drafter:
4606 propose_draft_token_ids(sampled_token_ids)
4607 else:
4608 if self.valid_sampled_token_count_event is not None:
4609 assert spec_decode_common_attn_metadata is not None
4610 next_token_ids, valid_sampled_tokens_count = (
4611 self.drafter.prepare_next_token_ids_padded(
4612 sampled_token_ids,
4613 self.requests,
4614 self.input_batch,
4615 self.discard_request_mask.gpu,
4616 )
4617 )
4618 self._copy_valid_sampled_token_count(
4619 next_token_ids, valid_sampled_tokens_count
4620 )
4621 if self.parallel_config.data_parallel_size > 1:
4622 # Prevent hang when DP ranks disagree on input_fits_in_drafter
4623 self.drafter.dummy_run(num_tokens=1)
4624 elif (
4625 spec_config.use_ngram_gpu()
4626 and not spec_config.disable_padded_drafter_batch
4627 ):
4628 assert isinstance(self.drafter, NgramProposerGPU)
4629 sampled_token_ids = sampler_output.sampled_token_ids
4630 if input_fits_in_drafter:
4631 propose_draft_token_ids(sampled_token_ids)
4632 elif self.valid_sampled_token_count_event is not None:
4633 assert spec_decode_common_attn_metadata is not None
4634 next_token_ids, valid_sampled_tokens_count, _ = (
4635 self.drafter.update_token_ids_ngram(
4636 sampled_token_ids,
4637 self.input_batch,
4638 self.token_ids_gpu_tensor,
4639 self.num_tokens_no_spec_gpu,
4640 self.discard_request_mask.gpu,
4641 )
4642 )
4643 self._copy_valid_sampled_token_count(
4644 next_token_ids, valid_sampled_tokens_count
4645 )
4646 else:
4647 # These drafters consume CPU sampled tokens, so they run
4648 # after bookkeeping.
4649 draft_after_bookkeeping = True
4650
4651 if not input_fits_in_drafter:
4652 # Zero out draft tokens so the scheduler doesn't schedule
4653 # stale drafts from the previous step.
4654 # For Nemotron-H: it is necessary to zero out the draft tokens,
4655 # otherwise the stale tokens will corrupt Mamba recurrent
4656 # state and logprobs for sequences near max_model_len.
4657 self._draft_token_ids = torch.zeros(
4658 1, device=self.device, dtype=torch.int32
4659 ).expand(len(self.input_batch.req_ids), self.num_spec_tokens)
4660 self._draft_probs = None
4661 self._draft_prob_req_ids = None
4662 self._copy_draft_token_ids_to_cpu(scheduler_output, zeros_only=True)
4663
4664 with record_function_or_nullcontext("gpu_model_runner: bookkeep"):
4665 (
4666 num_nans_in_logits,
4667 num_nans_device,
4668 logprobs_lists,
4669 valid_sampled_token_ids,
4670 prompt_logprobs_dict,
4671 req_ids_output_copy,
4672 req_id_to_index_output_copy,
4673 invalid_req_indices,
4674 ) = self._bookkeeping_sync(
4675 scheduler_output,
4676 sampler_output,
4677 logits,
4678 hidden_states,
4679 scheduler_output.total_num_scheduled_tokens,
4680 )
4681
4682 if draft_after_bookkeeping:
4683 # ngram and other speculative decoding methods use the sampled
4684 # tokens on the CPU, so they are run after bookkeeping.
4685 if input_fits_in_drafter:
4686 propose_draft_token_ids(valid_sampled_token_ids)
4687 elif (
4688 drafter_runs_model_forward
4689 and self.parallel_config.data_parallel_size > 1
4690 ):
4691 # Prevent hang when DP ranks disagree on input_fits_in_drafter
4692 assert isinstance(
4693 self.drafter,
4694 EagleProposer
4695 | DFlashProposer
4696 | DraftModelProposer
4697 | ExtractHiddenStatesProposer
4698 | Gemma4Proposer,
4699 )
4700 self.drafter.dummy_run(num_tokens=1)
4701
4702 # Finalize KV connector (wait_for_save + clear metadata) after
4703 # draft model runs. Deferred from target model forward to allow
4704 # draft model to also save its KV cache.
4705 if spec_config is not None:
4706 self.finalize_kv_connector()
4707
4708 with record_function_or_nullcontext("gpu_model_runner: eplb"):
4709 self.eplb_step()
4710
4711 # self.kv_connector_output may be modified during drafting
4712 kv_connector_output = self.kv_connector_output
4713 self.kv_connector_output = None
4714
4715 with record_function_or_nullcontext("gpu_model_runner: ModelRunnerOutput"):
4716 output = ModelRunnerOutput(
4717 req_ids=req_ids_output_copy,
4718 req_id_to_index=req_id_to_index_output_copy,
4719 sampled_token_ids=valid_sampled_token_ids,
4720 logprobs=logprobs_lists,
4721 prompt_logprobs_dict=prompt_logprobs_dict,
4722 kv_connector_output=kv_connector_output,
4723 ec_connector_output=ec_connector_output
4724 if self.supports_mm_inputs
4725 else None,
4726 num_nans_in_logits=num_nans_in_logits,
4727 cudagraph_stats=cudagraph_stats,
4728 )
4729
4730 if not self.use_async_scheduling:
4731 return output
4732
4733 with record_function_or_nullcontext(
4734 "gpu_model_runner: AsyncGPUModelRunnerOutput"
4735 ):
4736 async_output = AsyncGPUModelRunnerOutput(
4737 model_runner_output=output,
4738 sampled_token_ids=sampler_output.sampled_token_ids,
4739 logprobs_tensors=sampler_output.logprobs_tensors,
4740 invalid_req_indices=invalid_req_indices,
4741 async_output_copy_stream=self._get_or_create_async_output_copy_stream(),
4742 vocab_size=self.input_batch.vocab_size,
4743 check_ep_fault=self.check_ep_fault,
4744 num_nans=num_nans_device,
4745 )
4746 with record_function_or_nullcontext(
4747 "gpu_model_runner: set_async_sampled_token_ids"
4748 ):
4749 # Save ref of sampled_token_ids CPU tensor if the batch contains
4750 # any requests with sampling params that require output ids.
4751 self.input_batch.set_async_sampled_token_ids(
4752 async_output.sampled_token_ids_cpu,
4753 async_output.async_copy_ready_event,
4754 )
4755
4756 return async_output
4757
4758 def _pp_broadcast_prev_sampled_token_ids(
4759 self, sampled_token_ids: torch.Tensor
4760 ) -> None:
4761 """Broadcast sampled token ids (GPU) from last PP stage"""
4762 pp = get_pp_group()
4763 assert pp.is_last_rank
4764 # `prev_sampled_token_ids` is expected to have shape [num_reqs, 1].
4765 assert sampled_token_ids.dim() == 2 and sampled_token_ids.shape[-1] == 1, (
4766 "PP+async expects sampled_token_ids to have shape [num_reqs, 1]"
4767 )
4768 # Skip for chunked prefill: sampled tokens are dummy
4769 # and will be discarded, no need to broadcast.
4770 if not self._is_all_reqs_chunked_prefill():
4771 torch.distributed.broadcast(
4772 sampled_token_ids, src=pp.rank, group=pp.device_group
4773 )
4774
4775 def _pp_receive_prev_sampled_token_ids_to_input_batch(self) -> None:
4776 """Receive sampled token ids broadcast from last PP stage"""
4777 pp = get_pp_group()
4778 assert not pp.is_last_rank
4779 num_reqs = self.input_batch.num_reqs
4780 # `prev_sampled_token_ids` is expected to have shape [num_reqs, 1].
4781 recv = torch.empty((num_reqs, 1), dtype=torch.int32, device=self.device)
4782 # skip for chunked prefill.
4783 if not self._is_all_reqs_chunked_prefill():
4784 self._pp_recv_work = torch.distributed.broadcast(
4785 recv, src=pp.last_rank, group=pp.device_group, async_op=True
4786 )
4787 self.input_batch.prev_sampled_token_ids = recv
4788
4789 # construct `prev_req_id_to_index` here so `_prepare_input_ids`
4790 # can map req_id -> previous batch row
4791 discard_req_indices = np.nonzero(self.discard_request_mask.np[:num_reqs])[0]
4792 discard_req_indices_set = set(discard_req_indices)
4793 prev_req_id_to_index: dict[str, int] = {}
4794 for i, req_id in enumerate(self.input_batch.req_ids):
4795 if i in discard_req_indices_set:
4796 continue
4797 prev_req_id_to_index[req_id] = i
4798 # PP+async scheduling: advance per-request local cached output length by
4799 # appending a placeholder (-1) token id.
4800 if (req_state := self.requests.get(req_id)) is not None:
4801 req_state.output_token_ids.append(-1)
4802 pos = self.input_batch.num_tokens_no_spec[i]
4803 self.input_batch.is_token_ids[i, pos] = True
4804 self.input_batch.num_tokens_no_spec[i] = pos + 1
4805 self.input_batch.prev_req_id_to_index = prev_req_id_to_index
4806
4807 @contextmanager
4808 def preserve_serving_state(self):
4809 multi_block_table = self.input_batch.block_table
4810 saved = [
4811 (bt.block_table.gpu.clone(), bt.block_table.cpu.clone())
4812 for bt in multi_block_table.block_tables
4813 ]
4814 multi_block_table.clear()
4815 try:
4816 yield
4817 finally:
4818 for bt, (saved_gpu, saved_cpu) in zip(
4819 multi_block_table.block_tables, saved
4820 ):
4821 bt.block_table.gpu.copy_(saved_gpu)
4822 bt.block_table.cpu.copy_(saved_cpu)
4823
4824 def take_draft_token_ids(self) -> DraftTokenIds | None:
4825 if not self.num_spec_tokens or not self._draft_token_req_ids:
4826 return None
4827 draft_token_ids, req_ids = self._get_draft_token_ids_cpu()
4828 return DraftTokenIds(req_ids, draft_token_ids)
4829
4830 def _copy_draft_token_ids_to_cpu(
4831 self, scheduler_output: "SchedulerOutput", zeros_only: bool = False
4832 ) -> None:
4833 if torch.is_tensor(self._draft_token_ids):
4834 assert isinstance(self._draft_token_ids, torch.Tensor)
4835 self.prev_num_spec_tokens = self._draft_token_ids.shape[1]
4836 # Check if we need to copy draft tokens to CPU. In async scheduling,
4837 # we only copy when needed for structured output, penalties or bad_words.
4838 if self.use_async_scheduling and not (
4839 scheduler_output.has_structured_output_requests
4840 or self.input_batch.sampling_metadata.output_token_ids
4841 ):
4842 return
4843 # We must also set the corresponding request ids.
4844 self._draft_token_req_ids = self.input_batch.req_ids.copy()
4845
4846 draft_token_ids: torch.Tensor = self._draft_token_ids
4847 if not torch.is_tensor(draft_token_ids):
4848 return
4849 assert self.draft_token_ids_event is not None
4850 assert self.draft_token_ids_copy_stream is not None
4851 assert self.draft_token_ids_cpu is not None
4852 default_stream = torch.cuda.current_stream()
4853 num_reqs = draft_token_ids.shape[0]
4854 num_spec_tokens = draft_token_ids.shape[1]
4855 with torch.cuda.stream(self.draft_token_ids_copy_stream):
4856 if not zeros_only:
4857 # Trigger async copy of draft token ids to cpu.
4858 self.draft_token_ids_copy_stream.wait_stream(default_stream)
4859 self.draft_token_ids_cpu[:num_reqs, :num_spec_tokens].copy_(
4860 draft_token_ids, non_blocking=True
4861 )
4862 else:
4863 # No copy needed, just zero-out cpu tensor.
4864 self.draft_token_ids_cpu[:num_reqs, :num_spec_tokens] = 0
4865 self.draft_token_ids_event.record()
4866
4867 def _get_draft_token_ids_cpu(self) -> tuple[list[list[int]], list[str]]:
4868 if isinstance(self._draft_token_ids, list):
4869 return self._draft_token_ids, self.input_batch.req_ids
4870 req_ids = self._draft_token_req_ids
4871 if req_ids is None:
4872 return [], []
4873 assert self.draft_token_ids_event is not None
4874 assert self.draft_token_ids_cpu is not None
4875 self.draft_token_ids_event.synchronize()
4876 assert isinstance(self._draft_token_ids, torch.Tensor)
4877 num_spec_tokens = self._draft_token_ids.shape[1]
4878 return self.draft_token_ids_cpu[
4879 : len(req_ids), :num_spec_tokens
4880 ].tolist(), req_ids
4881
4882 def _copy_valid_sampled_token_count(
4883 self, next_token_ids: torch.Tensor, valid_sampled_tokens_count: torch.Tensor
4884 ) -> None:
4885 if self.valid_sampled_token_count_event is None:
4886 return
4887
4888 default_stream = torch.cuda.current_stream()
4889 # Initialize a new stream to overlap the copy operation with
4890 # prepare_input of draft model.
4891 with torch.cuda.stream(self.valid_sampled_token_count_copy_stream):
4892 self.valid_sampled_token_count_copy_stream.wait_stream(default_stream) # type: ignore
4893 counts = valid_sampled_tokens_count
4894 counts_cpu = self.valid_sampled_token_count_cpu
4895 assert counts_cpu is not None
4896 counts_cpu[: counts.shape[0]].copy_(counts, non_blocking=True)
4897 self.valid_sampled_token_count_event.record()
4898
4899 if self.use_async_spec_decode:
4900 # Stash for GPU-side correction in _prepare_inputs.
4901 self.valid_sampled_token_count_gpu = valid_sampled_tokens_count
4902 self.input_batch.prev_sampled_token_ids = next_token_ids.unsqueeze(1)
4903
4904 def _get_valid_sampled_token_count(self) -> list[int]:
4905 # Wait until valid_sampled_tokens_count is copied to cpu,
4906 prev_sampled_token_ids = self.input_batch.prev_sampled_token_ids
4907 sampled_count_event = self.valid_sampled_token_count_event
4908 if sampled_count_event is None or prev_sampled_token_ids is None:
4909 return []
4910
4911 counts_cpu = self.valid_sampled_token_count_cpu
4912 assert counts_cpu is not None
4913 sampled_count_event.synchronize()
4914 return counts_cpu[: prev_sampled_token_ids.shape[0]].tolist()
4915
4916 def _get_spec_decode_draft_probs(
4917 self, spec_decode_metadata: SpecDecodeMetadata
4918 ) -> torch.Tensor | None:
4919 if self._draft_probs is None or self._draft_prob_req_ids is None:
4920 return None
4921
4922 row_by_req_id = {
4923 req_id: idx for idx, req_id in enumerate(self._draft_prob_req_ids)
4924 }
4925 draft_probs_rows: list[torch.Tensor] = []
4926 for req_id, num_draft in zip(
4927 self.input_batch.req_ids, spec_decode_metadata.num_draft_tokens
4928 ):
4929 if num_draft == 0:
4930 continue
4931 row_idx = row_by_req_id.get(req_id)
4932 if row_idx is None:
4933 logger.warning(
4934 "Missing cached draft probabilities for request %s; "
4935 "falling back to legacy speculative rejection behavior.",
4936 req_id,
4937 )
4938 return None
4939 draft_probs_rows.append(self._draft_probs[row_idx, :num_draft])
4940
4941 if not draft_probs_rows:
4942 return None
4943 return torch.cat(draft_probs_rows, dim=0).contiguous()
4944
4945 def propose_draft_token_ids(
4946 self,
4947 scheduler_output: "SchedulerOutput",
4948 sampled_token_ids: torch.Tensor | list[list[int]],
4949 sampling_metadata: SamplingMetadata,
4950 hidden_states: torch.Tensor,
4951 sample_hidden_states: torch.Tensor,
4952 aux_hidden_states: list[torch.Tensor] | None,
4953 spec_decode_metadata: SpecDecodeMetadata | None,
4954 common_attn_metadata: CommonAttentionMetadata,
4955 slot_mappings: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None,
4956 ) -> list[list[int]] | torch.Tensor:
4957 num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
4958 spec_config = self.speculative_config
4959 assert spec_config is not None
4960 num_spec_tokens_to_schedule = scheduler_output.num_spec_tokens_to_schedule
4961 self._draft_probs = None
4962 self._draft_prob_req_ids = None
4963 if spec_config.method == "ngram":
4964 from vllm.v1.spec_decode.ngram_proposer import NgramProposer
4965
4966 assert isinstance(sampled_token_ids, list)
4967 assert isinstance(self.drafter, NgramProposer)
4968 draft_token_ids = self.drafter.propose(
4969 num_spec_tokens_to_schedule,
4970 sampled_token_ids,
4971 self.input_batch.num_tokens_no_spec,
4972 self.input_batch.token_ids_cpu,
4973 slot_mappings=slot_mappings,
4974 )
4975 elif spec_config.method == "custom_class":
4976 assert isinstance(sampled_token_ids, list)
4977 draft_token_ids = cast(Any, self.drafter).propose(
4978 sampled_token_ids,
4979 self.input_batch.num_tokens_no_spec,
4980 self.input_batch.token_ids_cpu,
4981 slot_mappings=slot_mappings,
4982 )
4983 elif spec_config.use_ngram_gpu():
4984 assert isinstance(self.drafter, NgramProposerGPU)
4985 (
4986 next_token_ids,
4987 valid_sampled_tokens_count,
4988 valid_sampled_token_ids_gpu,
4989 ) = self.drafter.update_token_ids_ngram(
4990 sampled_token_ids,
4991 self.input_batch,
4992 self.token_ids_gpu_tensor,
4993 self.num_tokens_no_spec_gpu,
4994 self.discard_request_mask.gpu,
4995 )
4996 self._copy_valid_sampled_token_count(
4997 next_token_ids, valid_sampled_tokens_count
4998 )
4999
5000 batch_size = next_token_ids.shape[0]
Showing first 5,000 of 7,522 lines. View raw