1. 为什么容器“看不见”GPU,以及这条访问路径到底由哪几层构成
我第一次在 Linux 服务器上尝试docker run --runtime=nvidia ... nvidia-smi时,宿主机侧一切正常:驱动装了,CUDA 装了,显卡信息在宿主机上输出得很好看。结果容器一启动,迎面而来的不是熟悉的显卡列表,而是No devices were found。这个场景我后来在论坛里见过几十次,几乎每周都有新朋友卡在同一堵墙上。要绕开这堵墙,你得先把一个问题看清楚:容器为什么会“天然看不见” GPU。
容器不是虚拟机。虚拟机里你装一套独立操作系统,驱动问题确实是 guest 自己的事;但容器共享宿主机内核,它只有独立的文件系统、进程空间和挂载视图。Linux 上的 NVIDIA GPU 在系统层面由三层协作组成:内核模块负责管理显存和上下文,设备节点/dev/nvidia0、/dev/nvidiactl、/dev/nvidia-uvm是用户态程序访问硬件的门把手,用户态库libcuda、libnvidia-ml是应用程序与驱动沟通的翻译官。容器启动时,它住在一个新的挂载命名空间里,默认看不到这些设备节点,也没继承宿主机上编译好的用户态库,所以你要么让容器的启动过程把这三样东西“递”进去,要么干脆用别人做好的镜像和运行时帮你递。
我刚接触这块时也犯过一个典型错误:在容器里执行apt install nvidia-driver。结果自然是失败,或者装上了也毫无意义。原因就一句话:驱动跟着内核模块走,而内核模块是宿主机加载的,容器里根本没有独立的内核可供你重复安装驱动。就算你装的是一个带完整编译工具链的特权容器,费劲加载模块也只是在折腾同一个内核,属于脱裤子放气。正确的思路不是“在容器里装驱动”,而是“让容器借用宿主机的驱动能力”。
理解了这一层,后面所有配置步骤都顺理成章了:你需要一个能在容器启动前挂载设备节点、注入运行库的工具,这就是 NVIDIA Container Toolkit 的职责。你也可以绕过它手动映射,但那样你就要自己维护设备文件和库文件的清单,复杂度完全不同。
1.1 GPU 不是网络接口那种“即插即用”资源
很多人容易把 GPU 和网卡类比,以为--device或者默认的容器共享机制会顺带把 GPU 暴露出来。其实 GPU 的访问链路比网卡长:你要过设备节点,过内核驱动,过用户态运行时,还要过 CUDA 的上下文创建。任何一个环节断了,容器里nvidia-smi可能都能执行,但实际跑 CUDA 程序就是失败。这也是为什么后面我会特别强调“nvidia-smi 能显示显卡不等于能算”这件事。
1.2 容器内驱动与用户态库的分工
容器内不装内核驱动,但它需要匹配的 CUDA 用户态库。官方nvidia/cuda镜像里自带 CUDA 运行时和工具,宿主机的驱动只需要提供内核模块和libcuda这一层的用户态能力。NVIDIA Container Toolkit 的库注入逻辑会自动把宿主机上一套匹配的库带进容器,并设置好环境变量。如果你选择手动挂载宿主机库文件,就得自己对二进制兼容性负责,否则分分钟遇到undefined symbol。
2. 动手前的硬件检查、驱动核对与运行时环境准备
配置 GPU 容器前,我强烈建议你先花五分钟确认宿主机状态。排错最怕变量太多:驱动、设备节点、Docker 运行时、镜像标签、环境变量,五样东西混在一起时,你根本不知道是哪一环出了问题。先把硬件和驱动这层变量排除掉,后面容器里出任何问题,你都能更准地定位。
2.1 先问三个问题:硬件在位、模块加载、驱动可用
用这几条命令逐层确认:
lspci | grep -i nvidia nvidia-smi lsmod | grep nvidia cat /proc/driver/nvidia/versionlspci确认系统是否识别到了显卡,如果这里都看不到,先检查 BIOS 里 PCIe 是否被禁用或显卡有没有插牢。nvidia-smi确认驱动是否正常工作,如果它直接报错,说明问题根本不在 Docker,先回驱动这边。lsmod看内核模块有没有加载,cat /proc/driver/nvidia/version能看到实际加载的模块版本,这一步可以避免“外壳工具是新版本,内核模块还是老版本”的尴尬情况。
如果你装完驱动后从来没重启过机器,在折腾容器之前先去重启一次。驱动加载和用户态库编译之间存在一个顺序问题,不重启就调用,偶尔能通,但遇到Failed to initialize NVML的概率非常大。
2.2 驱动版本和 CUDA 版本的匹配关系
nvidia-smi右上角的 “CUDA Version” 经常被误导。它不是说你机器上已经装好了哪个 CUDA,而是说当前驱动最高支持到哪个 CUDA 版本。容器使用哪个 CUDA 版本,取决于你拉取的镜像标签。举一个我踩过的例子:宿主机驱动是 535.104,右上角显示 CUDA Version 12.2,我用nvidia/cuda:12.3.1镜像跑程序,报错CUDA driver version is insufficient for CUDA runtime version。换成12.0的镜像后问题立刻消失。
兼容方向记得一句话:新驱动能跑老 CUDA runtime,老驱动跑不了新 CUDA runtime。所以一旦遇到版本不匹配,优先降镜像版本,而不是在容器里试图“覆盖”驱动。
2.3 Docker 引擎版本与工具链预期
Linux 上要使用 GPU,Docker Engine 版本最好在 19.03 以上。--gpus参数就是从这个版本开始支持的,老版本会直接报unknown flag: --gpus。运行docker version时重点看 Server 端的版本,客户端版本高不代表守护进程也高。
另一个预期是,你将要安装的 NVIDIA Container Toolkit 并不是 Docker 自带的。它由三个子组件协同工作:libnvidia-container负责在底层发现设备和注入库,nvidia-container-runtime-hook是给 runc 调用的 prestart 钩子,nvidia-container-runtime则是一个封装了默认 runc 的 shim。Docker 通过识别--gpus参数来触发这套运行时。你理解了这三层,后面的安装与排查就会有方向感。
3. 用 NVIDIA Container Toolkit 装载 GPU 访问能力
安装过程其实不复杂,但值得一步一步核对。我见过有人在daemon.json里手工加 runtime 配置时少写一个逗号,导致 Docker 整个起不来,反复重启后才发现是 JSON 语法问题。所以这里我建议用官方工具生成配置,而不是自己人肉写。
3.1 在 Ubuntu/Debian 系机器上安装工具包
以下命令在我常用的 Ubuntu 22.04 上实测可行:
distribution=$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | \ sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \ sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt-get update sudo apt-get install -y nvidia-container-toolkit有一点容易出问题:$distribution展开后可能是ubuntu22.04或ubuntu23.04这样的字符串。如果你的系统版本比较新,官方仓库里可能没有对应的目录,curl 出来的 list 文件会 404。遇到这种情况,手动把变量替换成ubuntu22.04或debian12这类稳定标识就能解决。这不算偷懒,只是用一个该仓库确实存在的标识去拉取软件源描述文件。
3.2 把 nvidia 运行时注册给 Docker
安装完成后执行:
sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart dockernvidia-ctk runtime configure会检测现有/etc/docker/daemon.json,把 nvidia 运行时段合并进去。你可以打开文件看一眼,大概长这样:
{ "runtimes": { "nvidia": { "path": "nvidia-container-runtime", "runtimeArgs": [] } } }重启 Docker 后用docker info检查注册情况:
docker info | grep -i runtime正常输出里应该有nvidia字样。如果没出现,立刻查journalctl -u docker,大概率是 daemon.json 解析失败。
3.3 为什么配置了运行时,容器默认还是看不到 GPU
这是新手第二阶段最容易困惑的点。明明docker info里已经能看到 nvidia 运行时,但直接docker run ubuntu nvidia-smi还是失败。原因是,注册运行时只是给了 Docker 一个“可选能力”,不会让所有容器默认启用。你必须通过--gpus参数,或者在 Compose 里声明 GPU 资源请求,运行时才会真正触发。
有人会建议把"default-runtime": "nvidia"写进daemon.json,让所有容器默认走 GPU 运行时。这个偷懒方案在单机开发环境可行,但在共享 CI 或生产集群里我不建议。每次启动都会触发钩子去做设备发现和库注入,非 GPU 容器白白增加开销,还有可能在与其他编排工具的 device plugin 协作时发生冲突。保持“显式申请、按需启用”更干净。
4. 从一行docker run开始,验证 GPU 配置是否真的打通
配置完成后,第一步永远是跑一个最小的 GPU 容器验证。这个命令要短、要直观、要能单行复制,最好还能一看到输出就知道问题在哪。
4.1 最小验证命令与预期输出
docker run --rm --gpus all nvidia/cuda:12.3.1-base-ubuntu22.04 nvidia-smi如果一切正常,容器里的nvidia-smi会列出你在宿主机上看到的同款显卡信息。如果报could not select device driver with capabilities: [[gpu]],说明运行时注册有问题,回到上一章检查docker info。如果出现No devices were found,则要往下看环境变量和设备节点。
单卡环境直接用all即可,多卡环境我更推荐用--gpus '"device=0"'来指定物理索引,避免容器同时挂上多块卡导致后续显存分配混乱:
docker run --rm --gpus '"device=0"' nvidia/cuda:12.3.1-base-ubuntu22.04 nvidia-smi4.2 别只停留在 nvidia-smi:跑一次真实 CUDA 验证
很多人验证到这里就结束了,认为“看到显卡信息就是成功”。但nvidia-smi能显示设备,只说明 NVML 库和字符设备访问没问题,不代表 CUDA runtime 能与驱动成功创建上下文。我建议再跑一个官方样例:
docker run --rm --gpus all nvidia/cuda:12.3.1-devel-ubuntu22.04 \ bash -c "apt-get update && apt-get install -y make g++ && \ cp -r /usr/local/cuda/samples /tmp/samples && \ cd /tmp/samples/1_Utilities/deviceQuery && make && ./deviceQuery"只要看到最后一行Result = PASS,整条链路才是真正打通。如果报cudaErrorInsufficientDriver,说明镜像里的 CUDA 版本高于驱动支持的上限,去换低版本镜像。如果报cudaErrorNoDevice,说明进程没有拿到设备节点,重点查运行时触发和环境变量。
4.3 用环境变量控制可见 GPU 和驱动能力
容器启动时,运行时通过环境变量把 GPU 选择结果告诉你。最常用的两个是:
docker run --rm --gpus all -e NVIDIA_VISIBLE_DEVICES=0,2 nvidia/cuda:12.3.1-runtime-ubuntu22.04 nvidia-smidocker run --rm --gpus all -e NVIDIA_DRIVER_CAPABILITIES=compute,utility nvidia/cuda:12.3.1-runtime-ubuntu22.04 nvidia-smiNVIDIA_VISIBLE_DEVICES支持数字索引、UUID,以及特殊值all和void。设成void时会故意不映射任何设备,用于测试“这台机器没有 GPU 时的表现”。NVIDIA_DRIVER_CAPABILITIES则控制注入哪些用户态库能力,典型值是compute,utility,如果你还需要跑图形应用,可能得加graphics或display。这俩变量不冲突,但各自管的事不一样。
4.4 CUDA 镜像标签怎么选
官方镜像常见三类标签:
base:只有 CUDA runtime 核心库,没有编译工具,适合跑现成二进制。runtime:在 base 基础上加更多运行库,比如 cuDNN、cuBLAS 的 runtime 部分。devel:在 runtime 基础上加编译器、头文件和样例源码,适合现场编译。
实际使用中,跑推理服务用runtime足够,做开发调试用devel,base适合最精简的验证环境。选发行版时尽量选 Ubuntu LTS,官方对 LTS 的镜像更新最及时。
5. 在 Compose 文件里描述 GPU 请求,把配置沉淀下来
命令行验证通过后,下一步通常是把容器方案落成 Compose 文件,方便团队复用。Compose 的 GPU 声明在旧版和新版写法上有差异,我建议优先使用新版deploy.resources字段。
5.1 Compose v2 的推荐写法
services: gpu-env: image: nvidia/cuda:12.3.1-base-ubuntu22.04 command: nvidia-smi deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu]这段配置的意思很直白:这个服务需要所有 GPU,并且要求具备gpu能力。如果你只要一块卡,把count: all改成count: 1;如果你只想用某张特定卡,可以按 UUID 过滤:
deploy: resources: reservations: devices: - driver: nvidia device_ids: ["GPU-1e4a2f5c-abcde-..." ] capabilities: [gpu]UUID 可以用nvidia-smi --query-gpu=uuid --format=csv查出来。不过我不太建议在 CI 流水线里硬编码 UUID,因为换卡时配置会失效;多卡同型号环境优先用count,省心。
5.2 旧版 runtime 字段怎么处理
如果你维护的是老项目,可能会看到这种写法:
runtime: nvidia environment: - NVIDIA_VISIBLE_DEVICES=all这种写法在 Compose 早期版本中很流行,目前在某些环境里仍然可用。但新版 Compose 如果同时出现runtime: nvidia和deploy.resources.reservations.devices,有的版本会忽略其中一个字段,有的会报错。迁移时我建议分成两步:先在暂存环境验证 GPU 可见性,再切换到生产分支,不要在同一个变更里同时动排布方式和资源声明。
5.3 资源限制的现实边界
经常有人问“Compose 里怎么限制容器只能用 8G 显存”。这个要泼一盆冷水:Docker 原生没有像--memory那样直接限制显存的参数。count和device_ids只能决定给容器分配哪几张物理卡,卡上的显存和算力仍然是和其他进程共享的。更细粒度的显存隔离依靠 NVIDIA MPS 或驱动层 vGPU 能力,不是一句 Compose 配置能搞定的。如果确实需要严格隔离,要么用多卡物理隔离,要么借助 Kubernetes+Device Plugin 配合 vGPU 方案,那个复杂度已经超出普通 Docker 用户的范围了。
6. 手动设备映射:受限环境里的备胎方案
有些场合你装不了 NVIDIA Container Toolkit,比如公司安全基线不允许新增系统组件,或者你的 Linux 发行版太冷门,官方仓库里没有对应软件源。这时候只能退回最原始的设备映射方式:把设备节点和库文件手工塞进容器。
6.1 需要映射哪些设备节点
NVIDIA 驱动在 Linux 下生成的节点主要有这些:
/dev/nvidia0、/dev/nvidia1:对应物理显卡/dev/nvidiactl:控制设备,用于上下文管理/dev/nvidia-uvm、/dev/nvidia-uvm-tools:统一虚拟内存管理/dev/nvidia-modeset:部分图形渲染功能需要
一张单卡环境的最小命令差不多长这样:
docker run -it --rm \ --device=/dev/nvidia0:/dev/nvidia0 \ --device=/dev/nvidiactl:/dev/nvidiactl \ --device=/dev/nvidia-uvm:/dev/nvidia-uvm \ -v /usr/lib/x86_64-linux-gnu/libcuda.so.1:/usr/lib/x86_64-linux-gnu/libcuda.so.1:ro \ -v /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:ro \ nvidia/cuda:12.3.1-base-ubuntu22.04 nvidia-smi你以为到这就完了?不一定。nvidia-smi可能能跑,但实际跑 CUDA 计算时还需要注入libnvidia-ptds.so、libnvidia-allocator.so、libnvidia-cfg.so等一系列运行库,否则应用会报找不到符号。这也是手动方案最烦的地方:看着对,但链条稍微缺一环就跑不通。
6.2 为什么手动挂载容易出“玄学”问题
根本原因是镜像里的 CUDA 运行库和宿主机驱动里的库存在版本错位。容器内libcuda.so.1可能来自 535 时代的镜像,而宿主机驱动已经升级到 550,两个动态库的符号集合不一致,运行时就会出现undefined symbol。手动方案不是不能用,而是你得对二进制兼容性有足够理解,还要每次更新驱动后同步维护挂载清单。我把它定位为“应急方案”,不推荐长期依赖。
6.3 Toolkit 和手动方案的对比
| 维度 | NVIDIA Container Toolkit | 手动设备映射 |
|---|---|---|
| 配置复杂度 | 一次性安装,后续简单 | 每条命令都要维护设备与库清单 |
| 驱动版本兼容 | 自动匹配注入 | 依赖手工挂载,极易错位 |
| 多卡选择 | 通过环境变量或 device 参数灵活控制 | 设备节点列表难以维护 |
| 调试成本 | 有nvidia-container-cli辅助 | 只能靠阅读驱动日志和文档 |
| 适用场景 | 生产环境与常规开发环境首选 | 受限环境、临时应急 |
如果有的选,把精力放在 Toolkit 路线上。手动方案了解原理就够了,真正作为长期依赖时坑很多。
7. 踩坑全过程:从报错反推根因的一线经验
最后这部分我按“现象 → 排查链路 → 根因”的方式来写,方便你遇到问题时直接对照。
7.1could not select device driver with capabilities到底是谁的错
这是最高频的报错之一。现象是docker run --gpus all直接起不来,大部分人在这一步以为 Docker 没装好。排查链路是:
docker info | grep -i runtime如果输出里没有nvidia,说明运行时没注册成功。重新执行sudo nvidia-ctk runtime configure --runtime=docker和sudo systemctl restart docker。如果docker info里已有nvidia,那问题多半出在参数传递,试试--gpus '"device=0"'或检查 Docker 版本是否过老。
7.2 容器内nvidia-smi显示No devices were found
排查顺序建议:
- 先看宿主机
/dev下有没有nvidia*节点,没有就先解决驱动加载和重启问题。 - 执行
docker inspect <容器ID> | grep NVIDIA,看环境变量NVIDIA_VISIBLE_DEVICES是否被设置成了不存在的索引。 - 执行
env | grep NVIDIA,确认容器内环境变量的终值。 - 确认你确实是通过
--gpus或deploy.resources显式触发运行时。
如果用的是NVIDIA_VISIBLE_DEVICES=all,在个别 runtime 版本里会因设备列表为空而表现异常,此时可以改成具体的0试一次。
7.3 容器里 nvidia-smi 能执行,但跑 CUDA 报版本不足
这条我在前面讲过,核心是对比nvidia-smi右上角的驱动支持 CUDA 版本和镜像自带 CUDA 版本。驱动支持的版本号大于等于镜像内 CUDA 版本时,一般正常;小于时则报CUDA driver version is insufficient。解决办法优先换低版本镜像,升级驱动是更大的工程。
7.4 驱动升级后容器内出现Unknown Error或 NVML 初始化失败
宿主机升级 NVIDIA 驱动后,如果nvidia-container-toolkit还是老版本,有可能出现这种诡异问题。我遇到过升级到 550 驱动后,容器内nvidia-smi显示的驱动版本还是 535,但内核模块已经变成 550,信息不一致导致工具报错。处理方式:同步升级 toolkit,然后跑一次最小的验证镜像。养成“驱动和 toolkit 一起升级”的习惯,能少踩一半的坑。
7.5 多个容器共享同一张 GPU 时的资源失控问题
Docker 层面没有显存限制参数,所以多个容器同时访问同一张卡时,显存互相挤占几乎是必然的。你可以在应用层限制CUDA_VISIBLE_DEVICES,但这只是把容器指向某张卡,不能限制它用多少显存。需要精细控制时,可以研究一下 NVIDIA MPS 的配置,或者使用驱动层 vGPU 能力。这一块不是纯 Docker 配置能解决的,但也算很多人从“单容器验证”走向“生产部署”时必须面对的问题。
7.6 一个能救急的最小调试习惯
最后分享一个我自己的习惯:我会在系统里预设一条最简验证命令,状态变更后随时跑一遍:
docker run --rm --gpus all nvidia/cuda:12.3.1-base-ubuntu22.04 nvidia-smi这条命令能覆盖运行时注册、设备节点、驱动注入三段链路。它报错时,你能根据报错内容快速定位是 Docker 层还是驱动层;它通过时,再去排查更复杂的业务镜像反而更有底气。
这整条路走下来,你会发现 GPU 容器访问其实不复杂,复杂的是它横跨了硬件、驱动、容器运行时、镜像标签四个不同的知识域,任何一个环节有一点偏差,表现出来的报错都像“Docker 坏了”。但只要理解了那一层“内核模块在宿主机、设备节点由运行时注入、用户态库靠镜像或 toolkit 提供”的模型,绝大多数问题都能靠逻辑推出来。