那台新服务器的场景我到现在还记得:驱动是装好的,nvidia-smi在终端里跑得闪闪发亮,结果一旦换成Docker容器,同样的命令就甩回来一句“could not select device driver “nvidia” with capabilities: [[gpu]]”。当时我还真愣了一下,第一反应是“是不是Docker版本的问题”,第二反应是“是不是驱动没装完整”。排查了半天,最后才发现,问题根本不在驱动上,而是Docker这一侧少装了一个关键的runtime组件。
这个报错可以说拦住了不少初次接触GPU容器的人。它跟显卡好坏无关,跟显卡驱动是否装上关系也不大,核心是:Docker本身不认识GPU,也不叫不出“nvidia”这个名字的device driver。解决它的标准动作就是安装并配置nvidia-container-runtime。这篇文章我就从报错原理、踩坑排查、安装步骤,到跑通之后的边界问题,完整地过一遍,希望能帮你在新机器上少折腾一两个小时。
如果你也在用Docker跑CUDA程序、给团队搭GPU推理或训练环境,或者在本地排查一个莫名其妙的--gpus报错,这篇内容应该正好对得上你的需求。
1. 报错背后的逻辑:Docker并不认识GPU
1.1 把报错拆开看,到底是谁在说话
报错信息是长这样一串:
could not select device driver “nvidia” with capabilities: [[gpu]]翻译成正常人类的语言是:你让我用“名字叫nvidia、具备gpu能力”的设备驱动,但我的注册列表里没有这个驱动,所以我不干活。
这里有一个经常被忽略的关键点——这句话不是Linux内核说的,也不是NVIDIA驱动说的,而是Docker daemon说的。Docker在收到docker run --gpus all这类请求时,会先去自己的runtime注册表里找一个叫“nvidia”的runtime。找不到,就抛出上述错误。
所以看到这个报错,第一反应应该是:Docker这一层的“nvidia runtime”没有注册成功。而不是宿主机显卡坏了,更不是驱动坏了。
1.2 runc、runtime和GPU之间缺了什么
Docker默认使用的OCI runtime叫runc,它负责创建容器、设置namespace、cgroup,然后把容器跑起来。runc的功能很纯粹,它只管把进程隔离起来,但它并不懂得什么是GPU、什么是/dev/nvidia0、什么是CUDA驱动库。
普通容器默认是看不到GPU的,因为GPU设备节点没有被映射进去,驱动库也没有被挂载进去。你就算把镜像跑起来,容器里的nvidia-smi也找不到设备。
为了解决这件事,NVIDIA官方搞了个东西叫nvidia-container-runtime。它的工作方式是在runc创建容器的流程里,插入一个prestart钩子,在容器启动前自动检查宿主机上的NVIDIA设备、驱动库、NVML库,然后把这些东西注入到容器的device white list里。
也就是说,nvidia-container-runtime是连接Docker和GPU之间的一座桥。如果没有这座桥,Docker根本不知道该去哪里找“nvidia”,--gpus也就变成了一句无人响应的指令。
打个比方:Docker是酒店前台,runc是给客房配送钥匙的服务员,nvidia-container-runtime是那张能刷开“GPU房间”的特殊房卡。房卡系统没装好,前台自然找不到一种叫“nvidia”的房型,你提需求也没用。
1.3 为什么改daemon.json能修复这个问题
Docker daemon启动的时候,会读取/etc/docker/daemon.json。这个文件里可以声明自定义的runtime,例如:
{ "runtimes": { "nvidia": { "path": "nvidia-container-runtime", "runtimeArgs": [] } } }只要daemon在启动时看到这条配置,它就会把“nvidia”注册到自己的runtime列表里。之后docker run --gpus all就能找到它,报错自然消失。
所以,could not select device driver “nvidia” with capabilities: [[gpu]]这个错误的修复路径,本质上是两个动作:
- 安装
nvidia-container-runtime这个可执行文件; - 让Docker daemon在启动时把它注册成名为
nvidia的runtime。
这两个动作缺一个都不行。很多人只装了runtime,却忘了改daemon.json,结果依然报错;也有一些人手动改了daemon.json,但路径写错了,结果Docker服务直接起不来。
2. 动手安装前,先按这个顺序排查宿主机
2.1 确认宿主机驱动是真的正常
不要急着安装,先把宿主机这一层确认清楚。第一条命令永远是:
nvidia-smi如果输出正常,能看到显卡型号、驱动版本、显存占用,说明驱动和内核模块这个层面是没有问题的。
如果nvidia-smi本身报错,比如显示“Failed to initialize NVML: Driver/library version mismatch”,那就要先处理驱动问题,再来折腾Docker。这两个问题的故障表现很像,但处理路径完全不同。
另外也可以用lsmod | grep nvidia看一眼内核模块是否已经加载。有些机器虽然驱动装了,但重启后DKMS没重建,模块没加载,也会造成nvidia-smi报错。
注意:nvidia-smi正常只能说明宿主机驱动没问题,不能说明Docker侧已经准备好了。这个结论很重要,因为后面很多人会在这里绕圈。
2.2 确认Docker当前的状态和已注册runtime
接下来查看Docker是否正常工作:
systemctl status docker docker version docker run --rm hello-worldhello-world能跑通说明Docker基础功能完好。
然后看Docker当前注册了哪些runtime:
docker info | grep -i runtime正常情况下输出是:
Runtimes: runc如果你看到Runtimes: nvidia runc,说明之前已经注册过了,那这个报错可能另有原因。如果你看到只有runc,那重点就要放在安装并注册nvidia这个runtime上。
这一步能让我们在动手之前就大概心里有数:问题到底出在“没装runtime二进制”还是“没注册runtime配置”。
2.3 检查daemon.json的语法和归属权
/etc/docker/daemon.json这个文件虽然是重点,但很多时候问题就出在它本身:
- 文件语法错误,导致Docker daemon启动失败;
- 文件权限不对,Docker运行时读不到;
- 文件名写错了,比如写成
daemon.json.bak放在同目录下,Docker又不认。
检查语法最简单的办法是用jq:
jq . /etc/docker/daemon.json如果输出的是格式化后的JSON,说明语法没问题。如果提示parse error,那就得先修JSON,再重启Docker。这一步虽然简单,但能省下不少时间。
2.4 一个容易忽略的排查误区:别急着重装驱动
很多遇到这个报错的人,第一反应是重装显卡驱动,或者是去更新Docker版本。其实这两件事和这个错误的因果关系都不大。
倒是有一个可能性要提一下:部分Docker发行版自带的版本比较旧,比如Docker CE版本低于19.03,--gpus参数可能不被识别。虽然报错信息不完全一样,但也会影响排查方向。比较好的习惯是让系统里的Docker保持当前的主流稳定版本,再配合最新的nvidia-container-toolkit使用。
如果经过一轮排查,确认了宿主机驱动正常、Docker正常、但Runtimes列表里没有nvidia,那就可以放心进入安装环节了。
3. 安装nvidia-container-runtime:从仓库到跑通的完整链路
3.1 推荐方案:安装nvidia-container-toolkit并自动配置
网上很多资源会让你直接安装nvidia-container-runtime这个包,但更推荐的做法是安装nvidia-container-toolkit。它内部包含nvidia-container-runtime的hook、nvidia-ctk工具以及配套的runtime二进制,后面的维护更新也更跟得上趟。
先看Debian系的安装方式,Ubuntu、Debian都走这一套:
curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg distribution=$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \ sed 's#deb #deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] #' | \ sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt-get update sudo apt-get install -y nvidia-container-toolkitCentOS/RHEL或者Rocky Linux这类的系统走下面这套:
curl -s -L https://nvidia.github.io/libnvidia-container/rhel8/libnvidia-container.repo | sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo sudo yum install -y nvidia-container-toolkit安装完成之后,最关键的步骤来了。新版本工具链里提供了一个自动配置命令:
sudo nvidia-ctk runtime configure --runtime=docker这条命令会自动帮你在/etc/docker/daemon.json里写入nvidiaruntime的配置,并且会保留原有的其他配置项。对比手动改文件,省事很多,也不太容易踩坏原有配置。
配置完成后,重启Docker:
sudo systemctl restart docker再验证一下:
docker info | grep -i runtime如果看到输出里有nvidia,说明runtime已经注册成功。
3.2 手动配置daemon.json的细节
如果你是老版本工具链,或者更习惯手动操作,那就要手动编辑/etc/docker/daemon.json。
先把原文件备份一份,这一步永远值得做:
sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak然后查看原文件内容:
cat /etc/docker/daemon.json如果文件不存在,直接创建并写入:
{ "runtimes": { "nvidia": { "path": "nvidia-container-runtime", "runtimeArgs": [] } } }如果文件里已经有其他配置,比如配置了镜像加速、日志策略等,不要整个覆盖,要用jq合并:
sudo jq '.runtimes.nvidia = {"path": "nvidia-container-runtime", "runtimeArgs": []}' /etc/docker/daemon.json > /tmp/daemon.json sudo mv /tmp/daemon.json /etc/docker/daemon.json改完先校验语法:
jq . /etc/docker/daemon.json确认输出正常再重启Docker。语法错误会导致Docker daemon起不来,到时候报错反而不是原来的“could not select device driver”,而是Docker服务启动失败,那就更折腾了。
3.3 验证容器能真正调用GPU
配置好runtime之后,跑一个最基础的验证:
docker run --rm --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi注意,这个镜像比较大,如果机器网络不好,可以先单独拉下来:
docker pull nvidia/cuda:12.2.0-base-ubuntu22.04再跑。如果容器里能正常输出显卡型号、驱动版本、CUDA版本,说明整条链路已经打通。
对于老一点的Docker,也可以看到有人用这样的命令:
docker run --rm --runtime=nvidia nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi--runtime=nvidia是早期的用法,--gpus all是后来的封装。其实底层走的都是同一个runtime,两种写法本质等价。
3.4 离线环境下的安装方式
有些生产环境是离线的,或者没有外网访问权限。这时候仓库源的方式就不好使了,可以换一种思路。
在一台有网的机器上,先把对应的安装包下载下来,比如.deb或.rpm,然后拷到目标机器上安装。Debian系:
dpkg -i nvidia-container-toolkit*.debRHEL系:
rpm -i nvidia-container-toolkit*.rpm依赖问题可能会让人头痛,因为nvidia-container-toolkit依赖libnvidia-container1。离线环境下最好把依赖包也一起下载。这个依赖在同类发行版之间兼容性通常还可以,但还是建议下载和目标机器操作系统版本一致的包。
装完之后,nvidia-ctk这个工具就存在于系统里了,后面配置daemon.json还是一样走nvidia-ctk runtime configure --runtime=docker。
4. 分清nvidia系列工具的关系,别再被各种教程带偏
4.1 名字相近的几个包,到底有什么区别
在这个领域,最让人混乱的就是名字相近的工具。nvidia-docker、nvidia-docker2、nvidia-container-runtime、nvidia-container-toolkit,看起来都跟NVIDIA容器有关,但各自角色不同。
| 包名 | 主要作用 | 当前状态 |
|---|---|---|
| nvidia-docker | 最早的Docker GPU支持方案,通过包装docker命令实现 | 基本废弃 |
| nvidia-docker2 | 第二代方案,会自动修改daemon.json并管理runtime | 已逐步被toolkit取代 |
| nvidia-container-runtime | 底层OCI runtime,实际完成GPU设备注入 | 作为toolkit的组件被安装 |
| nvidia-container-toolkit | 当前官方主推的工具集,包含runtime、CLI和配置工具 | 推荐安装 |
实际使用中,你只要安装nvidia-container-toolkit,它通常就会带上nvidia-container-runtime。所以很多教程里说“安装nvidia-container-runtime”,实际操作往往是安装toolkit包,然后手动确认runtime文件在系统里。
可以通过这个命令确认runtime文件的路径:
which nvidia-container-runtime正常情况下输出类似/usr/bin/nvidia-container-runtime。如果这个文件不存在,无论daemon.json怎么配置都没用,因为路径指向的二进制是空的。
4.2 为什么新版本Docker的--gpus也会走到nvidia runtime
有些人会有一个疑问:我用的Docker明明是19.03以上的版本,直接支持--gpus参数,为什么还要单独装runtime?
这里要澄清一下:--gpus不是Docker自研的GPU虚拟化能力,它只是一个上层入口。Docker检测到--gpus请求后,会去runtime列表里找nvidia。找到就交给nvidia-container-runtime去执行具体的设备注入,找不到就报“could not select device driver”。
也就是说,nvidia-container-runtime是底层执行者,--gpus只是给你一个更方便的调用方式。二者不是替代关系,而是上下层关系。
4.3 老教程里的nvidia-docker2还能用吗
如果你在网上搜到的是安装nvidia-docker2的教程,它也能用,但不够推荐。nvidia-docker2的设计思路是修改Docker的配置文件来注册runtime,它跟新版toolkit做的事情在最终效果上是类似的。不过官方的新工具链已经把管理方式统一到了nvidia-container-toolkit和nvidia-ctk上,新装的机器没必要再走老路。
还有一个实际坑需要注意:如果机器上同时装了nvidia-docker2和nvidia-container-toolkit,两者可能会互相覆盖daemon.json配置。这时候最好的做法是把旧包卸载干净,再装新包。
5. 跑通之后,生产环境里常见的边界问题
5.1 容器能启动,但nvidia-smi报NVML初始化失败
这是跑通runtime之后最容易遇到的第二个报错:
Failed to initialize NVML: Driver/library version mismatch这类错误的原因,通常是宿主机驱动版本太旧,跑不了镜像里对应的高版本CUDA。NVIDIA驱动本身是向下兼容的,但驱动版本太低,就带不动新的CUDA工具包。
解决办法有两种:
- 升级宿主机驱动到更新版本;
- 拉取与当前驱动兼容的CUDA基础镜像。
判断当前驱动最高支持到哪个CUDA版本,看nvidia-smi输出里的“CUDA Version”一栏即可。比如驱动显示CUDA Version: 12.2,那就说明它能支持CUDA 12.2及更低版本。
5.2 --gpus参数到底怎么指定多卡、单卡还是全部
--gpus all是最省事的写法,但生产环境经常需要只给容器分配某一块卡。
指定单块设备号:
docker run --rm --gpus '"device=0"' nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi指定多块设备号:
docker run --rm --gpus '"device=0,1"' nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi这里有个shell引号的坑:device=0,1需要写在一对双引号里,外层再套单引号。如果直接在--gpus后面写device=0,1不带引号,有时候会被shell解析出问题,行为会变得不可预期。
如果你更习惯用环境变量控制,也可以在docker run加:
-e NVIDIA_VISIBLE_DEVICES=0,1两种方式的效果在runtime层面是等价的。
5.3 docker-compose里要怎么写
生产环境里很少直接敲长串docker run,基本都是用compose文件。
新版compose支持的写法:
services: app: image: nvidia/cuda:12.2.0-base-ubuntu22.04 command: nvidia-smi gpus: all老一点的compose版本用deploy段:
services: app: image: nvidia/cuda:12.2.0-base-ubuntu22.04 command: nvidia-smi deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu]这里有个容易踩的点:compose里的driver: nvidia这个字段,和前面说的runtime名字是同一个东西。如果runtime没注册,compose启动时依然会报“could not select device driver “nvidia””。所以不要以为compose里的写法能绕过runtime安装,它只是配置方式不同,底层依赖还是一样。
5.4 daemon.json丢失或写坏了怎么恢复
有时候Docker升级、系统更新,甚至某些运维工具自动化清理,都可能让daemon.json被改动或丢失。之后旧容器还能跑,但新容器一加--gpus就报错。
恢复方式不复杂:
sudo systemctl stop docker sudo mv /etc/docker/daemon.json /etc/docker/daemon.json.bak sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl start docker先停Docker,再重新让nvidia-ctk生成配置,然后启动。这样至少能确保runtime配置恢复。如果之前还有其他配置项,再从备份文件里手动合并回来。
**注意:**不要在一个正在运行大量容器的机器上随手改daemon.json就重启Docker,会影响所有容器。最好在维护窗口操作,并且先确认当前容器是否会因Docker重启而受影响。
5.5 容器内看到0张卡,但启动没报错
还有一个小概率场景:启动没报错,但容器里执行nvidia-smi -L显示看不到卡,或者只看到一部分卡。
第一步先检查环境变量:
docker inspect <容器名> | jq '.[0].Config.Env'看有没有NVIDIA_VISIBLE_DEVICES被误设置成空值或者不存在的设备号。这个变量被runtime读取后,就决定了哪些GPU会被注入容器。把它删掉,或者设置为all,一般就能解决。
第二步检查是不是用了--gpus '"device=某个不存在的编号"',比如指定了device=3,但机器只有0和1两块卡。runtime通常不会报错,而只是注入了空设备。所以指定卡号之前,最好先在宿主机确认好nvidia-smi -L的输出。
最后分享一点我自己的环境初始化习惯
这个问题踩过几次之后,我形成了一套固定的新机器初始化顺序:先装宿主机显卡驱动并确认nvidia-smi正常,再装Docker,然后装nvidia-container-toolkit,执行nvidia-ctk runtime configure --runtime=docker,重启Docker,最后用一个CUDA基础镜像跑一次nvidia-smi验证。
另外我会把daemon.json的备份放在一个固定目录,并同步一份到版本仓库。新加机器时直接拷过去用,能省不少时间。每次升级完Docker或者系统内核,我也会顺手跑一下docker info | grep -i runtime,确认nvidia runtime还在。
说到底,这个报错并不可怕,它只是在提醒你:Docker和GPU之间还差一座桥。把nvidia-container-runtime装好、注册好,桥就搭起来了。后面再遇到类似问题,按着排查链路走一遍,基本都能在几分钟内定位到具体环节。