CUDA graph: opt-in per-buffer + per-shape decode CG memory logging

Attribute decode CUDA-graph GPU memory at capture time, gated by
SGLANG_LOG_CG_BUFFERS=1 (off by default, startup-only, rank0):
  1. Per-runner static input/output buffer table (name/shape/dtype/MB)
     across the 3 EAGLE families (target-verify, draft, draft-extend),
     flagging buffers that share_buffers() aliased onto an earlier family.
  2. Per-shape graph-pool growth deltas in the capture loop + a per-family
     TOTAL, isolating the shared activation pool from per-shape outputs.
Also fix --enable-profile-cuda-graph dumping all families to one
overwritten cuda_graph_runner_memory_usage.pickle: now writes a unique
cuda_graph_mem_<family>.pickle per runner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 16:28:15 +00:00
parent 144ae7acb1
commit 0da83747cf
3 changed files with 111 additions and 5 deletions

View File

@@ -21,7 +21,7 @@ import inspect
import logging
import os
from contextlib import contextmanager
from dataclasses import dataclass
from dataclasses import dataclass, fields, is_dataclass
from functools import partial
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union
@@ -95,6 +95,13 @@ if TYPE_CHECKING:
_has_foreach_copy = hasattr(torch, "_foreach_copy_")
# Opt-in (SGLANG_LOG_CG_BUFFERS=1): dump the per-buffer CUDA-graph memory
# breakdown at capture time. Startup-only, rank0; off by default.
_LOG_CG_BUFFERS = get_bool_env_var("SGLANG_LOG_CG_BUFFERS")
# Process-wide data_ptr -> first runner label, so the dump can flag buffers that
# share_buffers() coalesced into one physical allocation across the EAGLE families.
_cg_buffer_owner: Dict[int, str] = {}
def _grouped_foreach_copy_(dsts: List[torch.Tensor], srcs: List[torch.Tensor]) -> None:
"""Call torch._foreach_copy_ grouped by (dst_dtype, src_dtype) pairs."""
@@ -501,6 +508,57 @@ def set_global_graph_memory_pool(val):
global_graph_memory_pool = val
def _named_buffer_tensors(buffers) -> List[Tuple[str, torch.Tensor]]:
"""Flatten a *InputBuffers dataclass into (name, tensor) pairs, mirroring
share_buffers()'s traversal (recurses into dict / dataclass fields)."""
out: List[Tuple[str, torch.Tensor]] = []
for f in fields(buffers):
b = getattr(buffers, f.name)
if b is None:
continue
if is_dataclass(b):
b = vars(b)
if isinstance(b, dict):
for k, v in b.items():
if torch.is_tensor(v):
out.append((f"{f.name}.{k}", v))
elif torch.is_tensor(b):
out.append((f.name, b))
return out
def log_input_buffer_sizes(label: str, buffers) -> None:
"""One-time, rank0 dump of one runner's static CUDA-graph input/output
buffers: per-field shape/dtype/MB, flagging buffers that share_buffers()
aliased onto an earlier runner's allocation. Gated by SGLANG_LOG_CG_BUFFERS."""
if not _LOG_CG_BUFFERS:
return
rows = _named_buffer_tensors(buffers)
rows.sort(key=lambda nt: -nt[1].numel() * nt[1].element_size())
unique_mb = 0.0
total_mb = 0.0
lines = []
for name, t in rows:
mb = t.numel() * t.element_size() / 2**20
total_mb += mb
owner = _cg_buffer_owner.get(t.data_ptr())
if owner is None:
_cg_buffer_owner[t.data_ptr()] = label
unique_mb += mb
tag = ""
else:
tag = f" <alias of {owner}>"
lines.append(
f" {name:<40s} {str(tuple(t.shape)):<22s} "
f"{str(t.dtype).replace('torch.', ''):<10s} {mb:9.2f} MB{tag}"
)
log_info_on_rank0(
logger,
f"[CG buffers] {label}: static input/output buffers "
f"(new={unique_mb:.2f} MB, incl-aliased={total_mb:.2f} MB)\n" + "\n".join(lines),
)
class CudaGraphRunner:
"""A CudaGraphRunner runs the forward pass of a model with cuda graph and torch.compile."""
@@ -629,6 +687,7 @@ class CudaGraphRunner:
),
)
self.buffers.share_buffers()
log_input_buffer_sizes(str(self.capture_forward_mode), self.buffers)
self.tbo_plugin = TboCudaGraphRunnerPlugin()
@@ -732,8 +791,18 @@ class CudaGraphRunner:
torch.cuda.memory._record_memory_history()
return profile_context
def _cuda_graph_mem_snapshot_path(self):
# Per-runner filename so the 3 EAGLE families don't overwrite one file.
label = str(
getattr(self, "capture_forward_mode", None) or type(self).__name__
)
for ch in ".:/ ":
label = label.replace(ch, "_")
return f"cuda_graph_mem_{label}.pickle"
def _post_process_after_profile(self, prof_context):
torch.cuda.memory._dump_snapshot(f"cuda_graph_runner_memory_usage.pickle")
snapshot_path = self._cuda_graph_mem_snapshot_path()
torch.cuda.memory._dump_snapshot(snapshot_path)
torch.cuda.memory._record_memory_history(enabled=None)
log_message = (
"Sorted by CUDA Time:\n"
@@ -744,7 +813,7 @@ class CudaGraphRunner:
+ prof_context.key_averages(group_by_input_shape=True).table(
sort_by="cpu_time_total", row_limit=10
)
+ "\n\nMemory Usage is saved to cuda_graph_runner_memory_usage.pickle\n"
+ f"\n\nMemory Usage is saved to {snapshot_path}\n"
)
logger.info(log_message)
@@ -759,14 +828,20 @@ class CudaGraphRunner:
self.model_runner.gpu_id,
empty_cache=False,
)
is_rank0 = get_tensor_model_parallel_rank() == 0
cg_label = str(
getattr(self, "capture_forward_mode", None) or type(self).__name__
)
capture_start_avail = avail_mem
prev_avail = avail_mem
# Reverse the order to enable better memory sharing across cuda graphs.
capture_range = (
tqdm.tqdm(list(reversed(self.capture_bs)))
if get_tensor_model_parallel_rank() == 0
if is_rank0
else reversed(self.capture_bs)
)
for i, bs in enumerate(capture_range):
if get_tensor_model_parallel_rank() == 0:
if is_rank0:
avail_mem = get_available_gpu_memory(
self.model_runner.device,
self.model_runner.gpu_id,
@@ -791,6 +866,33 @@ class CudaGraphRunner:
self.graphs[key] = graph
self.output_buffers[key] = output_buffers
if _LOG_CG_BUFFERS and is_rank0:
after = get_available_gpu_memory(
self.model_runner.device,
self.model_runner.gpu_id,
empty_cache=False,
)
log_info_on_rank0(
logger,
f"[CG capture] {cg_label} bs={bs:>4d}: "
f"+{max(prev_avail - after, 0.0):6.3f} GB this shape "
f"(graph pool / attn workspace / outputs), avail={after:.3f} GB",
)
prev_avail = after
if _LOG_CG_BUFFERS and is_rank0:
final_avail = get_available_gpu_memory(
self.model_runner.device,
self.model_runner.gpu_id,
empty_cache=False,
)
log_info_on_rank0(
logger,
f"[CG capture] {cg_label} TOTAL non-static-buffer growth: "
f"{max(capture_start_avail - final_avail, 0.0):.3f} GB "
f"across {len(self.capture_bs)} shapes",
)
# Trigger CUDA graph capture for specific shapes.
# Capture the large shapes first so that the smaller shapes
# can reuse the memory pool allocated for the large shapes.

View File

@@ -13,6 +13,7 @@ from sglang.srt.model_executor.cuda_graph_runner import (
DeepEPCudaGraphRunnerAdapter,
get_batch_sizes_to_capture,
get_global_graph_memory_pool,
log_input_buffer_sizes,
model_capture_mode,
set_global_graph_memory_pool,
set_is_extend_in_batch,
@@ -157,6 +158,7 @@ class EAGLEDraftCudaGraphRunner:
global_num_tokens_for_logprob_gpu=global_num_tokens_for_logprob_gpu,
)
self.buffers.share_buffers()
log_input_buffer_sizes("eagle_draft", self.buffers)
# Capture
try:

View File

@@ -14,6 +14,7 @@ from sglang.srt.model_executor.cuda_graph_runner import (
LogitsProcessorOutput,
get_batch_sizes_to_capture,
get_global_graph_memory_pool,
log_input_buffer_sizes,
model_capture_mode,
set_global_graph_memory_pool,
set_is_extend_in_batch,
@@ -211,6 +212,7 @@ class EAGLEDraftExtendCudaGraphRunner:
global_num_tokens_for_logprob_gpu=global_num_tokens_for_logprob_gpu,
)
self.buffers.share_buffers()
log_input_buffer_sizes("eagle_draft_extend", self.buffers)
# Capture
try: