D2H 怎么会 hang 呢!

D2H 怎么会 hang 呢!

💡 原文中文,约7200字,阅读约需18分钟。
📝

内容提要

作者在分布式推理系统中遇到cudaMemcpyAsync永久挂起的问题,表现为GPU空闲但CPU自旋等待。通过排查,发现是pageable内存拷贝导致驱动轮询卡死,最终用pinned buffer替换所有pageable拷贝来规避,但未解决根本原因。

🔎

延伸解读

诊断判据:区分积压与真死锁

作者指出,tokenizer 的告警“Still waiting for request output”并不可靠,因为它无法区分请求积压和真正的挂起。真正可靠的判据是 scheduler 的 Prefill/Decode batch 日志永久停摆。这一经验提醒我们,在分布式系统中,告警信息可能混淆不同故障模式,需要找到更本质的指标来确认故障。

pageable 拷贝的隐藏风险

文章揭示,cudaMemcpyAsync 在 pageable 内存上可能永久挂起,且与尺寸、方向无关,从 5KB 到 192MiB 的 D2H 和 H2D 拷贝都曾触发。这提醒开发者,在使用 CUDA 异步拷贝时,应优先考虑使用 pinned memory,避免依赖驱动内部的 staging 机制,以降低潜在的自旋风险。

自动化复现与修复的实践

作者利用 qoder 自动化工具进行无人值守的复现实验,并构建了“发现→定位→修→重跑”的流水线,通过 watchdog 自动抓取栈信息,快速定位并修复问题。这种自动化方法显著提高了排查效率,值得在类似复杂系统问题中借鉴。

Q&A

cudaMemcpyAsync 真的会永久挂起吗?

是的,文章作者在实际系统中遇到了 cudaMemcpyAsync 永久不返回的情况,表现为 GPU 空闲但 CPU 在用户态自旋等待,最终通过替换为 pinned buffer 规避。

如何判断分布式推理系统中的 scheduler 是否真正卡死?

文章指出,不能仅依赖 tokenizer 的告警,因为请求积压也会触发相同告警。真正可靠的判据是 scheduler 的 Prefill batch / Decode batch 日志永久停摆,不再恢复。

D2H 拷贝挂起时,GPU 和 CPU 的状态是怎样的?

GPU 利用率极低(如 2%/0%/0%/0%),没有 kernel 在运行;CPU 主线程在用户态自旋,不是睡眠等待,而是空转。

文章作者如何利用 qoder 进行自动化排查?

作者将排查过程外包给 qoder,设置 goal 和 turn_budget,让它自动拉起 sglang、造流量、监控日志、抓取 py-spy 栈,并将结果记录到 NOTES.md 和 git 提交,作者只负责提要求和读结论。

文章最终得出的结论是什么?

结论分两部分:怎么死的已知,是用户态轮询等待一个永不翻转的状态字;为什么死未知,因为所有复现实验都未能复现。最终采取规避策略,将 pageable 拷贝替换为 pinned buffer。

文章中提到哪些常见的 D2H 挂起排查误区?

常见误区包括:误以为 stream 中有未完成的 kernel 导致等待(但通过 locals 和 synchronize 排除);误以为 CPU 在睡眠等待 GPU 通知(实际是自旋);误以为被其他 rank 拖住(但因果方向相反,是其他 rank 在等它)。

文章中的打地鼠成绩单说明了什么?

成绩单记录了多次线上抓到的自旋现场,涉及不同尺寸和方向的 pageable 拷贝,说明尺寸和方向都不是判据,共同原因是驱动 pageable staging 池的问题。每修掉一处,下一处仍为 pageable 的拷贝就会出问题。

🏷️

标签

➡️

继续阅读