Skip to content

Commit 6d95a7c

Browse files
feat(ubenchX): add SM-to-SM L2 latency distance benchmark with B200 results (#3881)
Port the L2 pointer-chase benchmark from SemiAnalysisAI/microbench-blackwell to ubenchX/sm_l2_distance/. The benchmark launches one warp per SM, chases a random linked list through the L2 cache, and computes the mean absolute difference of per-address latency profiles between every SM pair. This reveals GPC and die structure: SMs sharing a GPC see similar L2 profiles, SMs on different dies diverge further. B200 results (148 SMs, 11 GPCs, 2 dies): Intra-GPC mean: 75.6 cycles Inter-GPC mean: 188.9 cycles Range: 37.2 – 290.0 cycles Run details: NVIDIA B200 SXM, driver 580.159.03, CUDA 13.0, PyTorch 2.13.0+cu130, sglang-20260922 container, 5 passes. 中文:新增 SM 间 L2 延迟差异基准测试,包含 B200 结果 将 SemiAnalysisAI/microbench-blackwell 的 L2 指针追踪基准测试移植到 ubenchX/sm_l2_distance/。基准测试在每个 SM 上启动一个 warp,追踪 L2 缓存中的 随机链表,并计算每对 SM 之间每地址延迟差异的绝对值均值,从而揭示 GPC 和 die 结构。B200 结果:GPC 内均值 75.6 周期,GPC 间均值 188.9 周期。 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2f802c9 commit 6d95a7c

6 files changed

Lines changed: 22612 additions & 0 deletions

File tree

‎ubenchX/sm_l2_distance/Makefile‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
NVCC := nvcc
2+
CXXFLAGS := -O2 -std=c++17
3+
4+
# Auto-detect SM count; override with make NUM_SMS=148
5+
NUM_SMS ?= $(shell nvidia-smi --query-gpu=count --format=csv,noheader 2>/dev/null | head -1)
6+
7+
# Architecture flags — add more -gencode lines for older GPUs.
8+
# Blackwell (sm_100a / sm_120a):
9+
ARCH ?= -gencode=arch=compute_100a,code=\"sm_100a,compute_100a\"
10+
11+
TARGET := l2_pointer_chase
12+
SRC := l2_pointer_chase.cu
13+
14+
.PHONY: all clean run
15+
16+
all: $(TARGET)
17+
18+
$(TARGET): $(SRC)
19+
$(NVCC) $(CXXFLAGS) $(ARCH) -DNUM_SMS=$(NUM_SMS) -o $@ $<
20+
21+
run: $(TARGET)
22+
mkdir -p results
23+
./$(TARGET) > results/distance.csv
24+
25+
clean:
26+
rm -f $(TARGET)
27+
rm -rf results/*.csv

‎ubenchX/sm_l2_distance/bench.py‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""SM-to-SM L2 cache latency distance via pointer chasing.
2+
3+
Each SM chases a random linked list through the L2 cache. Comparing the
4+
per-address latency profiles of every SM pair reveals the GPU's internal
5+
structure: SMs in the same GPC (and die half) see near-identical profiles,
6+
while SMs on different dies diverge measurably.
7+
8+
Outputs an N*N pairwise CSV (sm_a, sm_b, mean_abs_diff, gpc_a, gpc_b) to
9+
stdout, plus results/sm_info.csv (per-SM GPC mapping and mean latency).
10+
Paste the CSV into ``results/`` by hand.
11+
12+
Requirements: CUDA toolkit (nvcc) with cooperative-groups support.
13+
14+
Usage:
15+
python ubenchX/sm_l2_distance/bench.py # auto-detect arch
16+
python ubenchX/sm_l2_distance/bench.py --arch sm_100a --num-sms 148
17+
18+
Ported from SemiAnalysisAI/microbench-blackwell (sm_l2_distance/).
19+
"""
20+
21+
import argparse
22+
import os
23+
import subprocess
24+
import sys
25+
import tempfile
26+
27+
28+
def detect_gpu_arch() -> str:
29+
"""Return the SM architecture string (e.g. 'sm_100a') for GPU 0."""
30+
result = subprocess.run(
31+
["nvidia-smi", "--query-gpu=compute_cap", "--format=csv,noheader", "--id=0"],
32+
capture_output=True,
33+
text=True,
34+
check=True,
35+
)
36+
cap = result.stdout.strip().split("\n")[0].strip()
37+
major, minor = cap.split(".")
38+
return f"sm_{major}{minor}"
39+
40+
41+
def detect_num_sms() -> int:
42+
"""Return the multiprocessor count for GPU 0."""
43+
result = subprocess.run(
44+
[
45+
"nvidia-smi",
46+
"--query-gpu=count",
47+
"--format=csv,noheader",
48+
"--id=0",
49+
],
50+
capture_output=True,
51+
text=True,
52+
check=True,
53+
)
54+
# nvidia-smi --query-gpu=count returns device count, not SM count.
55+
# Use CUDA deviceQuery-style approach via nvcc + a tiny program.
56+
prog = r'''
57+
#include <cstdio>
58+
#include <cuda_runtime.h>
59+
int main() {
60+
cudaDeviceProp p;
61+
cudaGetDeviceProperties(&p, 0);
62+
printf("%d\n", p.multiProcessorCount);
63+
return 0;
64+
}
65+
'''
66+
with tempfile.NamedTemporaryFile(suffix=".cu", mode="w", delete=False) as f:
67+
f.write(prog)
68+
src = f.name
69+
out = src.replace(".cu", "")
70+
try:
71+
subprocess.run(["nvcc", "-o", out, src], check=True, capture_output=True)
72+
r = subprocess.run([out], capture_output=True, text=True, check=True)
73+
return int(r.stdout.strip())
74+
finally:
75+
for path in (src, out):
76+
if os.path.exists(path):
77+
os.remove(path)
78+
79+
80+
def main():
81+
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
82+
parser.add_argument("--arch", help="SM architecture (e.g. sm_100a); auto-detected if omitted")
83+
parser.add_argument("--num-sms", type=int, help="SM count; auto-detected if omitted")
84+
parser.add_argument("--num-hops", type=int, default=0, help="Override hop count (0 = full chain)")
85+
parser.add_argument("--num-passes", type=int, default=5, help="Averaging passes (default: 5)")
86+
args = parser.parse_args()
87+
88+
script_dir = os.path.dirname(os.path.abspath(__file__))
89+
90+
arch = args.arch or detect_gpu_arch()
91+
num_sms = args.num_sms or detect_num_sms()
92+
93+
print(f"Detected arch={arch}, num_sms={num_sms}", file=sys.stderr)
94+
95+
# Compile
96+
src = os.path.join(script_dir, "l2_pointer_chase.cu")
97+
binary = os.path.join(script_dir, "l2_pointer_chase")
98+
gencode = f"-gencode=arch=compute_{arch.replace('sm_', '')},code=\"{arch},compute_{arch.replace('sm_', '')}\""
99+
compile_cmd = [
100+
"nvcc", "-O2", "-std=c++17",
101+
gencode,
102+
f"-DNUM_SMS={num_sms}",
103+
"-o", binary, src,
104+
]
105+
print(f"Compiling: {' '.join(compile_cmd)}", file=sys.stderr)
106+
subprocess.run(compile_cmd, check=True)
107+
108+
# Run
109+
results_dir = os.path.join(script_dir, "results")
110+
os.makedirs(results_dir, exist_ok=True)
111+
112+
run_cmd = [binary]
113+
if args.num_hops > 0:
114+
run_cmd.append(str(args.num_hops))
115+
run_cmd.append(str(args.num_passes))
116+
elif args.num_passes != 5:
117+
run_cmd.extend(["0", str(args.num_passes)])
118+
119+
print(f"Running: {' '.join(run_cmd)}", file=sys.stderr)
120+
# Binary writes the pairwise CSV to stdout, diagnostics to stderr
121+
result = subprocess.run(run_cmd, cwd=script_dir, check=True, capture_output=True, text=True)
122+
123+
# Print the CSV to stdout (caller can redirect)
124+
sys.stdout.write(result.stdout)
125+
# Forward stderr diagnostics
126+
sys.stderr.write(result.stderr)
127+
128+
129+
if __name__ == "__main__":
130+
main()

0 commit comments

Comments
 (0)