进入驾驶舱 - Pod 内部如何“用上” GPU?
上一篇,咱们成功扮演了“资源申请者”和“调度大师”,把需要 GPU 的 Pod 送到了有 GPU 的 Node 上,k8s 也大方地给这个 Pod 分配了指定的 GPU 资源。一切看起来都很完美!
但是,别忘了,容器(Container)就像一个独立的小房间,有着自己的文件系统、进程空间,默认情况下,它是与宿主机(Node)隔离的。它里面的程序,怎么才能“摸到”并“使唤”外面那块物理 GPU 呢?光靠 k8s 的一纸“分配令”可不够。
- 类比:想象一下,你在一个大型共享工坊 (
Node) 里申请到了使用一台高级精密仪器 (GPU) 的权限。仪器就在工坊的某个位置,而你被分配在了一个独立的操作隔间 (Container) 里工作。现在的问题是,你怎么在你的隔间里接通这台仪器的电源、拿到它的操作手册(驱动、库)、按下启动按钮,并确保你用的就是分配给你的那一台,而不是邻居的?
挑战:容器的“围墙”
这就是容器使用 GPU 面临的核心挑战:隔离性。具体来说,一个运行在容器里的 GPU 应用程序(比如 TensorFlow、PyTorch 训练脚本)要想工作,通常需要:
- 访问 NVIDIA 驱动:它需要能和运行在宿主机(Node)上的 NVIDIA 内核驱动程序通信。
- 访问 GPU 设备文件:需要能“看到”并操作特定的设备文件(比如
/dev/nvidia0,/dev/nvidiactl等)。 - 相应的 CUDA 库:应用程序本身是基于特定版本的 CUDA API 编写的,需要对应的 CUDA 运行时库来支撑。
这些东西默认都不在容器这个“小隔间”里。强行把驱动装进容器镜像?非常不推荐,会导致镜像臃肿、版本管理混乱,而且容器里的驱动通常也无法直接和宿主机的内核驱动交互。
解决方案:NVIDIA Container Toolkit (及相关组件)
为了优雅地解决这个问题,NVIDIA 提供了一套关键的工具——NVIDIA Container Toolkit(你可能也听过它以前的名字,比如 nvidia-docker2)。
- 类比:你可以把 NVIDIA Container Toolkit 想象成一个神通广大的**“现场工程师”或者一个“智能转接器”**。当
k8s(通过节点上的容器运行时,如 Docker 或 containerd)要启动一个申请了 GPU 的容器时,这个“工程师”就会自动介入。
它的核心工作流程(简化版)是这样的:
- 前提:NVIDIA Container Toolkit 需要预先安装在所有 GPU 节点上,并与节点上的容器运行时(Docker/containerd/CRI-O)集成好。这是搭建 GPU K8s 集群时的必要步骤。
- 拦截启动:当
kubelet指示容器运行时启动一个声明了nvidia.com/gpu资源的容器时,NVIDIA Container Toolkit 会“感知”到这个动作。 - 按需注入:Toolkit 会根据
k8s分配给这个 Pod 的具体是哪块(或哪些)GPU,在容器启动的瞬间,动态地把宿主机上的一些关键组件“注入”或“映射”到容器内部:
- 挂载驱动库:它会把宿主机上与内核驱动版本兼容的 NVIDIA 用户态驱动库 (user-space driver libraries) 挂载到容器内的特定路径下。注意,它挂载的不是内核驱动本身,而是应用程序需要调用的那些
.so动态链接库文件。 - 挂载设备文件:将宿主机上代表着被分配的那个 GPU 的设备文件(如
/dev/nvidia0)挂载到容器的/dev目录下。 - 设置环境变量:自动在容器内设置一些环境变量,最关键的是
NVIDIA_VISIBLE_DEVICES。这个变量会明确告诉容器内的 CUDA 程序:“你只能看到和使用这块(或这几块)GPU!” 它的值通常是被分配的 GPU 的 UUID 或者索引号。这样就保证了容器之间 GPU 使用的隔离性。
通过这一系列“神操作”,容器里的应用程序就感觉自己好像“原生”地拥有了所需的 NVIDIA 环境,可以顺畅地和分配给它的 GPU 进行通信了。
容器内部需要啥?(隐式的输入:镜像构建)
NVIDIA Container Toolkit 解决了容器与宿主机驱动和设备的连接问题,但容器镜像本身也需要包含应用程序运行所需的依赖。
CUDA 工具包/库
- 你的应用程序(如 TensorFlow, PyTorch, 或你自己写的 CUDA 代码)是基于某个 CUDA 版本编译和运行的。因此,你的容器镜像必须包含对应版本的 CUDA 运行时库(CUDA runtime libraries)。
- 最简单的方式是直接使用 NVIDIA 官方提供的 CUDA 基础镜像来构建你的应用镜像,例如咱们之前用到的
nvidia/cuda:12.3.2-base-ubuntu22.04。 - 你也可以在自己的 Dockerfile 里,基于一个普通的基础镜像(如 Ubuntu, CentOS)手动安装所需的 CUDA Toolkit 版本。
关键:版本兼容性
(划重点!这是最常见的坑之一!)
容器镜像里的 CUDA Toolkit 版本 必须与宿主机 Node 上的 NVIDIA 驱动版本 兼容!
- 类比:这就像你想在一台装了 Windows XP 的老爷机上运行一个需要 Windows 11 最新特性的软件,多半会失败。反之亦然。
NVIDIA 官方会提供一个兼容性列表,明确指出哪个驱动版本支持哪些 CUDA Toolkit 版本。在构建镜像和准备节点环境时,务必参考这个列表,确保版本匹配,否则你的程序在容器里很可能无法识别或使用 GPU,并报出各种奇怪的错误。
应用程序代码
当然,最后还需要你的实际应用程序代码(比如 Python 脚本、编译好的二进制文件等)也包含在镜像里。
验证:容器“看”到 GPU 了吗?(输出)
说了这么多,怎么确认容器里的程序真的能用 GPU 了呢?最常用的方法就是在运行的 Pod 内部执行 nvidia-smi 命令。
执行验证(输出检查):
假设你的 Pod cuda-test-pod 正在运行,执行以下命令:
kubectl exec cuda-test-pod -- nvidia-smi预期输出:
如果一切正常,nvidia-smi 命令应该会成功执行,并只显示分配给这个 Pod 的那块 GPU 的信息。例如,如果这个 Pod 只申请了 1 块 GPU,即使宿主机上有 4 块卡,nvidia-smi 的输出也应该只列出 1 块卡。这得益于 NVIDIA_VISIBLE_DEVICES 环境变量的隔离作用。
输出大概长这样:
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.125.06 Driver Version: 525.125.06 CUDA Version: 12.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla T4 On | 00000000:00:1E.0 Off | 0 |
| N/A 42C P8 10W / 70W | 0MiB / 15360MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+如果你能在 Pod 内部成功执行 nvidia-smi 并看到类似上面的输出(特别是 GPU 信息部分),那么恭喜你,容器已经成功“接入”GPU 驾驶舱了!
完整流程回顾
现在我们把从申请到使用的整个链条串起来:
- 你(用户): 编写
PodYAML,在resources.limits中声明nvidia.com/gpu: 1。 - K8s Scheduler: 找到一个有空闲
GPU且装好了 NVIDIA 驱动和 Container Toolkit 的Node,将Pod调度上去。 - Kubelet (在 Node 上): 指示容器运行时(如 containerd)启动
Pod中的容器。 - NVIDIA Container Toolkit (在 Node 上): 拦截启动请求,根据分配结果(比如分配的是节点上的第 0 号 GPU),将宿主机的驱动库、设备文件
/dev/nvidia0等挂载到容器内,并设置NVIDIA_VISIBLE_DEVICES=0。 - 容器运行时: 启动容器(该容器镜像是基于兼容的 CUDA 版本构建的)。
- 你的应用 (在容器内): 调用 CUDA API -> 使用容器内的 CUDA 库 -> 通过注入的驱动库与
/dev/nvidia0设备文件通信 -> 最终指令发送给宿主机内核驱动 -> 在物理 GPU 上执行计算!
看,虽然中间环节不少,但NVIDIA Container Toolkit 的自动化处理,整个过程对容器内的应用来说是相对透明的。
常见“拦路虎”
如果在容器里无法使用 GPU,可以检查以下常见问题:
- 版本不兼容:宿主机驱动版本与容器内 CUDA Toolkit 版本不匹配(最常见!)。
- 节点环境问题:GPU 节点上没有正确安装 NVIDIA 驱动或 NVIDIA Container Toolkit,或者 Toolkit 没有和容器运行时正确集成。
- 镜像问题:构建的容器镜像里忘记包含所需的 CUDA 运行时库。
- K8s 配置问题:虽然少见,但也可能 Pod 被调度到了一个没有 GPU 或环境配置错误的节点上(可以通过节点标签、污点等方式避免)。
小结与展望
这一篇,我们深入到了 Pod 的“驾驶舱”内部,理解了应用程序是如何在隔离的容器环境中,通过 NVIDIA Container Toolkit 这位“智能工程师”的帮助,最终成功访问并使用分配给它的 GPU。
现在,咱们的应用终于能在 k8s 的 Pod 里,欢快地调用 GPU 进行计算了!计算是跑起来了,但新的问题也来了:
- 这
GPU用得怎么样?是 996 模式火力全开,还是大部分时间在摸鱼划水? - 显存用了多少?会不会爆显存?
- 温度怎么样?会不会过热降频?
- 如果跑了很多 GPU 任务,怎么统一看它们的资源使用情况?
这些问题都指向了下一个重要主题——监控(Monitoring)。如何有效地监控 k8s 集群中的 GPU 资源使用情况?
这就是咱们下一篇要探讨的内容:《精打细算 - GPU 监控与资源管理进阶》。准备好拿起“放大镜”和“听诊器”,给我们的 GPU 做个体检吧!
















