尧图精选

PyTorch多GPU训练Bus error排查与修复指南

🕒 发布时间:2026/9/11 5:19:42 📁 来源:尧图网络
当初在多卡机上半夜跑训练日志里 loss 还在正常下降下一秒终端被一行Bus error (core dumped)刷屏文件系统里多出一个巨大的 core 文件那种“代码看起来没毛病但它就是死给你看”的体验我猜很多用 PyTorch 做模型训练的人都经历过。Bus error (core dumped)在 PyTorch 多 GPU 训练里并不罕见尤其容易出现在你用torchrun启动 DistributedDataParallel、开了多个 DataLoader worker、或者把训练进程塞进 Docker 容器里跑的时候。它不是普通的 Python 异常也不是模型代码里的报错而是一个操作系统层面的信号说明进程在访问某块内存映射时底层物理机制没有把数据成功交到你手上。换句话说问题往往不在你的forward()里而在训练系统周边的内存、共享内存、GPU 通信链路这类“基础设施”上。这篇文章我不会讲空泛的理论直接带你走一遍我实际操作中的排查流程先搞清楚 Bus error 到底是什么再按概率从高到低列出多 GPU 训练里最常见的触发根因然后给出可以照着敲的排查命令和修复配置最后附上几条真正帮我省过时间的避坑经验。无论你是在复现 YOLO 系模型、调 EasyOCR 训练自己的数据集还是自己写 DDP 训练脚本这套思路都通用。1. 先搞懂 Bus error 和多卡训练的关系1.1 Bus error 和段错误不是一回事第一次遇到 Bus error 的人很容易把它和 segmentation fault 混在一起因为两者最终都表现为进程崩溃、生成 core 文件。但它们的机制完全不同。Segmentation fault 是进程访问了“虚拟地址空间里不存在”的页面好比你去图书馆按索书号找到位置结果发现这个柜子从来不存在管理员直接说你找错了而 Bus error 是进程尝试访问一个“存在映射关系、但物理设备无法完成这次访问”的地址更像柜子编号都在系统里登记着可你刷卡开门时柜门被焊死了或者柜子整个被搬走了。内核只能通过SIGBUS信号告诉你这次访问没法完成。在 PyTorch 多 GPU 训练里最常踩中 Bus error 的地方是共享内存和内存映射文件。DataLoader 的多个 worker 之间传递张量、DDP 的梯度通信、NCCL 在单机多卡场景里的内部缓冲都会用到/dev/shm或者mmap机制。一旦这些共享内存被写满、权限异常、或者对应的物理页被提前释放进程就会收到 Bus error。1.2 为什么多卡训练是重灾区很多人的第一反应是把锅甩给显卡驱动实际上多卡训练之所以更频繁触发 Bus error是因为它把“多进程 共享内存 GPU 通信”这三个风险因素叠在了同一时刻。单卡训练时主进程独占一块 CUDA context即使使用多个 CPU worker数据交换路径也比较简单。但到了多卡DDP 会为每一张卡拉起一个独立进程进程之间需要不断做梯度 allreduce。NCCL 在单机多卡下会优先尝试通过共享内存和 NVLink/PCIe 做点对点传输这个过程中产生的临时缓冲区、队列、锁全部依赖系统的共享内存和内存映射能力。任何一个环节的资源不够或链路异常崩的不只是某一张卡对应的进程而是整个训练作业。所以多卡训练里的 Bus error 往往不是“某个线程写越界”这种代码 bug而是资源上限、通信链路、驱动状态三者共同作用下的系统级故障。这也是为什么很多人把 PyTorch 重装好几遍还是崩因为在错误方向上使劲。2. 触发 Bus error 的常见根因和判断线索2.1 /dev/shm 共享内存不足是头号杀手我处理过的大多数多 GPU 训练 Bus error最终都指向同一个地方/dev/shm空间被耗尽。/dev/shm是 Linux 下的共享内存文件系统默认大小通常是物理内存的一半但如果你把训练跑在 Docker 容器里情况就不一样了。Docker 默认只给容器分配 64MB 的/dev/shm这一点在很多使用 PyTorch 的镜像里都没有被主动改过。DataLoader 只要开了num_workers0并且pin_memoryTrue或者使用 SharedMemory 传递数据就会不断往/dev/shm写入数据块。我自己遇到过这样的情况训练一个图像分类模型每张样本预处理后大约 2MBbatch_size32一个 batch 大约 64MB。DataLoader 开了 8 个 worker、prefetch_factor2 时共享内存里需要同时容纳大约num_workers × prefetch_factor × batch_size × 单样本大小的数据算下来是 8×2×64MB1GB。容器里只有 64MB训练开始后过不了多久就把共享内存写满然后某个 worker 在 mmap 写入时撞上 Bus error整个进程直接崩溃。判断方法非常简单在容器里执行df -h /dev/shm如果看到大小是 64M基本可以锁定问题。即便不是在容器里长期跑大任务也要留意这个目录的剩余空间。2.2 DataLoader 和 pin_memory 叠加后的内存放大除了/dev/shm本身不够用pin_memoryTrue带来的页锁定内存压力也容易被忽略。pin_memory的作用是把 CPU 侧张量锁在物理内存里不让它被换出到 swap这样 GPU 拷贝数据时能走 DMA 通道速度更快。但在多卡训练中如果每个 CPU worker 都在做 pin memory并且训练进程本身已经把大量显存占满此时 CUDA 驱动申请新的锁页内存可能会失败。某些驱动版本在这种情况下不会返回一个优雅的 Python 异常而是直接让进程收到 SIGBUS。还有一点num_workers开得过大并不会让数据加载线性变快。每个 worker 都是从主进程 fork 出来的它们会把父进程的内存空间复制出一份“视图”包括 PyTorch 的 CUDA context 和 DataLoader 的缓冲队列。在多卡环境下这些 worker 占用的 CPU 内存和共享内存会成倍放大。如果你发现 Bus error 总是在一个 epoch 刚开始、或者数据加载阶段出现优先怀疑这里。2.3 NCCL 通信链路异常DDP 多卡训练依赖 NCCL 做梯度同步。NCCL 在单机多卡时会尝试启用 P2P 传输也就是让 GPU 之间直接通过 NVLink 或 PCIe Bar 映射访问对方显存。这个机制对性能很有帮助但在某些硬件组合或驱动版本下并不稳定。如果显卡型号不一致、NVLink 桥接松动、PCIe switch 配置有问题或者主板上开启了 IOMMU 导致 DMA 映射异常NCCL 初始化或运行过程中就可能触发 Bus error。这类崩溃有一个特征日志里不一定有明确的 CUDA 报错通常停在 NCCL 的初始化日志附近或者在第一个 allreduce 环节就崩。遇到这种问题可以先关闭 P2P 试试用环境变量NCCL_P2P_DISABLE1强制走共享内存和 PCIe 的拷贝路径。虽然速度会慢一些但能大幅提高稳定性。如果关了 P2P 就不崩了基本可以确认是 GPU 间直连链路的映射问题。2.4 GPU 显存 OOM 之后的连锁崩溃还有一个容易被误判的情况GPU 显存先 OOM随后进程才以 Bus error 的形式崩溃。正常情况下PyTorch 显存不足会抛出torch.cuda.OutOfMemoryError这是一次可捕获的 Python 异常。但在多卡训练中如果某一张卡的显存率先耗尽而 DDP 的通信线程和 DataLoader 的 worker 还在继续尝试读写内存可能触发更底层的分配失败。此时 CUDA driver 内部状态已经不稳定错误信号传导到 CPU 侧就变成了 SIGBUS。这类问题不能只盯着 Bus error 本身排障。你需要在前面的日志里找OutOfMemoryError或CUDA error: out of memory的字样。如果有说明真正的根因是显存不够Bus error 只是后续连锁反应。用自动混合精度、梯度累积、减小 batch_size 把显存峰值压下来比排查共享内存更有效。2.5 驱动、固件或硬件不稳定最后一种情况最让人无奈软件和配置全都没问题但某张显卡或者整台服务器的硬件已经不稳定了。在多卡服务器上显卡长期满载运行供电模组老化、散热不足、PCIe 金手指接触不良、显存颗粒损坏都可能让训练在随机时间点崩溃。如果 Bus error 出现的时间完全没有规律今天跑 10 分钟崩明天跑 3 小时也不崩而且重装驱动、调整/dev/shm、关闭 P2P 都没有本质改善就要认真考虑硬件因素。判断硬件问题可以用两条命令一是dmesg -T | grep -iE nvrm|xid|pcie看内核日志里有没有硬件报错二是运行 GPU 压力测试观察崩溃是否在高负载下复现。这种情况不是写代码能解决的该换卡就换卡该查供电就查供电。3. 定位实操我的五步排查路线3.1 第一步先量内存和共享内存我拿到一个 Bus error 的报错第一件事永远是看内存相关指标而不是打开训练脚本检查。在宿主机和容器里分别执行下面几条命令df -h /dev/shm free -h cat /proc/meminfo | grep -i shmem重点看两个地方/dev/shm的剩余空间以及free -h里 available 内存是否充足。如果是在 Docker 容器里还可以用下面这条命令确认容器的共享内存配置docker inspect 容器名 --format{{.HostConfig.ShmSize}}输出的单位是字节。如果发现/dev/shm只有 64MB并且你的训练脚本里 DataLoader 开了多个 worker我建议先别折腾代码直接用docker run --shm-size16g重新起容器跑一遍。很多时候这一步就把问题解决了。3.2 第二步翻内核日志和 GPU 错误如果共享内存没有问题下一步就是看内核和 GPU 驱动有没有留下线索。dmesg -T | grep -iE nvrm|xid|pcie|bus error|segfault|out of memory | tail -100注意看有没有NVRM: Xid开头的日志。Xid 是 NVIDIA 驱动的硬件错误编号比如 Xid 79、Xid 56 等不同的编号对应不同类型的 GPU 异常。如果在崩溃时间附近有 Xid 日志说明问题出在 GPU 硬件或驱动层而不是 PyTorch 代码。同时可以开一个窗口运行nvidia-smi -l实时监控多张卡的状态看看崩溃前是否有某张卡的显存或温度异常。也可以用nvidia-smi -q -d TEMPERATURE,POWER,CLOCK查看详细硬件状态。3.3 第三步用 faulthandler 和 gdb 抓崩溃现场不要只靠core dumped猜位置。给训练脚本加一段代码让 Python 在收到 SIGBUS 时打印出当时的线程调用栈。import faulthandler import signal faulthandler.enable() # 对 SIGBUS 也启用 faulthandler默认可能只处理 SIGSEGV faulthandler.register(signal.SIGBUS)在脚本开头加上这段再次运行训练。崩溃发生时终端会打印出完整的 Python 调用栈你能直接看到是卡在 DataLoader 里、NCCL 初始化里、还是某个具体的数据预处理函数里。如果 faulthandler 的输出还不够就用 gdb 分析 core 文件gdb python /path/to/core -ex bt -ex quit或者直接在程序运行时挂载gdb -p pid -ex bt -ex quit用 gdb 看堆栈时重点关注崩溃点附近的函数名。常见的情况是堆栈里有nccl或NCCL相关符号说明死在通信库有shared_memory或Multiprocess相关符号说明死在数据加载有cudaMemcpy相关符号说明死在显存拷贝。这个判断能帮你把排查范围缩小一大半。3.4 第四步关掉变量做对照实验如果日志信息还不足以定位问题就做一组对照实验排除法永远是系统级排障里最可靠的手段。我会按顺序测试以下组合每改一个变量就跑一次小规模训练实验组合目的num_workers0保持多卡排除 DataLoader worker 和共享内存干扰pin_memoryFalse保持多卡排除锁页内存压力单卡跑同一个脚本排除 NCCL 和 GPU 间通信影响多卡但设置NCCL_P2P_DISABLE1排除 GPU 直连映射问题换一台机器或换一个 Docker 镜像跑排除环境配置污染这种“一改一测”的方式看起来慢实际上最快。因为我踩过太多次“凭直觉改了一个参数发现两个问题同时存在”的坑对照实验可以让你明确知道到底是哪个变量导致的崩溃。3.5 第五步做最小复现脚本如果上面的实验还不能稳定复现就再做一个小脚本用一个简单张量计算模拟多卡通信排除你原本项目里复杂模型的影响。import torch import torch.distributed as dist import torch.multiprocessing as mp import torch.nn as nn def demo(rank, world_size): dist.init_process_group(nccl, rankrank, world_sizeworld_size) model nn.Linear(4096, 4096).cuda(rank) input_tensor torch.randn(4096, 4096, devicerank) loss model(input_tensor).sum() loss.backward() dist.all_reduce(loss) dist.destroy_process_group() if __name__ __main__: world_size torch.cuda.device_count() mp.spawn(demo, args(world_size,), nprocsworld_size)注意用python直接跑这个小脚本不要用torchrun这样能去掉启动器层面的干扰。如果小脚本也崩问题基本锁定在环境或硬件层如果小脚本正常说明问题藏在项目的数据加载或模型代码里。4. 修复方案和防复发配置4.1 容器启动参数与系统共享内存调整修复/dev/shm不足最直接的方法是在容器启动时指定更大的共享内存docker run --gpus all --shm-size16g -it your_image bash16g是一个比较实用的起步值如果模型和数据量特别大可以直接给 32g 或 64g。担心留太大浪费不用担心/dev/shm是 tmpfs占用的空间会随着文件释放而回收只是“上限”提高了不是一启动就占满物理内存。如果你不是用 Docker而是在宿主机上直接跑绝大多数发行版默认/dev/shm是物理内存的一半对多 GPU 训练通常够用。但也可以临时调整mount -o remount,size32G /dev/shm注意这个命令在容器内不一定有效宿主机上执行也要谨慎最好在业务低峰期操作避免影响其他进程。4.2 DataLoader 参数到底怎么调不要盲目追求“worker 开得越多加载越快”。在多卡环境下DataLoader 的参数要结合 CPU 内存和共享内存一起看。我推荐一个相对稳的参数组合适合大多数图像和文本训练任务torch.utils.data.DataLoader( dataset, batch_sizebatch_size, shuffleTrue, num_workers4, pin_memoryFalse, persistent_workersTrue, prefetch_factor2, )这里有两个地方需要解释。第一num_workers不是越多越好。如果每个 worker 的峰值内存是 2GB开 16 个 worker 就是 32GB还没算模型和 CUDA context 本身就占了大量内存。通常我建议从 CPU 核心数的一半开始测试比如 8 核机器开 4 个 worker16 核机器开 8 个 worker如果 Bus error 复现就减半再试。第二pin_memory在有大量 CPU 内存时可以提升训练速度但在内存吃紧的环境里反而是风险源。如果你发现共享内存或物理内存经常告警把它设为False速度损失通常不超过 10%但稳定性提升是肉眼可见的。也可以设置prefetch_factor只预取一个 batch降低 DataLoader 在共享内存里的堆积量不过prefetch_factor要求num_workers 0否则会报错。4.3 torchrun 和 NCCL 环境变量如何设置多卡训练现在的主流启动方式是用 torchrun下面是单机 4 卡的典型启动方式export NCCL_DEBUGINFO export NCCL_DEBUG_FILE/tmp/nccl_rank_%h_%p.log torchrun \ --nnodes1 \ --nproc_per_node4 \ --master_port29500 \ train.py把NCCL_DEBUGINFO打开后NCCL 会输出详细的初始化日志包括它选用了什么传输方式、是否启用 P2P、通过哪条路径做 allreduce。这是判断通信阶段 Bus error 的一手资料。如果确认是 P2P 链路导致的崩溃在启动前显式关闭export NCCL_P2P_DISABLE1 export NCCL_IB_DISABLE1NCCL_IB_DISABLE1是关闭 InfiniBand 通信只影响多机场景单机时不影响。关闭 P2P 后NCCL 会走共享内存和 PCIe 的常规拷贝路径虽然通信效率降低但在硬件链路不稳定的机器上稳定压倒一切。另外提个建议不要把NCCL_SHM_DISABLE1当成默认选项。这个变量会关闭 NCCL 对共享内存的使用它确实可能避开/dev/shm空间不足的问题但代价是通信路径被彻底改变某些集群环境下反而更容易报错。只有在你明确测试过它有效时才考虑使用。4.4 在训练代码里增加兜底逻辑环境层面的问题修完后还可以在代码里做几道防线尤其是防止“显存 OOM 演变成 Bus error”这种连锁崩溃。给训练主循环加上 OOM 捕获try: loss.backward() optimizer.step() except torch.cuda.OutOfMemoryError: torch.cuda.empty_cache() logger.warning(GPU OOM at step %d, skip and save checkpoint, step) # 保存当前权重下次从 checkpoint 恢复 torch.save(model.state_dict(), fresume_{step}.pt) raise SystemExit(1)别小看这个处理。实践里我发现很多 Bus error 看起来随机其实前几秒已经在别的日志里出现过 OOM 警告只是被终端滚动刷过去了。一旦显存告警后续的通信环节很容易把错误放大成 SIGBUS。所以尽早退出并保留 checkpoint比硬撑着继续跑更科学。另外如果你的训练脚本支持断点续训可以在启动参数里加上--resume逻辑。崩溃后不需要从头开始损失的可能只有最后几个 step。5. 实战踩坑记录与避坑建议5.1 快速定位速查表我把最常见的几种现象、可能性、排查命令和解决方案整理成一张表你可以直接截图保存。崩溃场景最高概率原因先执行的命令理想解法容器里跑DataLoader 阶段崩/dev/shm 不足df -h /dev/shmdocker run --shm-size16g多卡训练刚初始化就崩NCCL P2P / NVLink 异常dmesg -T | grep -i xidNCCL_P2P_DISABLE1训练中途随机崩时间不确定硬件或驱动不稳定nvidia-smi -q -d TEMPERATURE,POWER检查供电散热换卡测试崩之前有显存警告GPU OOM 连锁反应搜索日志里的OutOfMemory减小 batch_size开 AMP只有 DDP 崩单卡正常通信库或共享内存问题NCCL_DEBUGINFO看日志按日志调整 NCCL 环境变量关掉 pin_memory 后明显改善锁页内存压力大free -h看 available保持pin_memoryFalse5.2 三个我认为最有参考价值的案例第一个案例来自一个朋友的项目他用 EasyOCR 训练自己的数据集单卡跑得很正常一上 4 卡 DDP 就随机崩报错就是 Bus error。折腾了两天重装驱动、换 PyTorch 版本都没解决。我让他进容器跑了一下df -h /dev/shm显示 64M问题当场破案。把容器共享内存改成 16G 之后连续训练 20 小时没崩过。第二个案例是我自己接手的一个 YOLO 系检测模型训练任务。现象很典型每次都在第一个 epoch 的梯度同步阶段崩faulthandler 抓到的堆栈指向ncclAllReduce。我用NCCL_DEBUGINFO看了日志发现 NCCL 在尝试通过 P2P 建立 GPU 间映射时失败。那台机器的显卡是不同型号混插的P2P 支持不完整设置NCCL_P2P_DISABLE1后问题消失。代价是通信慢了一点但训练能稳定跑完。第三个案例更朴素。一台老旧的 8 卡服务器做模型训练时频繁随机 Bus error内核日志里有明显的 Xid 79 错误。这种错误代表 GPU 在尝试读取显存时发生硬件错误。软件层面怎么调都没有用后来硬件同事拆机检查发现有两张卡的供电线老化更换电源模组后故障消失。这三个案例覆盖了 Bus error 最常见的三种归属配置问题、通信问题、硬件问题。你的问题大概率也能归入这三类之一。5.3 那些浪费时间的错误排障方向最后说几个我见过太多人踩进去的误区希望你别重复。第一反复重装 PyTorch。说实话Bus error (core dumped)很少是 PyTorch 安装包本身的问题。重装三遍不如先执行一遍df -h /dev/shm和dmesg -T。在你最终确认是软件包问题之前重装基本属于浪费生命。第二盲目增加num_workers。有些同学觉得加载慢就开 32 个 worker结果共享内存直接被打爆。正确的姿势是先小后大逐个档位测试。记住数据加载的瓶颈未必在 CPU 并行度上也可能在磁盘 IO、数据解码或者内存带宽。第三见到 Bus error 就怀疑代码里有内存越界。虽然 C 扩展写得不规范确实可能触发这类信号但对绝大多数 PyTorch 纯 Python 训练脚本来说越界访问会被解释器捕获更常见的表现是IndexError不会是 SIGBUS。代码越界的概率远低于共享内存和通信链路的概率。第四忽略 core 文件的生成位置。很多人的 core_pattern 指向了一个没有写入权限的目录导致崩溃时根本没生成 core 文件后续想分析也没有素材。可以先用cat /proc/sys/kernel/core_pattern确认一下必要时临时设置sysctl -w kernel.core_pattern/tmp/core.%e.%p把 core 文件固定到方便查看的位置。我自己在实际排障中的体会是处理 Bus error 这类系统级问题最重要的不是某一招特别神奇而是不要按“代码出错”的惯性思维去排查。先把环境变量、共享内存、内核日志这些底层信息捞一遍再回到模型逻辑上找原因顺序反了便宜事都让你走了。最后再分享一个小技巧也算是我现在的默认习惯凡是跑多 GPU 训练的 Docker 启动命令不管临时可能用不用得上都主动加一行--shm-size16g。这一行命令几乎零成本但能帮你挡掉一大半莫名其妙的 Bus error。这个习惯我保持到现在踩坑的次数明显少了很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →