SGLang 分布式 Hang / GPU 调试指南

SGLang 分布式 Hang / GPU 调试指南
gogongxtSGLang 分布式 Hang / GPU 调试指南
1. 快速分流(1 分钟)
1
2
3
nvidia-smi
pgrep -af 'sglang::scheduler'
py-spy dump --native --pid <scheduler_pid>| 信号 | 类型 | 走 |
|---|---|---|
| util 0%,进程活着 | 通信 hang(rank 在等 collective) | 手段 1 + 2 |
| util 100% 卡死 | compute kernel hang | 手段 1 + 4/5 |
进程已崩(illegal memory access / NaN) |
数值 / shape bug | 手段 7(+9) |
| 启动阶段卡住 | init hang(watchdog 不计时) | 手动 py-spy |
补充判别:
- 通信 hang 三大根因:size mismatch(不同 rank 传不同大小,最常见)/ branch divergence(一个 rank 进 collective、另一个跳过)/ rank 掉队(OOM、崩溃)。
- compute hang 判别:py-spy 栈只有空洞的
cuStreamSynchronize(CPU 在等 GPU,看不透哪个 kernel);NCCL log 无异常(根本没进 collective)。
2. 手段总览
| # | 手段 | 类别 | 自动? | 看 CPU | 看 GPU kernel | 看通信 | 支持 CUDA graph? | 主要用途 |
|---|---|---|---|---|---|---|---|---|
| 1 | watchdog + py-spy | 进程级 | ✅ 自动 | ✅ CPU 栈 | ❌ 只到 sync 点 | ✅ 栈里见 collective | ✅ graph/eager 都有效(forward_ct 在
run_batch 开头无条件 +1,重放也算) |
看 CPU 卡在哪 |
| 2 | NCCL log | 通信级 | ✅ 自动 | — | ❌ | ✅ collective/size/rank | ⚠️ 部分:重放期 collective 已录进 graph,日志不逐次打印;捕获期可见 | 通信 hang 定位 |
| 3 | --crash-dump-folder |
请求级 | ✅ 随 SIGQUIT | ❌ | ❌ | ❌ | ✅ 无关(纯请求快照,不碰 graph) | 复现请求快照 |
| 4 | cuda-gdb | GPU 调试级 | ❌ 手动 | ✅ CPU/thread 状态 | ✅ kernel、线程、断点、异常现场 | ✅ 可见 NCCL kernel / 调用栈 | ✅ 支持重放:cudaGraphLaunch 触发的 kernel
同样可断住/检查 |
交互式定位 kernel hang / crash |
| 5 | GPU coredump + cuda-gdb | kernel 级 | ❌ 手动 | ❌ | ✅ 卡住的 kernel 名、线程状态、寄存器/内存现场 | ✅ NCCL kernel 名 | ✅ graph 重放也能抓 | GPU hang 后分析现场 |
| 6 | Nsight Systems + NVTX | 时间线 | ⚠️ 需提前开 | ✅ | ✅ 所有 kernel 名+耗时 | ✅ | ✅ 重放的 kernel 也抓得到(驱动层记录) | 看调用全貌 |
| 7 | SGLANG_KERNEL_API_LOGLEVEL |
API 级 | ✅ 自动 | ✅ API 入参 | ⚠️ 仅 eager 段 | ⚠️ | ❌ 重放期完全失效(整图 cudaGraphLaunch,不走
Python);eager 段仍有效 |
数值/shape bug |
| 8 | per-rank 日志(自埋点) | 逻辑级 | ✅ 自动 | ✅ 自定义 | ❌ | ✅ 自定义 | ⚠️ 看埋点位置:录图段内会被跳过;埋在 scheduler step 间安全 | 找 rank 间分歧 |
| 9 | CUDA_LAUNCH_BLOCKING=1 |
launch 级 | ✅ 自动 | ✅ 报错即崩点 | ✅ 崩溃的 kernel | ⚠️ | ❌ 重放期无效(整图一次 launch);捕获期有害(同步破坏录图)。必须关 graph | 同步定位崩溃 kernel |
3. 手段详解
3.1 watchdog + py-spy
定位 CPU 卡在哪一行。最先开,零成本。
原理:每个 scheduler 进程有后台线程,每
timeout/2 秒读一次 forward_ct(在
run_batch() 开头无条件 +1)。计数 N 秒不动 → 触发:先打印
dump_info(batch/reqs/内存池不变量检查)→ 对本进程跑
py-spy dump(先 --native,失败降级)。
| 模式 | 参数(默认) | 触发后 |
|---|---|---|
| 硬 watchdog | --watchdog-timeout N(300) |
dump + py-spy → sleep 5s → SIGQUIT 父进程(父进程侧再全量 dump + 杀树,见 3.3/3.5) |
| 软 watchdog | --soft-watchdog-timeout N(None 不启用) |
dump + py-spy → 不杀进程,留现场给手段 4/5 |
计时条件:is_initializing or cur_batch is not None。Scheduler
__init__ 期间(模型加载、内存池、NCCL 建连、CUDA
graph 捕获)不计时;空闲不计时;warmup
期间已计时(首跑 JIT 编译单次 forward 可达数秒,timeout 别
<60s,或 --skip-server-warmup)。
1
2
--soft-watchdog-timeout 60 # hang 调试推荐:吐栈不杀进程
--watchdog-timeout 300 # 硬 watchdog 兜底,须 > 最慢正常 forward读栈:
- 栈里有
all_gather/all_reduce/c10d::ProcessGroup→ 通信 hang,转手段 2 - 只有
cuStreamSynchronize→ CPU 在等 GPU,看不透 kernel,转手段 4/5
手动触发(watchdog 未触发时):
1
py-spy dump --native --pid $(pgrep -f 'sglang::scheduler_TP0')3.2 NCCL log
定位卡在哪个 collective、传多大、谁等谁。通信 hang 首选。
1
2
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=COLL # 只看 collective;建连问题加 INIT超时控制:NCCL_TIMEOUT
不是有效变量,别设。运行时 collective 超时由 PyTorch PG timeout 控制 →
--dist-timeout N(会传入
init_distributed_environment,并被 model-parallel
子组复用;不设走 PyTorch 默认 ~30min)。设小后卡住的 collective 会在 N
秒后抛异常崩溃,配 TORCH_NCCL_ASYNC_ERROR_HANDLING=1
可让超时 abort 进程、TORCH_NCCL_BLOCKING_WAIT=1
阻塞等待。
读法:hang 时日志里最后一条 collective 就是卡住的那步——
- 不同 rank 的
count不一样 → size mismatch 根因(TP hang 最常见死法) - 哪个 rank 没打印这条 → 它没进 collective(branch divergence / 掉队)
坑:compute kernel hang 时它什么都不报(没进 collective)。
3.3 --crash-dump-folder
复现用请求快照。不是 coredump——产出是 pickle,不含栈/kernel 信息。
1
--crash-dump-folder /tmp/sglang_crash触发:仅父进程(TokenizerManager)收到 SIGQUIT 时——① 硬
watchdog 超时链;② 子进程崩溃被 SubprocessWatchdog 检测到;③
子进程错误路径。父进程被 kill -9 打死则不产出。
产出:<dir>/<hostname>/crash_dump_<时间>.pkl,dict
三 key:server_args(trust-remote-code 时 pickle 失败会置
None 重试)、requests(崩溃前 5 分钟已完成 +
当时未完成,每个是 (请求, 输出, 创建时间, 结束时间)
元组)、launch_command。5 分钟前的完成请求已被滑窗丢掉。
0.5.16:设置该参数会自动注入全套 CUDA
coredump
环境变量(CUDA_ENABLE_USER_TRIGGERED_COREDUMP=1、CUDA_COREDUMP_PIPE=/tmp/corepipe.cuda.%h.%p、skip_*
体积控制等),即手段 5 的 FIFO 触发口自动就绪。
3.4 cuda-gdb(attach 交互式)
直接看卡住的 kernel 名与线程状态,不用等 35min 的 coredump。要求进程活着 → 配软 watchdog。
1
/usr/local/cuda-12.9/bin/cuda-gdb -p <scheduler_pid>1
2
3
4
info cuda kernels # 正在执行的 kernel(含 graph 重放启动的):名字 + grid/block
bt # 当前焦点线程栈
break <kernel符号名> # 对 kernel 入口下断点
set cuda breakon launch on # 每次 kernel launch 断住(重放也断)- attach 会冻结整个进程——对已 hang 的进程无所谓,别在生产正常服务上用
- 通信 hang 时能看到
ncclDevKernel_AllGather_*之类 kernel,兼看通信 - 容器内不在 PATH,用全路径(本机
/usr/local/cuda-12.9/bin/cuda-gdb)
3.5 GPU coredump + cuda-gdb
卡住 kernel 的离线现场:kernel 名、grid/block、寄存器/内存。compute hang 深挖用。
原理:CUDA 驱动为每个 CUDA 进程建命名 FIFO;往 FIFO 写字节 → 驱动冻结 GPU 逐 SM 抓状态 → 写 coredump 文件 → cuda-gdb 离线分析。
环境(启动前;或直接加
--crash-dump-folder 自动注入):
1
2
3
4
5
export CUDA_ENABLE_USER_TRIGGERED_COREDUMP=1
export CUDA_COREDUMP_PIPE="/tmp/corepipe.cuda.%h.%p"
export CUDA_COREDUMP_FILE="/tmp/cuda_coredump_%h_%p"
export CUDA_COREDUMP_SHOW_PROGRESS=1
export CUDA_COREDUMP_GENERATION_FLAGS='skip_nonrelocated_elf_images,skip_global_memory,skip_shared_memory,skip_local_memory,skip_constbank_memory'skip_* 是体积关键:加 ~180MB,不加几十 GB。
触发(两条路):
1
2
3
4
5
6
7
8
# A. 手动(hang 后上机器)
pgrep -af 'sglang::scheduler' # 找 pid
ls /tmp/corepipe.cuda.*<pid> # FIFO 在进程 CUDA 初始化后才出现
dd if=/dev/zero of=/tmp/corepipe.cuda.<host>.<pid> bs=1 count=1
ls -la /tmp/cuda_coredump_*<pid> # 等写完,看 SHOW_PROGRESS
# B. 自动(0.5.16):硬 watchdog → SIGQUIT → 父进程对所有 scheduler 自动触发 FIFO 写,
# 等 SGLANG_CUDA_COREDUMP_BEFORE_CRASH_WAIT_SECS(默认 60s)再杀树分析:
1
2
3
4
/usr/local/cuda-12.9/bin/cuda-gdb --batch \
-ex "target cudacore <coredump_file>" \
-ex "info cuda kernels" \
-ex "bt"输出第一行即答案:ncclDevKernel_AllGather_RING_LL<<<...>>>
之类——卡在哪个 kernel。
坑(实测):
- 忙 kernel 的 dump
极慢:
~16s/SM × 132 SM ≈ 35min(H800,GPU 空闲时 ~1s)。自动链默认只等 60s——要拿到完整 dump,把SGLANG_CUDA_COREDUMP_BEFORE_CRASH_WAIT_SECS调到 2400+,或用软 watchdog + 手动触发。 SIGABRT/SIGQUIT本身不产 GPU coredump,必须写 FIFO,或CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1(仅 GPU 异常崩溃时,普通 hang 不触发)。另有SGLANG_CUDA_COREDUMP=1(SGLang 内置,异常时自动产,同样只管崩溃)。- 别用
cuda-gdb的generate-core-file/gcore:产出 48GB CPU+GPU 混合 core,target cudacore打不开(ELF 校验失败)。 - 容器 core_pattern 损坏不影响 GPU coredump(走 CUDA 驱动,不走系统 core_pattern)。
3.6 Nsight Systems + NVTX
所有 kernel 的名字/耗时/顺序全貌,含 graph 重放(驱动层记录)。间歇性问题首选。
1
2
3
# 提前开,包住启动
nsys profile -o /tmp/sglang_trace --stats=true \
python3 -m sglang.launch_server ... --enable-layerwise-nvtx-markerhang 时时间线断在卡住的 kernel 上,名字和卡了多久一目了然。NVTX 标记把 kernel 关联到模型层。
坑:必须提前开,事后补不了;有性能开销,不常开;产物要
GUI 或 nsys stats 看。
3.7
SGLANG_KERNEL_API_LOGLEVEL
@debug_kernel_api装饰过的 API 入参 shape/dtype/device/数值。数值 bug 利器。
1
2
3
4
export SGLANG_KERNEL_API_LOGLEVEL=3 # 1=函数名 3=+入参元数据 5=+数值统计 10=+落盘
export SGLANG_KERNEL_API_LOGDEST=/tmp/kernel.log # stdout/stderr/文件,%i=pid
export SGLANG_KERNEL_API_DUMP_DIR=/tmp/kernel_dumps # level10 用
export SGLANG_KERNEL_API_DUMP_INCLUDE='*attention*' # 只 dump 匹配 API(可省)与 CUDA graph 的三态关系(关键):
| 阶段 | 行为 |
|---|---|
| 捕获期(录图) | 1/3 有效;5 的数值统计、10 的落盘被 skip(拷数据会破坏录图) |
| 重放期 | 整图一次 cudaGraphLaunch,Python
装饰器根本不被调用,所有 level 全失效 |
| torch.compile 段 | 同样跳过 |
0.5.16 默认 decode=full
整图、prefill=breakable。仍保持 eager 的段(采样层、spec
verify 边界、动态/捕获表外 batch size、warmup
首跑)完整有效。只覆盖装饰过的 API,裸 PyTorch op
记不到。
实用做法:怀疑数值/shape 时,关 graph 跑一次让 logging 完整生效:
1
2
--cuda-graph-backend-decode=disabled --cuda-graph-backend-prefill=disabled
# (旧 --disable-cuda-graph 已废弃,等价于上面两个)定位到具体 API + shape 后再开回 graph。level 5+ 有同步开销,可能改变时序。
3.8 per-rank 日志(自埋点)
rank 间从哪一步开始分歧。级联问题(浮点非确定性 → 采样分歧 → batch 结构漂移 → size mismatch)的唯一可靠解。
1
2
3
4
5
6
7
# Scheduler.__init__ 里,self.ps(ParallelState)就绪后:
self._debug_file = open(f"/tmp/debug_rank{self.ps.tp_rank}.log", "w")
def _dbg(self, msg):
if os.environ.get("SGLANG_DEBUG_HANG"): # 自加开关,避免生产开销
self._debug_file.write(f"step={self.forward_ct} {msg}\n")
self._debug_file.flush()rank 用 self.ps.tp_rank / pp_rank / dp_rank,步数用
self.forward_ct(单调递增)。
记什么:结构化事件,大写前缀便于 grep/diff:
1
2
self._dbg(f"SCHED_BATCH num_reqs={n} extend_lens={lens}")
self._dbg(f"VERIFY predict_hash={h} accept_len={alen}")Hash 大 tensor(别 dump 原始数据):
1
2
h = hashlib.md5(tensor.cpu().numpy().tobytes()).hexdigest()[:8]
h = hashlib.md5(str(ids.tolist()).encode()).hexdigest()[:8] # token id 列表两个坑:
- 埋点位置 vs graph:埋在录图段内,重放期被跳过;埋在 scheduler step 之间(GPU idle)安全。
- 隐式同步:
.cpu()/.tolist()/.numpy()触发 CUDA 同步,会改变时序;夹在两个背靠背 collective 之间会死锁。优先记 CPU 已有的值(int、长度、rid),必须 hash GPU tensor 时在 GPU idle 点做。
diff 找第一分歧:
1
2
3
grep -c "^VERIFY" /tmp/debug_rank*.log # 数量不同 → 已分歧
grep "^VERIFY" /tmp/debug_rank0.log > /tmp/v0; grep "^VERIFY" /tmp/debug_rank1.log > /tmp/v1
diff /tmp/v0 /tmp/v1 | head # 第一行分歧 = 根因步然后二分回溯:列出该操作全部输入、逐个加 hash,匹配的排除、不匹配的追上游,直到根因。
3.9
CUDA_LAUNCH_BLOCKING=1
每次 kernel launch 同步执行 → 崩溃时报错点即出错的 kernel。只用于定位 crash,不用于 hang(会无限慢)。
1
2
3
4
5
export CUDA_LAUNCH_BLOCKING=1
# 必须同时关 graph,两个原因:
# 重放期无效:整图一次 cudaGraphLaunch,无法逐 kernel 阻塞
# 捕获期有害:同步 launch 破坏录图
--cuda-graph-backend-decode=disabled --cuda-graph-backend-prefill=disabled配合手段 7 一起用:CUDA_LAUNCH_BLOCKING=1 告诉你崩在哪个
kernel,SGLANG_KERNEL_API_LOGLEVEL=3 告诉你那一步的入参
shape/dtype。
4. 按场景组合
| 场景 | 判别 | 组合 |
|---|---|---|
| A. NCCL 通信 hang | py-spy 栈有 collective;util 0% | 2 + 1(基本不用上机器)→ 深挖上游用 8 找 batch 分歧 |
| B. compute kernel hang | 空洞 cuStreamSynchronize;util 100% |
软 watchdog(1)+ cuda-gdb attach(4) 快;要离线现场再 coredump(5);必须上机器 |
| C. 数值/shape crash | illegal memory access / NaN |
7 + 关 graph;加 9 定位崩点 kernel;level10 落盘可重放 |
| D. 间歇性 hang | 偶发难复现 | 6(提前开)+ 8 常开,等自然发生留证据 |
| E. 启动/init hang | 服务起不来 | 手动 py-spy(watchdog 初始化期不计时)+ 启动日志 |
5. 决策流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
hang 发生
│
├─ 自动留下的(看日志即可):
│ ① py-spy 栈(watchdog) ② NCCL log ③ crash-dump pkl(当时哪些请求在飞)
│
├─ nvidia-smi + py-spy 栈分叉:
│ ├─ util 0% + 栈有 all_gather/all_reduce → 通信 hang
│ │ → NCCL log 看 size mismatch / 谁没进 collective,基本定位
│ │ → 深挖上游:手段 8 找分歧步
│ │
│ └─ util 100% + 空洞 cuStreamSynchronize → compute hang
│ → cuda-gdb attach 看 kernel(快)/ FIFO coredump(慢但全)
│ → 软 watchdog 保证进程活着
│
└─ 还要更多信息:
├─ 数值/shape 细节 → 关 graph + SGLANG_KERNEL_API_LOGLEVEL=3(+CUDA_LAUNCH_BLOCKING)
├─ 调用全貌/间歇问题 → 提前开 Nsight + NVTX
└─ rank 间分歧 → per-rank 日志自埋点6. 速查表
Server 参数
| 参数 | 默认 | 作用 |
|---|---|---|
--watchdog-timeout N |
300 | 硬 watchdog:forward N 秒不动 → py-spy + SIGQUIT 杀进程 |
--soft-watchdog-timeout N |
None | 软 watchdog:只吐 py-spy 不杀进程(hang 调试推荐) |
--crash-dump-folder <dir> |
None | SIGQUIT 时写请求快照 pkl;自动注入 CUDA coredump 环境 |
--dist-timeout N |
None | PG 超时(初始化 + model-parallel 子组运行时 collective;默认 ~30min) |
--cuda-graph-backend-{decode,prefill} <b> |
decode=full / prefill=breakable | full / breakable / tc_piecewise / disabled;关 graph 用
disabled |
--disable-cuda-graph |
— | 已废弃,等价于两个 backend 都设 disabled |
--enable-layerwise-nvtx-marker |
False | NVTX 层标记(配合 Nsight) |
--skip-server-warmup |
False | 跳过 warmup(避免 JIT 慢 forward 误触发 watchdog) |
环境变量
| 变量 | 作用 |
|---|---|
NCCL_DEBUG=INFO /
NCCL_DEBUG_SUBSYS=COLL |
NCCL collective 日志 |
TORCH_NCCL_BLOCKING_WAIT=1 |
collective 超时阻塞等待 |
TORCH_NCCL_ASYNC_ERROR_HANDLING=1 |
collective 超时/错误后 abort 进程 |
CUDA_ENABLE_USER_TRIGGERED_COREDUMP=1 |
开 FIFO 触发口(CUDA 初始化后建 FIFO) |
CUDA_COREDUMP_PIPE /
CUDA_COREDUMP_FILE |
FIFO 路径 / coredump 输出(%h=host %p=pid) |
CUDA_COREDUMP_GENERATION_FLAGS=skip_* |
跳过各类内存,~180MB |
CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1 |
GPU 异常自动产 coredump(仅崩溃,不 hang) |
SGLANG_CUDA_COREDUMP=1 / _DIR |
SGLang 内置异常 coredump(仅崩溃) |
SGLANG_PYSPY_DUMP_BEFORE_CRASH |
默认 True:SIGQUIT 链自动 dump 所有 scheduler 的 py-spy |
SGLANG_CUDA_COREDUMP_BEFORE_CRASH |
默认 True:SIGQUIT 链自动触发所有 scheduler 的 GPU coredump |
SGLANG_CUDA_COREDUMP_BEFORE_CRASH_WAIT_SECS |
默认 60:自动链等待时间;忙 kernel dump ~35min,要调大 |
SGLANG_KERNEL_API_LOGLEVEL |
1/3/5/10,见 3.7 |
SGLANG_KERNEL_API_LOGDEST / _DUMP_DIR /
_DUMP_INCLUDE / _DUMP_EXCLUDE |
API logging 的输出与过滤 |
推荐调试启动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 环境变量
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=COLL
export CUDA_ENABLE_USER_TRIGGERED_COREDUMP=1 # --crash-dump-folder 也会自动注入
export CUDA_COREDUMP_PIPE="/tmp/corepipe.cuda.%h.%p"
export CUDA_COREDUMP_FILE="/tmp/cuda_coredump_%h_%p"
export CUDA_COREDUMP_GENERATION_FLAGS='skip_nonrelocated_elf_images,skip_global_memory,skip_shared_memory,skip_local_memory,skip_constbank_memory'
export CUDA_COREDUMP_SHOW_PROGRESS=1
# server 参数(在原命令上追加)
python3 -m sglang.launch_server \
... 原参数 ... \
--soft-watchdog-timeout 60 \
--watchdog-timeout 300 \
--crash-dump-folder /tmp/sglang_crash



