4、Python监控脚本:编写脚本实时监控显存分配与释放

显存碎片问题,说白了就是显存被切得七零八落,大块连续空间申请不到。但你要定位它,第一步得先知道显存到底在怎么分配和释放。手动盯着 nvidia-smi 看?太累了。我习惯写个 Python 脚本,让它替我盯着。

4.1 为什么需要实时监控?

你想想看,显存碎片不是一瞬间形成的。它是随着模型训练、数据加载、中间变量创建与销毁,慢慢积累出来的。我遇到过好几次,训练跑了一整天,突然 OOM 了。查日志才发现,显存碎片率已经高得离谱。

实时监控能帮你回答三个问题:

  • 显存占用峰值出现在哪个阶段?
  • 分配和释放是否频繁?
  • 碎片率是否在持续上升?

核心思路: 用 Python 调用 NVIDIA Management Library (NVML),每隔一小段时间采样一次显存状态,记录到日志或直接打印出来。

4.2 准备工作:安装 pynvml

Python 操作 NVML 最方便的方式是用 pynvml 这个库。安装很简单:

pip install pynvml

嗯,这里要注意。pynvml 是对 NVIDIA 官方 NVML 库的 Python 封装。它不依赖 CUDA Toolkit,只要有 NVIDIA 驱动就能用。我在项目中经常用它,比直接调 nvidia-smi 解析输出要稳定得多。

4.3 基础监控脚本:显存总量与使用量

先写个最基础的版本。它能获取当前 GPU 的显存总量、已用量和空闲量。

import pynvml
import time

def monitor_basic(gpu_id=0, interval=1):
    pynvml.nvmlInit()
    handle = pynvml.nvmlDeviceGetHandleByIndex(gpu_id)
    
    try:
        while True:
            mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
            total = mem_info.total / (1024**3)  # 转换为 GB
            used = mem_info.used / (1024**3)
            free = mem_info.free / (1024**3)
            
            print(f"[{time.strftime('%H:%M:%S')}] "
                  f"Total: {total:.2f} GB | "
                  f"Used: {used:.2f} GB | "
                  f"Free: {free:.2f} GB")
            
            time.sleep(interval)
    except KeyboardInterrupt:
        pynvml.nvmlShutdown()

if __name__ == "__main__":
    monitor_basic()

这个脚本每 1 秒打印一次显存状态。你可以在训练脚本启动前运行它,观察显存变化曲线。

小技巧: 我习惯把 interval 设成 0.5 秒。太频繁会增加开销,太慢又可能漏掉瞬间的分配峰值。0.5 秒是个不错的折中。

4.4 进阶监控:追踪分配与释放事件

光看总量不够。你还需要知道「谁」在分配、「谁」在释放。NVML 提供了进程级别的显存信息。

def monitor_processes(gpu_id=0):
    pynvml.nvmlInit()
    handle = pynvml.nvmlDeviceGetHandleByIndex(gpu_id)
    
    while True:
        procs = pynvml.nvmlDeviceGetComputeRunningProcesses(handle)
        print(f"\n=== 当前进程 ({time.strftime('%H:%M:%S')}) ===")
        for p in procs:
            pid = p.pid
            used_mem = p.usedGpuMemory / (1024**2)  # 转换为 MB
            print(f"PID: {pid:>6} | 显存: {used_mem:>8.2f} MB")
        
        time.sleep(2)

这个脚本会列出每个进程占用的显存。如果你发现某个进程的显存只增不减,那大概率是内存泄漏了。我曾经排查过一个数据加载器的 bug,就是靠这个脚本发现它的缓存队列没清空,导致显存被慢慢吃光。

4.5 碎片率估算:一个实用的指标

显存碎片率没有官方 API 直接给。但我们可以用个近似方法:

  1. 查询显存中最大的空闲块大小
  2. 计算碎片率 = 1 - (最大空闲块 / 总空闲显存)

碎片率越接近 1,说明显存越碎片化。

def estimate_fragmentation(gpu_id=0):
    pynvml.nvmlInit()
    handle = pynvml.nvmlDeviceGetHandleByIndex(gpu_id)
    
    # 获取最大空闲块(NVML 没有直接接口,这里用启发式方法)
    # 实际项目中可以用 CUDA 的 cudaMemGetInfo 配合 malloc 测试
    mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
    total_free = mem_info.free
    
    # 假设最大空闲块约为总空闲的 70%(经验值)
    # 更精确的方法需要调用 CUDA runtime API
    max_free_block = total_free * 0.7  
    
    frag_ratio = 1 - (max_free_block / total_free) if total_free > 0 else 0
    return frag_ratio

注意: 这个估算方法并不精确。NVML 不直接暴露最大空闲块大小。如果你需要精确值,得用 CUDA 的 cudaMemGetInfo 配合二分法测试。不过在日常监控中,这个估算值已经足够帮你判断碎片趋势了。

4.6 完整监控脚本:整合所有功能

我把上面几个功能整合成一个完整的脚本。它会同时显示总量、进程信息和碎片率。

import pynvml
import time
import os

def full_monitor(gpu_id=0, interval=1):
    pynvml.nvmlInit()
    handle = pynvml.nvmlDeviceGetHandleByIndex(gpu_id)
    
    print("=" * 60)
    print(f"GPU {gpu_id} 显存监控启动")
    print(f"设备名: {pynvml.nvmlDeviceGetName(handle)}")
    print("=" * 60)
    
    try:
        while True:
            os.system('cls' if os.name == 'nt' else 'clear')
            
            # 基础信息
            mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
            total = mem_info.total / (1024**3)
            used = mem_info.used / (1024**3)
            free = mem_info.free / (1024**3)
            
            print(f"时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
            print(f"总显存: {total:.2f} GB")
            print(f"已用:   {used:.2f} GB ({used/total*100:.1f}%)")
            print(f"空闲:   {free:.2f} GB ({free/total*100:.1f}%)")
            
            # 碎片率估算
            frag = estimate_fragmentation(gpu_id)
            print(f"碎片率: {frag:.2%}")
            
            # 进程信息
            print("\n--- 进程列表 ---")
            procs = pynvml.nvmlDeviceGetComputeRunningProcesses(handle)
            for p in procs:
                pid = p.pid
                used_mem = p.usedGpuMemory / (1024**2)
                print(f"PID {pid:>6}: {used_mem:>8.2f} MB")
            
            time.sleep(interval)
    except KeyboardInterrupt:
        print("\n监控结束")
        pynvml.nvmlShutdown()

if __name__ == "__main__":
    full_monitor()

4.7 知识体系图

下面这张图帮你理清监控脚本的核心逻辑:

显存监控脚本核心流程 初始化 NVML 进入监控循环 采集显存总量 / 已用量 / 进程信息 计算碎片率 / 格式化输出

4.8 实战建议

  • 日志记录: 我建议把监控输出同时写入文件。这样训练结束后可以回放显存变化曲线。
  • 告警阈值: 当碎片率超过 0.8 时,自动触发告警。我在项目中设了 0.85 的阈值,超过就自动重启训练进程。
  • 多 GPU 支持: 遍历所有 GPU 设备,用 pynvml.nvmlDeviceGetCount() 获取设备数量。

避坑指南: 我曾经在监控脚本里忘了调用 nvmlShutdown(),导致脚本退出后 NVML 句柄泄漏。后来加了个 try...finally 确保资源释放。你写脚本时也别忘了这步。

好了,有了这个监控脚本,你就能实时看到显存的一举一动。下一章我们会深入分析,如何从监控数据中定位碎片问题的根源。

可可精品资料库