ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker容器中使用GPU:从NVIDIA Container Toolkit到避坑实战

Docker容器中使用GPU:从NVIDIA Container Toolkit到避坑实战 1. 为什么容器“看不见”GPU以及这条访问路径到底由哪几层构成我第一次在 Linux 服务器上尝试docker run --runtimenvidia ... 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 上要使用 GPUDocker 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 --runtimedocker 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 device0来指定物理索引避免容器同时挂上多块卡导致后续显存分配混乱docker run --rm --gpus device0 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_DEVICES0,2 nvidia/cuda:12.3.1-runtime-ubuntu22.04 nvidia-smidocker run --rm --gpus all -e NVIDIA_DRIVER_CAPABILITIEScompute,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足够做开发调试用develbase适合最精简的验证环境。选发行版时尽量选 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-gpuuuid --formatcsv查出来。不过我不太建议在 CI 流水线里硬编码 UUID因为换卡时配置会失效多卡同型号环境优先用count省心。5.2 旧版 runtime 字段怎么处理如果你维护的是老项目可能会看到这种写法runtime: nvidia environment: - NVIDIA_VISIBLE_DEVICESall这种写法在 Compose 早期版本中很流行目前在某些环境里仍然可用。但新版 Compose 如果同时出现runtime: nvidia和deploy.resources.reservations.devices有的版本会忽略其中一个字段有的会报错。迁移时我建议分成两步先在暂存环境验证 GPU 可见性再切换到生产分支不要在同一个变更里同时动排布方式和资源声明。5.3 资源限制的现实边界经常有人问“Compose 里怎么限制容器只能用 8G 显存”。这个要泼一盆冷水Docker 原生没有像--memory那样直接限制显存的参数。count和device_ids只能决定给容器分配哪几张物理卡卡上的显存和算力仍然是和其他进程共享的。更细粒度的显存隔离依靠 NVIDIA MPS 或驱动层 vGPU 能力不是一句 Compose 配置能搞定的。如果确实需要严格隔离要么用多卡物理隔离要么借助 KubernetesDevice 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 --runtimedocker和sudo systemctl restart docker。如果docker info里已有nvidia那问题多半出在参数传递试试--gpus device0或检查 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_DEVICESall在个别 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 提供”的模型绝大多数问题都能靠逻辑推出来。
返回列表