SGLang 分布式 Hang / GPU 调试指南

SGLang 分布式 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_ctrun_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=1CUDA_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。

坑(实测):

  1. 忙 kernel 的 dump 极慢:~16s/SM × 132 SM ≈ 35min(H800,GPU 空闲时 ~1s)。自动链默认只等 60s——要拿到完整 dump,把 SGLANG_CUDA_COREDUMP_BEFORE_CRASH_WAIT_SECS 调到 2400+,或用软 watchdog + 手动触发。
  2. SIGABRT/SIGQUIT 本身不产 GPU coredump,必须写 FIFO,或 CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1(仅 GPU 异常崩溃时,普通 hang 不触发)。另有 SGLANG_CUDA_COREDUMP=1(SGLang 内置,异常时自动产,同样只管崩溃)。
  3. 别用 cuda-gdbgenerate-core-file/gcore:产出 48GB CPU+GPU 混合 core,target cudacore 打不开(ELF 校验失败)。
  4. 容器 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-marker

hang 时时间线断在卡住的 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 列表

两个坑:

  1. 埋点位置 vs graph:埋在录图段内,重放期被跳过;埋在 scheduler step 之间(GPU idle)安全。
  2. 隐式同步:.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