Profiling Enhancements [1/3]: cuda graph profile traces (#24370)
Co-authored-by: Basit <mohbasit@ctr2-alola-ctrl-01.amd.com> Co-authored-by: HAI <hixiao@gmail.com>
This commit is contained in:
@@ -328,6 +328,10 @@ class Envs:
|
||||
)
|
||||
SGLANG_RECORD_STEP_TIME = EnvBool(False)
|
||||
SGLANG_ENABLE_CUDA_GRAPH_CAPTURE_TRACE = EnvBool(False)
|
||||
# Opt-in: emit one CUDA-graph capture trace per captured batch size (per-bs).
|
||||
# SGLANG_ENABLE_CUDA_GRAPH_CAPTURE_TRACE (single combined trace) takes
|
||||
# precedence when both are set.
|
||||
SGLANG_GRAPH_BATCH_CAPTURE = EnvBool(False)
|
||||
SGLANG_FORCE_SHUTDOWN = EnvBool(False)
|
||||
SGLANG_DEBUG_MEMORY_POOL = EnvBool(False)
|
||||
SGLANG_DSPARK_DEBUG_CONFIDENCE_PREFIX_SCHEDULER = EnvBool(False)
|
||||
|
||||
@@ -28,6 +28,7 @@ from __future__ import annotations
|
||||
import contextlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Union
|
||||
|
||||
@@ -102,7 +103,10 @@ from sglang.srt.utils import (
|
||||
require_mlp_tp_gather,
|
||||
)
|
||||
from sglang.srt.utils.device_timer import device_timer_ctx
|
||||
from sglang.srt.utils.profile_utils import export_cuda_graph_capture_trace
|
||||
from sglang.srt.utils.profile_utils import (
|
||||
export_cuda_graph_capture_trace,
|
||||
graph_capture_profile_dir,
|
||||
)
|
||||
|
||||
try:
|
||||
from kt_kernel import KTMoEWrapper
|
||||
@@ -682,11 +686,63 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
|
||||
and capture_hidden_mode_matches
|
||||
)
|
||||
|
||||
def _init_profile_context_and_memory_record(self):
|
||||
profile_context = profile(
|
||||
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
|
||||
record_shapes=True,
|
||||
def _graph_batch_capture_active(self) -> bool:
|
||||
"""Whether the per-batch-size capture-trace feature is active.
|
||||
|
||||
Gated by SGLANG_GRAPH_BATCH_CAPTURE. The original single-trace export
|
||||
(SGLANG_ENABLE_CUDA_GRAPH_CAPTURE_TRACE) takes precedence: when both are
|
||||
set we fall back to the original behavior.
|
||||
"""
|
||||
return (
|
||||
envs.SGLANG_GRAPH_BATCH_CAPTURE.get()
|
||||
and not envs.SGLANG_ENABLE_CUDA_GRAPH_CAPTURE_TRACE.get()
|
||||
)
|
||||
|
||||
def _init_profile_context_and_memory_record(self):
|
||||
if self._graph_batch_capture_active():
|
||||
# Per-batch-size capture traces (SGLANG_GRAPH_BATCH_CAPTURE): a
|
||||
# scheduled profiler is stepped once per batch size (see
|
||||
# FullCudaGraphBackend.capture_one) and on_trace_ready writes one
|
||||
# chrome trace per bs.
|
||||
rank = get_parallel().tp_rank
|
||||
runner_name = type(self).__name__
|
||||
trace_dir = graph_capture_profile_dir()
|
||||
os.makedirs(trace_dir, exist_ok=True)
|
||||
|
||||
# Track which BS is currently being captured for trace file naming
|
||||
self._profile_bs_list = list(reversed(self.capture_bs))
|
||||
self._profile_bs_idx = 0
|
||||
|
||||
def on_trace_ready(prof):
|
||||
bs = self._profile_bs_list[self._profile_bs_idx]
|
||||
trace_file = os.path.join(
|
||||
trace_dir, f"{runner_name}_bs_{bs}_rank{rank}.json.gz"
|
||||
)
|
||||
prof.export_chrome_trace(trace_file)
|
||||
logger.info(f"Saved trace for bs={bs} to {trace_file}")
|
||||
self._profile_bs_idx += 1
|
||||
|
||||
profile_context = profile(
|
||||
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
|
||||
# Schedule: wait=2 (skip 2 dummy runs), warmup=0, active=1
|
||||
# (capture run); repeat=0 repeats the cycle so each batch size
|
||||
# gets its own trace.
|
||||
schedule=torch.profiler.schedule(wait=2, warmup=0, active=1, repeat=0),
|
||||
record_shapes=True,
|
||||
with_stack=True,
|
||||
with_flops=True,
|
||||
profile_memory=True,
|
||||
on_trace_ready=on_trace_ready,
|
||||
)
|
||||
else:
|
||||
# a single unscheduled pass over the whole
|
||||
# capture. The combined trace (if any) is exported in
|
||||
# _post_process_after_profile via export_cuda_graph_capture_trace,
|
||||
# gated by SGLANG_ENABLE_CUDA_GRAPH_CAPTURE_TRACE.
|
||||
profile_context = profile(
|
||||
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
|
||||
record_shapes=True,
|
||||
)
|
||||
torch.cuda.memory._record_memory_history()
|
||||
return profile_context
|
||||
|
||||
@@ -706,10 +762,10 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
|
||||
)
|
||||
logger.info(log_message)
|
||||
|
||||
# Optionally persist the shaped capture trace (record_shapes=True) for
|
||||
# offline per-kernel analysis -- opt-in via
|
||||
# SGLANG_ENABLE_CUDA_GRAPH_CAPTURE_TRACE; the in-log tables above are
|
||||
# unchanged.
|
||||
# single-trace export for the whole capture pass; no-op unless
|
||||
# SGLANG_ENABLE_CUDA_GRAPH_CAPTURE_TRACE is set. In per-bs mode
|
||||
# (SGLANG_GRAPH_BATCH_CAPTURE) that env is unset, so this stays a no-op
|
||||
# and the per-bs on_trace_ready handles export instead.
|
||||
export_cuda_graph_capture_trace(
|
||||
prof_context,
|
||||
runner_name=type(self).__name__,
|
||||
@@ -886,8 +942,15 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
|
||||
self.model_runner, self.captured_req_width
|
||||
)
|
||||
profile_context = empty_context()
|
||||
# Holds the active torch profiler during capture so the backend can
|
||||
# advance its schedule (profiler.step()) per batch size. Only the
|
||||
# scheduled per-bs profiler (SGLANG_GRAPH_BATCH_CAPTURE) needs stepping;
|
||||
# the original unscheduled pass leaves this None.
|
||||
self._profiler = None
|
||||
if self.enable_profile_cuda_graph:
|
||||
profile_context = self._init_profile_context_and_memory_record()
|
||||
if self._graph_batch_capture_active():
|
||||
self._profiler = profile_context
|
||||
|
||||
# share_buffers() coalesces seq_lens / seq_lens_cpu through the process-
|
||||
# wide pool, so they may alias a buffer seeded by an earlier runner (the
|
||||
@@ -924,6 +987,7 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
|
||||
|
||||
if self.enable_profile_cuda_graph:
|
||||
self._post_process_after_profile(prof)
|
||||
self._profiler = None
|
||||
|
||||
# No pool-side pin to clear: the captured full-physical write loc rides the
|
||||
# backend's `ForwardMetadata.out_cache_loc_full_physical` (-> KVWriteLoc.full_loc).
|
||||
|
||||
@@ -58,6 +58,7 @@ class FullCudaGraphBackend(BaseCudaGraphBackend):
|
||||
self._graphs: Dict[Any, torch.cuda.CUDAGraph] = {}
|
||||
self._outputs: Dict[Any, Any] = {}
|
||||
self._pool = None
|
||||
self._cuda_graph_runner = cuda_graph_runner
|
||||
self._device_module = cuda_graph_runner.device_module
|
||||
self._tp_group = cuda_graph_runner.model_runner.tp_group
|
||||
self._capture_stream: Optional[torch.cuda.Stream] = None
|
||||
@@ -84,12 +85,29 @@ class FullCudaGraphBackend(BaseCudaGraphBackend):
|
||||
capture_inputs: Optional[Any] = None,
|
||||
post_warmup_hook: Optional[Callable[[], None]] = None,
|
||||
) -> None:
|
||||
# When per-bs capture traces are enabled (--enable-profile-cuda-graph +
|
||||
# SGLANG_GRAPH_BATCH_CAPTURE), the runner created a scheduled
|
||||
# torch profiler (wait=2, active=1) and exposed it as _profiler. We step()
|
||||
# past the two warmup runs so only the capture run is recorded, and each
|
||||
# batch size produces its own trace via the profiler's on_trace_ready.
|
||||
# With --enable-profile-cuda-graph alone the runner leaves _profiler None
|
||||
# (its unscheduled profiler records the whole capture in one pass), so no
|
||||
# stepping happens here.
|
||||
runner = self._cuda_graph_runner
|
||||
profiler = (
|
||||
getattr(runner, "_profiler", None)
|
||||
if getattr(runner, "enable_profile_cuda_graph", False)
|
||||
else None
|
||||
)
|
||||
|
||||
# Two warmups so kernels are loaded and one-time setup is paid before capture.
|
||||
# post_warmup_hook lets the attention backend reset state that warmup mutated.
|
||||
for _ in range(2):
|
||||
self._device_module.synchronize()
|
||||
self._tp_group.barrier()
|
||||
forward_fn()
|
||||
if profiler is not None:
|
||||
profiler.step()
|
||||
if post_warmup_hook is not None:
|
||||
post_warmup_hook()
|
||||
|
||||
@@ -110,6 +128,9 @@ class FullCudaGraphBackend(BaseCudaGraphBackend):
|
||||
with graph_ctx(cuda_graph=graph, pool=self._pool, stream=self._capture_stream):
|
||||
out = forward_fn()
|
||||
|
||||
if profiler is not None:
|
||||
profiler.step()
|
||||
|
||||
self._graphs[shape_key] = graph
|
||||
self._outputs[shape_key] = out
|
||||
|
||||
|
||||
@@ -30,6 +30,19 @@ if _is_npu:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Single source of truth for the CUDA-graph capture trace output directory,
|
||||
# shared by both the original single-trace export and the per-batch-size
|
||||
# (SGLANG_GRAPH_BATCH_CAPTURE) traces so they land in the same place.
|
||||
GRAPH_CAPTURE_PROFILE_DIRNAME = "graph_capture_profile"
|
||||
|
||||
|
||||
def graph_capture_profile_dir() -> str:
|
||||
"""``<SGLANG_TORCH_PROFILER_DIR>/graph_capture_profile`` — the one directory
|
||||
both capture-trace modes write to. Change the location here only."""
|
||||
return os.path.join(
|
||||
envs.SGLANG_TORCH_PROFILER_DIR.get(), GRAPH_CAPTURE_PROFILE_DIRNAME
|
||||
)
|
||||
|
||||
|
||||
def export_cuda_graph_capture_trace(prof_context, *, runner_name: str, tp_rank: int):
|
||||
"""Persist a CUDA-graph capture profiler trace (chrome trace) to disk.
|
||||
@@ -37,15 +50,13 @@ def export_cuda_graph_capture_trace(prof_context, *, runner_name: str, tp_rank:
|
||||
Opt-in via ``SGLANG_ENABLE_CUDA_GRAPH_CAPTURE_TRACE`` (no-op otherwise). The
|
||||
capture profiler must have run with ``record_shapes=True`` so the trace can
|
||||
be inspected offline as a per-kernel shape/identity record. The file lands in
|
||||
``<SGLANG_TORCH_PROFILER_DIR>/graph_capture_profile/`` and is namespaced by
|
||||
runner class and TP rank so concurrent capture passes (e.g. EAGLE3
|
||||
target/draft/draft-extend) and ranks don't overwrite each other.
|
||||
``graph_capture_profile_dir()`` and is namespaced by runner class and TP rank
|
||||
so concurrent capture passes (e.g. EAGLE3 target/draft/draft-extend) and
|
||||
ranks don't overwrite each other.
|
||||
"""
|
||||
if not envs.SGLANG_ENABLE_CUDA_GRAPH_CAPTURE_TRACE.get():
|
||||
return
|
||||
output_dir = os.path.join(
|
||||
envs.SGLANG_TORCH_PROFILER_DIR.get(), "graph_capture_profile"
|
||||
)
|
||||
output_dir = graph_capture_profile_dir()
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
path = os.path.join(
|
||||
output_dir, f"cuda_graph_capture-{runner_name}-TP-{tp_rank}.json.gz"
|
||||
|
||||
Reference in New Issue
Block a user