2. 显存监控工具

显存监控这事儿,说实话,是很多AI训练工程师容易忽略的一环。我刚开始做大规模训练时,也踩过这个坑——模型跑着跑着突然OOM,然后一脸懵地开始猜到底哪一步爆了显存。后来我学乖了,先把监控工具玩明白,再动手调优。

这一章,我就带你把这几个核心工具捋一遍。从最基础的nvidia-smi,到可视化神器nvtop,再到PyTorch自带的Profiler,最后我们自己动手写个监控脚本。嗯,一步步来。

2.1 nvidia-smi:你每天都在用,但可能没用好

nvidia-smi是NVIDIA官方提供的命令行工具,几乎每个用GPU的人都会敲。但说实话,大部分人只用了它20%的功能。

基础用法

# 最常用的查看GPU状态
nvidia-smi

# 每秒刷新一次,持续监控
watch -n 1 nvidia-smi

输出里你会看到显存使用量、GPU利用率、温度、功耗等信息。我个人习惯重点关注两个指标:显存使用量和GPU利用率。前者告诉你显存够不够,后者告诉你计算单元有没有在干活。

核心指标解读:

  • Memory-Usage:当前显存占用 / 总显存。如果接近100%,小心OOM。
  • GPU-Util:计算单元利用率。训练时一般90%以上才算正常。
  • Volatile GPU-Util:这个值波动大,别太当真,看平均值更靠谱。

进阶用法:查询进程级显存占用

# 查看每个进程的显存占用
nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv

# 输出示例
pid, process_name, used_memory [MiB]
12345, python, 10240
12346, python, 8192

我在项目中遇到过一个问题:明明只跑了一个模型,显存却占了30多GB。用这个命令一查,发现有个残留的Python进程占着20GB没释放。嗯,这种坑很常见。

小技巧:可以用nvidia-smi --loop=1 --format=csv --query-gpu=index,memory.used,utilization.gpu输出CSV格式,方便后续用脚本解析。

2.2 nvtop:让显存监控可视化

nvtop是GPU版的htop,界面友好,信息一目了然。我个人非常推荐,尤其是调试时盯着终端看数字变化太枯燥了。

安装

# Ubuntu/Debian
sudo apt install nvtop

# 或者从源码编译
git clone https://github.com/Syllo/nvtop.git
mkdir -p nvtop/build && cd nvtop/build
cmake ..
make
sudo make install

使用

nvtop

启动后你会看到每个GPU的实时状态,包括显存使用曲线、进程列表、温度等。按F2可以设置刷新频率,我一般设成500ms。

注意:nvtop依赖libdrm和libncurses,如果编译报错,先装这两个库。我曾经在CentOS上折腾了半天,最后发现是依赖没装全。

2.3 PyTorch Profiler:精准定位显存瓶颈

前面两个工具只能看到全局显存占用,但具体是哪一层、哪个操作吃了显存,它们就无能为力了。这时候就需要PyTorch自带的Profiler出马。

基本用法

import torch
from torch.profiler import profile, record_function, ProfilerActivity

# 定义一个简单的模型
model = torch.nn.Linear(1024, 1024).cuda()
input_tensor = torch.randn(1024, 1024).cuda()

with profile(
    activities=[ProfilerActivity.CUDA, ProfilerActivity.CPU],
    record_shapes=True,
    profile_memory=True
) as prof:
    with record_function("model_inference"):
        output = model(input_tensor)

# 打印显存占用最高的操作
print(prof.key_averages().table(sort_by="self_cuda_memory_usage", row_limit=10))

输出会告诉你每个操作(比如矩阵乘法、激活函数)占了多少显存。我在调优一个Transformer模型时,发现softmax操作居然占了20%的显存,后来换成flash_attention直接降了一半。

进阶:追踪训练循环

from torch.profiler import profile, ProfilerActivity

model = MyModel().cuda()
optimizer = torch.optim.Adam(model.parameters())

with profile(
    activities=[ProfilerActivity.CUDA, ProfilerActivity.CPU],
    profile_memory=True,
    record_shapes=True,
    with_stack=True
) as prof:
    for step in range(10):
        optimizer.zero_grad()
        loss = model(input_tensor)
        loss.backward()
        optimizer.step()
        prof.step()  # 标记每一步

# 按显存占用排序
print(prof.key_averages(group_by_input_shape=True).table(
    sort_by="self_cuda_memory_usage", row_limit=20))

关键参数说明:

  • profile_memory=True:开启显存追踪,默认是关闭的。
  • record_shapes=True:记录张量形状,方便定位问题。
  • with_stack=True:记录调用栈,知道是哪行代码触发的。

2.4 自定义显存监控脚本

有时候现成的工具不够灵活,比如你想在训练过程中自动记录显存峰值、或者当显存超过阈值时自动报警。这时候就得自己写脚本了。

方案一:基于nvidia-smi的Python封装

import subprocess
import re
import time

def get_gpu_memory():
    """获取所有GPU的显存使用情况"""
    result = subprocess.run(
        ['nvidia-smi', '--query-gpu=index,memory.used,memory.total',
         '--format=csv,noheader,nounits'],
        capture_output=True, text=True
    )
    lines = result.stdout.strip().split('\n')
    gpu_info = []
    for line in lines:
        parts = line.split(', ')
        gpu_info.append({
            'index': int(parts[0]),
            'used_mb': int(parts[1]),
            'total_mb': int(parts[2])
        })
    return gpu_info

def monitor_memory(interval=1, threshold=0.9):
    """监控显存,超过阈值报警"""
    print("开始监控显存...")
    while True:
        gpu_info = get_gpu_memory()
        for gpu in gpu_info:
            usage_ratio = gpu['used_mb'] / gpu['total_mb']
            if usage_ratio > threshold:
                print(f"[警告] GPU {gpu['index']} 显存使用率: {usage_ratio:.1%}")
            else:
                print(f"GPU {gpu['index']}: {gpu['used_mb']}/{gpu['total_mb']} MB ({usage_ratio:.1%})")
        time.sleep(interval)

if __name__ == '__main__':
    monitor_memory()

方案二:集成到训练代码中

import torch
import time

class MemoryMonitor:
    def __init__(self, device_id=0):
        self.device_id = device_id
        self.peak_memory = 0

    def step(self):
        """记录当前显存,更新峰值"""
        current = torch.cuda.memory_allocated(self.device_id)
        self.peak_memory = max(self.peak_memory, current)
        return current

    def reset_peak(self):
        self.peak_memory = 0

# 在训练循环中使用
monitor = MemoryMonitor()
for epoch in range(10):
    monitor.reset_peak()
    for batch in dataloader:
        # 前向传播
        loss = model(batch)
        # 记录显存
        current_mem = monitor.step()
        if current_mem > 0.9 * torch.cuda.get_device_properties(0).total_memory:
            print(f"显存告警!当前使用: {current_mem / 1024**3:.2f} GB")
        loss.backward()
        optimizer.step()
    print(f"Epoch {epoch} 峰值显存: {monitor.peak_memory / 1024**3:.2f} GB")

个人经验:我习惯在训练脚本里加一个--monitor参数,默认开启显存监控。这样线上跑任务时,一旦显存异常能立刻发现,不用等OOM了才去排查。

2.5 知识体系总览

下面这张图把本章的核心内容串起来了,方便你理解各个工具之间的关系和适用场景。

显存监控工具知识体系 显存监控 nvidia-smi nvtop PyTorch Profiler 自定义脚本 基础状态查看 进程级追踪 CSV格式输出 可视化界面 实时曲线 进程列表 操作级追踪 显存排序 调用栈记录 阈值报警 峰值记录 集成训练循环 从全局监控 → 进程级追踪 → 操作级定位 → 自动化报警 层层递进,精准定位显存问题

这张图展示了四个工具的定位:nvidia-smi负责全局概览,nvtop提供可视化体验,PyTorch Profiler深入操作级别,自定义脚本则实现自动化监控。实际项目中,我通常组合使用——先用nvidia-smi快速扫一眼,发现异常后用Profiler精确定位,最后写个脚本固化监控流程。

避坑指南:我曾经在生产环境里只用nvidia-smi看显存,结果模型跑着跑着突然OOM,查了半天才发现是PyTorch的缓存机制导致的显存碎片化。后来加上Profiler才看到,原来是某个中间变量的形状没控制好,导致显存暴涨。所以,别只依赖一个工具,组合拳才是王道。

好了,这一章的内容就到这里。工具是死的,人是活的。多动手试试,找到最适合自己工作流的监控方案。