GPU kernel profiles of vllm bench forward cases.
With --profile-dir, each case's graph is replayed a few more times under
torch.profiler after timing. Each rank writes a Perfetto trace and a kernel
summary (<case>.rank<r>.kernels.json): per-kernel time per forward, sorted.
Summarize one file or compare two (e.g. before/after a change):
python -m vllm.benchmarks.forward.profile A.kernels.json [B.kernels.json].
The same replays run between cudaProfilerStart/Stop in an NVTX range per
case (with or without --profile-dir), so Nsight Systems can record just them:
nsys profile --capture-range=cudaProfilerApi --capture-range-end=repeat
--cuda-graph-trace=node vllm bench forward ....
Functions:
-
profile_graph
–
Replay replays times between cudaProfilerStart/Stop, in an NVTX
-
short_name
–
void ns::foo<T, 4>(args) -> ns::foo: return type, template and
-
summarize
–
Per-forward kernel times from the GPU activities of replays replays.
_match_back(text, end, open_ch, close_ch)
Index of the bracket opening the one closing at end, skipping
brackets of the other kind nested inside parentheses.
Source code in vllm/benchmarks/forward/profile.py
| def _match_back(text: str, end: int, open_ch: str, close_ch: str) -> int:
"""Index of the bracket opening the one closing at `end`, skipping
brackets of the other kind nested inside parentheses."""
depth, parens = 0, 0
for i in range(end, -1, -1):
ch = text[i]
if open_ch == "<" and ch in "()":
parens += 1 if ch == ")" else -1
continue
if parens:
continue
if ch == close_ch:
depth += 1
elif ch == open_ch:
depth -= 1
if depth == 0:
return i
return -1
|
profile_graph(graph, name, prefix, skip, replays=3)
Replay replays times between cudaProfilerStart/Stop, in an NVTX
range named after the case, for nsys --capture-range=cudaProfilerApi.
With prefix, also trace them with torch.profiler and write
prefix.trace.json.gz and prefix.kernels.json. Every rank must call
this (replays may contain collectives).
Source code in vllm/benchmarks/forward/profile.py
| def profile_graph(
graph: torch.cuda.CUDAGraph,
name: str,
prefix: str | None,
skip: int,
replays: int = 3,
) -> dict[str, Any] | None:
"""Replay `replays` times between `cudaProfilerStart`/`Stop`, in an NVTX
range named after the case, for `nsys --capture-range=cudaProfilerApi`.
With `prefix`, also trace them with `torch.profiler` and write
`prefix.trace.json.gz` and `prefix.kernels.json`. Every rank must call
this (replays may contain collectives).
"""
def run() -> None:
# `cudaProfilerStart`/`Stop` and NVTX ranges are CUDA-only (for
# Nsight Systems); other accelerators just replay.
if current_platform.is_xpu():
for _ in range(replays):
graph.replay()
torch.accelerator.synchronize()
else:
torch.cuda.profiler.start()
with torch.cuda.nvtx.range(f"forward/{name}"):
for _ in range(replays):
graph.replay()
torch.accelerator.synchronize()
torch.cuda.profiler.stop()
if prefix is None:
run()
return None
activities = get_default_torch_profiler_activities()
with torch.profiler.profile(activities=activities) as prof:
run()
trace = f"{prefix}.trace.json.gz"
prof.export_chrome_trace(trace)
with gzip.open(trace, "rt") as f:
events = json.load(f)["traceEvents"]
summary = {"trace": trace, **summarize(events, replays, skip)}
with open(f"{prefix}.kernels.json", "w") as f:
json.dump(summary, f, indent=1)
return summary
|
short_name(name)
void ns::foo<T, 4>(args) -> ns::foo: return type, template and
function arguments dropped. Generic launchers (e.g.
cutlass::device_kernel<Op<...>>) keep their first template argument.
Source code in vllm/benchmarks/forward/profile.py
| def short_name(name: str) -> str:
"""`void ns::foo<T, 4>(args)` -> `ns::foo`: return type, template and
function arguments dropped. Generic launchers (e.g.
`cutlass::device_kernel<Op<...>>`) keep their first template argument."""
if name.startswith("kernel_cutlass_"):
return _CUTE_DSL_SUFFIX.split(name, maxsplit=1)[0]
head = name.strip()
if head.endswith(")"):
head = head[: max(_match_back(head, len(head) - 1, "(", ")"), 0)].rstrip()
template = ""
if head.endswith(">"):
start = _match_back(head, len(head) - 1, "<", ">")
if start > 0:
head, template = head[:start], head[start + 1 : -1]
base = head.split()[-1] if head.split() else name
if _GENERIC.search(base) and template:
arg = re.split(r"[<>(,]", template, maxsplit=1)[0].strip()
if arg:
return f"{base}<{arg.removeprefix('void ')}>"
return base
|
summarize(events, replays, skip)
Per-forward kernel times from the GPU activities of replays replays.
Each replay first runs skip activities that aren't part of the timed
forward (the KV state restore and the rank rendezvous); they are reported
separately as pre_forward_us.
Source code in vllm/benchmarks/forward/profile.py
| def summarize(events: list[dict], replays: int, skip: int) -> dict[str, Any]:
"""Per-forward kernel times from the GPU activities of `replays` replays.
Each replay first runs `skip` activities that aren't part of the timed
forward (the KV state restore and the rank rendezvous); they are reported
separately as `pre_forward_us`.
"""
acts = sorted(
(e for e in events if e.get("cat") in _GPU_CATEGORIES and "dur" in e),
key=lambda e: e["ts"],
)
if not acts:
raise ValueError(
"No GPU activity recorded. Another profiler (e.g. nsys) may hold "
"CUPTI; don't combine --profile-dir with it."
)
if len(acts) % replays:
raise ValueError(
f"{len(acts)} GPU activities don't split into {replays} replays"
)
per_replay = len(acts) // replays
if per_replay <= skip:
raise ValueError(f"{per_replay} activities per replay, {skip} to skip")
kernels: dict[str, dict[str, Any]] = defaultdict(
lambda: {"total_us": 0.0, "calls": 0, "example": ""}
)
spans, busy, pre = [], [], []
for r in range(replays):
chunk = acts[r * per_replay : (r + 1) * per_replay]
pre.append(sum(e["dur"] for e in chunk[:skip]))
forward = chunk[skip:]
spans.append(
max(e["ts"] + e["dur"] for e in forward) - min(e["ts"] for e in forward)
)
busy.append(sum(e["dur"] for e in forward))
for e in forward:
name = short_name(e["name"]) if e["cat"] == "kernel" else e["name"]
k = kernels[name]
k["total_us"] += e["dur"]
k["calls"] += 1
k["example"] = k["example"] or e["name"][:300]
total = sum(k["total_us"] for k in kernels.values())
rows = [
{
"name": name,
"us": round(k["total_us"] / replays, 2),
"pct": round(100 * k["total_us"] / total, 2),
"calls": k["calls"] // replays,
"example": k["example"],
}
for name, k in kernels.items()
]
rows.sort(key=lambda row: -row["us"])
return {
"replays": replays,
# First to last forward activity; busy can exceed it when streams overlap.
"span_us": round(statistics.median(spans), 2),
"busy_us": round(statistics.median(busy), 2),
"pre_forward_us": round(statistics.median(pre), 2),
"activities": per_replay - skip,
"kernels": rows,
}
|