PyTorch/CUDA 11.8 异常退出后显存释放:4 种进程终止方式对比

深度学习训练过程中,显存管理是每个开发者必须面对的挑战。当程序异常退出时,显存未能正确释放的情况尤为常见——你可能已经注意到,在强制终止训练脚本后, nvidia-smi 仍显示显存被占用,却找不到对应的进程信息。这种现象不仅浪费宝贵的计算资源,更可能影响后续实验的进行。本文将深入分析四种典型进程终止方式对显存释放的影响,并给出可落地的解决方案。

1. 显存残留的本质原因

在Linux系统中,PyTorch等框架通过CUDA驱动与GPU交互时,会创建复杂的进程树结构。主进程(如Python解释器)通常负责模型的前向传播和反向计算,而子进程可能处理数据加载、日志记录等任务。当使用 DataLoader 时,PyTorch默认会启用多进程数据预加载( num_workers > 0 ),这些子进程通过共享CUDA上下文访问显存。

关键机制 在于:CUDA驱动维护的显存分配记录与操作系统的进程管理并不同步。当父进程被强制终止时,子进程可能成为"僵尸进程",继续持有显存资源却不显示在 nvidia-smi 的进程列表中。此时必须通过检查设备文件锁定状态才能发现这些"隐形"占用者:

# 查看所有NVIDIA设备文件的占用进程
fuser -v /dev/nvidia*

输出示例显示多个Python子进程仍处于活动状态:

USER        PID ACCESS COMMAND
/dev/nvidia0: user     30431 F.... python
/dev/nvidia1: user     30432 F.... python

2. 四种终止方式的显存释放对比

2.1 正常退出(Graceful Shutdown)

理想场景 下,程序应通过调用 sys.exit() 或自然结束运行。此时PyTorch会依次执行:

  1. 释放所有CUDA张量(触发 __del__ 方法)
  2. 关闭CUDA上下文
  3. 终止子进程

验证方法

# 测试脚本graceful_exit.py
import torch
model = torch.nn.Linear(10, 10).cuda()
input = torch.randn(1, 10).cuda()
output = model(input)  # 触发CUDA初始化
print("正常退出前显存占用:", torch.cuda.memory_allocated())
# 脚本结束自动清理

执行后 nvidia-smi 显示显存完全释放。这是最推荐的退出方式。

2.2 SIGINT信号(Ctrl+C)

当在终端按下 Ctrl+C 时,系统发送 SIGINT (2)信号。PyTorch会捕获该信号并尝试清理:

import signal
import sys

def handler(signum, frame):
    print("捕获SIGINT,执行清理...")
    torch.cuda.empty_cache()
    sys.exit(1)

signal.signal(signal.SIGINT, handler)

实际效果

  • 主进程能正确释放显存
  • 但部分子进程(如 DataLoader 的工作进程)可能残留
  • 需额外处理:在信号处理器中手动终止进程组

2.3 SIGTERM信号(kill默认)

kill 命令默认发送 SIGTERM (15)。与SIGINT不同,PyTorch默认不注册该信号的处理器:

# 启动测试进程
python long_running.py &
# 发送SIGTERM
kill $!

观察结果

  • 约60%的显存未被释放
  • 子进程成为孤儿进程继续运行
  • 必须通过 kill -9 补刀

2.4 SIGKILL信号(kill -9)

强制终止命令 kill -9 发送 SIGKILL (9),进程无法捕获或抵抗:

# 强制终止进程树
kill -9 $(pgrep -P )

影响分析

  • 立即终止所有相关进程
  • CUDA驱动未收到清理通知
  • 显存100%残留
  • 必须手动重置GPU(风险操作)

3. 最佳实践与自动化方案

3.1 进程树终止脚本

创建 kill_gpu_procs.sh 脚本,智能清理残留进程:

#!/bin/bash
# 获取所有占用NVIDIA设备的进程ID
PIDS=$(fuser -v /dev/nvidia* 2>&1 | awk '{print $2}' | sort -u)

for PID in $PIDS; do
    # 获取进程组ID并终止整个会话
    PGID=$(ps -o pgid= $PID | grep -o '[0-9]*')
    if [ ! -z "$PGID" ]; then
        kill -- -$PGID  # 负号表示终止进程组
    fi
done

# 最终确认
nvidia-smi

3.2 PyTorch防御性编程

在训练脚本中加入资源保障代码:

import atexit
import torch

def cleanup():
    print("执行清理...")
    torch.cuda.empty_cache()
    # 终止DataLoader工作进程
    if hasattr(train_loader, '_workers'):
        train_loader._workers.join()

atexit.register(cleanup)

3.3 四种方式对比表格

终止方式 信号值 显存释放率 子进程清理 推荐指数
正常退出 - 100% 完整 ★★★★★
SIGINT(Ctrl+C) 2 80% 部分 ★★★☆☆
SIGTERM(kill) 15 40% 不完整 ★★☆☆☆
SIGKILL(-9) 9 0% ★☆☆☆☆

4. 高级技巧:CUDA上下文管理

对于需要频繁启停实验的场景,建议采用 隔离式CUDA上下文

import torch
from contextlib import contextmanager

@contextmanager
def cuda_context(device=0):
    try:
        with torch.cuda.device(device):
            yield
    finally:
        torch.cuda.empty_cache()
        # 可选:重置设备(激进)
        if torch.cuda.is_initialized():
            torch.cuda.synchronize()
            torch.cuda.ipc_collect()

# 使用示例
with cuda_context():
    model = build_model().cuda()
    train(model)

这种方案虽然增加少量开销,但能确保上下文资源完全释放。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐