前阵子某高校的异构计算实验室放出了一段内部测试报告里面有一个结论让圈子里的人都愣了一下某种极端计算负载能让整台 8 卡 GPU 服务器在运行过程中出现长达 11 秒的系统级无响应。第一次看到这个数字我第一反应是“标题党”但后来拿到报告细节又在自己这边的小规模环境里做了完全复现才发现这背后根本不是什么简单的性能抖动而是驱动、硬件、调度三方同时踩进了一个连锁坑里。这篇文章我想把整个现象、复现过程、根因分析和日常防护都拆开来讲对跑 GPU 集群、做并行计算优化或者正在被“GPU 利用率突然掉零”折磨的运维朋友应该都有参考价值。1. 现象看起来像“死机”其实只是驱动层的连锁崩溃1.1 “整台服务器卡住”到底是什么状态先说清楚一件事这 11 秒并不是传统意义上服务器关机或操作系统崩溃而是一种更隐蔽的“假死”状态。测试时的现场是这样的GPU 利用率从 97% 瞬间掉到 0%进程没有退出SSH 敲命令开始出现秒级延迟厂商自带的管理接口读取设备状态直接超时。我刚开始拿到这段录像以为是机器死机了但过去看机器面板电源灯正常网络也能 ping 通只是任何和 GPU 相关的操作都像被按了暂停键。大概 11 秒之后所有指标恢复任务继续往下跑没有重启也没有报硬件错误。这种状态其实比“直接宕机”更麻烦因为整个系统仍在运行但对外表现为“卡死”。尤其在生产环境里调度器会以为 GPU 任务还在跑实际上任务已经卡在驱动层一动不动。1.2 卡顿瞬间抓到的几个现场证据我们后来在多台机器上抓过日志有几个共通的证据内核日志里出现“计算单元长时间未返回完成通知”之类的告警随后驱动进入恢复流程。驱动的跟踪事件显示卡顿窗口里 GPU 完成中断数量为零也就是没有任何指令被真正执行完。恢复后的第一瞬间GPU 利用率出现一个短促的突刺说明被积压的任务在重新排队执行。这些证据放在一起基本可以下一个结论这 11 秒里GPU 硬件本身没有烧坏也没有掉卡而是驱动层判断某个执行单元失控切断了正常调度进入了一套“自我抢救”的流程。整个抢救过程卡住了进而影响到了所有想和 GPU 打交道的进程。2. 复现实验的具体设计与观察指标2.1 测试环境与被压测的负载形态参与复现的机器是一台 8 卡 GPU 服务器双路 CPU256GB 内存用的是一套主流并行计算框架。负载不是常见的矩阵乘法或者大模型训练而是一个内部压力测试脚本在显存里分配一个全局计数器然后让大量线程同时对这个地址做小粒度原子累加。这种写法本身在并行编程里很常见正常场景下不会有几千个线程同时挤同一个计数器。但压力测试就是把并发数放大到极端放大之后问题就彻底暴露了。我用伪代码描述一下负载的核心逻辑分配一个全局计数器 counter[0] 启动 N 个线程 每个线程重复执行 atomic_add(counter[0], 1)看起来人畜无害但实际上这行代码就是让整台服务器卡住 11 秒的起点。2.2 从单卡卡顿到整机卡顿的复现路线我们做了三个等级的复现。负载等级并发规模停顿时长恢复方式单卡单任务256 万线程0.8 秒自动恢复单卡多任务512 万线程3.2 秒自动恢复多卡同步任务多卡同时压测并相互等待11 秒自动恢复第一级出现的停顿很短不仔细看以为是调度波动第二级开始管理工具读设备状态就开始卡了到第三级把多张卡同时压上并且让它们通过高速互联总线做同步等待整个服务器才出现了完整的 11 秒无响应窗口。这个递进关系很关键说明问题不是单点故障而是“局部卡顿经过多卡联动被放大”的过程。2.3 监控手段的局限利用率不能说明一切厂商自带的管理工具默认一秒刷新一次在 11 秒停顿窗口内它显示出来的 GPU 利用率是 0%。如果只盯这个指标你会以为 GPU 闲着没事干但实际情况恰恰相反硬件并没有真正空闲只是管理通道被驱动层的锁堵塞了真实状态报不出来。我们后来改用 Linux 内核自带的性能采样工具去看 CPU 侧热点再用驱动提供的事件接口去对齐停顿窗口才把问题定位到毫秒级。这里要特别提醒GPU 利用率掉零不一定代表 GPU 在歇着也可能是它想报告自己很忙但报告不出来。监控系统如果只依赖利用率这种 11 秒级别的故障会被直接当成“任务正常结束又重开”日志里什么都留不下来。3. 根因拆解同步锁、原子操作与驱动看门狗的三角关系3.1 同地址原子操作为什么会拖垮整个芯片GPU 的原子操作本身是很强的特别是分散在不同地址上的原子操作硬件可以并行处理。但问题是所有线程都涌向同一个地址时情况就不一样了。我们可以想象一个只有一名业务员的小柜台正常业务窗口有几十个每个人去不同窗口办事互不干扰。但你把所有人都叫到同一个窗口队伍只能一级一级往前挪。GPU 内部的原子执行单元也是这样对同一个内存地址的原子操作必须严格串行化后一个操作要等前一个操作完成硬件并行度直接变成 1。当线程数量膨胀到百万级别这个“单窗口队伍”就越排越长。执行单元被卡住指令无法退休后面的新指令全部阻塞整个计算单元的资源被占满。这就像一个路口被一辆堵死的车占住后面的车无论走哪条道都绕不开。有人会问为什么不是所有原子操作都会卡因为分散地址的原子操作可以并行同一个地址的操作只能排队。压测脚本恰恰把所有线程的访问地址收拢到一个点上等于人为制造了一个极端瓶颈。3.2 驱动看门狗的超时判定与恢复驱动层为了防止单个失控任务拖死整个系统通常会有一个看门狗机制。它会定期检查内核执行状态或者检查提交到设备的指令有没有完成。一旦发现某个执行单元长时间没有返回看门狗就会介入。介入的第一步一般是尝试抢占。如果可以抢占驱动会把目标内核停掉然后恢复调度。但在这次实验里问题恰恰出在“想抢却抢不下来”执行单元内部堆满了等待原子操作的线程硬件资源被占满驱动发出抢占指令后硬件无法在预期时间内完成响应。这时候驱动就会进入更高级别的恢复流程也就是设备重置。重置过程需要等待硬件把所有在途操作清空而这个清空动作同样要等那个原子操作队列排完。于是看门狗的超时等待、抢占尝试、重置等待被一层层叠加起来整个恢复流程本身也变成了一个长时间阻塞的过程。3.3 11 秒这个数字从哪里来很多人第一眼看到 11 秒会觉得夸张想确认是不是厂商的某个“神秘超时时间”。根据报告和我这边的复测11 秒并不是某个单一的定时器值而是三段耗时的叠加阶段大致耗时实际发生的事看门狗检测5 秒左右等待执行单元返回完成信号超时抢占与重置4 秒左右发出抢占指令、等待硬件清空、触发设备重置驱动重初始化2 秒左右重新加载设备状态、重建上下文队列这三个数字加起来正好是 11 秒。换一块不同型号的 GPU或者换一个驱动版本这个总时长可能会变成 8 秒、13 秒、15 秒但底层逻辑是一样的检测超时加上恢复流程被阻塞两者叠加形成了系统级的短暂“失联”。这个结论也解释了为什么不是 1 秒或者 2 秒就能缓过来因为恢复流程本身也需要资源而在资源被占满的情况下驱动只能等硬件自己慢慢把队列排完。4. 从用户态到内核态一条完整的故障链路4.1 请求在驱动队列里的排队真相正常情况下一个 GPU 任务从用户态提交到真正执行要经过这么一条路径用户程序把指令打包提交给运行时库运行时库调用驱动接口驱动把指令写进硬件命令队列硬件前端取队列里的指令派发到计算单元执行执行完成后产生一个中断通知 CPU 回收结果。在这次卡顿里中断这一步直接断了。原子操作在执行单元内部排队指令无法完成所以没有完成中断产生。驱动在等待中断时陷入超时进入错误处理流程。而错误处理流程需要先拿到驱动内部的一个全局锁这个锁同时又被其他等待任务占用于是所有后续想调用 GPU 接口的进程全都被堵在驱动入口。这就是为什么卡顿期间不只是那个压测任务卡住连管理工具、状态查询、甚至其他和 GPU 无关但依赖 GPU 驱动的服务全部一起无响应。GPU 从一个“计算硬件”变成了“系统级瓶颈”。4.2 多卡互联让局部卡顿演变成整机无响应单卡场景下卡顿最多影响跑在该卡上的任务和查询该卡状态的进程。但多卡服务器里通常存在一套高速互联总线多张卡之间可以做内存共享或者同步协作。当一张卡先进入卡顿状态它发送给其他卡的同步请求就会停下来其他卡会一直等它的回应。等待的时间越长积压的同步操作越多最终所有卡都进入相互等待状态。更麻烦的是驱动的重置流程往往把多张卡作为一个整体来管理。重置一张卡之后驱动可能还需要逐个检查其他卡的状态而这时候其他卡也处于等待中一卡卡住全机陪跑。这也是为什么复现实验做到第三级时才出现完整的 11 秒整机无响应单卡压测的卡顿根本不会波及到整机层面。5. 这类长卡顿的应急处理与日常防护5.1 先分清“假死”和“真死”再决定要不要重启遇到 GPU 服务器突然卡住第一反应别急着强制重启。强制重启会丢掉内核日志和驱动跟踪数据后续定位原因会变得非常困难。我的建议是先确认 SSH 还能不能通能通就打开另一个终端窗口把内核日志流盯住然后等 10 到 20 秒看看是不是自动恢复。如果自动恢复了第一件事是把日志保存下来再检查任务状态。如果超过 20 秒还没恢复再考虑针对 GPU 设备做软重置最后才考虑重启机器。很多时间里这种卡顿都是可恢复的强行断电反而会把本来没问题的硬件搞出问题。5.2 负载侧可以做的三类限制从这次复现里我总结出三类在实际生产中能落地的限制手段第一尽量避免在同地址上做超大并发的原子操作。如果确实需要归约统计可以让每个线程块先算局部结果再用少量线程去合并全局计数器把并发竞争从“百万线程抢一个地址”降成“几十个线程抢一个地址”。第二利用驱动提供的抢占模式。把任务标记为可抢占或者调整内核执行超时阈值让看门狗能够更早介入。不过这里要谨慎阈值调得太短会导致正常的长内核任务被误杀需要按业务负载的耗时分布来定。第三在任务调度层面对单个任务可占用的 GPU 时间片做限制。尤其是共享 GPU 服务器上给每个任务设置最大执行时长发现超时就主动终止避免一个异常任务把整机拖进无响应状态。5.3 监控与告警配置的侧重点通过这次复现我强烈建议在 GPU 监控里多加三个指标驱动事件错误计数包括看门狗超时、设备重置、上下文丢失等。完成中断数量正常情况下应该是持续变化的如果一段时间内中断数恒为零说明执行链路已经断了。管理通道读取延迟这个指标能直接反映驱动锁是否被卡住。告警规则也要改一改不要只看“GPU 利用率为 0%”要配合“显存有分配但利用率持续为 0”以及“驱动错误计数增加”来一起判断。我后来在监控脚本里加了一条连续三个采样周期 GPU 利用率为 0%且显存占用没有下降就触发告警并自动抓取内核日志。这个配置帮我们抓到了好几次早期征兆比单纯盯利用率好用得多。6. 我在复现之后留下的几点操作习惯做完整个复现以后我有几个很深的体会。第一个是不再轻易相信单一指标。GPU 利用率、显存占用、进程状态任何一个单独拿出来都不能说明问题必须把驱动日志、中断计数、管理通道延迟放在一起看。很多时候系统告诉你“一切正常”只是它自己也卡住了没来得及说。第二个是做多卡压测之前我一定会先跑一段低并发的原子操作压力测试从单卡开始往上加。不要一上来就全卡同步压否则问题一旦出现就是整台机器一起卡给你看排查难度翻倍。第三个是在共享 GPU 平台上绝对不能再让一个任务无限期占用整台设备。就算是内部测试也要给任务加执行时长限制和超时退出机制。一个任务卡住不可怕可怕的是整个平台所有人的任务都被它拖住。最后再分享一个小技巧如果条件允许可以在驱动层开启更细粒度的跟踪日志平时不常开但压测和排查阶段开上就能在故障窗口里拿到非常完整的调用栈和硬件事件时间线。这次能把这 11 秒的链路彻底拆明白靠的就是这些细节日志。下次你再遇到 GPU 服务器“突然卡住”不妨先按这个思路查一遍大概率能少走不少冤枉路。
阅读完成 · 觉得有帮助?