GPU 进程与资源管理
基于 8 × A100-SXM4-80GB (NVSwitch Gen2) 生产环境实测。GPU 是共享资源——多用户、多任务同时提交时,如何确保正确的人在正确的卡上运行,如何隔离显存、如何避免 OOM、如何排查残留进程。本文从 Compute Mode 到 NUMA 亲和性完整覆盖。
1. Compute Mode:GPU 的共享策略
GPU 的 Compute Mode 决定多个进程能否同时在该 GPU 上执行计算:
nvidia-smi --query-gpu=index,name,compute_mode --format=csv
当前环境输出(全部 Default):
0, NVIDIA A100-SXM4-80GB, Default
3, NVIDIA A100-SXM4-80GB, Default
7, NVIDIA A100-SXM4-80GB, Default
| Mode | 含义 | 适用场景 |
|---|---|---|
| Default | 多个进程可以在同一 GPU 上同时运行 CUDA 代码 | 共享集群、开发环境 |
| EXCLUSIVE_PROCESS | 同一时刻只有一个进程的 CUDA context 能在该 GPU 上存在 | 专机专用训练、性能 benchmark |
| PROHIBITED | 该 GPU 禁止任何 CUDA 程序使用 | 预留 GPU(如留给 MIG 实验) |
| EXCLUSIVE_THREAD | 已废弃,同 EXCLUSIVE_PROCESS | — |
# 设置 GPU 3 为独占模式(需要 root)
nvidia-smi -i 3 -c 1 # 1=EXCLUSIVE_PROCESS
# 恢复默认可共享
nvidia-smi -i 3 -c 0 # 0=Default
Default 模式的隐患:两个训练任务同时占用同一 GPU 时不会报错,各自认为自己拥有全部显存,直到 OOM。Default 模式适合开发环境,生产训练建议 EXCLUSIVE_PROCESS。
2. MPS:多进程共享 GPU 计算资源
MPS(Multi-Process Service)是另一种 GPU 共享机制,与 Compute Mode 互补:
| 方式 | 显存 | 计算资源 | 适用场景 |
|---|---|---|---|
| Default + CUDA_VISIBLE_DEVICES | 共享(可超额) | 时间片轮转 | 开发环境 |
| MPS | 共享 | 真正并行(CUDA kernel 并发执行) | 多个小推理服务共享一张 GPU |
| MIG | 硬切分(独立显存) | 硬切分(独立 SM) | 严格隔离的生产推理 |
| EXCLUSIVE_PROCESS | 独占 | 独占 | 训练、benchmark |
MPS 的核心价值:让多个进程的 CUDA kernel 在 GPU 上真正并行执行,而不是 Default 模式下的时间片切换。Volta (SM 7.0) 及以上架构支持。
# 启动 MPS 服务(需 root)
nvidia-cuda-mps-control -d
# 验证
ps aux | grep mps
# 停止
echo quit | nvidia-cuda-mps-control
启动后,所有 CUDA 进程自动通过 MPS 调度,无需修改代码。限制:MPS 与 MIG 互斥——启用了 MIG 的 GPU 不能同时使用 MPS;MPS 不提供显存隔离,一个进程 OOM 仍会影响同 GPU 上的其他进程。
典型部署:一张 A100 上同时跑 4 个轻量推理服务 → 开启 MPS,用
CUDA_VISIBLE_DEVICES或 MIG 做隔离,MPS 让计算资源得到充分利用。
3. 查看 GPU 上的进程
3.1 nvidia-smi 进程列表
nvidia-smi --query-compute-apps=pid,process_name,used_memory,gpu_name --format=csv
当前环境(摘录):
pid, process_name, used_gpu_memory [MiB], gpu_name
2159821, sglang::scheduler_TP1, 9752 MiB, NVIDIA A100-SXM4-80GB
2160317, sglang::scheduler_TP0, 9752 MiB, NVIDIA A100-SXM4-80GB
2332308, VLLM::EngineCore, 75172 MiB, NVIDIA A100-SXM4-80GB
1105912, ada_be, 4770 MiB, NVIDIA A100-SXM4-80GB
2095211, VLLM::EngineCore, 75260 MiB, NVIDIA A100-SXM4-80GB
- sglang 占用 GPU 0/1 各 ~9.5 GB — TP=2 推理
- vllm 占用 GPU 2/6 各 ~75 GB — 大模型推理
- ada_be 占用 GPU 6 的 4.7 GB — 开发环境
- GPU 3/4/5 空闲(仅 4-130 MiB 驱动占用)
3.2 fuser:排查残留进程
nvidia-smi 列出的都是有进程名的活跃进程。但有时进程已退出、显存却未释放:
fuser -v /dev/nvidia*
正常输出会列出每个 /dev/nvidia* 设备文件的打开进程。如果列出的 PID 在 nvidia-smi 中看不到 → 僵尸 GPU context(进程退出但 CUDA context 未释放)。
清理僵尸进程:
# 1. 确认进程已退出
ps aux | grep <PID>
# 2. 等待 CUDA context 自动回收(通常 < 30s)——大多数情况这样就够了
# 3. 如果等待后仍未释放,重置持久化模式强制清理
nvidia-smi -pm 0 && nvidia-smi -pm 1
常见场景:Python 脚本
Ctrl+C中断后,PyTorch 的 CUDA context 可能延迟数秒才释放。nvidia-persistenced保持驱动常驻,重置持久化模式会触发 context 回收——比杀守护进程更轻量。
4. CUDA_VISIBLE_DEVICES:进程级 GPU 选择
这是最常用的 GPU 隔离方式——设置环境变量,让进程只能”看到”指定的 GPU:
# 只使用 GPU 3
CUDA_VISIBLE_DEVICES=3 python train.py
# 使用 GPU 0,3,5(映射为 device 0,1,2)
CUDA_VISIBLE_DEVICES=0,3,5 python train.py
# 排除 GPU 7
CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6 python train.py
映射规则:
物理 GPU: 0 1 2 3 4 5 6 7
设置: 3,5,7
映射后: ─ ─ ─ 0 ─ 1 ─ 2 ← 进程内的 device ID
(不可见)
常见用法:
| 场景 | 命令 | 说明 |
|---|---|---|
| 每个用户分配专属 GPU | CUDA_VISIBLE_DEVICES=3 |
进程只看得到 GPU 3,映射为 device 0 |
| 训练避开生产 GPU | CUDA_VISIBLE_DEVICES=3,4,5 |
用空闲的 GPU 3/4/5 |
| 多 GPU 分布式 | CUDA_VISIBLE_DEVICES=0,1,2,3 torchrun --nproc_per_node=4 |
在 4 GPU 上并行 |
| 排除故障 GPU | 排除 GPU 7(NVLink 故障) | 避免 NCCL fallback 到 PCIe |
限制:
CUDA_VISIBLE_DEVICES只对单个进程生效,不影响其他用户。多个用户仍可通过CUDA_VISIBLE_DEVICES=3同时使用同一 GPU(除非设置了 EXCLUSIVE_PROCESS)。
5. GPU-NUMA 亲和性绑定
A100 通过 PCIe 连接到特定的 NUMA node。如果 GPU 和 CPU 线程不在同一 NUMA node,H2D/D2H 传输需要跨 socket 经 QPI/UPI,延迟显著增加。
4.1 查看 GPU 的 NUMA 归属
nvidia-smi topo -m
GPU0 ... GPU3 GPU4 ... GPU7 CPU Affinity NUMA Affinity
GPU3 ... X NV12 ... NV12 0-23,48-71 0
GPU7 ... SYS PXB ... X 24-47,72-95 1
- GPU 0-3 → NUMA node 0, CPUs 0-23, 48-71
- GPU 4-7 → NUMA node 1, CPUs 24-47, 72-95
4.2 NUMA 绑定:taskset + numactl
# 将进程绑定到 GPU 3 所属的 NUMA node 0 的 CPU
taskset -c 0-23,48-71 CUDA_VISIBLE_DEVICES=3 python train.py
# 或用 numactl
numactl --cpunodebind=0 --membind=0 CUDA_VISIBLE_DEVICES=3 python train.py
为什么重要:
| 场景 | 无 NUMA 绑定 | 有 NUMA 绑定 | 影响 |
|---|---|---|---|
| GPU 3 + CPU 线程在 node 1 | H2D 跨 socket (距离 32) | — | 延迟翻倍 |
| GPU 3 + CPU 线程在 node 0 | ✅ 本地访问 (距离 10) | 可进一步固化 | 最优 |
# 查看 NUMA 距离
numactl --hardware
# node 0 → node 1: 32 (跨 socket, 慢)
# node 0 → node 0: 10 (本地, 快)
经验法则:单 GPU 推理无需 NUMA 绑定(H2D 只发生一次)。用
taskset绑定;多 GPU 分布式训练建议用torchrun/mpirun的 NUMA 感知启动器。
6. 显存管理
5.1 检查显存使用
nvidia-smi --query-gpu=index,memory.used,memory.free,memory.total --format=csv
GPU 3/4/5 的显存状态:
3, 130 MiB, 81790 MiB, 81920 MiB
4, 4 MiB, 81916 MiB, 81920 MiB
5, 4 MiB, 81916 MiB, 81920 MiB
- 130 MiB(GPU 3):之前跑过测试后 CUDA context 可能未完全释放
- 4 MiB(GPU 4/5):纯空闲,仅驱动开销
5.2 PyTorch 显存控制
import torch
# 限制 PyTorch 可用的最大显存比例(多进程共享 GPU 时有用)
torch.cuda.set_per_process_memory_fraction(0.5) # 只用 50%
# 控制显存分配器的碎片化行为
import os
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:128"
set_per_process_memory_fraction在多进程共享同一 GPU 时最有用——限制每个进程不会抢光全部显存。单进程独占 GPU 时不需要设。
5.3 清理显存
# 释放 PyTorch 缓存的显存(不释放仍被引用的 tensor)
torch.cuda.empty_cache()
gc.collect() # 先回收 Python 侧的无效引用
torch.cuda.empty_cache() # 再释放 CUDA 侧缓存
empty_cache()只释放 PyTorch 分配器缓存的空闲块,不会释放正在使用的 tensor。训练中 OOM 的根因通常是 batch size 太大或模型太大——先降 batch,再考虑 gradient checkpointing。排查流程见 §7 常见问题。
7. 常见问题与排查
| 问题 | 现象 | 排查 | 解决 |
|---|---|---|---|
| 显存泄漏 | nvidia-smi 显示显存占用但无进程 |
fuser -v /dev/nvidia* 找僵尸 PID |
kill -9 <PID> 或重启 persistenced |
| 多用户冲突 | 两个训练任务 OOM | nvidia-smi --query-compute-apps 看是否同一 GPU |
设置 EXCLUSIVE_PROCESS 或用 CUDA_VISIBLE_DEVICES |
| 跨 NUMA 延迟高 | H2D 带宽远低于预期 (~15 GB/s vs ~28 GB/s) | numactl --hardware 看 GPU 和 CPU 的 NUMA 归属 |
taskset 绑定 CPU 到对应 NUMA node |
| CUDA out of memory | 训练中途 OOM | nvidia-smi 看剩余显存 |
减小 batch size / 启用 gradient checkpointing |
| GPU 3 (130 MiB) 不是 0 | CUDA context 未完全释放 | nvidia-smi 无活跃进程但 memory.used > 0 |
nvidia-smi -pm 0 && nvidia-smi -pm 1 重置 persistenced |
8. 相关文档
03_nvidia_smi_guide.md:nvidia-smi 基础命令详解06_gpu_health_check.md:进程残留检查是 L1 健康检查的一部分- NVLink 诊断与实操:GPU 7 NVLink 故障 → 建议在分布式训练中排除
- GPU P2P 带宽实测:NUMA 亲和性直接影响 P2P 带宽