A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 12, 03:30 UTC
Add logging for cudagraph related info (#29825)
Signed-off-by: Yong Hoon Shin <yhshin@meta.com>
Yong Hoon Shin committed
Dec 2, 2025 at 23:01 UTC
69520bc695ff8fa7fda66ef7c1a16761824ad354
9 files changed
+161
-6
vllm/compilation/cuda_graph.py
+94
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
import dataclasses
5
+from collections import Counter
6
from collections.abc import Callable
7
from contextlib import ExitStack
8
from typing import Any
@@ -22,6 +23,99 @@ from vllm.utils.torch_utils import weak_ref_tensors
23
logger = init_logger(__name__)
24
25
26
+@dataclasses.dataclass(frozen=True)
27
+class CUDAGraphStat:
28
+ num_unpadded_tokens: int
29
+ num_padded_tokens: int
30
+ num_paddings: int
31
+ runtime_mode: str
32
+
33
+
34
+class CUDAGraphLogging:
35
+ """Aggregate and log cudagraph metrics"""
36
+
37
+ COLUMN_HEADERS = [
38
+ "Unpadded Tokens",
39
+ "Padded Tokens",
40
+ "Num Paddings",
41
+ "Runtime Mode",
42
+ "Count",
43
+ ]
44
+
45
+ def __init__(self, cg_mode: CUDAGraphMode, cg_capture_sizes: list[int] | None):
46
+ self.reset()
47
+ self.cg_mode = str(cg_mode)
48
+ self.cg_capture_sizes = str(cg_capture_sizes or [])
49
+
50
+ self.settings_header = (
51
+ "**CUDAGraph Config Settings:**\n\n"
52
+ f"- Mode: {self.cg_mode}\n"
53
+ f"- Capture sizes: {self.cg_capture_sizes}\n\n"
54
+ "**CUDAGraph Stats:**\n\n"
55
+ )
56
+
57
+ def reset(self):
58
+ self.stats = []
59
+
60
+ def observe(self, cudagraph_stat: CUDAGraphStat):
61
+ self.stats.append(cudagraph_stat)
62
+
63
+ def generate_metric_table(self) -> str:
64
+ stats_counts = Counter(self.stats)
65
+
66
+ # Convert stats to rows of strings, in descending order of observed frequencies
67
+ rows = []
68
+ for stat, count in sorted(
69
+ stats_counts.items(), key=lambda item: item[1], reverse=True
70
+ ):
71
+ rows.append(
72
+ [
73
+ str(stat.num_unpadded_tokens),
74
+ str(stat.num_padded_tokens),
75
+ str(stat.num_paddings),
76
+ stat.runtime_mode,
77
+ str(count),
78
+ ]
79
+ )
80
+
81
+ # Calculate column widths (max of header and data)
82
+ col_widths = []
83
+ for i, header_text in enumerate(self.COLUMN_HEADERS):
84
+ max_width = len(header_text)
85
+ for row in rows:
86
+ max_width = max(max_width, len(row[i]))
87
+ col_widths.append(max_width)
88
+
89
+ table_header_list = [
90
+ h.ljust(w) for h, w in zip(self.COLUMN_HEADERS, col_widths)
91
+ ]
92
+ table_header = "| " + " | ".join(table_header_list) + " |\n"
93
+
94
+ table_separator = "|" + "|".join("-" * (w + 2) for w in col_widths) + "|\n"
95
+
96
+ # Create data rows with proper alignment
97
+ data_rows = []
98
+ for row in rows:
99
+ formatted_row = [
100
+ str(val).ljust(width) for val, width in zip(row, col_widths)
101
+ ]
102
+ data_rows.append("| " + " | ".join(formatted_row) + " |")
103
+
104
+ return (
105
+ self.settings_header
106
+ + table_header
107
+ + table_separator
108
+ + "\n".join(data_rows)
109
+ + "\n"
110
+ )
111
+
112
+ def log(self, log_fn=logger.info):
113
+ if not self.stats:
114
+ return
115
+ log_fn(self.generate_metric_table())
116
+ self.reset()
117
+
118
+
119
@dataclasses.dataclass
120
class CUDAGraphEntry:
121
batch_descriptor: BatchDescriptor
vllm/config/observability.py
+4
@@ -55,6 +55,10 @@ class ObservabilityConfig:
55
kv_cache_metrics_sample: float = Field(default=0.01, gt=0, le=1)
56
"""Sampling rate for KV cache metrics (0.0, 1.0]. Default 0.01 = 1% of blocks."""
57
58
+ cudagraph_metrics: bool = False
59
+ """Enable CUDA graph metrics (number of padded/unpadded tokens, runtime cudagraph
60
+ dispatch modes, and their observed frequencies at every logging interval)."""
61
+
62
@cached_property
63
def collect_model_forward_time(self) -> bool:
64
"""Whether to collect model forward time for the request."""
vllm/engine/arg_utils.py
+6
@@ -518,6 +518,7 @@ class EngineArgs:
518
kv_cache_metrics_sample: float = get_field(
519
ObservabilityConfig, "kv_cache_metrics_sample"
520
)
521
+ cudagraph_metrics: bool = ObservabilityConfig.cudagraph_metrics
522
scheduling_policy: SchedulerPolicy = SchedulerConfig.policy
523
scheduler_cls: str | type[object] | None = SchedulerConfig.scheduler_cls
524
@@ -1021,6 +1022,10 @@ class EngineArgs:
1022
"--kv-cache-metrics-sample",
1023
**observability_kwargs["kv_cache_metrics_sample"],
1024
)
1025
+ observability_group.add_argument(
1026
+ "--cudagraph-metrics",
1027
+ **observability_kwargs["cudagraph_metrics"],
1028
+ )
1029
1030
# Scheduler arguments
1031
scheduler_kwargs = get_kwargs(SchedulerConfig)
@@ -1698,6 +1703,7 @@ class EngineArgs:
1703
collect_detailed_traces=self.collect_detailed_traces,
1704
kv_cache_metrics=self.kv_cache_metrics,
1705
kv_cache_metrics_sample=self.kv_cache_metrics_sample,
1706
+ cudagraph_metrics=self.cudagraph_metrics,
1707
)
1708
1709
# Compilation config overrides
vllm/v1/core/sched/scheduler.py
+7
-1
@@ -7,6 +7,7 @@ from collections.abc import Iterable
7
from typing import Any
8
9
from vllm import envs
10
+from vllm.compilation.cuda_graph import CUDAGraphStat
11
from vllm.config import VllmConfig
12
from vllm.distributed.ec_transfer.ec_connector.base import (
13
ECConnectorMetadata,
@@ -1037,6 +1038,7 @@ class Scheduler(SchedulerInterface):
1038
pooler_outputs = model_runner_output.pooler_output
1039
num_nans_in_logits = model_runner_output.num_nans_in_logits
1040
kv_connector_output = model_runner_output.kv_connector_output
1041
+ cudagraph_stats = model_runner_output.cudagraph_stats
1042
1043
outputs: dict[int, list[EngineCoreOutput]] = defaultdict(list)
1044
spec_decoding_stats: SpecDecodingStats | None = None
@@ -1219,7 +1221,9 @@ class Scheduler(SchedulerInterface):
1221
finished_req_ids.clear()
1222
1223
if (
1222
- stats := self.make_stats(spec_decoding_stats, kv_connector_stats)
1224
+ stats := self.make_stats(
1225
+ spec_decoding_stats, kv_connector_stats, cudagraph_stats
1226
+ )
1227
) is not None:
1228
# Return stats to only one of the front-ends.
1229
if (eco := next(iter(engine_core_outputs.values()), None)) is None:
@@ -1420,6 +1424,7 @@ class Scheduler(SchedulerInterface):
1424
self,
1425
spec_decoding_stats: SpecDecodingStats | None = None,
1426
kv_connector_stats: KVConnectorStats | None = None,
1427
+ cudagraph_stats: CUDAGraphStat | None = None,
1428
) -> SchedulerStats | None:
1429
if not self.log_stats:
1430
return None
@@ -1444,6 +1449,7 @@ class Scheduler(SchedulerInterface):
1449
kv_cache_eviction_events=eviction_events,
1450
spec_decoding_stats=spec_stats,
1451
kv_connector_stats=connector_stats_payload,
1452
+ cudagraph_stats=cudagraph_stats,
1453
)
1454
1455
def make_spec_decoding_stats(
vllm/v1/metrics/loggers.py
+14
@@ -10,6 +10,7 @@ from typing import TypeAlias
10
from prometheus_client import Counter, Gauge, Histogram
11
12
import vllm.envs as envs
13
+from vllm.compilation.cuda_graph import CUDAGraphLogging
14
from vllm.config import SupportsMetricsInfo, VllmConfig
15
from vllm.distributed.kv_transfer.kv_connector.v1.metrics import (
16
KVConnectorLogging,
@@ -106,6 +107,12 @@ class LoggingStatLogger(StatLoggerBase):
107
self.spec_decoding_logging = SpecDecodingLogging()
108
kv_transfer_config = self.vllm_config.kv_transfer_config
109
self.kv_connector_logging = KVConnectorLogging(kv_transfer_config)
110
+ self.cudagraph_logging = None
111
+ if self.vllm_config.observability_config.cudagraph_metrics:
112
+ self.cudagraph_logging = CUDAGraphLogging(
113
+ self.vllm_config.compilation_config.cudagraph_mode,
114
+ self.vllm_config.compilation_config.cudagraph_capture_sizes,
115
+ )
116
self.last_prompt_throughput: float = 0.0
117
self.last_generation_throughput: float = 0.0
118
self.engine_is_idle = False
@@ -161,6 +168,11 @@ class LoggingStatLogger(StatLoggerBase):
168
self.spec_decoding_logging.observe(scheduler_stats.spec_decoding_stats)
169
if kv_connector_stats := scheduler_stats.kv_connector_stats:
170
self.kv_connector_logging.observe(kv_connector_stats)
171
+ if (
172
+ self.cudagraph_logging is not None
173
+ and scheduler_stats.cudagraph_stats is not None
174
+ ):
175
+ self.cudagraph_logging.observe(scheduler_stats.cudagraph_stats)
176
if not self.aggregated:
177
self.last_scheduler_stats = scheduler_stats
178
if mm_cache_stats:
@@ -240,6 +252,8 @@ class LoggingStatLogger(StatLoggerBase):
252
253
self.spec_decoding_logging.log(log_fn=log_fn)
254
self.kv_connector_logging.log(log_fn=log_fn)
255
+ if self.cudagraph_logging is not None:
256
+ self.cudagraph_logging.log(log_fn=log_fn)
257
258
def log_engine_initialized(self):
259
if self.vllm_config.cache_config.num_gpu_blocks:
vllm/v1/metrics/stats.py
+3
@@ -7,6 +7,7 @@ from dataclasses import dataclass, field
7
from typing import TYPE_CHECKING, Any
8
9
import vllm.envs as envs
10
+from vllm.compilation.cuda_graph import CUDAGraphStat
11
from vllm.v1.spec_decode.metrics import SpecDecodingStats
12
13
if TYPE_CHECKING:
@@ -183,6 +184,8 @@ class SchedulerStats:
184
waiting_lora_adapters: dict[str, int] = field(default_factory=dict)
185
running_lora_adapters: dict[str, int] = field(default_factory=dict)
186
187
+ cudagraph_stats: CUDAGraphStat | None = None
188
+
189
190
@dataclass
191
class RequestStateStats:
vllm/v1/outputs.py
+4
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, NamedTuple
8
import numpy as np
9
import torch
10
11
+from vllm.compilation.cuda_graph import CUDAGraphStat
12
from vllm.v1.core.sched.output import SchedulerOutput
13
14
if TYPE_CHECKING:
@@ -169,6 +170,9 @@ class ModelRunnerOutput:
170
# req_id -> num_nans_in_logits
171
num_nans_in_logits: dict[str, int] | None = None
172
173
+ # information related to cudagraph execution
174
+ cudagraph_stats: CUDAGraphStat | None = None
175
+
176
177
# ModelRunnerOutput wrapper for async scheduling.
178
class AsyncModelRunnerOutput(ABC):
vllm/v1/worker/gpu_model_runner.py
+28
-4
@@ -27,7 +27,7 @@ from vllm.attention.backends.abstract import (
27
)
28
from vllm.attention.layer import Attention, MLAAttention
29
from vllm.compilation.counter import compilation_counter
30
-from vllm.compilation.cuda_graph import CUDAGraphWrapper
30
+from vllm.compilation.cuda_graph import CUDAGraphStat, CUDAGraphWrapper
31
from vllm.compilation.monitor import set_cudagraph_capturing_enabled
32
from vllm.config import (
33
CompilationMode,
@@ -257,6 +257,7 @@ class ExecuteModelState(NamedTuple):
257
sample_hidden_states: torch.Tensor
258
aux_hidden_states: list[torch.Tensor] | None
259
ec_connector_output: ECConnectorOutput | None
260
+ cudagraph_stats: CUDAGraphStat | None
261
262
263
class GPUModelRunner(
@@ -2755,7 +2756,11 @@ class GPUModelRunner(
2756
force_uniform_decode: bool | None = None,
2757
force_has_lora: bool | None = None,
2758
) -> tuple[
2758
- CUDAGraphMode, BatchDescriptor, UBatchSlices | None, torch.Tensor | None
2759
+ CUDAGraphMode,
2760
+ BatchDescriptor,
2761
+ UBatchSlices | None,
2762
+ torch.Tensor | None,
2763
+ CUDAGraphStat | None,
2764
]:
2765
num_tokens_padded = self._pad_for_sequence_parallelism(num_tokens)
2766
uniform_decode = (
@@ -2820,7 +2825,22 @@ class GPUModelRunner(
2825
# num_tokens_across_dp will no-longer be valid
2826
assert batch_descriptor.num_tokens == num_tokens_padded
2827
2823
- return cudagraph_mode, batch_descriptor, ubatch_slices, num_tokens_across_dp
2828
+ cudagraph_stats = None
2829
+ if self.vllm_config.observability_config.cudagraph_metrics:
2830
+ cudagraph_stats = CUDAGraphStat(
2831
+ num_unpadded_tokens=num_tokens,
2832
+ num_padded_tokens=batch_descriptor.num_tokens,
2833
+ num_paddings=batch_descriptor.num_tokens - num_tokens,
2834
+ runtime_mode=str(cudagraph_mode),
2835
+ )
2836
+
2837
+ return (
2838
+ cudagraph_mode,
2839
+ batch_descriptor,
2840
+ ubatch_slices,
2841
+ num_tokens_across_dp,
2842
+ cudagraph_stats,
2843
+ )
2844
2845
@torch.inference_mode()
2846
def execute_model(
@@ -2918,6 +2938,7 @@ class GPUModelRunner(
2938
batch_desc,
2939
ubatch_slices,
2940
num_tokens_across_dp,
2941
+ cudagraph_stats,
2942
) = self._determine_batch_execution_and_padding(
2943
num_tokens=num_tokens_unpadded,
2944
num_reqs=num_reqs,
@@ -3067,6 +3088,7 @@ class GPUModelRunner(
3088
sample_hidden_states,
3089
aux_hidden_states,
3090
ec_connector_output,
3091
+ cudagraph_stats,
3092
)
3093
self.kv_connector_output = kv_connector_output
3094
return None
@@ -3102,6 +3124,7 @@ class GPUModelRunner(
3124
sample_hidden_states,
3125
aux_hidden_states,
3126
ec_connector_output,
3127
+ cudagraph_stats,
3128
) = self.execute_model_state
3129
# Clear ephemeral state.
3130
self.execute_model_state = None
@@ -3217,6 +3240,7 @@ class GPUModelRunner(
3240
if self.supports_mm_inputs
3241
else None,
3242
num_nans_in_logits=num_nans_in_logits,
3243
+ cudagraph_stats=cudagraph_stats,
3244
)
3245
3246
if not self.use_async_scheduling:
@@ -3937,7 +3961,7 @@ class GPUModelRunner(
3961
3962
num_sampled_tokens = np.ones(num_reqs, dtype=np.int32)
3963
3940
- _cudagraph_mode, batch_desc, ubatch_slices, num_tokens_across_dp = (
3964
+ _cudagraph_mode, batch_desc, ubatch_slices, num_tokens_across_dp, _ = (
3965
self._determine_batch_execution_and_padding(
3966
num_tokens=num_tokens_unpadded,
3967
num_reqs=num_reqs,
vllm/v1/worker/gpu_worker.py
+1
-1
@@ -564,7 +564,7 @@ class Worker(WorkerBase):
564
# TODO(lucas): This is pretty gross; ideally we should only ever call
565
# `_determine_batch_execution_and_padding` once (will get called again
566
# in `execute_model`) but this requires a larger refactor of PP.
567
- _, batch_desc, _, _ = (
567
+ _, batch_desc, _, _, _ = (
568
self.model_runner._determine_batch_execution_and_padding(
569
num_tokens=num_scheduled_tokens,
570
num_reqs=len(num_scheduled_tokens_np),