[Profiler] Unify platform-aware torch profiling (#57460)

czhu-cohere committed Sep 20, 2026 at 19:17 UTC 3918f3c5a3e389fb17d9c0d629811f3745014db0
9 files changed +408 -110
docs/contributing/profiling.md
+4
@@ -22,6 +22,10 @@ To use the `torch.profiler` module, set the `profiler` entry to `'torch'` and `t
22 - `torch_profiler_with_flops` to enable recording FLOPs, off by default
23 - `torch_profiler_use_gzip` to control gzip-compressing profiling files, on by default
24 - `torch_profiler_dump_cuda_time_total` to control dumping and printing the aggregated CUDA self time table, on by default
25 +- `torch_profiler_activities` to select worker activities. Defaults are
26 + platform-specific: `["CPU"]` on CPU, `["CPU", "CUDA"]` on NVIDIA GPUs, and
27 + `["CPU", "XPU"]` on Intel GPUs. Selecting `["CUDA"]` omits CPU annotations
28 + and the AsyncLLM CPU trace to reduce profiling overhead and trace size.
29
30 When using `vllm bench serve`, you can enable profiling by passing the `--profile` flag.
31
tests/v1/engine/test_async_llm.py
+30 -7
@@ -60,13 +60,7 @@ VISION_PROMPT = {
60 }
61
62
63 -def test_cuda_profiler_requests_reach_engine_core(monkeypatch: pytest.MonkeyPatch):
64 - vllm_config = MagicMock()
65 - vllm_config.observability_config.otlp_traces_endpoint = None
66 - vllm_config.scheduler_config.stream_interval = 1
67 - vllm_config.profiler_config.profiler = "cuda"
68 - vllm_config.profiler_config.ignore_frontend = False
69 -
63 +def _mock_async_llm_dependencies(monkeypatch: pytest.MonkeyPatch):
64 renderer = MagicMock()
65 engine_core = MagicMock()
66 engine_core.profile_async = AsyncMock()
@@ -90,6 +84,16 @@ def test_cuda_profiler_requests_reach_engine_core(monkeypatch: pytest.MonkeyPatc
84 "make_async_mp_client",
85 MagicMock(return_value=engine_core),
86 )
87 + return engine_core
88 +
89 +
90 +def test_cuda_profiler_requests_reach_engine_core(monkeypatch: pytest.MonkeyPatch):
91 + vllm_config = MagicMock()
92 + vllm_config.observability_config.otlp_traces_endpoint = None
93 + vllm_config.scheduler_config.stream_interval = 1
94 + vllm_config.profiler_config.profiler = "cuda"
95 + vllm_config.profiler_config.ignore_frontend = False
96 + engine_core = _mock_async_llm_dependencies(monkeypatch)
97
98 engine = AsyncLLM(vllm_config, MagicMock(), log_stats=False)
99
@@ -103,6 +107,25 @@ def test_cuda_profiler_requests_reach_engine_core(monkeypatch: pytest.MonkeyPatc
107 engine_core.profile_async.assert_has_awaits([call(True, None), call(False)])
108
109
110 +def test_cuda_only_torch_profiler_skips_frontend_cpu_trace(
111 + monkeypatch: pytest.MonkeyPatch,
112 +):
113 + vllm_config = MagicMock()
114 + vllm_config.observability_config.otlp_traces_endpoint = None
115 + vllm_config.scheduler_config.stream_interval = 1
116 + vllm_config.profiler_config.profiler = "torch"
117 + vllm_config.profiler_config.ignore_frontend = False
118 + vllm_config.profiler_config.torch_profiler_activities = ["CUDA"]
119 + _mock_async_llm_dependencies(monkeypatch)
120 + profiler = MagicMock()
121 + monkeypatch.setattr(async_llm_module, "TorchProfilerWrapper", profiler)
122 +
123 + engine = AsyncLLM(vllm_config, MagicMock(), log_stats=False)
124 +
125 + assert engine.profiler is None
126 + profiler.assert_not_called()
127 +
128 +
129 async def generate(
130 engine: AsyncLLM,
131 request_id: str,
tests/v1/worker/test_gpu_profiler.py
+231 -10
@@ -7,6 +7,7 @@ from unittest.mock import MagicMock, Mock, call, patch
7 from uuid import UUID
8
9 import pytest
10 +import torch
11 from pydantic import ValidationError
12
13 from vllm.config import (
@@ -17,10 +18,17 @@ from vllm.config import (
18 )
19 from vllm.config.profiler import _is_uri_path
20 from vllm.platforms import current_platform
20 -from vllm.profiler.wrapper import ProtonProfilerWrapper, WorkerProfiler
21 +from vllm.profiler.wrapper import (
22 + ProtonProfilerWrapper,
23 + TorchProfilerWrapper,
24 + WorkerProfiler,
25 + create_worker_profiler,
26 + validate_worker_profiler_config,
27 +)
28 from vllm.v1.core.sched.output import CachedRequestData
29 from vllm.v1.worker.gpu_model_runner import GPUModelRunner
30 from vllm.v1.worker.gpu_worker import Worker
31 +from vllm.v1.worker.xpu_worker import XPUWorker
32
33
34 class ConcreteWorkerProfiler(WorkerProfiler):
@@ -51,6 +59,213 @@ def default_profiler_config():
59 )
60
61
62 +def test_torch_profiler_rebuilds_one_shot_profiler_each_round(tmp_path):
63 + config = ProfilerConfig(
64 + profiler="torch",
65 + torch_profiler_dir=str(tmp_path),
66 + torch_profiler_activities=["CUDA"],
67 + torch_profiler_dump_cuda_time_total=False,
68 + warmup_iterations=2,
69 + )
70 + profilers = [MagicMock(), MagicMock()]
71 + for profiler in profilers:
72 + profiler.profiler = MagicMock()
73 +
74 + with patch(
75 + "vllm.profiler.wrapper.torch.profiler.profile", side_effect=profilers
76 + ) as profile:
77 + wrapper = TorchProfilerWrapper(
78 + config, worker_name="worker", local_rank=0, activities=["CUDA"]
79 + )
80 + profile.assert_not_called()
81 +
82 + wrapper.start()
83 + assert not wrapper.should_annotate
84 + assert not wrapper._profiler_step()
85 + wrapper.stop()
86 + wrapper.start()
87 + assert not wrapper._profiler_step()
88 + wrapper.stop()
89 +
90 + assert profile.call_count == 2
91 + assert profile.call_args.kwargs["activities"] == [
92 + torch.profiler.ProfilerActivity.CUDA
93 + ]
94 + for profiler in profilers:
95 + profiler.start.assert_called_once_with()
96 + profiler.step.assert_called_once_with()
97 + profiler.stop.assert_called_once_with()
98 +
99 +
100 +def test_torch_profiler_records_each_profile_round(tmp_path):
101 + traces: list[torch.profiler.profile] = []
102 + wrapper = TorchProfilerWrapper(
103 + ProfilerConfig(
104 + profiler="torch",
105 + torch_profiler_dir=str(tmp_path),
106 + torch_profiler_dump_cuda_time_total=False,
107 + ),
108 + worker_name="worker",
109 + local_rank=1,
110 + activities=["CPU"],
111 + on_trace_ready=traces.append,
112 + )
113 +
114 + for run in range(2):
115 + wrapper.start()
116 + with torch.profiler.record_function(f"run_{run}"):
117 + pass
118 + wrapper.stop()
119 +
120 + assert len(traces) == 2
121 + for run, trace in enumerate(traces):
122 + assert {
123 + event.name for event in trace.events() if event.name.startswith("run_")
124 + } == {f"run_{run}"}
125 +
126 +
127 +@pytest.mark.parametrize(
128 + "activities", [["CPU", "CUDA"], ["CUDA"], ["CPU", "XPU"], ["XPU"]]
129 +)
130 +@pytest.mark.parametrize("dump_device_time", [True, False])
131 +def test_torch_profiler_device_summary(tmp_path, capsys, activities, dump_device_time):
132 + """Device summaries honor the dump option on both CUDA and XPU."""
133 + config = ProfilerConfig(
134 + profiler="torch",
135 + torch_profiler_dir=str(tmp_path),
136 + torch_profiler_dump_cuda_time_total=dump_device_time,
137 + )
138 + with patch("vllm.profiler.wrapper.torch.profiler.profile") as profile:
139 + profile.return_value.key_averages.return_value.table.return_value = (
140 + "device times"
141 + )
142 + wrapper = TorchProfilerWrapper(
143 + config, worker_name="worker", local_rank=0, activities=activities
144 + )
145 + wrapper.start()
146 + wrapper.stop()
147 +
148 + summary = tmp_path / "profiler_out_0.txt"
149 + assert summary.exists() == dump_device_time
150 + assert ("device times" in capsys.readouterr().out) == dump_device_time
151 + if dump_device_time:
152 + assert summary.read_text() == "device times\n"
153 +
154 +
155 +@pytest.mark.parametrize(
156 + "activities",
157 + [[], ["CPU", "CPU"], ["INVALID"]],
158 +)
159 +def test_torch_profiler_activities_reject_invalid_values(activities):
160 + with pytest.raises(ValueError, match="torch_profiler_activities"):
161 + ProfilerConfig(
162 + profiler="torch",
163 + torch_profiler_dir="/tmp/mock",
164 + torch_profiler_activities=activities,
165 + )
166 +
167 +
168 +def test_torch_profiler_activities_require_torch_profiler():
169 + with pytest.raises(ValueError, match="only applicable"):
170 + ProfilerConfig(torch_profiler_activities=["CPU"])
171 +
172 +
173 +@pytest.mark.parametrize(
174 + ("device_type", "activities"),
175 + [
176 + ("cuda", ["XPU"]),
177 + ("xpu", ["CUDA"]),
178 + ("cpu", ["CUDA"]),
179 + ("cpu", ["XPU"]),
180 + ],
181 +)
182 +def test_worker_rejects_unsupported_activities_at_startup(device_type, activities):
183 + config = ProfilerConfig(
184 + profiler="torch",
185 + torch_profiler_dir="/tmp/mock",
186 + torch_profiler_activities=activities,
187 + )
188 +
189 + with (
190 + patch.object(current_platform, "device_type", device_type),
191 + pytest.raises(ValueError, match="Unsupported torch profiler activities"),
192 + ):
193 + validate_worker_profiler_config(config)
194 +
195 +
196 +@pytest.mark.parametrize("device_type", ["cpu", "xpu"])
197 +def test_worker_rejects_cuda_profiler_on_other_devices(device_type):
198 + with (
199 + patch.object(current_platform, "device_type", device_type),
200 + pytest.raises(ValueError, match="Unsupported profiler type"),
201 + ):
202 + validate_worker_profiler_config(ProfilerConfig(profiler="cuda"))
203 +
204 +
205 +@pytest.mark.parametrize(
206 + ("device_type", "activities", "expected"),
207 + [
208 + ("cuda", None, ("CPU", "CUDA")),
209 + ("xpu", None, ("CPU", "XPU")),
210 + ("cpu", None, ("CPU",)),
211 + ("cuda", ["CUDA"], ("CUDA",)),
212 + ("xpu", ["XPU"], ("XPU",)),
213 + ("cuda", ["CPU"], ("CPU",)),
214 + ("xpu", ["CPU"], ("CPU",)),
215 + ("cpu", ["CPU"], ("CPU",)),
216 + ],
217 +)
218 +def test_worker_creates_platform_torch_profiler(device_type, activities, expected):
219 + config = ProfilerConfig(
220 + profiler="torch",
221 + torch_profiler_dir="/tmp/mock",
222 + torch_profiler_activities=activities,
223 + )
224 +
225 + with (
226 + patch.object(current_platform, "device_type", device_type),
227 + patch("vllm.profiler.wrapper.TorchProfilerWrapper") as wrapper,
228 + ):
229 + validate_worker_profiler_config(config)
230 + profiler = create_worker_profiler(
231 + config,
232 + worker_name="rank0",
233 + local_rank=0,
234 + )
235 +
236 + assert profiler is wrapper.return_value
237 + wrapper.assert_called_once_with(
238 + config,
239 + worker_name="rank0",
240 + local_rank=0,
241 + activities=expected,
242 + )
243 +
244 +
245 +@pytest.mark.parametrize("worker_type", [Worker, XPUWorker])
246 +def test_worker_reuses_torch_wrapper_across_profile_rounds(worker_type):
247 + worker = object.__new__(worker_type)
248 + worker.rank = 0
249 + worker.local_rank = 0
250 + worker.profiler = None
251 + worker.profiler_config = ProfilerConfig(
252 + profiler="torch", torch_profiler_dir="/tmp/mock"
253 + )
254 +
255 + with (
256 + patch("vllm.distributed.utils.get_worker_rank_suffix", return_value="rank0"),
257 + patch("vllm.profiler.wrapper.TorchProfilerWrapper") as wrapper,
258 + ):
259 + worker.profile()
260 + worker.profile(is_start=False)
261 + worker.profile()
262 +
263 + assert worker.profiler is wrapper.return_value
264 + wrapper.assert_called_once()
265 + assert wrapper.return_value.start.call_count == 2
266 + wrapper.return_value.stop.assert_called_once_with()
267 +
268 +
269 def test_immediate_start_stop(default_profiler_config):
270 """Test standard start without delay."""
271 profiler = ConcreteWorkerProfiler(default_profiler_config)
@@ -318,13 +533,17 @@ class TestAnnotateProfile:
533 "execute_5_context_1(sq4sk4sqsq16sqsk16)_generation_1(sq1sk11sqsq1sqsk11)"
534 )
535
321 - def test_skips_annotation_work_after_profiler_stops(self):
536 + def test_skips_annotation_work_when_profiler_does_not_annotate(self):
537 worker = MagicMock()
323 - worker.profiler.is_running = False
538 + worker.profiler.should_annotate = False
539
325 - context = Worker.annotate_profile(worker, scheduler_output=None)
540 + with patch(
541 + "vllm.v1.worker.gpu_worker.compute_iteration_details"
542 + ) as compute_iteration_details:
543 + context = Worker.annotate_profile(worker, scheduler_output=None)
544
545 worker.profiler.step.assert_called_once_with()
546 + compute_iteration_details.assert_not_called()
547 worker.profiler.annotate_context_manager.assert_not_called()
548 assert isinstance(context, nullcontext)
549
@@ -837,17 +1056,18 @@ class TestProtonProfilerWrapper:
1056
1057 @_requires_cuda_for_proton
1058 def test_gpu_worker_creates_proton_profiler():
840 - worker = MagicMock()
1059 + worker = object.__new__(Worker)
1060 worker.rank = 1
1061 + worker.local_rank = 1
1062 worker.profiler = None
843 - worker.profiler_config.profiler = "proton"
1063 + worker.profiler_config = MagicMock(profiler="proton")
1064
1065 with (
1066 patch(
1067 "vllm.distributed.utils.get_worker_rank_suffix",
1068 return_value="rank1",
1069 ),
850 - patch("vllm.v1.worker.gpu_worker.ProtonProfilerWrapper") as wrapper,
1070 + patch("vllm.profiler.wrapper.ProtonProfilerWrapper") as wrapper,
1071 ):
1072 Worker.profile(worker)
1073
@@ -857,17 +1077,18 @@ def test_gpu_worker_creates_proton_profiler():
1077
1078 @_requires_cuda_for_proton
1079 def test_gpu_worker_recreates_proton_profiler_for_each_run():
860 - worker = MagicMock()
1080 + worker = object.__new__(Worker)
1081 worker.rank = 1
1082 + worker.local_rank = 1
1083 worker.profiler = None
863 - worker.profiler_config.profiler = "proton"
1084 + worker.profiler_config = MagicMock(profiler="proton")
1085
1086 with (
1087 patch(
1088 "vllm.distributed.utils.get_worker_rank_suffix",
1089 return_value="rank1",
1090 ),
870 - patch("vllm.v1.worker.gpu_worker.ProtonProfilerWrapper") as wrapper,
1091 + patch("vllm.profiler.wrapper.ProtonProfilerWrapper") as wrapper,
1092 ):
1093 wrapper.return_value.has_cuda_graph_session = False
1094 Worker.profile(worker, profile_prefix="first")
vllm/config/profiler.py
+25 -1
@@ -14,6 +14,7 @@ from vllm.utils.hashing import safe_hash
14 logger = init_logger(__name__)
15
16 ProfilerKind = Literal["torch", "cuda", "proton"]
17 +TorchProfilerActivity = Literal["CPU", "CUDA", "PrivateUse1", "XPU"]
18 ProtonBackend = Literal["cupti"]
19 ProtonContext = Literal["shadow", "python"]
20 ProtonData = Literal["tree", "trace"]
@@ -51,6 +52,12 @@ class ProfilerConfig:
52 worker's traces (CPU & GPU) will be saved under this directory. Note that
53 it must be an absolute path."""
54
55 + torch_profiler_activities: list[TorchProfilerActivity] | None = Field(
56 + default=None, min_length=1
57 + )
58 + """Activities recorded by workers using the torch profiler. When unset,
59 + each worker uses its platform default: CPU; CPU and CUDA; or CPU and XPU."""
60 +
61 proton_profiler_dir: str = ""
62 """Directory to save Triton Proton profiles. Each worker writes a
63 separate rank-qualified file."""
@@ -170,7 +177,24 @@ class ProfilerConfig:
177 @model_validator(mode="after")
178 def _validate_profiler_config(self) -> Self:
179 has_delay_or_limit = self.delay_iterations > 0 or self.max_iterations > 0
173 - if self.profiler == "torch" and has_delay_or_limit and not self.ignore_frontend:
180 + activities = self.torch_profiler_activities
181 + if activities is not None:
182 + if self.profiler != "torch":
183 + raise ValueError(
184 + "torch_profiler_activities is only applicable when profiler "
185 + "is set to 'torch'"
186 + )
187 + if len(activities) != len(set(activities)):
188 + raise ValueError(
189 + "torch_profiler_activities must not contain duplicates"
190 + )
191 + records_cpu_activity = activities is None or "CPU" in activities
192 + if (
193 + self.profiler == "torch"
194 + and has_delay_or_limit
195 + and not self.ignore_frontend
196 + and records_cpu_activity
197 + ):
198 logger.warning_once(
199 "Using 'torch' profiler with delay_iterations or max_iterations "
200 "while ignore_frontend is False may result in high overhead."
vllm/profiler/wrapper.py
+106 -11
@@ -6,10 +6,9 @@ import inspect
6 import json
7 import os
8 from abc import ABC, abstractmethod
9 -from collections.abc import Callable, Iterator
9 +from collections.abc import Callable, Iterator, Sequence
10 from contextlib import AbstractContextManager, contextmanager, nullcontext, suppress
11 from glob import glob
12 -from typing import Literal
12 from uuid import uuid4
13
14 import torch
@@ -18,7 +17,7 @@ from typing_extensions import override
17
18 import vllm.version
19 from vllm.config import ProfilerConfig
21 -from vllm.config.profiler import _is_uri_path
20 +from vllm.config.profiler import ProfilerKind, TorchProfilerActivity, _is_uri_path
21 from vllm.logger import init_logger
22 from vllm.platforms import current_platform
23
@@ -57,6 +56,11 @@ class WorkerProfiler(ABC):
56 """Whether the underlying profiler is currently collecting data."""
57 return self._running
58
59 + @property
60 + def should_annotate(self) -> bool:
61 + """Whether worker iterations should receive profiler annotations."""
62 + return self._running
63 +
64 @abstractmethod
65 def _start(self) -> None:
66 """Start the profiler."""
@@ -176,7 +180,6 @@ class WorkerProfiler(ABC):
180 return nullcontext()
181
182
179 -TorchProfilerActivity = Literal["CPU", "CUDA", "PrivateUse1", "XPU"]
183 TorchProfilerActivityMap = {
184 "CPU": torch.profiler.ProfilerActivity.CPU,
185 "CUDA": torch.profiler.ProfilerActivity.CUDA,
@@ -191,7 +194,7 @@ class TorchProfilerWrapper(WorkerProfiler):
194 profiler_config: ProfilerConfig,
195 worker_name: str,
196 local_rank: int,
194 - activities: list[TorchProfilerActivity],
197 + activities: Sequence[TorchProfilerActivity],
198 on_trace_ready: Callable[[torch.profiler.profile], None] | None = None,
199 ) -> None:
200 super().__init__(profiler_config)
@@ -224,7 +227,12 @@ class TorchProfilerWrapper(WorkerProfiler):
227 use_gzip=profiler_config.torch_profiler_use_gzip,
228 )
229
227 - self.dump_cpu_time_total = "CPU" in activities and len(activities) == 1
230 + self._records_cpu_activity = "CPU" in activities
231 + self.dump_device_time_total = (
232 + any(activity != "CPU" for activity in activities)
233 + and profiler_config.torch_profiler_dump_cuda_time_total
234 + )
235 + self.dump_cpu_time_total = self._records_cpu_activity and len(activities) == 1
236
237 # Create profiler schedule if warmup or wait iterations are configured
238 profiler_schedule = None
@@ -244,7 +252,7 @@ class TorchProfilerWrapper(WorkerProfiler):
252 profiler_config.active_iterations,
253 )
254
247 - self.profiler = torch.profiler.profile(
255 + self._profiler_kwargs = dict(
256 activities=[TorchProfilerActivityMap[activity] for activity in activities],
257 schedule=profiler_schedule,
258 record_shapes=profiler_config.torch_profiler_record_shapes,
@@ -253,6 +261,7 @@ class TorchProfilerWrapper(WorkerProfiler):
261 with_flops=profiler_config.torch_profiler_with_flops,
262 on_trace_ready=trace_handler,
263 )
264 + self.profiler: torch.profiler.profile
265
266 # Track if we're using a schedule (need to call step())
267 self._uses_schedule = profiler_schedule is not None
@@ -260,10 +269,11 @@ class TorchProfilerWrapper(WorkerProfiler):
269 # Subtract 1 because profiler.start() already consumes step 0
270 # (WAIT or WARMUP), so only wait + warmup - 1 non-active steps
271 # remain to be advanced through via profiler.step() calls.
263 - self._warmup_steps_remaining = max(
272 + self._initial_warmup_steps_remaining = max(
273 profiler_config.wait_iterations + profiler_config.warmup_iterations - 1,
274 0,
275 )
276 + self._warmup_steps_remaining = self._initial_warmup_steps_remaining
277 self._version_metadata_added = False
278
279 def _build_profiler_table(
@@ -319,6 +329,9 @@ class TorchProfilerWrapper(WorkerProfiler):
329
330 @override
331 def _start(self) -> None:
332 + self.profiler = torch.profiler.profile(**self._profiler_kwargs)
333 + self._warmup_steps_remaining = self._initial_warmup_steps_remaining
334 + self._version_metadata_added = False
335 self.profiler.start()
336 # No-schedule case: Kineto is live immediately. With a schedule this
337 # no-ops and _profiler_step stamps it once WAIT ends.
@@ -328,10 +341,9 @@ class TorchProfilerWrapper(WorkerProfiler):
341 def _stop(self) -> None:
342 self.profiler.stop()
343
331 - profiler_config = self.profiler_config
344 rank = self.local_rank
333 - if profiler_config.torch_profiler_dump_cuda_time_total:
334 - table = self._build_profiler_table(sort_key="self_cuda_time_total")
345 + if self.dump_device_time_total:
346 + table = self._build_profiler_table(sort_key="self_device_time_total")
347 self._write_profiler_table(rank, table)
348
349 # only print profiler results on rank 0
@@ -367,8 +379,15 @@ class TorchProfilerWrapper(WorkerProfiler):
379 return False
380 return True
381
382 + @property
383 + @override
384 + def should_annotate(self) -> bool:
385 + return self._running and self._records_cpu_activity
386 +
387 @override
388 def annotate_context_manager(self, name: str):
389 + if not self.should_annotate:
390 + return nullcontext()
391 return torch.profiler.record_function(name)
392
393
@@ -606,6 +625,82 @@ class CudaProfilerWrapper(WorkerProfiler):
625 return torch.cuda.nvtx.range(name)
626
627
628 +_DEFAULT_TORCH_PROFILER_ACTIVITIES: dict[str, tuple[TorchProfilerActivity, ...]] = {
629 + "cpu": ("CPU",),
630 + "cuda": ("CPU", "CUDA"),
631 + "xpu": ("CPU", "XPU"),
632 +}
633 +_SUPPORTED_TORCH_PROFILER_ACTIVITIES = {
634 + device: frozenset(activities)
635 + for device, activities in _DEFAULT_TORCH_PROFILER_ACTIVITIES.items()
636 +}
637 +_SUPPORTED_PROFILER_KINDS: dict[str, frozenset[ProfilerKind]] = {
638 + "cpu": frozenset(("torch",)),
639 + "cuda": frozenset(("torch", "cuda", "proton")),
640 + "xpu": frozenset(("torch",)),
641 +}
642 +
643 +
644 +def validate_worker_profiler_config(profiler_config: ProfilerConfig) -> None:
645 + """Validate profiler selections against the worker's capabilities."""
646 + profiler_type = profiler_config.profiler
647 + if profiler_type is None:
648 + return
649 + device_type = current_platform.device_type
650 + if device_type not in _SUPPORTED_PROFILER_KINDS:
651 + raise ValueError(f"Unsupported profiler device type: {device_type}")
652 + supported_kinds = _SUPPORTED_PROFILER_KINDS[device_type]
653 + if profiler_type not in supported_kinds:
654 + supported_names = ", ".join(sorted(supported_kinds))
655 + raise ValueError(
656 + f"Unsupported profiler type for {device_type}: "
657 + f"{profiler_type}. Supported profiler types: {supported_names}."
658 + )
659 + if profiler_type == "torch":
660 + default_activities = _DEFAULT_TORCH_PROFILER_ACTIVITIES[device_type]
661 + supported_activities = _SUPPORTED_TORCH_PROFILER_ACTIVITIES[device_type]
662 + configured = profiler_config.torch_profiler_activities
663 + activities = default_activities if configured is None else configured
664 + unsupported = set(activities) - supported_activities
665 + if unsupported:
666 + unsupported_names = ", ".join(sorted(unsupported))
667 + supported_names = ", ".join(sorted(supported_activities))
668 + raise ValueError(
669 + f"Unsupported torch profiler activities for "
670 + f"{device_type}: {unsupported_names}. "
671 + f"Supported activities: {supported_names}."
672 + )
673 +
674 +
675 +def create_worker_profiler(
676 + profiler_config: ProfilerConfig,
677 + *,
678 + worker_name: str,
679 + local_rank: int,
680 +) -> WorkerProfiler:
681 + """Create a profiler using a validated config and platform defaults."""
682 + profiler_type = profiler_config.profiler
683 + if profiler_type == "torch":
684 + default_activities = _DEFAULT_TORCH_PROFILER_ACTIVITIES[
685 + current_platform.device_type
686 + ]
687 + configured = profiler_config.torch_profiler_activities
688 + logger.debug("Starting torch profiler with trace name: %s", worker_name)
689 + return TorchProfilerWrapper(
690 + profiler_config,
691 + worker_name=worker_name,
692 + local_rank=local_rank,
693 + activities=default_activities if configured is None else tuple(configured),
694 + )
695 + if profiler_type == "cuda":
696 + logger.debug("Starting CUDA profiler")
697 + return CudaProfilerWrapper(profiler_config)
698 +
699 + assert profiler_type == "proton", f"Unknown profiler type: {profiler_type}"
700 + logger.debug("Starting Proton profiler with trace name: %s", worker_name)
701 + return ProtonProfilerWrapper(profiler_config, worker_name=worker_name)
702 +
703 +
704 def create_graph_capture_profiler(
705 profiler_config: ProfilerConfig, global_rank: int
706 ) -> WorkerProfiler | None:
vllm/v1/engine/async_llm.py
+2
@@ -208,9 +208,11 @@ class AsyncLLM(EngineClient):
208 pass
209
210 self.profiler = profiler
211 + configured_activities = vllm_config.profiler_config.torch_profiler_activities
212 if (
213 vllm_config.profiler_config.profiler == "torch"
214 and not vllm_config.profiler_config.ignore_frontend
215 + and (configured_activities is None or "CPU" in configured_activities)
216 ):
217 profiler_dir = vllm_config.profiler_config.torch_profiler_dir
218 logger.info(
vllm/v1/worker/cpu_worker.py
-22
@@ -7,7 +7,6 @@ import vllm.v1.worker.cpu.shm # noqa # isort: skip
7 import math
8 import os
9 import sys
10 -from typing import Any
10
11 import psutil
12 import torch
@@ -16,7 +15,6 @@ from vllm import envs
15 from vllm.config import CompilationMode, VllmConfig
16 from vllm.logger import init_logger
17 from vllm.platforms import CpuArchEnum, current_platform
19 -from vllm.profiler.wrapper import TorchProfilerWrapper
18 from vllm.utils.cpu_resource_utils import (
19 get_allowed_cpu_list,
20 get_memory_node_info,
@@ -106,18 +104,6 @@ class CPUWorker(Worker):
104
105 self.parallel_config.disable_custom_all_reduce = True
106
109 - # Torch profiler. Enabled and configured through profiler_config.
110 - self.profiler: Any | None = None
111 - profiler_config = vllm_config.profiler_config
112 - if profiler_config.profiler == "torch":
113 - worker_name = f"{vllm_config.instance_id}-rank-{self.rank}"
114 - self.profiler = TorchProfilerWrapper(
115 - profiler_config,
116 - worker_name=worker_name,
117 - local_rank=self.local_rank,
118 - activities=["CPU"],
119 - )
120 -
107 def init_device(self):
108 self.device = torch.device("cpu")
109
@@ -287,11 +273,3 @@ class CPUWorker(Worker):
273 language_model=self.compilation_config.compilation_time,
274 encoder=self.compilation_config.encoder_compilation_time,
275 )
290 -
291 - def profile(self, is_start: bool = True, profile_prefix: str | None = None):
292 - if self.profiler is None:
293 - raise RuntimeError("Profiler is not enabled.")
294 - if is_start:
295 - self.profiler.start()
296 - else:
297 - self.profiler.stop()
vllm/v1/worker/gpu_worker.py
+10 -31
@@ -61,10 +61,9 @@ from vllm.model_executor.warmup.kernel_warmup import kernel_warmup
61 from vllm.multimodal.gpu_ipc_memory import reserve_mm_ipc_gpu_memory
62 from vllm.platforms import current_platform
63 from vllm.profiler.wrapper import (
64 - CudaProfilerWrapper,
65 - ProtonProfilerWrapper,
66 - TorchProfilerWrapper,
64 create_graph_capture_profiler,
65 + create_worker_profiler,
66 + validate_worker_profiler_config,
67 )
68 from vllm.sequence import IntermediateTensors
69 from vllm.tasks import SupportedTask
@@ -222,6 +221,7 @@ class Worker(WorkerBase):
221 # so we have all the information needed for proper trace naming.
222 self.profiler: Any | None = None
223 self.profiler_config = vllm_config.profiler_config
224 + validate_worker_profiler_config(self.profiler_config)
225
226 self.use_v2_model_runner = vllm_config.use_v2_model_runner
227
@@ -1038,7 +1038,7 @@ class Worker(WorkerBase):
1038 return nullcontext()
1039
1040 self.profiler.step()
1041 - if not self.profiler.is_running:
1041 + if not self.profiler.should_annotate:
1042 return nullcontext()
1043
1044 iteration_details = compute_iteration_details(scheduler_output)
@@ -1262,7 +1262,6 @@ class Worker(WorkerBase):
1262 )
1263
1264 if is_start:
1265 - profiler_type = self.profiler_config.profiler
1265 # Generate the trace name by combining prefix with comprehensive rank suffix
1266 from vllm.distributed.utils import get_worker_rank_suffix
1267
@@ -1274,36 +1273,16 @@ class Worker(WorkerBase):
1273 else:
1274 trace_name = rank_suffix
1275
1277 - if profiler_type == "proton" and self.profiler is not None:
1276 + if self.profiler_config.profiler == "proton" and self.profiler is not None:
1277 self.profiler.set_output_name(trace_name)
1278
1279 # Create the profiler wrapper only on the first start call
1280 if self.profiler is None:
1282 - if profiler_type == "torch":
1283 - self.profiler = TorchProfilerWrapper(
1284 - self.profiler_config,
1285 - worker_name=trace_name,
1286 - local_rank=self.local_rank,
1287 - activities=["CPU", "CUDA"],
1288 - )
1289 - logger.debug(
1290 - "Starting torch profiler with trace name: %s", trace_name
1291 - )
1292 - elif profiler_type == "cuda":
1293 - self.profiler = CudaProfilerWrapper(self.profiler_config)
1294 - logger.debug("Starting CUDA profiler")
1295 - elif profiler_type == "proton":
1296 - self.profiler = ProtonProfilerWrapper(
1297 - self.profiler_config, worker_name=trace_name
1298 - )
1299 - logger.debug(
1300 - "Starting Proton profiler with trace name: %s", trace_name
1301 - )
1302 - else:
1303 - # Config validation should prevent this code being reached
1304 - raise ValueError(
1305 - f"Invalid profiler value of {self.profiler_config.profiler}"
1306 - )
1281 + self.profiler = create_worker_profiler(
1282 + self.profiler_config,
1283 + worker_name=trace_name,
1284 + local_rank=self.local_rank,
1285 + )
1286
1287 self.profiler.start()
1288 else:
vllm/v1/worker/xpu_worker.py
-28
@@ -8,7 +8,6 @@ import torch
8 from vllm.config import VllmConfig
9 from vllm.logger import init_logger
10 from vllm.platforms import current_platform
11 -from vllm.profiler.wrapper import TorchProfilerWrapper
11 from vllm.utils.mem_utils import MemorySnapshot, format_gib
12 from vllm.utils.torch_utils import set_random_seed
13 from vllm.v1.utils import report_usage_stats
@@ -225,33 +224,6 @@ class XPUWorker(Worker):
224 # If usage stat is enabled, collect relevant info.
225 report_usage_stats(self.vllm_config)
226
228 - def profile(self, is_start: bool = True, profile_prefix: str | None = None):
229 - if self.profiler_config is None or self.profiler_config.profiler is None:
230 - raise RuntimeError(
231 - "Profiling is not enabled. Please set --profiler-config to enable "
232 - "profiling. Example: "
233 - "'--profiler-config.profiler=torch --profiler-config.torch_profiler_dir"
234 - "=YOUR_DIR_PATH_TO_DUMP_TRACE'"
235 - )
236 -
237 - if is_start and self.profiler is None:
238 - from vllm.distributed.utils import get_worker_rank_suffix
239 -
240 - rank_suffix = get_worker_rank_suffix(global_rank=self.rank)
241 - trace_name = (
242 - f"{profile_prefix}_{rank_suffix}" if profile_prefix else rank_suffix
243 - )
244 -
245 - self.profiler = TorchProfilerWrapper(
246 - self.profiler_config,
247 - worker_name=trace_name,
248 - local_rank=self.local_rank,
249 - activities=["CPU", "XPU"],
250 - )
251 - logger.debug("Starting torch profiler with trace name: %s", trace_name)
252 -
253 - super().profile(is_start=is_start, profile_prefix=profile_prefix)
254 -
227 def shutdown(self) -> None:
228 logger.info(
229 "XPUWorker shutdown: cleaning up (rank=%d, local_rank=%d)",