Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ cython_debug/
# VSCode
.vscode/

# Cursor
.cursor/

Comment thread
izhuhaoran marked this conversation as resolved.
Outdated
# Claude
CLAUDE.md
.claude/
Expand Down
196 changes: 151 additions & 45 deletions vllm/v1/worker/gpu/cudagraph_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from collections.abc import Callable, Iterable
from collections.abc import Callable
from typing import Any

import numpy as np
Expand All @@ -11,7 +11,8 @@
from vllm.config import VllmConfig
from vllm.config.compilation import CUDAGraphMode
from vllm.distributed.parallel_state import graph_capture, is_global_first_rank
from vllm.forward_context import set_forward_context
from vllm.forward_context import BatchDescriptor, set_forward_context
from vllm.utils.math_utils import cdiv
from vllm.v1.attention.backend import AttentionMetadataBuilder
from vllm.v1.kv_cache_interface import KVCacheConfig
from vllm.v1.worker.gpu.attn_utils import build_attn_metadata
Expand Down Expand Up @@ -50,6 +51,12 @@ def __init__(
self.cudagraph_mode,
)

# Compute uniform decode query length for spec decode support
spec_config = vllm_config.speculative_config
self.uniform_decode_query_len = 1
if spec_config is not None:
self.uniform_decode_query_len = 1 + spec_config.num_speculative_tokens

self.graphs: dict[int, torch.cuda.CUDAGraph] = {}
self.pool = torch.cuda.graph_pool_handle()
self.hidden_states: torch.Tensor | None = None
Expand All @@ -59,28 +66,41 @@ def needs_capture(self) -> bool:

def get_cudagraph_size(
self,
num_tokens_after_padding: int,
num_tokens_per_request: Iterable[int],
num_tokens: int,
) -> int | None:
return get_cudagraph_size(
num_tokens_after_padding,
num_tokens_per_request,
self.cudagraph_sizes,
self.cudagraph_mode,
)
return self.cudagraph_sizes.get(num_tokens)

def capture_graph(
self,
num_tokens: int,
capture_cudagraph_mode: CUDAGraphMode,
model: nn.Module,
input_buffers: InputBuffers,
mrope_positions: torch.Tensor | None,
inputs_embeds: torch.Tensor | None,
block_tables: BlockTables,
attn_metadata_builders: list[AttentionMetadataBuilder],
kv_cache_config: KVCacheConfig,
has_lora: bool = False,
uniform_decode: bool = False,
) -> None:
num_reqs = min(num_tokens, self.max_num_reqs)
# select and check capture function
if capture_cudagraph_mode == CUDAGraphMode.PIECEWISE:
capture_fn = self._capture_piecewise_graph
elif capture_cudagraph_mode == CUDAGraphMode.FULL:
capture_fn = self._capture_full_graph
else:
raise ValueError(
f"Invalid capture_cudagraph_mode for capture: {capture_cudagraph_mode}"
)
Comment thread
izhuhaoran marked this conversation as resolved.
Outdated
# prepare inputs
if uniform_decode:
num_reqs = min(
cdiv(num_tokens, self.uniform_decode_query_len),
self.max_num_reqs,
)
else:
num_reqs = min(num_tokens, self.max_num_reqs)
input_ids = input_buffers.input_ids[:num_tokens]
positions = input_buffers.positions[:num_tokens]
if self.uses_mrope:
Expand All @@ -96,6 +116,9 @@ def capture_graph(
attn_metadata_builders,
self.max_model_len,
kv_cache_config,
uniform_decode_query_len=(
self.uniform_decode_query_len if uniform_decode else 0
),
)
num_tokens_across_dp = make_num_tokens_across_dp(self.dp_size, num_tokens)

Expand All @@ -115,13 +138,38 @@ def capture_graph(
if self.hidden_states is None:
self.hidden_states = torch.empty_like(hidden_states)

capture_fn(
num_tokens=num_tokens,
num_reqs=num_reqs,
model=model,
input_ids=input_ids,
positions=positions,
inputs_embeds=inputs_embeds,
num_tokens_across_dp=num_tokens_across_dp,
attn_metadata=attn_metadata,
has_lora=has_lora,
)

def _capture_full_graph(
self,
num_tokens: int,
num_reqs: int,
model: nn.Module,
input_ids: torch.Tensor,
positions: torch.Tensor,
inputs_embeds: torch.Tensor | None,
num_tokens_across_dp: torch.Tensor,
attn_metadata: dict[str, Any] | None,
has_lora: bool = False,
) -> None:
assert attn_metadata is not None
# Capture the graph.
assert num_tokens not in self.graphs
graph = torch.cuda.CUDAGraph()
with (
set_forward_context(
attn_metadata,
self.vllm_config,
attn_metadata=attn_metadata,
vllm_config=self.vllm_config,
num_tokens=num_tokens,
cudagraph_runtime_mode=CUDAGraphMode.NONE,
num_tokens_across_dp=num_tokens_across_dp,
Expand All @@ -133,9 +181,47 @@ def capture_graph(
positions=positions,
inputs_embeds=inputs_embeds,
)
assert self.hidden_states is not None
self.hidden_states[:num_tokens] = hidden_states
self.graphs[num_tokens] = graph

def _capture_piecewise_graph(
self,
num_tokens: int,
num_reqs: int,
model: nn.Module,
input_ids: torch.Tensor,
positions: torch.Tensor,
inputs_embeds: torch.Tensor | None,
num_tokens_across_dp: torch.Tensor,
attn_metadata: dict[str, Any] | None,
has_lora: bool = False,
) -> None:
# create batch descriptor for piecewise cudagraph dispatch key
batch_descriptor = BatchDescriptor(
num_tokens=num_tokens,
num_reqs=None,
uniform=False,
has_lora=has_lora,
)

# Capture run - CUDAGraphWrapper inside torch.compile will auto capture.
with set_forward_context(
attn_metadata=None, # piecewise no need attn_metadata
vllm_config=self.vllm_config,
num_tokens=num_tokens,
cudagraph_runtime_mode=CUDAGraphMode.PIECEWISE,
num_tokens_across_dp=num_tokens_across_dp,
batch_descriptor=batch_descriptor,
):
hidden_states = model(
input_ids=input_ids,
positions=positions,
inputs_embeds=inputs_embeds,
)
assert self.hidden_states is not None
self.hidden_states[:num_tokens] = hidden_states

@torch.inference_mode()
def capture(
self,
Expand All @@ -146,22 +232,51 @@ def capture(
block_tables: BlockTables,
attn_metadata_builders: list[AttentionMetadataBuilder],
kv_cache_config: KVCacheConfig,
has_lora: bool = False,
) -> None:
capture_graphs(
self.cudagraph_sizes,
self.device,
self.capture_graph,
common_kwargs = dict(
model=model,
input_buffers=input_buffers,
mrope_positions=mrope_positions,
inputs_embeds=inputs_embeds,
block_tables=block_tables,
attn_metadata_builders=attn_metadata_builders,
kv_cache_config=kv_cache_config,
has_lora=has_lora,
)

def run(self, num_tokens: int) -> torch.Tensor:
assert num_tokens in self.graphs
# Phase 1: Capture for mixed prefill-decode batches if needed.
mixed_mode = self.cudagraph_mode.mixed_mode()
if mixed_mode != CUDAGraphMode.NONE:
capture_graphs(
cudagraph_sizes=self.cudagraph_sizes,
device=self.device,
capture_fn=self.capture_graph,
Comment thread
izhuhaoran marked this conversation as resolved.
Outdated
capture_cudagraph_mode=mixed_mode,
desc=f"Capturing CUDA graphs (mixed, {mixed_mode.name})",
uniform_decode=False,
**common_kwargs,
)

# Phase 2: Capture FULL graphs for uniform decode batches if needed.
# This is only needed if we use a separate routine for decode batches
# and the decode_mode is FULL.
if (
self.cudagraph_mode.decode_mode() == CUDAGraphMode.FULL
and self.cudagraph_mode.separate_routine()
):
capture_graphs(
cudagraph_sizes=self.cudagraph_sizes,
device=self.device,
capture_fn=self.capture_graph,
Comment thread
izhuhaoran marked this conversation as resolved.
Outdated
capture_cudagraph_mode=CUDAGraphMode.FULL,
desc="Capturing CUDA graphs (decode, FULL)",
uniform_decode=True,
**common_kwargs,
)

def run_fullgraph(self, num_tokens: int) -> torch.Tensor:
assert num_tokens in self.graphs, f"No cudagraph for {num_tokens} tokens"
self.graphs[num_tokens].replay()
assert self.hidden_states is not None
return self.hidden_states[:num_tokens]
Expand All @@ -173,7 +288,8 @@ def get_cudagraph_sizes(
max_num_tokens: int,
cudagraph_mode: CUDAGraphMode,
) -> dict[int, int]:
if not cudagraph_mode.has_full_cudagraphs():
# Support both FULL and PIECEWISE cudagraph modes
if cudagraph_mode == CUDAGraphMode.NONE:
return {}
if not capture_sizes:
return {}
Expand All @@ -198,42 +314,28 @@ def get_cudagraph_sizes(
return cudagraph_sizes


def get_cudagraph_size(
num_tokens_after_dp_padding: int,
num_tokens_per_request: Iterable[int],
cudagraph_sizes: dict[int, int],
cudagraph_mode: CUDAGraphMode,
) -> int | None:
if not cudagraph_mode.has_full_cudagraphs():
# No full CUDA graph is used.
return None

size = cudagraph_sizes.get(num_tokens_after_dp_padding)
if size is None:
# No CUDA graph for this size.
return None

is_mixed = any(x > 1 for x in num_tokens_per_request)
if is_mixed and cudagraph_mode.mixed_mode() != CUDAGraphMode.FULL:
# Prefill is included, and this mode doesn't use CUDA graph for it.
return None
return size


def capture_graphs(
cudagraph_sizes: dict[int, int],
device: torch.device,
capture_fn: Callable,
capture_cudagraph_mode: CUDAGraphMode,
desc: str = "Capturing CUDA graphs",
uniform_decode: bool = False,
**capture_kwargs,
) -> None:
# Capture larger graphs first.
sizes_to_capture = sorted(set(cudagraph_sizes.values()), reverse=True)
if is_global_first_rank():
sizes_to_capture = tqdm(sizes_to_capture, desc="Capturing CUDA graphs")
sizes_to_capture = tqdm(sizes_to_capture, desc=desc)

with graph_capture(device=device):
for size in sizes_to_capture:
capture_fn(size, **capture_kwargs)
capture_fn(
size,
capture_cudagraph_mode,
uniform_decode=uniform_decode,
**capture_kwargs,
)


def prepare_inputs_to_capture(
Expand All @@ -244,8 +346,12 @@ def prepare_inputs_to_capture(
attn_metadata_builders: list[AttentionMetadataBuilder],
max_model_len: int,
kv_cache_config: KVCacheConfig,
uniform_decode_query_len: int = 0,
) -> dict[str, Any]:
num_tokens_per_req = num_tokens // num_reqs
if uniform_decode_query_len > 0:
num_tokens_per_req = uniform_decode_query_len
else:
num_tokens_per_req = num_tokens // num_reqs

query_start_loc_np = np.arange(num_reqs + 1, dtype=np.int32) * num_tokens_per_req
query_start_loc_np[-1] = num_tokens
Expand Down
Loading