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 2, 06:00 UTC
[MM][CG] Profile encoder CUDA graph pool memory (#41714)
Signed-off-by: JooHo Lee <jooho414@gmail.com>
JooHo Lee committed
Jun 2, 2026 at 13:27 UTC
a045c7425f39997f4a319d9e671c9d00590e321a
4 files changed
+173
-92
tests/v1/cudagraph/test_encoder_cudagraph.py
+18
-2
@@ -109,6 +109,7 @@ def _make_manager_with_budgets(budgets: list[int]) -> EncoderCudaGraphManager:
109
mgr.max_batch_size = 16
110
mgr.use_dp = False
111
mgr.budget_graphs = {}
112
+ mgr.graph_pool = None
113
mgr.graph_hits = 0
114
mgr.graph_misses = 0
115
mgr.log_stats_interval = 100
@@ -180,6 +181,10 @@ class TestFindBudgetGraph:
181
# Budget selection still works correctly after sorting
182
assert mgr._find_smallest_fitting_budget_given_tokens(3000) == 4096
183
184
+ def test_num_graphs_to_capture_tracks_budgets(self):
185
+ mgr = _make_manager_with_budgets([8192, 2048, 4096])
186
+ assert mgr.get_num_graphs_to_capture() == 3
187
+
188
189
# ---------------------------------------------------------------------------
190
# get_cumulative_stats
@@ -409,6 +414,7 @@ def _make_manager_for_gpu(
414
)
415
mgr.use_dp = False
416
mgr.budget_graphs = {}
417
+ mgr.graph_pool = None
418
mgr.graph_hits = 0
419
mgr.graph_misses = 0
420
mgr.log_stats_interval = 100
@@ -467,7 +473,8 @@ class TestEncoderCudaGraphCaptureReplay:
473
self.mgr = _make_manager_for_gpu(
474
self.model, _BUDGETS, _MAX_BATCH, self.device, self.dtype
475
)
470
- self.mgr.capture()
476
+ self.graph_pool = current_platform.graph_pool_handle()
477
+ self.mgr.capture(graph_pool=self.graph_pool)
478
479
# --- capture ---
480
@@ -475,6 +482,14 @@ class TestEncoderCudaGraphCaptureReplay:
482
assert len(self.mgr.budget_graphs) == len(_BUDGETS)
483
assert set(self.mgr.budget_graphs.keys()) == set(_BUDGETS)
484
485
+ def test_capture_uses_supplied_graph_pool(self):
486
+ assert self.mgr.graph_pool is self.graph_pool
487
+
488
+ def test_clear_releases_graphs_and_pool(self):
489
+ self.mgr.clear()
490
+ assert self.mgr.budget_graphs == {}
491
+ assert self.mgr.graph_pool is None
492
+
493
# --- output shape ---
494
495
def test_execute_returns_one_tensor_per_image(self):
@@ -742,7 +757,8 @@ class TestEncoderCudaGraphVideoReplay:
757
self.dtype,
758
max_frames_per_batch=_VIDEO_MAX_FRAMES,
759
)
745
- self.mgr.capture()
760
+ self.graph_pool = current_platform.graph_pool_handle()
761
+ self.mgr.capture(graph_pool=self.graph_pool)
762
763
# --- capture ---
764
vllm/utils/import_utils.py
+1
-1
@@ -404,7 +404,7 @@ def _has_module(module_name: str) -> bool:
404
if importlib.util.find_spec(module_name) is None:
405
return False
406
importlib.import_module(module_name)
407
- except ImportError:
407
+ except Exception:
408
logger.warning(
409
"Module %s was found but failed to import", module_name, exc_info=True
410
)
vllm/v1/worker/encoder_cudagraph.py
+14
-3
@@ -153,6 +153,7 @@ class EncoderCudaGraphManager:
153
)
154
155
self.budget_graphs: dict[int, BudgetGraphMetadata] = {}
156
+ self.graph_pool: Any | None = None
157
self.graph_hits = 0
158
self.graph_misses = 0
159
self.log_stats_interval = 100
@@ -183,9 +184,16 @@ class EncoderCudaGraphManager:
184
"""Check if a modality is supported by this manager."""
185
return modality in self.config.modalities
186
186
- def capture(self):
187
+ def clear(self) -> None:
188
+ """Release captured encoder CUDA graphs and the manager-local pool."""
189
+ self.budget_graphs.clear()
190
+ self.graph_pool = None
191
+
192
+ def capture(self, graph_pool: Any):
193
"""Capture CUDA graphs for all token budgets."""
188
- for token_budget in self.token_budgets:
194
+ self.graph_pool = graph_pool
195
+
196
+ for token_budget in sorted(self.token_budgets, reverse=True):
197
self._capture_budget_graph(token_budget)
198
199
logger.info(
@@ -193,6 +201,9 @@ class EncoderCudaGraphManager:
201
len(self.budget_graphs),
202
)
203
204
+ def get_num_graphs_to_capture(self) -> int:
205
+ return len(self.token_budgets)
206
+
207
def _capture_budget_graph(self, token_budget: int):
208
"""Capture CUDA graph for a single token budget."""
209
logger.debug(
@@ -218,7 +229,7 @@ class EncoderCudaGraphManager:
229
output_buffer = torch.empty_like(output)
230
231
graph = torch.cuda.CUDAGraph()
221
- with torch.inference_mode(), torch.cuda.graph(graph):
232
+ with torch.inference_mode(), torch.cuda.graph(graph, pool=self.graph_pool):
233
output = self.model.encoder_cudagraph_forward({**values})
234
output_buffer.copy_(output)
235
vllm/v1/worker/gpu_model_runner.py
+140
-86
@@ -6304,6 +6304,42 @@ class GPUModelRunner(
6304
6305
logger.debug("Cleaned up profiling KV cache and CUDA graphs")
6306
6307
+ @torch.inference_mode()
6308
+ def _create_encoder_cudagraph_manager(self) -> "EncoderCudaGraphManager | None":
6309
+ if not (
6310
+ self.compilation_config.cudagraph_mm_encoder and self.supports_mm_inputs
6311
+ ):
6312
+ return None
6313
+
6314
+ # Use get_model() to unwrap CUDAGraphWrapper/UBatchWrapper, because
6315
+ # @runtime_checkable Protocol isinstance() checks do not work through
6316
+ # __getattr__ forwarding.
6317
+ from vllm.model_executor.models.interfaces import (
6318
+ SupportsEncoderCudaGraph,
6319
+ supports_encoder_cudagraph,
6320
+ )
6321
+ from vllm.v1.worker.encoder_cudagraph import (
6322
+ EncoderCudaGraphManager,
6323
+ )
6324
+
6325
+ raw_model = self.get_model()
6326
+ if not supports_encoder_cudagraph(raw_model):
6327
+ return None
6328
+
6329
+ return EncoderCudaGraphManager(
6330
+ vllm_config=self.vllm_config,
6331
+ device=self.device,
6332
+ dtype=self.dtype,
6333
+ model=cast(SupportsEncoderCudaGraph, raw_model),
6334
+ )
6335
+
6336
+ @torch.inference_mode()
6337
+ def _maybe_init_encoder_cudagraph_manager(self) -> None:
6338
+ if self.encoder_cudagraph_manager is None:
6339
+ self.encoder_cudagraph_manager = self._create_encoder_cudagraph_manager()
6340
+ if self.encoder_cudagraph_manager is not None:
6341
+ logger.info("Initialized EncoderCudaGraphManager for vision encoder")
6342
+
6343
@torch.inference_mode()
6344
def profile_cudagraph_memory(self) -> int:
6345
with set_current_vllm_config(self.vllm_config):
@@ -6312,24 +6348,40 @@ class GPUModelRunner(
6348
saved_num_cudagraph_captured = compilation_counter.num_cudagraph_captured
6349
6350
capture_descs = self.cudagraph_dispatcher.get_capture_descs()
6351
+ # Use a temporary manager for memory profiling. The persistent manager
6352
+ # is initialized later so it does not keep profiling-only graph state.
6353
+ encoder_cudagraph_manager = self._create_encoder_cudagraph_manager()
6354
6316
- total_graphs = sum(len(descs) for _, descs in capture_descs)
6355
+ decoder_graphs = sum(len(descs) for _, descs in capture_descs)
6356
+ encoder_graphs = (
6357
+ encoder_cudagraph_manager.get_num_graphs_to_capture()
6358
+ if encoder_cudagraph_manager is not None
6359
+ else 0
6360
+ )
6361
+ total_graphs = decoder_graphs + encoder_graphs
6362
if total_graphs == 0:
6363
logger.debug("No CUDA graphs will be captured, skipping profiling")
6364
self._cleanup_profiling_kv_cache()
6365
return 0
6366
6322
- logger.info(
6323
- "Profiling CUDA graph memory: %s",
6324
- ", ".join(
6367
+ graph_groups = [
6368
+ *(
6369
f"{mode.name}={len(descs)} (largest={descs[0].num_tokens})"
6370
for mode, descs in capture_descs
6371
if descs
6372
),
6329
- )
6373
+ ]
6374
+ if encoder_graphs > 0:
6375
+ graph_groups.append(
6376
+ f"ENCODER={encoder_graphs} "
6377
+ f"(largest={encoder_cudagraph_manager.token_budgets[-1]})"
6378
+ )
6379
+
6380
+ logger.info("Profiling CUDA graph memory: %s", ", ".join(graph_groups))
6381
6382
# Use a temporary pool for profiling to avoid fragmentation in the main pool.
6383
profiling_pool = current_platform.graph_pool_handle()
6384
+ encoder_profiling_pool = current_platform.graph_pool_handle()
6385
original_pools: dict[int, Any] = {}
6386
all_wrappers = list(CUDAGraphWrapper._all_instances) + list(
6387
BreakableCUDAGraphWrapper._all_instances
@@ -6338,73 +6390,98 @@ class GPUModelRunner(
6390
original_pools[id(instance)] = instance.graph_pool
6391
instance.graph_pool = profiling_pool
6392
6341
- set_cudagraph_capturing_enabled(True)
6342
- with self._freeze_gc(), graph_capture(device=self.device):
6343
- shared_memory_estimate = {}
6344
- per_graph_estimate = {}
6345
- torch.accelerator.synchronize()
6346
- torch.accelerator.empty_cache()
6393
+ shared_memory_estimate = {}
6394
+ per_graph_estimate = {}
6395
+ encoder_memory_estimate = 0
6396
6348
- for mode, descs in capture_descs:
6349
- profile_descs = descs[:2]
6350
- mem_samples: list[int] = []
6397
+ # Cleanup-only guard: CUDA graph capture errors should still propagate
6398
+ # because encoder graph capture is opt-in.
6399
+ try:
6400
+ set_cudagraph_capturing_enabled(True)
6401
+ with self._freeze_gc(), graph_capture(device=self.device):
6402
+ torch.accelerator.synchronize()
6403
+ torch.accelerator.empty_cache()
6404
+
6405
+ for mode, descs in capture_descs:
6406
+ profile_descs = descs[:2]
6407
+ mem_samples: list[int] = []
6408
+
6409
+ for i, desc in enumerate(profile_descs):
6410
+ mem_before = torch.cuda.mem_get_info()[0]
6411
+ self._warmup_and_capture(
6412
+ desc,
6413
+ cudagraph_runtime_mode=mode,
6414
+ profile_seq_lens=(
6415
+ min(
6416
+ self.max_model_len,
6417
+ self.max_num_tokens // desc.num_tokens,
6418
+ )
6419
+ if mode == CUDAGraphMode.FULL and i == 0
6420
+ else None
6421
+ ),
6422
+ )
6423
+ torch.accelerator.synchronize()
6424
+ free_after = torch.cuda.mem_get_info()[0]
6425
+ mem_samples.append(mem_before - free_after)
6426
+
6427
+ first_capture = mem_samples[0]
6428
+ # Use at least 1 MiB per graph for driver overhead
6429
+ per_graph = max(
6430
+ mem_samples[1] if len(mem_samples) > 1 else 0, 1 << 20
6431
+ )
6432
6352
- for i, desc in enumerate(profile_descs):
6353
- mem_before = torch.cuda.mem_get_info()[0]
6354
- self._warmup_and_capture(
6355
- desc,
6356
- cudagraph_runtime_mode=mode,
6357
- profile_seq_lens=(
6358
- min(
6359
- self.max_model_len,
6360
- self.max_num_tokens // desc.num_tokens,
6361
- )
6362
- if mode == CUDAGraphMode.FULL and i == 0
6363
- else None
6364
- ),
6433
+ shared_memory_estimate[mode] = first_capture
6434
+ per_graph_estimate[mode] = per_graph * (len(descs) - 1)
6435
+
6436
+ logger.debug(
6437
+ "Estimated %s CUDA graph memory: "
6438
+ "%.2f MiB first-capture + (%d-1) × %.2f MiB per-graph",
6439
+ mode.name,
6440
+ first_capture / (1 << 20),
6441
+ len(descs),
6442
+ per_graph / (1 << 20),
6443
)
6444
+
6445
+ if encoder_cudagraph_manager is not None:
6446
+ mem_before = torch.cuda.mem_get_info()[0]
6447
+ encoder_cudagraph_manager.capture(graph_pool=encoder_profiling_pool)
6448
torch.accelerator.synchronize()
6449
free_after = torch.cuda.mem_get_info()[0]
6368
- mem_samples.append(mem_before - free_after)
6369
-
6370
- first_capture = mem_samples[0]
6371
- # Use at least 1 MiB per graph for driver overhead
6372
- per_graph = max(mem_samples[1] if len(mem_samples) > 1 else 0, 1 << 20)
6373
-
6374
- shared_memory_estimate[mode] = first_capture
6375
- per_graph_estimate[mode] = per_graph * (len(descs) - 1)
6376
-
6377
- logger.debug(
6378
- "Estimated %s CUDA graph memory: "
6379
- "%.2f MiB first-capture + (%d-1) × %.2f MiB per-graph",
6380
- mode.name,
6381
- first_capture / (1 << 20),
6382
- len(descs),
6383
- per_graph / (1 << 20),
6384
- )
6450
+ encoder_memory_estimate = max(mem_before - free_after, 0)
6451
6386
- set_cudagraph_capturing_enabled(False)
6387
- CUDAGraphWrapper.clear_all_graphs()
6388
- BreakableCUDAGraphWrapper.clear_all_graphs()
6389
- all_wrappers = list(CUDAGraphWrapper._all_instances) + list(
6390
- BreakableCUDAGraphWrapper._all_instances
6391
- )
6392
- for instance in all_wrappers:
6393
- if id(instance) in original_pools:
6394
- instance.graph_pool = original_pools[id(instance)]
6395
- for key_set in self.cudagraph_dispatcher.cudagraph_keys.values():
6396
- key_set.clear()
6397
- self.cudagraph_dispatcher.keys_initialized = False
6398
- self.maybe_remove_all_loras(self.lora_config)
6399
- self._cleanup_profiling_kv_cache()
6400
- compilation_counter.num_cudagraph_captured = saved_num_cudagraph_captured
6452
+ logger.debug(
6453
+ "Estimated encoder CUDA graph memory: %.2f MiB for %d graphs",
6454
+ encoder_memory_estimate / (1 << 20),
6455
+ encoder_graphs,
6456
+ )
6457
+ finally:
6458
+ set_cudagraph_capturing_enabled(False)
6459
+ CUDAGraphWrapper.clear_all_graphs()
6460
+ BreakableCUDAGraphWrapper.clear_all_graphs()
6461
+ if encoder_cudagraph_manager is not None:
6462
+ encoder_cudagraph_manager.clear()
6463
+ all_wrappers = list(CUDAGraphWrapper._all_instances) + list(
6464
+ BreakableCUDAGraphWrapper._all_instances
6465
+ )
6466
+ for instance in all_wrappers:
6467
+ if id(instance) in original_pools:
6468
+ instance.graph_pool = original_pools[id(instance)]
6469
+ for key_set in self.cudagraph_dispatcher.cudagraph_keys.values():
6470
+ key_set.clear()
6471
+ self.cudagraph_dispatcher.keys_initialized = False
6472
+ self.maybe_remove_all_loras(self.lora_config)
6473
+ self._cleanup_profiling_kv_cache()
6474
+ compilation_counter.num_cudagraph_captured = saved_num_cudagraph_captured
6475
6476
# FULL and PIECEWISE graphs share the global pool at runtime and are
6477
# never replayed concurrently, so the pool overlays their memory.
6478
# Take the max to avoid double-counting the overlap.
6405
- total_estimate = max(shared_memory_estimate.values()) + sum(
6479
+ decoder_estimate = max(shared_memory_estimate.values(), default=0) + sum(
6480
per_graph_estimate.values()
6481
)
6482
+ # Encoder graphs use a manager-local pool at runtime, separate from the
6483
+ # decoder pool, so add their estimate instead of overlaying it.
6484
+ total_estimate = decoder_estimate + encoder_memory_estimate
6485
logger.info(
6486
"Estimated CUDA graph memory: %.2f GiB total",
6487
total_estimate / (1 << 30),
@@ -6422,31 +6499,7 @@ class GPUModelRunner(
6499
return 0
6500
6501
# Initialize encoder CUDA graph manager if enabled.
6425
- # Use get_model() to unwrap CUDAGraphWrapper/UBatchWrapper,
6426
- # because @runtime_checkable Protocol isinstance() checks do not
6427
- # work through __getattr__ forwarding.
6428
- if (
6429
- self.compilation_config.cudagraph_mm_encoder
6430
- and self.supports_mm_inputs
6431
- and self.encoder_cudagraph_manager is None
6432
- ):
6433
- from vllm.model_executor.models.interfaces import (
6434
- SupportsEncoderCudaGraph,
6435
- supports_encoder_cudagraph,
6436
- )
6437
- from vllm.v1.worker.encoder_cudagraph import (
6438
- EncoderCudaGraphManager,
6439
- )
6440
-
6441
- raw_model = self.get_model()
6442
- if supports_encoder_cudagraph(raw_model):
6443
- self.encoder_cudagraph_manager = EncoderCudaGraphManager(
6444
- vllm_config=self.vllm_config,
6445
- device=self.device,
6446
- dtype=self.dtype,
6447
- model=cast(SupportsEncoderCudaGraph, raw_model),
6448
- )
6449
- logger.info("Initialized EncoderCudaGraphManager for vision encoder")
6502
+ self._maybe_init_encoder_cudagraph_manager()
6503
6504
compilation_counter.num_gpu_runner_capture_triggers += 1
6505
@@ -6473,7 +6526,8 @@ class GPUModelRunner(
6526
6527
# Capture encoder CUDA graphs if enabled
6528
if self.encoder_cudagraph_manager is not None:
6476
- self.encoder_cudagraph_manager.capture()
6529
+ encoder_graph_pool = current_platform.graph_pool_handle()
6530
+ self.encoder_cudagraph_manager.capture(graph_pool=encoder_graph_pool)
6531
6532
torch.accelerator.synchronize()
6533
end_free_gpu_memory = torch.cuda.mem_get_info()[0]