1. 训练中断那一刻:Bus Error到底在报什么
1.1 先分清Segmentation Fault和Bus Error不是一回事
先说说我自己是怎么撞上这个坑的。当时我在一台双卡服务器上训练YOLOv8,数据集是自采的工业零部件图片,单张分辨率不高,但总量有十几万张,训练脚本用的是ultralytics标准写法,model.train(data=xxx.yaml, epochs=300, batch=64, workers=16) 这样的配置。跑起来的前几百step一切正常,loss也在稳步下降,结果到了某个epoch之后进程毫无征兆地消失,终端只留下一句:
code复制Bus error (core dumped)
我的第一反应是硬件问题。因为"Bus Error"这个名字听起来就像主板总线、内存条、PCIe链路出了故障,我甚至让人去机房重新插拔了内存。折腾一晚上之后才发现,这个问题和显卡、内存条半毛钱关系都没有,真正的瓶颈在Linux的共享内存,也就是 /dev/shm 这个挂载点。
很多人容易把SegmentFault和Bus Error混为一谈。Segmentation Fault(段错误)是进程访问了虚拟地址空间里非法区域,比如空指针、越界数组,本质是代码逻辑错误导致的内存访问违规。Bus Error则不一样,它是进程试图访问一块"物理上无法完成读写"的地址,比如映射了一个文件之后,文件被截断了,或者底层共享内存块已经被回收,后续访存指令就会触发SIGBUS信号。在x86平台上Bus Error平时很少见,所以一旦遇到,绝大多数人的排查方向都会像我一样跑偏。
1.2 YOLO训练场景里Bus Error的高发位置在哪里
我后来把YOLO训练中遇到Bus Error的场景归纳成三类,这几个位置基本覆盖了90%的病例:
- 数据集规模大、ultralytics自动缓存标签或缓存图像时
- DataLoader开了多worker,batch size又调得比较大
- 在Docker容器里跑训练,且没有给容器单独设置共享内存大小
这三种情况表面上看起来不一样,根因却是同一个:/dev/shm这个共享内存区域被写满了。数据加载的worker进程在尝试继续写入或读取共享数据块的时候,因为底层内存无法扩展或已经被回收,直接触发了SIGBUS信号,进程崩溃。
要解释清楚这个问题,就得从PyTorch的DataLoader工作机制说起。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从DataLoader到/dev/shm:共享内存是怎么被耗空的
2.1 PyTorch的DataLoader为什么依赖共享内存
PyTorch的DataLoader在单进程模式(num_workers=0)下,数据加载在主进程里同步完成,不涉及进程间通信。但只要设置num_workers>0,主进程和worker进程之间就要传递批量数据。如果通过管道传递,免不了反复序列化和拷贝,效率很低。
所以PyTorch采用了一种更高效的方式:每个worker把准备好的batch数据序列化后写入一块共享内存,然后通过IPC消息通知主进程"数据准备好了",主进程直接读取这块共享内存,整个batch的数据就传过去了。这个共享内存,在Linux上默认落在 /dev/shm 这个tmpfs挂载点上。
这里关键的一点是:num_workers越大,同一时刻并行准备数据的worker就越多,每个worker还会预取一个batch。主进程如果消费速度跟不上,多个batch就会同时驻留在共享内存里。这些临时数据的总大小,就是压垮 /dev/shm 的直接力量。
2.2 /dev/shm的默认大小和你想象的完全不一样
/dev/shm是Linux内核提供的tmpfs文件系统,数据直接存放在物理内存中,读写速度接近内存本身,专门用来做进程间共享内存。普通Linux发行版上,/dev/shm默认大小通常是物理内存的一半,这个容量一般来说够用。但在Docker容器里,默认值只有64MB——这是Docker为了安全隔离设置的很小的初始值。
64MB有多容易爆?我算给你看。YOLO系列训练默认输入尺寸一般是640x640x3的RGB图,单张图像的原始数据大约是640x640x3=1,228,800字节,约1.17MB。如果你的batch size是64,仅仅这一个batch的原始图像数据就要占用75MB左右,这还只是原始像素,没算数据增强的中间对象、边界框标注缓存、解码临时缓冲区。也就是说,你在Docker容器里设batch=64,DataLoader只要预取一个batch,64MB的shm已经不够用了。如果再叠加多worker并发,每个worker都在往共享内存塞数据,那几乎是开局就炸。
2.3 YOLO的数据增强链路会把共享内存压力进一步放大
YOLO训练默认开启Mosaic、随机仿射变换、HSV抖动、水平翻转等增强策略。这些操作不只是简单地复制图像,而是要临时生成多张拼接图、仿射变换矩阵、mask等中间对象。问题在于,无论中间计算是在哪个进程完成的,只要最终结果要通过DataLoader的多进程通道送到主进程,这些数据就会以临时共享内存块的形式先落到shm里。
我在同样一台机器上做过对比测试:Docker容器里训练YOLOv8,batch=32、workers=8时,/dev/shm的峰值占用大约3.5GB;batch提到64、workers提到16,峰值直接冲到6GB以上。这还只是常规增强配置。如果你用了更重的增强策略,或者输入分辨率调到1280,单样本数据量会放大好几倍,shm需求按比例暴涨。
所以,在容器里训练,64MB的默认shm几乎必然爆掉,只是时间早晚问题。很多人好奇"为什么我跑到第几个epoch才崩"——因为前几个epoch数据缓存还在慢慢填满shm,等到某一次多batch并发预取到来时,shm彻底耗尽,某个worker写入失败触发SIGBUS,整个进程就没了。
3. 排查链路:从报错到定位问题只要四步
3.1 第一步:确认触发时机,先排除硬件因素
遇到Bus Error,先别急着换硬件,做两件事:
- 记录崩溃发生的时间点。是训练一开始就崩,还是运行到某个epoch后崩?如果每次都固定出现在数据加载阶段,和硬件关系基本不大。
- 立刻在终端执行
df -h /dev/shm查看共享内存使用率。
如果看到 /dev/shm 的已用空间接近100%,或者挂载信息里出现64M这样的数字,病因就已经找到了九成。我当时第一次执行后看到的是100%,可我还是不死心,又折腾了半天硬件检测,纯属浪费时间。
3.2 第二步:找出shm里到底堆了什么东西
确认shm紧张之后,接着执行:
bash复制du -sh /dev/shm/* | sort -hr
你会看到一堆类似 torch_xxx、pymp-xxxx 或 nvidia_xxx 之类的临时文件,这些都是PyTorch DataLoader和相关库创建的共享内存块。如果这些文件的总和几乎等于shm总容量,基本可以判断就是DataLoader把共享内存塞满了。
更精细一点,还可以用 lsof +D /dev/shm 查看当前持有这些共享文件的具体进程。训练在崩溃时进程已经退出,但这个命令在训练运行中执行非常有用,能直接看到哪个PID、哪个进程在占用shm空间。我当时用这个命令看到了十几个python worker进程各自占着几百MB共享块,一目了然。
3.3 第三步:检查容器和系统层面的限制
如果你用的是Docker容器,执行:
bash复制docker exec -it 容器名 mount | grep shm
输出类似:
code复制tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=65536k)
看到 size=65536k,就是64MB,这就是问题所在。如果你直接在宿主机上训练,没有容器隔离,则查看 /etc/fstab 文件里是否有对shm的显式配置;没配置的话,系统默认是物理内存的一半,一般不容易满。
顺手再执行 dmesg | tail -20 看下内核日志。如果出现和tmpfs、内存分配相关的异常条目,可以作为辅助佐证。但注意,shm写满时不一定每次都有内核日志,这个只能当参考,不是充分条件。
3.4 第四步:用最小化变量验证结论
最直接的验证方法:把DataLoader的worker数量改成0,重新启动训练。workers=0 意味着所有数据加载都在主进程同步完成,完全不使用共享内存。如果Bus Error消失,那就可以拍板,确实是shm容量不足导致的。
bash复制model.train(data=xxx.yaml, epochs=300, batch=64, workers=0)
注意,workers=0会明显拖慢数据加载速度,这个方案只是为了验证问题。验证完再恢复多worker,然后按下一节的方案扩大shm容量,才能彻底解决。
4. 解决方案:从小到大按优先级操作
4.1 临时止血:先调小workers和batch
如果眼下实验正在赶进度,最快速的止血方式就是降并发。把workers从16降到4,batch从64降到32,shm里的临时数据峰值会大幅下降,64MB在大多数场景下能勉强跑起来。这个方案不是长久之计,但能让你先保住一轮实验,而不是卡在环境问题上干瞪眼。
也顺带提一句:如果你用的是YOLOv8的 predict 模式做推理,碰见Bus Error的情况比训练少得多。因为推理默认不开多进程数据加载,shm压力主要来自训练阶段。
4.2 Docker用户:--shm-size一劳永逸
如果你在Docker里训练,最推荐的做法是在启动容器时显式指定共享内存大小:
bash复制docker run --gpus all --shm-size=4g -it your_image /bin/bash
或者用docker-compose管理环境,在服务定义里加上shm_size字段:
yaml复制services:
yolo_train:
image: your_image
shm_size: '4g'
改完之后进容器重新执行 mount | grep shm,确认size已经变成4G。再跑一次之前崩掉的YOLO训练,你会发现Bus Error消失了。4G是我个人比较推荐的下限值;如果你的batch大、增强开得全,可以直接给到8G或16G。tmpfs的特点是动态占用空间,给大一点不意味着立刻吃掉全部内存,只有实际写入数据才会占用物理内存。
也有一些开发者习惯用 --ipc=host 让容器和宿主机共享同一个IPC命名空间。这样做确实省事,但副作用是容器和宿主机之间不再隔离共享内存,安全边界被削弱了。我的建议很简单:除非你明确知道自己在做什么,否则优先用 --shm-size,别图省事。
4.3 宿主机环境:改挂载大小并持久化配置
没走Docker、直接在宿主机上跑训练的情况,如果shm也爆了,多半是机器物理内存本身比较紧张,或者有人手动改过shm大小。可以临时扩容:
bash复制sudo mount -o remount,size=8G /dev/shm
这条命令立即生效,不需要重启系统,但重启之后会恢复原样。想要永久生效,修改 /etc/fstab 文件,加入一行:
code复制tmpfs /dev/shm tmpfs defaults,size=8g 0 0
保存后执行 sudo mount -a,或者重启验证。要注意的是,扩大shm不等于可以无限制压榨内存。shm占用的内存会计入进程总体内存消耗,如果机器只有16GB物理内存,你给shm分8G,再叠加其他系统进程和GPU数据拷贝的开销,内存压力依然不小。扩容时一定要结合机器实际配置来评估。
4.4 代码层面的降耗:persistent_workers与prefetch_factor
有时候你没法改Docker启动参数,比如用的是公司内部统一的训练平台,这时候可以在代码层面做调整。实测有效的方法有这么几个:
第一个是设置 persistent_workers=True。这个参数让worker进程在epoch结束后不销毁、下次epoch直接复用,减少反复初始化和销毁worker带来的临时内存波动。
第二个是降低 prefetch_factor。PyTorch的DataLoader默认prefetch_factor为2,意思是每个worker预取两个batch的数据。降到1,shm里的滞留数据量立刻减半。
第三个是把数据集转换成语义化的内存映射格式读取。这个改动稍大,但治标也治本。我当时用在一个超大数据集上,把图片和标注打包成LMDB格式,训练时直接从文件映射读取,不走共享内存转发,shm的压力几乎消失。
如果你在ultralytics框架里找不到prefetch_factor的直接配置入口,可以自己改训练脚本里DataLoader的构造参数,或者干脆用torch原生DataLoader写自己的训练循环。改之前注意一点:降低prefetch_factor会影响数据加载吞吐量,建议通过调整workers数量做补偿,找到两者之间的平衡点。
4.5 用监控确认问题真正解决
改完配置之后不要只看一眼"没报错"就收工。我习惯在训练启动后,另开一个终端持续监控:
bash复制watch -n 1 'df -h /dev/shm'
观察shm的使用率曲线。如果训练稳定跑了几百个step,shm使用率始终没有逼近100%,说明配置留出了足够余量。如果使用率还在缓慢爬升,说明并发参数依然偏大,需要继续下调或者进一步扩容。
我见过一些人改完fstab之后,因为没有验证shm实际值,等到下次重启后又被打回原形,继续莫名其妙地崩。验证这两个字,是在环境配置里最容易被忽略、也最值钱的环节。
5. 这些坑我也踩过:实操细节与补充提醒
5.1 不同YOLO版本的报错文本可能不一样
YOLOv5和YOLOv8对shm不足的表现不太一样。YOLOv5在某些版本里直接报 OSError: [Errno 28] No space left on device,YOLOv8则更常见于 Bus error (core dumped)。用其他框架比如mmdetection、Detectron2的人,看到的报错文本可能又不一样。
所以不要死记报错字符串,要掌握"多进程数据加载 + 共享内存写入失败"这个判断模型。报错文本只是表象,底层逻辑是同一套。这也是为什么网上搜"Bus error"会搜出一堆看起来毫不相关的结果,因为触发SIGBUS的场景有很多种,共享内存只是其中最常见的之一。
5.2 shm过小不只是YOLO的问题
有些读者可能有疑问:我只跑YOLO训练,又不跑数据库和消息队列,shm小一点无所谓吧?实际上很多软件都会用到/dev/shm。GCC编译过程的某些临时文件、Java虚拟机的某些内存映射、OpenCV的图像处理路径,都可能依赖它。我就在一台容器里同时做数据预处理和编译C++依赖时,遇到过shm不足导致编译崩溃的情况。
所以如果你在训练镜像里还要做数据转换、源码编译这类操作,给shm留足空间是一种通用的工程习惯,不只是YOLO的专属要求。
5.3 多卡训练会放大shm需求
多卡训练时,DDP会拉起多个进程,每个进程各有一套DataLoader。也就是说,n张卡就有n套数据加载副本,共享内存的占用几乎是单卡的n倍。这就是为什么很多人在单卡上跑得风平浪静,一上多卡就各种崩溃。
我遇到过一个案例,同事在单卡上batch=128跑得稳稳当当,换成4卡DDP同样配置直接刷爆shm,第一反应是怀疑分布式通信库配置有问题,查了一整天才发现又是shm不够。如果你要用多卡训练,建议按"单卡shm峰值x卡数"的公式先估算一下需求,然后直接在上限上再乘1.5到2倍留裕量。
5.4 给团队和自助搭建环境的朋友一个建议
如果你是在帮团队搭建训练环境,或者给开源项目写部署文档,我建议把shm配置写进README,并且放到显眼的位置。因为这是一个"不配就会爆炸,配了没人记得"的典型配置项。我见过太多人踩了坑之后,改好配置就以为是自己的环境配置问题,从没想过要把这个经验沉淀到团队文档里。
最省心的方式是做一个环境检查脚本,在启动训练之前自动检测 /dev/shm 的可用空间,低于某个阈值就输出警告。这个脚本成本很低,但能把全组人从同样的坑里解救出来。
这个问题我前前后后踩过三次,每次都在不同环境、不同框架里遇到。踩的次数多了之后最大的感受是:它本质上是一个"环境容量问题",不是代码bug,更不是模型结构问题。排查顺序对了,解决起来非常快;排查顺序错了,可能在硬件和驱动上浪费一整天。现在再看到YOLO训练报Bus Error,我已经能直接说出那句话:先去看 /dev/shm,基本不用想别的。
