心情:像个侦探,从愤怒中寻找逻辑任务:排查 CUDA Out Of Memory (OOM) 错误,清理僵尸进程,优化显存配置关键词:CUDA OOM, Zombie Process, Mixed Precision (AMP), Batch Size
早上 10:00,办公室的气氛有点紧张。 李博士黑着脸找我:“YY,你这批 P4d 机器是不是那是次品?还是 AWS 给我们的卡是阉割版的?”
我一愣:“怎么可能,AWS 的 SLA 可是 99.99%。”
李博士指着屏幕上的红色报错:RuntimeError: CUDA out of memory. Tried to allocate 512.00 MiB (GPU 0; 40.00 GiB total capacity; 35.80 GiB already allocated...)
“你看,”李博士说,“我的模型参数加上梯度,算下来最多占 28G。这张卡是 40G 的 A100。现在才跑了几步就报 OOM(内存溢出),而且我看nvidia-smi,显存一直被占得满满的,根本不释放。肯定是有内存泄漏或者硬件故障。”
作为 AWS SAP,我第一反应是看 CloudWatch。一切正常。 但我知道,要解决这个问题,我必须深入到GPU 内存模型内部。
第一步:拆穿nvidia-smi的谎言
我登录到报错的节点10.100.1.10。 输入nvidia-smi:
Memory-Usage:
39800MiB / 40536MiB(确实满了)
但我知道一个 AI 运维的冷知识:nvidia-smi显示的是“被占用的显存”,而不是“正在使用的显存”。PyTorch 为了速度,申请显存后不会立即还给操作系统,而是自己管理(Caching Allocator)。
我打开了一个 Python Shell,敲入了几行代码来“自证清白”:
import torch print(f"Allocated: {torch.cuda.memory_allocated() / 1024**3:.2f} GB") print(f"Reserved: {torch.cuda.memory_reserved() / 1024**3:.2f} GB")结果显示:
Allocated(实际正在用的):22 GBReserved(PyTorch 占着坑没用的):38 GB
我对李博士说:“博士,机器没坏。是 PyTorch 占着茅坑不拉屎,这是它的缓存机制。真正导致 OOM 的是瞬间峰值。”
第二步:清理战场的“幽灵” —— 僵尸进程
虽然解释了原理,但李博士尝试重启训练,依然秒崩。 他很困惑:“我都关掉程序了,为什么显存还是满的?”
我立刻意识到这是分布式训练常见的坑:僵尸进程 (Zombie Processes)。 当torchrun启动的 32 个进程中,如果有一个崩了,其他进程有时候不会自动退出,而是变成了僵尸,死死抓着显存不放。
运维神技:一把梭清理我没有一个个去 kill,而是用了一个核武器命令:
# 查找所有占用 NVIDIA 显卡的进程 PID fuser -v /dev/nvidia* # 或者更暴力的清理(慎用,会杀掉这台机器上所有 AI 任务) sudo kill -9 $(lsof -t /dev/nvidia*)执行完后,再运行nvidia-smi。Memory-Usage:4MiB / 40536MiB。 世界清静了。显存全部释放。
第三步:解决真正的 OOM —— 运维不懂算法也要懂配置
清理完环境,李博士重跑代码。 跑了 10 分钟,又崩了。这次是真的显存不够用了。
李博士叹气:“看来 70B 的模型太大了,Batch Size 设成 1 都跑不动。YY,能不能再加几台机器?或者换 80G 显存的 H100?”
加机器?那可是翻倍的成本。 换 H100?现在全球缺货,根本排不到队。 作为运维,我必须帮老板省钱。我凭借这两天恶补的知识,提出了三个**“不花钱”的优化方案**:
1. 开启混合精度训练 (Mixed Precision / AMP)我问:“博士,你是不是用的默认 FP32(32位浮点数)?” 博士点点头。 我说:“现在的 A100 专门优化了FP16或BF16。你代码里加一行torch.cuda.amp.autocast(),显存占用能砍一半,速度还能快一倍。精度损失微乎其微。”
2. 梯度累加 (Gradient Accumulation)我建议:“如果 Batch Size 设成 64 跑不动,咱们就设成 16。然后跑 4 次不更新权重,把梯度攒起来,第 4 次再一起更新。这样数学上等价于 Batch Size 64,但显存只需要 1/4。”
3. 激活重计算 (Activation Checkpointing)“用时间换空间。把中间计算结果扔掉,反向传播时再算一遍。”
李博士看着我,眼神变了:“YY,你以前不是搞 SAP 的吗?怎么连 Checkpointing 都懂?” 我笑了笑:“为了不让你找我加预算,我昨晚看了通宵文档。”
第四步:实战调优
李博士采纳了建议,开启了BF16(Brain Floating Point 16,A100 的绝活) 和梯度累加。
再次启动torchrun。 这一次,我盯着nvidia-smi:
显存占用稳定在32GB / 40GB。
GPU 利用率稳定在98%。
风扇狂转,热浪滚滚,但模型开始稳定产出 Loss 值了。
没有 OOM,没有报错。 原本需要加倍机器才能跑的任务,通过运维层面的参数建议,在现有硬件上跑通了。
下午 6:00:复盘 —— 运维的新价值
下班前,我在 Wiki 里更新了《AI 故障排查手册》,加了一条粗体字:
遇到 OOM 不要急着重启或加机器。先查僵尸进程,再查精度配置。
这一天,我意识到 AI 运维不仅仅是管服务器。 在传统运维里,我们管不到 Java 代码里的 Heap Size。 但在 AI 运维里,硬件和代码是强耦合的。不懂显存特性的运维,只能沦为无情的“重启机器工”;懂一点模型优化的运维,能帮公司省下几百万。
今日总结:
nvidia-smi是会骗人的:必须要懂 PyTorch 的显存缓存机制。僵尸进程是显存杀手:
fuser和lsof是清理显卡的必备工具。性能优化 = 成本优化:通过 AMP 和梯度累加,用软件技巧解决了硬件瓶颈。
📎 附件:YY 的 AI 运维学习笔记(Day 4 - 性能调优篇)
文档说明:
Day 4 被李博士怼了之后,我深刻意识到:不懂算法原理的运维不是好运维。为了不背锅,我调研了以下 7 个核心概念。
适用人群:懂 AWS 和 Linux,但看到 PyTorch 报错就头大的运维兄弟。
1. 关于 AWS 资源:A100 和 H100 真的那么难搞吗?
现状:是的,极其难搞,且极其贵。
A100 (P4d 实例):这是上一代机皇。目前在 AWS 属于“紧俏物资”。如果你是新账号,配额几乎 100% 是 0。必须签大额承诺消费(EDP)或者找客户经理特批。价格约$32.77/小时。
H100 (P5 实例):这是当今的显卡皇帝。比 A100 快 3-6 倍。在 AWS 上,这玩意儿几乎不对普通公众开放,通常只给战略级大客户。价格约$98.32/小时。
运维启示:正因为硬件稀缺且昂贵,所以我们才要拼命做“软件优化”(Day 4 做的那些事),而不是一遇到慢就加机器。
2. PyTorch 到底是个什么鬼?是那个 Web 界面吗?
误区:
Jupyter(那个 Web 界面) =IDE / 编辑器(类似于 VSCode / Eclipse)。
PyTorch=库 / 框架(类似于 Spring Boot / React)。
正解:
PyTorch 是一组 Python 的代码库(Library)。
李博士写的代码里第一行
import torch,就是在加载这个库。它负责指挥显卡进行矩阵运算。它不是一个常驻后台的服务(像 Nginx 或 MySQL 那样),它更像是一个脚本,跑完就退出了。
3. 什么是“混合精度 (AMP)”和autocast?
通俗解释:
FP32 (单精度):传统的计算方式。存一个数字(比如权重)要占32 bit显存。就像用原本的 4K 画质看电影,极其清晰但文件很大。
FP16 / BF16 (半精度):存一个数字只占16 bit。显存占用直接砍半。就像用 1080P 看电影,虽然模糊了一点点,但为了流畅度(速度)和存储空间,完全可以接受。
torch.cuda.amp.autocast():这是 PyTorch 的一个开关。开启后,它会自动判断:
哪几步运算需要高精度(FP32),就用 FP32。
哪几步运算可以用低精度(FP16),就自动切成 FP16。
这就是Mixed Precision (混合精度)。
运维价值:一行代码,显存省一半,速度快一倍。这是性价比最高的优化手段。
4. 什么是 Batch Size?什么又是梯度累加?
这两个概念是解决 OOM(显存溢出)的核心。
Batch Size (BS):
定义:GPU 一口能吃掉多少条数据。
比喻:就像搬砖。
BS = 64:用一辆大卡车,一次拉 64 块砖。效率高,但卡车(显存)可能装不下,会爆胎(OOM)。
BS = 1:用一个小推车,一次拉 1 块砖。卡车肯定装得下,但要在路上跑 64 趟,效率极低。
梯度累加 (Gradient Accumulation):
场景:显存太小,装不下大卡车(BS=64),但我又想达到 BS=64 的训练效果(因为 BS 太小会导致模型学不准)。
做法:
用小推车(BS=16),连拉 4 趟。
但是!前 3 趟拉完后,不卸货(不更新模型权重),把砖头先堆在工地上(累加梯度)。
等到第 4 趟拉完,凑够了 64 块,再一次性砌墙(更新权重)。
公式:$16 (\text{Micro Batch}) \times 4 (\text{Accumulation Steps}) = 64 (\text{Global Batch Size})$
运维价值:用时间换空间。虽然慢了一点点,但在有限的显存下跑通了大模型。
5. 激活重计算 (Activation Checkpointing) 是干嘛的?
问题:训练大模型就像爬山,爬山过程中为了能原路返回(反向传播),你需要把沿途的所有风景(中间计算结果/激活值)都拍下来存到手机(显存)里。结果还没到山顶,手机内存满了。
解决:激活重计算。
我不存照片了。当我需要回看半山腰的风景时,我重新再爬一次那一段路,现场算出来。
运维价值:极端省显存(甚至能省 70%),代价是计算量增加(会慢 20%-30%)。这是最后的救命稻草。
6. 为什么是“再次启动torchrun”,而不是“启动 PyTorch”?
区别:
PyTorch是代码库,你不能直接运行它。你运行的是Python 脚本(比如
python train.py)。torchrun是 PyTorch 官方提供的一个启动器工具 (Launcher Utility)。
为什么要用它:
在单机单卡时,用
python train.py没问题。但在多机多卡(Day 2 搭建的集群)环境下,你需要告诉程序:
一共有几个节点?(
--nnodes)我是谁?(
--node_rank)老大的 IP 是多少?(
--master_addr)每个节点几张卡?(
--nproc_per_node)
手动配这些环境变量会死人的。
torchrun帮你自动处理好这些脏活累活,拉起分布式的 PyTorch 进程。
7. 总结:AMP 和 梯度累加
AMP (自动混合精度):
核心:压缩数据。
效果:显存减半,速度变快。
副作用:极小(基本无损)。
优先级:最高,必开。
梯度累加 (Gradient Accumulation):
核心:拆分任务。
效果:显存占用大幅降低,模拟大 Batch Size。
副作用:训练总时长略微增加。
优先级:中等,遇到 OOM 时开启。
[图示:显存优化三板斧]
AMP:把 32 位的胖子变成 16 位的瘦子。
梯度累加:把一大口饭分四口吃。
激活重计算:忘掉中间过程,用到时再算一遍。