3.2 显存、算力与设备视图的隔离边界 读者读完这一节,应该能一句话说出他学到了什么:多人共用一张卡时,「谁能看见这张卡」由设备视图(如 )决定,而「每张卡用多少」要靠 MPS(进程级共享上下文省显存)、MIG(硬件级把卡切成实例)、cgroup(容器级算力/显存限额)三道不同粒度的闸来切,且它们管的是不同维度、可以叠加。 3.1 解决了「容器怎么看到 GPU」。但真实训练机从不是一个人独占八卡——五六个算法同事、十几个实验任务,全挤在同一台八卡 A100 上。如果不隔离,灾难是必然的:某人的训练把 80 GB 显存吃满,你的推理服务直接 OOM 被杀;某人写了死循环把整卡算力压到 100%,别人的任务全在排队干等。 这一节,就是把「共用不互踩」这件麻烦事,拆成三种可组合的隔离粒度。
读者读完这一节,应该能一句话说出他学到了什么:多人共用一张卡时,「谁能看见这张卡」由设备视图(如 NVIDIA_VISIBLE_DEVICES)决定,而「每张卡用多少」要靠 MPS(进程级共享上下文省显存)、MIG(硬件级把卡切成实例)、cgroup(容器级算力/显存限额)三道不同粒度的闸来切,且它们管的是不同维度、可以叠加。
3.1 解决了「容器怎么看到 GPU」。但真实训练机从不是一个人独占八卡——五六个算法同事、十几个实验任务,全挤在同一台八卡 A100 上。如果不隔离,灾难是必然的:某人的训练把 80 GB 显存吃满,你的推理服务直接 OOM 被杀;某人写了死循环把整卡算力压到 100%,别人的任务全在排队干等。
这一节,就是把「共用不互踩」这件麻烦事,拆成三种可组合的隔离粒度。
假设一台 8×A100(每卡 80 GB)的机器,五个人都在上面跑任务。如果不做任何隔离,会发生什么?
红框任务之间没有闸。这恰是大多数团队的起步状态,也是事故温床。下面三道闸,分别卡在不同维度。
最浅、也最常用的一层,就是 3.1 提到过的 NVIDIA_VISIBLE_DEVICES。它决定「这个容器/进程能看见哪几张卡」。
典型用法:给同事 A 分配卡 0、1,给同事 B 分配卡 2、3,通过环境变量把设备视图切开。这样 A 的 torch.cuda.device_count() 只返回 2,他压根看不见别人的卡,自然也碰不到。
# 容器 A:只看得见 0、1 号卡 NVIDIA_VISIBLE_DEVICES=0,1 # 容器 B:只看得见 2、3 号卡 NVIDIA_VISIBLE_DEVICES=2,3
但这道闸只切「视图」,不切「额度」。我必须再强调一次:如果两个容器都被设成 NVIDIA_VISIBLE_DEVICES=0,它们都只看见 0 号卡,但 0 号卡的 80 GB 显存和全部算力是共享的——谁先占满谁说了算,没有保护。所以在「一人一块卡」的粗放分配里够用,在「多人共用一张卡」的场景里远远不够。
判断标准:如果你要求「两个人能同时用同一张卡但互不干扰」,光靠设备视图做不到,必须上 MPS 或 MIG。
MPS(Multi-Process Service,多进程服务) 是 NVIDIA 提供的一个用户态守护进程。它干的事,可以通俗理解为:让多个进程共用同一个 CUDA 上下文(context)。
为什么这有用?默认情况下,每个进程各自建一个 CUDA context,每个 context 都要预占一块显存(上下文本身开销,常达数百 MB 到上 GB),且多个 context 在同一张卡上调度时会反复切换,浪费算力。MPS 把这些进程的请求汇聚到一个共享 context,带来两个好处:
但 MPS 有一个必须说清的边界:它做的是「共享与复用」,不是「硬性配额」。MPS 能让你多个任务友好地共用一张卡、减少开销,但它不会替你限制「每个任务最多用 40 GB 显存」。如果一个任务真要爆显存,它照样能拖垮同卡的其他任务。换句话说,MPS 解决的是「共用更省、更顺」,不是「互相限额互不踩」。
左:各自建 context,开销叠加、易切换抖动。右:经 MPS 汇聚成单一 context,省开销、更顺滑。
MIG(Multi-Instance GPU,多实例 GPU) 是 A100/H100 等数据中心级 GPU 支持的能力。它和 MPS 的根本区别在于:MIG 是在硬件层面把一张物理卡切成多个彼此隔离的「实例」。
每个 MIG 实例拥有自己独立的一部分显存、一部分算力(SM)、以及独立的显存带宽和缓存,彼此之间硬件级隔离——一个实例崩了、满了、被玩坏了,完全不影响另一个实例。这是目前「多人共用一张卡又必须互不踩」最彻底的解决方案。
一个典型切分(示意,具体档位以你的 GPU 型号与官方说明为准):
每个实例可以像一张「独立的小卡」那样,用 NVIDIA_VISIBLE_DEVICES 分配给不同容器。于是「给实习生半个 A100 算力」这种需求,就能通过 MIG 切出一个实例来精确实现——这是纯靠 docker 参数做不到的。
MIG 的代价与限制:不是所有 GPU 支持(消费级如 4090 不支持,主要是 A100/H100 及部分 A30 等);切分粒度由硬件档位决定,不是任意细;开启 MIG 后整张卡进入 MIG 模式,管理上要多一道配置。但它换来的「硬隔离」是 MPS 永远给不了的。
回到 docker 本身。即便不碰 MPS/MIG,你也能用 cgroup(控制组) 给容器设算力与显存的上限。Docker 原生支持通过运行时参数对 GPU 资源做限制,例如限制某容器最多用多少显存、占用多少 GPU 算力比例。
这里要分清:cgroup 是 Linux 内核的资源限额机制,docker 把它暴露成容器参数。在 GPU 场景下,它能约束「这个容器的 GPU 显存上限」「这个容器能用的算力份额」。它管的是额度,和 MIG 的硬隔离互补:cgroup 是软性配额(进程超了会被限制或 OOM),MIG 是硬件切分(物理上就分不到别人的那一份)。
工程里没有银弹,但有清晰的组合逻辑。我按团队常见规模给建议(具体命令与参数以你所用 NVIDIA 驱动/ Toolkit 版本的官方说明为准):
NVIDIA_VISIBLE_DEVICES)把卡平分到人即可。简单、零额外运维。这是大多数小团队起步的最优解。我的总体判断:默认从「设备视图平分 + cgroup 保险」起步;当共用同一张卡成为常态时,按场景 B/C 决定是否上 MPS 或 MIG;MIG 是硬隔离终点,但只在硬件支持且确实需要时才上,别为了用而用。
新手最容易犯的错,是把上面这些当成「选一个用」。实际上它们是叠加的分层闸:设备视图决定谁能进哪扇门,MPS/MIG 决定门内的资源怎么分,cgroup 决定每人最多拿多少。一个「实习生 + 生产推理」同机的稳健配置,可能是:
这种「分层叠加」的思路,才是工程现场的真实做法。教程的价值,就是让你知道每层闸管什么、能叠到哪一层,而不是背一条命令。
闸装好了,还要能「看见谁吃了多少」。三个实用抓手:
nvidia-smi 实时看:能直接看到每张卡的显存占用、算力利用率、跑在上面的进程 PID。这是排查「谁把卡吃满了」的第一现场。NVIDIA_VISIBLE_DEVICES、MIG 实例分配、cgroup 限额全部写进 docker-compose 或启动脚本,固化为可版本化的模板。这才是第二章「可复用骨架」在资源层的延伸。读完前面,你可能会想:既然各管不同维度,那 MPS 和 MIG 是不是能叠加,效果翻倍?这里要泼一盆冷水,并讲清边界。
一句话:MIG 管边界,MPS 管边界内的复用,二者可组合但顺序有讲究。别为了「听起来高级」同时上,先想清楚你的首要矛盾是哪一层。
这一节最容易被人学完就忘的,是「闸都懂了,但没人装」。工程现场里,资源隔离失效,九成不是因为不会配,而是因为靠口头约定:「你用 0、1 卡,我用 2、3 卡」。结果某人手滑把 NVIDIA_VISIBLE_DEVICES 设成了 all,灾难就来了。
所以这一节真正要交付的,是把隔离固化进模板:
NVIDIA_VISIBLE_DEVICES、cgroup 限额全部写成声明式配置,进版本库、可 diff、可 review。当你做到「资源分配不再靠人脑、靠模板」的那一刻,这台八卡机才真正从「公用机房」升级成「团队基础设施」。这也是整本教程一直在强调的那句话的工程兑现:把「环境」和「资源」都当作一等公民来版本化。
本节你拿到了「共用不互踩」的四道闸:设备视图切「谁能看见卡」、MPS 做进程级共享省显存提效率、MIG 做硬件级硬隔离、cgroup 做额度保险。它们的维度不同、可以叠加,选型取决于你的卡多人少关系与「是否必须硬隔离」。
到这一章结束,原理脊梁就立起来了:2.3 让卡进了容器,3.1 讲清它为什么能进,3.2 讲清多人怎么共用不踩。下一章(第四章)我们离开原理,进入最过瘾的实战——把真实的训练任务和推理服务真正塞进沙箱,把前面所有模块拼成一条能跑通的工程链路。