Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

10. 【Linux】cgroups:容器化GPU隔离的基石

在 Docker 容器里运行 nvidia-smi,报错 Failed to initialize NVML: Unknown Error

为什么宿主机能看到 8 块 GPU,容器里却"找不到设备"?

答案藏在 Linux 的 cgroups 机制里。


1. cgroups 是什么?

核心问题:Linux 如何让进程 A 只能用 GPU 0,进程 B 只能用 GPU 1?

答案是控制组(Control Groups,简称 cgroups) —— Linux 内核提供的资源隔离机制。

它不仅能限制 CPU 和内存,更重要的是能控制进程能访问哪些设备。

1.1 三个核心概念

cgroups 由三个关键组件构成:

控制组(cgroup):进程的"资源配额组"。每个 cgroup 就像一个容器,规定了组内进程能使用多少资源、能访问哪些设备。

子系统(subsystem):不同类型的资源控制器。每种资源有独立的控制器:

  • cpu:控制 CPU 时间分配
  • memory:限制内存使用量
  • devices:控制设备访问权限(本文重点
  • blkio:限制磁盘 IO 带宽

层次结构(hierarchy):cgroup 以树形方式组织。父组的限制会自动应用到所有子组。

1.2 直白版:公司门禁系统

想象一个公司的门禁系统:

公司(根cgroup)
├─ 研发部(子cgroup)
│  ├─ 门禁权限:能进实验室、机房
│  └─ 设备权限:能用GPU服务器
├─ 财务部(子cgroup)
│  ├─ 门禁权限:能进财务室
│  └─ 设备权限:只能用办公电脑
  • 控制组 = 部门分组
  • 子系统 = 不同类型的权限(门禁、设备使用)
  • 层次 = 组织架构(公司 → 部门 → 小组)

1.3 cgroups 在文件系统中的样子

Linux 把 cgroups 实现为一个特殊的文件系统,挂载在 /sys/fs/cgroup/ 下:

$ ls /sys/fs/cgroup/
blkio/      # 磁盘IO控制
cpu/        # CPU时间控制
devices/    # 设备访问控制 ← GPU隔离用这个
memory/     # 内存限制
...

每个子系统是一个独立目录,目录下可以创建子目录来组织 cgroup 层次:

$ tree /sys/fs/cgroup/devices/
/sys/fs/cgroup/devices/
├── devices.allow          # 根cgroup的规则文件
├── devices.deny
├── cgroup.procs          # 属于这个cgroup的进程列表
├── docker/               # Docker创建的子cgroup
│   └── <容器ID>/
│       ├── devices.allow
│       └── cgroup.procs
└── slurm/                # Slurm创建的子cgroup
    └── uid_1000/
        └── job_12345/

关键点:每个 cgroup 目录下都有配置文件,通过写入这些文件来设置规则。


2. devices 子系统——设备访问的守门员

核心问题:如何让进程"看不见"某个 GPU?

答案是devices 子系统。它使用白名单机制:默认拒绝所有设备访问,只有明确允许的设备才能打开。

2.1 Linux 如何表示设备

在 Linux 中,所有硬件设备都以文件形式出现在 /dev/ 目录下。GPU 也不例外:

$ ls -l /dev/nvidia*
crw-rw-rw- 1 root root 195, 0   Oct 21 10:00 /dev/nvidia0
crw-rw-rw- 1 root root 195, 1   Oct 21 10:00 /dev/nvidia1
crw-rw-rw- 1 root root 195, 2   Oct 21 10:00 /dev/nvidia2
crw-rw-rw- 1 root root 195, 255 Oct 21 10:00 /dev/nvidiactl
crw-rw-rw- 1 root root 510, 0   Oct 21 10:00 /dev/nvidia-uvm

关键信息解读:

字段含义GPU 示例
第 1 列首字符设备类型:c=字符设备,b=块设备c(GPU 是字符设备)
第 5 列主设备号(标识驱动程序)195(NVIDIA GPU 驱动)
第 6 列次设备号(标识具体设备)0(第一块 GPU)
文件名设备节点路径/dev/nvidia0

主设备号 195 代表 NVIDIA GPU 驱动。所有 NVIDIA GPU 共享这个主设备号,通过次设备号区分:

  • 195:0/dev/nvidia0(第一块 GPU)
  • 195:1/dev/nvidia1(第二块 GPU)
  • 195:255/dev/nvidiactl(控制设备,所有 GPU 操作必需)

设备号 510 是 NVIDIA UVM(Unified Virtual Memory)驱动:

  • 510:0/dev/nvidia-uvm(CUDA 统一内存管理)

2.2 devices 子系统的工作原理

devices 子系统通过两个文件控制设备访问:

devices.allow:添加设备到白名单

# 格式:<type> <major>:<minor> <permissions>
echo "c 195:0 rwm" > devices.allow

devices.deny:从白名单移除设备

echo "c 195:1 rwm" > devices.deny

规则格式详解:

部分可选值说明GPU 示例
typec(字符)或 b(块)设备类型c
major数字或 *(所有)主设备号195
minor数字或 *(所有)次设备号0, 1, 255
permissionsr/w/m的组合r=读,w=写,m=mknod 创建节点rwm

通配符示例

  • c 195:* rwm → 允许所有 NVIDIA GPU(次设备号任意)
  • a *:* rwm → 允许所有设备(a 表示所有类型)

2.3 直白版:门禁卡系统

把 devices 子系统想象成公司的门禁系统:

默认状态:
├─ 所有房间的门都锁上(默认拒绝一切设备)
└─ 员工没有任何钥匙

devices.allow 操作:
├─ "发一张195:0号房间的钥匙给这个进程"
└─ 现在进程能打开 /dev/nvidia0 这扇门

devices.deny 操作:
├─ "收回195:1号房间的钥匙"
└─ 进程不能再访问 /dev/nvidia1

关键点:当进程尝试 open("/dev/nvidia0") 时,内核会检查:

  1. 进程属于哪个 cgroup?
  2. 该 cgroup 的 devices.allow 里有 c 195:0 吗?
  3. 有 → 允许打开;没有 → 返回 Permission denied

2.4 为什么需要 nvidiactl 和 nvidia-uvm?

使用 GPU 不是打开单个设备文件那么简单。CUDA 程序需要访问三类设备:

设备设备号作用必需性
/dev/nvidia0195:0GPU 0 的计算核心✅ 必需(如果用 GPU 0)
/dev/nvidiactl195:255GPU 驱动控制接口✅ 必需(任何 GPU 操作)
/dev/nvidia-uvm510:0统一内存管理✅ 必需(CUDA 6.0+)

实例:如果只允许 195:0 而不允许 195:255,CUDA 程序会在初始化时失败:

import torch
torch.cuda.is_available()  # False
# 内部尝试打开 /dev/nvidiactl 失败

3. 实战:手动隔离 GPU 访问

场景:让进程 A 只能用 GPU 0,进程 B 只能用 GPU 1,进程 C 完全看不到 GPU。

3.1 准备工作:确认设备号

首先确认你的 GPU 设备号(不同驱动版本可能不同):

$ ls -l /dev/nvidia* /dev/nvidia-uvm
crw-rw-rw- 1 root root 195, 0   /dev/nvidia0
crw-rw-rw- 1 root root 195, 1   /dev/nvidia1
crw-rw-rw- 1 root root 195, 255 /dev/nvidiactl
crw-rw-rw- 1 root root 510, 0   /dev/nvidia-uvm

# 确认cgroups v1已挂载
$ mount | grep cgroup
cgroup on /sys/fs/cgroup/devices type cgroup (rw,devices)

如果没有挂载,执行:

sudo mount -t cgroup -o devices none /sys/fs/cgroup/devices

3.2 创建隔离 cgroup:只能用 GPU 0

完整流程(需要 root 权限):

# 步骤1:创建cgroup目录
sudo mkdir -p /sys/fs/cgroup/devices/gpu0_only

# 步骤2:默认拒绝所有设备(可选,大多数系统已配置)
# 如果 devices.list 是空的,需要先继承父组规则
sudo bash -c 'cat /sys/fs/cgroup/devices/devices.list > /sys/fs/cgroup/devices/gpu0_only/devices.list'

# 步骤3:只允许必需的设备
# 3.1 允许GPU 0
sudo bash -c 'echo "c 195:0 rwm" > /sys/fs/cgroup/devices/gpu0_only/devices.allow'
# 3.2 允许nvidiactl(必需)
sudo bash -c 'echo "c 195:255 rwm" > /sys/fs/cgroup/devices/gpu0_only/devices.allow'
# 3.3 允许nvidia-uvm(必需)
sudo bash -c 'echo "c 510:0 rwm" > /sys/fs/cgroup/devices/gpu0_only/devices.allow'

# 步骤4:把当前shell进程加入cgroup
echo $$ | sudo tee /sys/fs/cgroup/devices/gpu0_only/cgroup.procs

为什么用 bash -c? 因为重定向 > 操作由 shell 执行,sudo echo 只提升了 echo 的权限,重定向仍是普通用户。正确做法是 sudo bash -c 'echo ... > ...'

3.3 验证隔离效果

现在这个 shell 和它启动的子进程都受 cgroup 限制:

# 测试1:能看到几块GPU?
$ python3 -c "import torch; print(f'可见GPU数量: {torch.cuda.device_count()}')"
可见GPU数量: 1

# 测试2:是GPU 0吗?
$ python3 -c "import torch; print(torch.cuda.get_device_name(0))"
NVIDIA A100-SXM4-40GB

# 测试3:尝试强制使用GPU 1
$ CUDA_VISIBLE_DEVICES=1 python3 -c "import torch; print(torch.cuda.device_count())"
可见GPU数量: 0  # ✅ 看不到GPU 1

# 测试4:底层设备访问
$ python3 -c "open('/dev/nvidia0', 'r')"  # 成功

$ python3 -c "open('/dev/nvidia1', 'r')"  # 失败
PermissionError: [Errno 13] Permission denied: '/dev/nvidia1'

关键观察

  • torch.cuda.device_count() 返回 1,因为 CUDA 驱动尝试打开 /dev/nvidia1 失败
  • 直接打开设备文件会得到 Permission denied 错误
  • CUDA_VISIBLE_DEVICES=1 不起作用,因为 cgroup 在更底层拦截

3.4 查看当前 cgroup 的规则

# 查看这个cgroup允许哪些设备
$ cat /sys/fs/cgroup/devices/gpu0_only/devices.list
c 195:0 rwm    # GPU 0
c 195:255 rwm  # nvidiactl
c 510:0 rwm    # nvidia-uvm
# (实际输出可能还包含其他系统设备,如 /dev/null)

# 查看当前进程属于哪个cgroup
$ cat /proc/self/cgroup | grep devices
8:devices:/gpu0_only

3.5 创建只能用 GPU 1 的 cgroup

同样的方法创建另一个隔离组:

sudo mkdir -p /sys/fs/cgroup/devices/gpu1_only
sudo bash -c 'echo "c 195:1 rwm" > /sys/fs/cgroup/devices/gpu1_only/devices.allow'
sudo bash -c 'echo "c 195:255 rwm" > /sys/fs/cgroup/devices/gpu1_only/devices.allow'
sudo bash -c 'echo "c 510:0 rwm" > /sys/fs/cgroup/devices/gpu1_only/devices.allow'

# 在新的shell中运行
sudo bash  # 启动新shell
echo $$ | sudo tee /sys/fs/cgroup/devices/gpu1_only/cgroup.procs

# 验证
python3 -c "import torch; print(torch.cuda.get_device_name(0))"
# 现在看到的"第0块GPU"实际是物理GPU 1

3.6 创建完全无 GPU 的 cgroup

sudo mkdir -p /sys/fs/cgroup/devices/no_gpu
# 不添加任何 195:* 设备到白名单

echo $$ | sudo tee /sys/fs/cgroup/devices/no_gpu/cgroup.procs

$ python3 -c "import torch; print(torch.cuda.is_available())"
False  # ✅ 完全看不到GPU

3.7 退出 cgroup

将进程移回根 cgroup 即可恢复完整权限:

echo $$ | sudo tee /sys/fs/cgroup/devices/cgroup.procs

3.8 完整自动化脚本

#!/bin/bash
# create_gpu_cgroup.sh - 创建GPU隔离cgroup

if [ "$#" -ne 1 ]; then
    echo "用法: $0 <GPU编号>"
    echo "示例: $0 0  # 创建只能用GPU 0的cgroup"
    exit 1
fi

GPU_ID=$1
CGROUP_NAME="gpu${GPU_ID}_only"
CGROUP_PATH="/sys/fs/cgroup/devices/${CGROUP_NAME}"

# 创建cgroup
sudo mkdir -p "$CGROUP_PATH"

# 允许必需的设备
sudo bash -c "echo 'c 195:${GPU_ID} rwm' > ${CGROUP_PATH}/devices.allow"
sudo bash -c "echo 'c 195:255 rwm' > ${CGROUP_PATH}/devices.allow"
sudo bash -c "echo 'c 510:0 rwm' > ${CGROUP_PATH}/devices.allow"

echo "✅ cgroup创建成功: $CGROUP_NAME"
echo "运行以下命令进入隔离环境:"
echo "  echo \$\$ | sudo tee ${CGROUP_PATH}/cgroup.procs"

使用示例:

$ ./create_gpu_cgroup.sh 0
✅ cgroup创建成功: gpu0_only

$ echo $$ | sudo tee /sys/fs/cgroup/devices/gpu0_only/cgroup.procs
$ python3 my_model.py  # 只会用GPU 0

4. cgroups v1 vs v2:两个版本的差异

核心问题:为什么有两套 cgroups 系统?

Linux 社区在 cgroups v1 使用多年后发现了设计缺陷,于 2016 年推出了重构的 v2 版本。两者并存,系统默认使用其中之一。

4.1 关键差异对比

维度cgroups v1cgroups v2影响
挂载点/sys/fs/cgroup/<subsystem>//sys/fs/cgroup/ 统一v2 更简洁
层次结构每个子系统独立树所有子系统统一树v1 可能冲突
devices 控制devices.allow/deny 文件eBPF 程序v2 更灵活但复杂
进程分配可在叶子和内部节点只能在叶子节点v2 更严格
内核版本2.6.24+(2008 年)4.5+(2016 年)v2 需新内核
系统默认RHEL 7, Ubuntu 18.04RHEL 8+, Ubuntu 20.04+检查你的系统

4.2 如何判断系统使用哪个版本?

方法 1:检查挂载点

$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec)
# 有输出 → 使用v2(unified hierarchy)

$ mount | grep cgroup | grep devices
cgroup on /sys/fs/cgroup/devices type cgroup (rw,devices)
# 有独立devices挂载 → 使用v1

方法 2:检查目录结构

# v1:每个子系统独立目录
$ ls /sys/fs/cgroup/
blkio/  cpu/  devices/  memory/  ...

# v2:统一目录,无子系统子目录
$ ls /sys/fs/cgroup/
cgroup.controllers  cgroup.procs  memory.max  ...

方法 3:读取 cgroup 版本文件

$ cat /proc/cgroups | grep devices
devices 1 234 1  # 第3列>0 → v1启用

4.3 对 GPU 隔离的实际影响

好消息:容器工具(Docker、Podman、Kubernetes)会自动适配版本。你不需要修改配置。

如果你需要手动操作

cgroups v1(本文前面的示例)

# 直接操作文件
echo "c 195:0 rwm" > /sys/fs/cgroup/devices/my_cgroup/devices.allow

cgroups v2(使用 eBPF)

# 需要通过systemd或编写eBPF程序
systemd-run --unit=my-gpu-app \
    --property=DeviceAllow="/dev/nvidia0 rwm" \
    python3 my_model.py

v2 的 devices 控制改用 eBPF(extended Berkeley Packet Filter)实现,提供了更灵活的过滤逻辑,但直接操作更复杂。实际使用中推荐:

  • 生产环境:通过容器引擎或资源管理器(Docker、K8s、Slurm)
  • 调试开发:用 v1 的简单文件接口(如果系统支持)

4.4 混合模式(Hybrid)

部分发行版支持 v1 和 v2 同时挂载:

$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2        # v2统一层次
cgroup on /sys/fs/cgroup/devices type cgroup  # v1 devices子系统

这种配置下:

  • 新应用优先用 v2
  • 需要 devices 控制的传统应用仍可用 v1 接口

5. 容器引擎如何使用 cgroups

核心问题:Docker 的 --device--gpus 参数背后做了什么?

5.1 Docker 的设备隔离

当你运行:

docker run --gpus '"device=0"' nvidia/cuda:11.8.0-base nvidia-smi

Docker 在背后执行了这些操作(cgroups v1 为例):

1. 创建cgroup
   mkdir /sys/fs/cgroup/devices/docker/<容器ID>

2. 配置devices规则
   echo "c 195:0 rwm" > .../devices.allow     # GPU 0
   echo "c 195:255 rwm" > .../devices.allow   # nvidiactl
   echo "c 510:0 rwm" > .../devices.allow     # nvidia-uvm

3. 启动容器进程
   PID=$(docker-containerd-shim ...)

4. 加入cgroup
   echo $PID > /sys/fs/cgroup/devices/docker/<容器ID>/cgroup.procs

验证:查看运行中的容器

$ docker ps
CONTAINER ID   IMAGE
abc123def456   nvidia/cuda

$ cat /sys/fs/cgroup/devices/docker/abc123def456*/devices.list
c 195:0 rwm
c 195:255 rwm
c 510:0 rwm
...

5.2 --device vs --gpus 的区别

Docker 提供两种方式指定 GPU:

方式 1--device(手动指定设备文件)

docker run \
    --device=/dev/nvidia0 \
    --device=/dev/nvidiactl \
    --device=/dev/nvidia-uvm \
    nvidia/cuda:11.8.0-base
  • 直接操作 devices cgroup
  • 需要手动列出所有必需设备
  • 适合精确控制

方式 2--gpus(高级 GPU 支持,需要 nvidia-docker2)

docker run --gpus '"device=0,1"' nvidia/cuda:11.8.0-base
  • 通过 nvidia-container-toolkit 自动处理
  • 自动添加所有必需设备(nvidiactl、uvm、libraries)
  • 自动设置环境变量(NVIDIA_VISIBLE_DEVICES

对比表格

特性--device--gpus
需要 nvidia-docker2❌ 否✅ 是
自动处理依赖设备❌ 需手动指定✅ 自动
支持 MIG 分区❌ 否✅ 是
挂载 CUDA 库❌ 需手动-v✅ 自动
设置环境变量❌ 需手动-e✅ 自动
适用场景简单测试生产环境

5.3 Kubernetes 的 Device Plugin 机制

Kubernetes 通过 Device Plugin 框架管理 GPU。当你创建 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  containers:
    - name: cuda-container
      image: nvidia/cuda:11.8.0-base
      resources:
        limits:
          nvidia.com/gpu: 1 # 请求1块GPU

背后的流程

1. kubelet调用nvidia-device-plugin
   "有哪些可用GPU?"
   → 插件返回:nvidia0, nvidia1, nvidia2...

2. Scheduler分配
   "给这个Pod分配nvidia0"

3. kubelet配置容器
   ├─ 设置cgroup规则(允许访问 195:0)
   ├─ 挂载设备文件(/dev/nvidia0)
   └─ 设置环境变量(CUDA_VISIBLE_DEVICES=0)

4. 容器启动
   只能看到被分配的GPU

验证:查看 Pod 的 cgroup

# 找到Pod的容器ID
$ kubectl describe pod gpu-pod | grep "Container ID"
Container ID: containerd://xyz789

# 查看cgroup配置
$ cat /sys/fs/cgroup/devices/kubepods/pod<uid>/xyz789/devices.list
c 195:0 rwm
c 195:255 rwm
c 510:0 rwm

5.4 Slurm 的 task/cgroup 插件

Slurm 使用 task/cgroup 插件自动管理作业的资源隔离。配置示例:

slurm.conf

# 启用cgroup任务管理
TaskPlugin=task/cgroup

# 约束设备访问
ConstrainDevices=yes

# cgroup配置文件
TaskPluginParam=autobind=yes

cgroup.conf

CgroupAutomount=yes
ConstrainDevices=yes

# 设置cgroup挂载点
CgroupMountpoint=/sys/fs/cgroup

提交作业

$ sbatch --gres=gpu:1 my_job.sh
Submitted batch job 12345

Slurm 创建的 cgroup 结构

/sys/fs/cgroup/devices/slurm/
└── uid_1000/              # 用户ID
    └── job_12345/         # 作业ID
        └── step_0/        # 作业步骤
            ├── devices.allow
            └── cgroup.procs

查看作业的 GPU 权限

$ cat /sys/fs/cgroup/devices/slurm/uid_1000/job_12345/step_0/devices.list
c 195:0 rwm    # Slurm自动根据--gres=gpu:1分配了GPU 0
c 195:255 rwm
c 510:0 rwm

自动隔离的效果

  • 作业 A 的进程只能访问分配给它的 GPU
  • 作业 B 的进程无法"看到"作业 A 的 GPU
  • 作业结束后自动清理 cgroup

5.5 对比:三种工具的 cgroup 使用

工具cgroup 创建方式GPU 隔离粒度配置方式
Docker每个容器一个 cgroup按容器--device--gpus
KubernetesPod/Container 层次按容器(Device Plugin)Pod YAML 的 resources.limits
Slurm按用户/作业/步骤按作业--gres=gpu:N
手动mkdir 创建自定义直接写 devices.allow

6. 实战案例:多用户 GPU 共享服务器

场景:一台 8 卡 GPU 服务器,3 个用户(Alice、Bob、Charlie),如何保证隔离?

6.1 需求分析

用户分配 GPU要求
AliceGPU 0, 1不能看到其他 GPU
BobGPU 2, 3, 4不能看到其他 GPU
CharlieGPU 5, 6, 7不能看到其他 GPU

6.2 方案设计

使用 cgroups 创建用户级隔离:

#!/bin/bash
# setup_gpu_isolation.sh

# 为Alice创建cgroup(GPU 0,1)
sudo mkdir -p /sys/fs/cgroup/devices/user_alice
for gpu in 0 1; do
    echo "c 195:${gpu} rwm" | sudo tee -a /sys/fs/cgroup/devices/user_alice/devices.allow
done
echo "c 195:255 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_alice/devices.allow
echo "c 510:0 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_alice/devices.allow

# 为Bob创建cgroup(GPU 2,3,4)
sudo mkdir -p /sys/fs/cgroup/devices/user_bob
for gpu in 2 3 4; do
    echo "c 195:${gpu} rwm" | sudo tee -a /sys/fs/cgroup/devices/user_bob/devices.allow
done
echo "c 195:255 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_bob/devices.allow
echo "c 510:0 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_bob/devices.allow

# 为Charlie创建cgroup(GPU 5,6,7)
sudo mkdir -p /sys/fs/cgroup/devices/user_charlie
for gpu in 5 6 7; do
    echo "c 195:${gpu} rwm" | sudo tee -a /sys/fs/cgroup/devices/user_charlie/devices.allow
done
echo "c 195:255 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_charlie/devices.allow
echo "c 510:0 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_charlie/devices.allow

echo "✅ GPU隔离cgroup创建完成"

6.3 用户登录时自动加入 cgroup

编辑 /etc/security/limits.conf 或使用 PAM 模块,在用户登录时自动加入对应 cgroup:

# /usr/local/bin/cgroup_login.sh
#!/bin/bash
case "$PAM_USER" in
    alice)
        echo $$ | sudo tee /sys/fs/cgroup/devices/user_alice/cgroup.procs
        ;;
    bob)
        echo $$ | sudo tee /sys/fs/cgroup/devices/user_bob/cgroup.procs
        ;;
    charlie)
        echo $$ | sudo tee /sys/fs/cgroup/devices/user_charlie/cgroup.procs
        ;;
esac

/etc/pam.d/sshd 添加:

session required pam_exec.so /usr/local/bin/cgroup_login.sh

6.4 验证隔离效果

Alice 登录

alice@server:~$ python3 -c "import torch; print(f'可见GPU: {torch.cuda.device_count()}')"
可见GPU: 2

alice@server:~$ nvidia-smi --query-gpu=index,name --format=csv
index, name
0, NVIDIA A100-SXM4-40GB
1, NVIDIA A100-SXM4-40GB
# ✅ 只能看到GPU 0和1

Bob 登录

bob@server:~$ python3 -c "import torch; print(f'可见GPU: {torch.cuda.device_count()}')"
可见GPU: 3

bob@server:~$ nvidia-smi --query-gpu=index --format=csv
index
0
1
2
# ✅ 看到的是GPU 2,3,4(但CUDA内部编号为0,1,2)

关键点:CUDA 会重新编号它能访问的 GPU。Bob 看到的"GPU 0"实际是物理 GPU 2。


7. 调试与故障排查

7.1 常见问题诊断表

症状可能原因排查命令
容器内 nvidia-smi 失败devices 规则缺失cat /sys/fs/cgroup/devices/.../devices.list
Permission denied 访问/dev/nvidia0未授权该设备ls -l /dev/nvidia* 确认设备号
cgroup 文件不存在cgroups 未挂载mount | grep cgroup
隔离不生效进程不在 cgroup 内cat /proc/<PID>/cgroup
CUDA 初始化失败缺少 nvidiactl 或 uvm检查是否允许 195:255 和 510:0
能看到所有 GPU进程在根 cgroup检查 /proc/self/cgroup

7.2 调试技巧

技巧 1:查看进程的 cgroup 归属

# 方法1:查看当前shell
cat /proc/self/cgroup

# 方法2:查看特定进程
cat /proc/<PID>/cgroup | grep devices
# 输出示例:8:devices:/docker/abc123

# 方法3:查看Python进程
python3 -c "import os; print(open(f'/proc/{os.getpid()}/cgroup').read())"

技巧 2:查看 cgroup 的完整规则

# 找到进程的devices cgroup路径
CGROUP_PATH=$(cat /proc/self/cgroup | grep devices | cut -d: -f3)

# 查看该cgroup允许的所有设备
cat /sys/fs/cgroup/devices${CGROUP_PATH}/devices.list

技巧 3:测试设备访问权限

# 直接测试能否打开设备文件
python3 -c "
import os
for i in range(8):
    try:
        fd = os.open(f'/dev/nvidia{i}', os.O_RDONLY)
        os.close(fd)
        print(f'✅ GPU {i}: 可访问')
    except PermissionError:
        print(f'❌ GPU {i}: 权限拒绝')
    except FileNotFoundError:
        print(f'⚠️  GPU {i}: 设备不存在')
"

技巧 4:临时提权测试

# 如果怀疑是cgroup限制,临时移到根cgroup测试
original_cgroup=$(cat /proc/self/cgroup | grep devices | cut -d: -f3)
echo $$ | sudo tee /sys/fs/cgroup/devices/cgroup.procs

# 测试程序
python3 test_gpu.py

# 恢复
echo $$ | sudo tee /sys/fs/cgroup/devices${original_cgroup}/cgroup.procs

7.3 性能影响

cgroups 的 devices 检查非常轻量,几乎无性能影响:

操作开销说明
检查 devices 规则~10 ns内核内存查找
首次打开设备+50 ns一次性开销
后续 GPU 操作0设备已打开,无额外检查
CUDA kernel 执行0cgroup 不影响 GPU 计算

关键点:cgroup 只在打开设备文件时检查,之后 GPU 操作完全不受影响。

7.4 清理 cgroup

如果测试创建了很多 cgroup,清理方法:

# 删除cgroup前,必须先移出所有进程
# 1. 查看cgroup内的进程
cat /sys/fs/cgroup/devices/gpu0_only/cgroup.procs

# 2. 移动进程到根cgroup
for pid in $(cat /sys/fs/cgroup/devices/gpu0_only/cgroup.procs); do
    echo $pid | sudo tee /sys/fs/cgroup/devices/cgroup.procs
done

# 3. 删除空cgroup
sudo rmdir /sys/fs/cgroup/devices/gpu0_only

批量清理脚本

#!/bin/bash
for cgroup in /sys/fs/cgroup/devices/gpu*_only; do
    if [ -d "$cgroup" ]; then
        # 移出所有进程
        while read pid; do
            echo $pid | sudo tee /sys/fs/cgroup/devices/cgroup.procs
        done < "$cgroup/cgroup.procs"
        # 删除cgroup
        sudo rmdir "$cgroup"
        echo "已删除: $cgroup"
    fi
done

8. 总结

8.1 核心要点

  1. cgroups 是进程资源的访问控制系统

    • 通过文件系统接口操作
    • 影响所有子进程
    • 内核级强制执行
  2. devices 子系统使用白名单机制

    • 默认拒绝所有设备访问
    • 通过 devices.allow 明确授权
    • 规则格式:<type> <major>:<minor> <permissions>
  3. GPU 隔离需要三个设备

    • /dev/nvidia0(195:0):计算核心
    • /dev/nvidiactl(195:255):控制接口
    • /dev/nvidia-uvm(510:0):统一内存管理
  4. cgroups v1 vs v2

    • v1:简单文件接口,直接操作
    • v2:统一层次,eBPF 实现,容器工具自动适配
    • 生产环境:通过容器引擎管理,无需关心版本
  5. 容器工具自动使用 cgroups

    • Docker:--device--gpus
    • Kubernetes:Device Plugin 框架
    • Slurm:task/cgroup 插件

8.2 适用场景对比

场景推荐方案管理复杂度隔离强度
单机开发测试手动 cgroup
多用户共享服务器Slurm + cgroup
容器化部署Docker/K8s低(自动化)
云原生 GPU 应用K8s Device Plugin低(自动化)
HPC 集群Slurm + cgroup

8.3 进一步学习

内核文档

# cgroups v1 devices子系统
/usr/src/linux/Documentation/cgroup-v1/devices.txt

# cgroups v2
/usr/src/linux/Documentation/cgroup-v2.txt

实践项目

  • 用 cgroups 实现简易"容器":隔离进程的 CPU、内存、设备
  • 编写 Device Plugin:为 K8s 添加自定义资源类型
  • 分析 Docker 源码:看容器引擎如何操作 cgroups

相关技术

  • Namespace:隔离进程的视图(网络、PID、挂载点)
  • eBPF:cgroups v2 使用的底层技术
  • NVIDIA MIG:GPU 物理分区(与 cgroups 互补)

附录:快速参考卡片

# === 查看系统信息 ===
# GPU设备号
ls -l /dev/nvidia*

# cgroups版本
mount | grep cgroup

# 进程的cgroup
cat /proc/self/cgroup

# === 创建GPU隔离cgroup(v1) ===
# 创建cgroup
sudo mkdir -p /sys/fs/cgroup/devices/my_cgroup

# 允许GPU 0
echo "c 195:0 rwm" | sudo tee /sys/fs/cgroup/devices/my_cgroup/devices.allow
echo "c 195:255 rwm" | sudo tee /sys/fs/cgroup/devices/my_cgroup/devices.allow
echo "c 510:0 rwm" | sudo tee /sys/fs/cgroup/devices/my_cgroup/devices.allow

# 加入cgroup
echo $$ | sudo tee /sys/fs/cgroup/devices/my_cgroup/cgroup.procs

# === 验证隔离 ===
# Python测试
python3 -c "import torch; print(f'GPU数量: {torch.cuda.device_count()}')"

# 设备文件测试
python3 -c "open('/dev/nvidia0', 'r')"  # 应该成功
python3 -c "open('/dev/nvidia1', 'r')"  # 应该失败

# === 调试 ===
# 查看允许的设备
cat /sys/fs/cgroup/devices/my_cgroup/devices.list

# 查看cgroup内的进程
cat /sys/fs/cgroup/devices/my_cgroup/cgroup.procs

# === 清理 ===
# 退出cgroup
echo $$ | sudo tee /sys/fs/cgroup/devices/cgroup.procs

# 删除cgroup
sudo rmdir /sys/fs/cgroup/devices/my_cgroup

关键命令速查

需求命令
查看 GPU 设备号ls -l /dev/nvidia*
创建 cgroupmkdir /sys/fs/cgroup/devices/<name>
允许设备echo "c 195:0 rwm" > .../devices.allow
加入 cgroupecho $$ > .../cgroup.procs
查看进程 cgroupcat /proc/self/cgroup
查看允许的设备cat .../devices.list
退出 cgroupecho $$ > /sys/fs/cgroup/devices/cgroup.procs
删除 cgrouprmdir /sys/fs/cgroup/devices/<name>