尧图精选

SGLang 分布式推理卡死(Hang)调试全指南:从 py-spy 到 CUDA Coredump 的定位与修复方法论

🕒 发布时间:2026/9/10 22:51:46 📁 来源:尧图网络
SGLang 分布式推理卡死Hang调试全指南从 py-spy 到 CUDA Coredump 的定位与修复方法论【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang导读当 SGLang 在 TP张量并行/ PP流水线并行/ DP数据并行/ EP专家并行的多卡分布式推理中出现进程冻结、集体通信Collective超时或无响应时根因通常是各 rank 的状态发生分歧state divergence导致 AllGather / AllReduce / Broadcast / Barrier 等集体操作互相等待形成死锁。本文基于 SGLang 官方调试方法论系统讲解一整套可落地的分布式卡死排查流程先用 watchdog、py-spy 与 CUDA coredump 确认卡死位置再用按 rank 分文件的结构化日志对比出状态分歧点最后通过二分回溯定位根因并给出常见修复模式。读完本文你将掌握一套在任何多卡 SGLang 运行挂起时可直接复用的六步调试实战方案。为什么多卡推理会卡死分歧Divergence的本质分布式推理中的卡死极少是真正的死机而是各 rank 状态不一致的必然结果。当一个 rank 与其它 rank 的本地状态batch 结构、张量形状、分支路径不再同步时集体通信操作就会永远等不到所有参与者形成死锁。SKILL 文档归纳了四类最常见诱因尺寸不匹配Size mismatch不同 rank 向同一个集体操作传入了不同大小的张量例如extend_seq_lens在各 rank 上不一致导致 AllGather 尺寸不同分支分歧Branch divergence某个 rank 进入了集体操作而另一个 rank 因条件判断不同跳过了它级联状态漂移Cascading state drift一个微小的非确定性例如浮点运算逐步传播最终演变成不同的 batch 结构资源耗尽Resource exhaustion某个 rank 发生 OOM 或崩溃其余 rank 永远等待它的通信消息。理解这一点后调试的核心思路就非常清晰找到第一个分歧发生的位置first diverge point而不是盯着最后的死锁现场。前置准备调试工具链py-spypip install py-spy或使用系统包。附加到正在运行的进程需要 root 权限或CAP_SYS_PTRACE能力多卡场景下通常以 root 运行服务进程即可cuda-gdb随 CUDA Toolkit 分发需要确保它位于PATH中用于解析 GPU coredump。Step 1确认并定位卡死位置1a. Watchdog 自动转储与 py-spySGLang 内建了调度器 watchdog实现见 python/sglang/srt/utils/watchdog.py在超时时自动执行 py-spy 转储。Scheduler 初始化 watchdog 的入口位于 python/sglang/srt/managers/scheduler.py其包装函数create_scheduler_watchdog定义于 python/sglang/srt/managers/scheduler_components/invariant_checker.py。超时后你会看到类似输出Scheduler watchdog timeout (self.watchdog_timeout300, self.softFalse)py-spy 转储会显示每个线程的栈轨迹挂起的线程通常阻塞在 CUDA 同步或 NCCL 集体操作上Thread (active): MainThread cuStreamSynchronize (libcuda.so) ... forward_extend (model_runner.py)SGLang 的 watchdog 有两种模式对应 watchdog.py 中的WatchdogRaw硬 watchdogsoftFalse默认先转储 py-spy 轨迹再向父进程发送SIGQUIT将其杀死源码中parent_process.send_signal(signal.SIGQUIT)避免服务无限期挂死软 watchdogsoftTrue仅记录超时日志而不杀进程为你手动附加调试器或收集 coredump 争取时间。对应到启动参数定义于 python/sglang/srt/arg_groups/fields/device.py参数默认值作用--watchdog-timeout300秒若一次 forward batch 超过该时长服务崩溃以防挂死硬 watchdog--soft-watchdog-timeoutNone若设置超过该时长只转储调试信息而不崩溃软 watchdogpy-spy 的转储逻辑实现在 python/sglang/srt/utils/cudacore_pyspy_dump_utils.py 的pyspy_dump_schedulers中它会递归查找所有sglang::scheduler子进程并分别执行py-spy dump --native --pid pid先尝试带--native以便展示 C/C 帧失败后回退到纯 Python 帧。如果 watchdog 没有触发可以手动转储py-spy dump --pid scheduler_pid1b. NCCL 调试日志export NCCL_DEBUGINFO export NCCL_DEBUG_SUBSYSCOLL关注挂起前最后一条被记录的集体操作日志。尺寸不匹配的表现通常是一个 rank 在等待而另一个 rank 从未进入该操作。1c. CUDA Coredump定位阻塞的 GPU kernel当进程挂起时可以在运行前设置以下环境变量按需触发 GPU coredump 查看哪个 kernel 卡住export CUDA_ENABLE_USER_TRIGGERED_COREDUMP1 export CUDA_COREDUMP_PIPE/tmp/cuda_pipe_%h_%p export CUDA_COREDUMP_FILE/tmp/cuda_coredump_%h_%p export CUDA_COREDUMP_SHOW_PROGRESS1 export CUDA_COREDUMP_GENERATION_FLAGSskip_nonrelocated_elf_images,skip_global_memory,skip_shared_memory,skip_local_memory,skip_constbank_memory进程挂起期间通过/proc/pid/fd/找到 coredump 管道并写入以触发转储ls /proc/pid/fd/ -la 2/dev/null | grep cuda_pipe dd if/dev/zero bs1M count1 /tmp/cuda_pipe_hostname_pid如果不需要保持进程存活kill -SIGABRT pid也能触发 CUDA coredump但会终止进程。另外需要注意SGLang 仓库中的trigger_cuda_user_coredump工具函数见 cudacore_pyspy_dump_utils.py会校验CUDA_ENABLE_USER_TRIGGERED_COREDUMP1必须在 CUDA 初始化前设置否则管道不会创建——所以上述环境变量必须在启动服务之前就导出。随后用cuda-gdb打开转储文件cuda-gdb --batch -ex target cudacore coredump_file加载后它会立即显示哪个 kernel 卡住例如Opening GPU coredump: coredump_file [Current focus set to CUDA kernel 0, grid 622721, cluster (4,0,0), block (16,0,0), thread (64,0,0), device 0, sm 0, warp 0, lane 0] #0 0x00007f8029b2b040 in ncclDevKernel_AllGather_RING_LL(ncclDevKernelArgsStorage4096ul)(24,1,1),(512,1,1) ()一个真实案例是coredump 显示卡死发生在 NCCL AllGather而非计算 kernel中结合 py-spy 栈指向LogitsProcessor.forward→tensor_model_parallel_all_gather即可判定是 TP rank 之间的 AllGather 尺寸不匹配。tensor_model_parallel_all_gather的实现在 python/sglang/srt/distributed/communication_op.py它直接调用get_tp_group().all_gather(input_, dim)——同一文件还提供了tensor_model_parallel_all_reduce、tensor_model_parallel_gather、broadcast_tensor_dict等 TP 通信原语调试时需要根据栈轨迹确认具体命中的是哪一个。1d. 明确集体操作的身份从栈轨迹和日志中确认三件事哪个集体操作挂起AllGather / AllReduce / Broadcast哪条代码路径调用了它例如LogitsProcessor、tensor_model_parallel_all_gather是尺寸不匹配还是参与者缺失。Step 2按 Rank 分文件日志Per-Rank Logging这是整套方法论的核心技巧每个 rank 写自己的日志文件之后才能做 diff。建立调试文件import os _debug_files {} def get_debug_file(rank): key frank{rank} if key not in _debug_files: _debug_files[key] open(f/tmp/debug_rank{rank}.log, w) return _debug_files[key]用环境变量控制开关避免生产环境的额外开销。注意SGLANG_DEBUG_HANG不是SGLang 内建的环境变量在仓库源码中搜索不到任何对应实现你需要在自己要插桩的代码里自行添加这个检查if os.environ.get(SGLANG_DEBUG_HANG): f get_debug_file(rank) f.write(fEVENT_NAME key1{val1} key2{val2}\n) f.flush()记录什么在关键状态变更点记录结构化事件f.write(fSCHED_BATCH step{step} num_reqs{n} extend_lens{lens}\n) f.write(fVERIFY predict_hash{hash} accept_len{alen}\n) f.write(fCACHE_INSERT rid{rid} num_tokens{n}\n)事件名使用统一的大写前缀方便 grep 和 diff。对张量做哈希而非直接转储import hashlib h hashlib.md5(tensor.cpu().numpy().tobytes()).hexdigest()[:8] f.write(fLOGITS logits_hash{h}\n)对于 token ID 列表用字符串编码即可h hashlib.md5(str(tensor.tolist()).encode()).hexdigest()[:8]避免隐式同步Implicit Synchronizationtensor.cpu()、tensor.tolist()、tensor.numpy()都会触发 CUDA 同步这会带来两个风险改变时序可能掩盖或移动卡死位置如果日志点位于两个必须背靠背连续执行的集体操作之间同步本身就可能造成死锁。因此优先记录已经在 CPU 上的值Python 整数、列表长度、请求 ID。必须对 GPU 张量做哈希时选择 GPU 已经空闲的时机例如两个 scheduler step 之间而不要放在模型 forward 内部。Step 3Diff 找出分歧点基础 diff# 提取特定事件类型 grep ^VERIFY /tmp/debug_rank0.log /tmp/v_r0.txt grep ^VERIFY /tmp/debug_rank1.log /tmp/v_r1.txt diff /tmp/v_r0.txt /tmp/v_r1.txt | head -20统计事件数量grep -c ^VERIFY /tmp/debug_rank*.log如果各 rank 计数不同说明某个 rank 多执行了若干迭代——这本身就是分歧信号。定位第一个分歧第一个 diff 行会精确告诉你各 rank 在哪一步开始分道扬镳。它之前的所有行都是相同的——根因就在这一步或更早。Step 4二分回溯根因Binary-Search the Root Cause找到分歧事件后沿着数据流向前回溯4a. 列出该操作的输入为每个输入添加哈希日志f.write( fOP_INPUTS input_a_hash{h_a} input_b_hash{h_b} finput_c_hash{h_c} input_d_hash{h_d}\n )4b. 跨 rank 对比输入哈希。有些输入一致、有些不一致——不一致的那个输入就是分歧进入的地方。4c. 递归对不一致的输入追溯它是在哪里产生的重复哈希其输入 → 跨 rank diff → 找出分歧输入的过程直到抵达根因。Step 5常见根因与修复模式浮点非确定性Floating-Point Non-Determinism症状所有逻辑输入都一致AllGather 后的 logits 相同但派生的浮点值softmax、概率在不同 GPU 上有细微差异。典型例子EAGLE 投机解码场景下F.softmax→top_k_renorm_prob→top_p_renorm_prob在各 GPU 上产生略微不同的target_probs这些重归一化函数实际被 SGLang 的采样层调用见 python/sglang/srt/layers/sampler.py随后采样 kernel 选出不同的 token流入output_ids→ radix cache → 不同的前缀匹配深度 → 不同的extend_seq_lens→ AllGather 尺寸不匹配 → 卡死。随机数分歧Random Number Divergence症状使用torch.rand的操作在各 rank 上产生不同值。修复在 rank 0 上生成后广播或使用共享种子。条件代码路径Conditional Code Paths症状某个条件例如显存检查、队列长度在各 rank 上求值结果不同导致一个 rank 进入集体操作而另一个跳过。修复在分支前同步条件值或重构代码确保所有 rank 走同一条路径。流水线并行PPSend/Recv 不匹配症状PP 场景下某一 stage 发出了send而下一 stage 永远没有对应的recv或反之导致双方无限阻塞。与 TP 卡死集体操作不匹配不同PP 卡死通常涉及点对点操作。修复确保所有 stage 在 microbatch 数量以及每个 microbatch 的 send/recv 调用序列上达成一致。Step 6验证修复对失败的测试多次运行以确认修复稳定。间歇性卡死需要更多次验证——一个约 30% 概率卡死的测试至少需要连续 10 次通过才能建立信心。快速参考表技术手段适用时机py-spy dump第一步——查看每个 rank 卡在何处NCCL_DEBUGINFO确认是哪个集体操作及尺寸CUDA coredump cuda-gdb查看哪个 GPU kernel 被阻塞按 rank 分文件日志随时间对比各 rank 状态张量哈希高效跨 rank 对比大张量对提取事件做diff精确定位分歧发生的步骤broadcast(result, src0)修复浮点或采样非确定性关键源码索引python/sglang/srt/utils/watchdog.pyWatchdog 硬/软模式实现、SIGQUIT触发逻辑python/sglang/srt/utils/cudacore_pyspy_dump_utils.pypyspy_dump_schedulers与trigger_cuda_user_coredump工具函数python/sglang/srt/managers/scheduler_components/invariant_checker.pycreate_scheduler_watchdog及超时时的调度器状态转储python/sglang/srt/managers/scheduler.py调度器 watchdog 初始化入口python/sglang/srt/arg_groups/fields/device.py--watchdog-timeout/--soft-watchdog-timeout参数定义python/sglang/srt/distributed/communication_op.pytensor_model_parallel_all_gather等 TP 通信原语python/sglang/srt/layers/sampler.pytop_k_renorm_prob/top_p_renorm_prob的调用点浮点分歧的典型来源。结语分布式卡死调试的本质是把神秘的集体操作死锁转化为可对比的 rank 状态差异。SGLang 的 watchdog 与 py-spy 机制--watchdog-timeout/--soft-watchdog-timeout提供了第一道自动化防线CUDA coredump 能把 GPU kernel 级别的阻塞现场冻结下来而按 rank 分文件的哈希日志配合二分回溯则能在分钟级内把根因收敛到某一具体操作。掌握这套方法论后无论是 TP 的 AllGather 尺寸不匹配、投机解码的采样漂移还是 PP 的 send/recv 失配都能按同一套框架系统性定位与修复。【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →