从零搭建一个 AI Infra 实验室⑫:Docker 为什么默认看不到 GPU

前几篇,我们已经一路从:

复制代码
Model
  ↓
Runtime
  ↓
Serving Engine
  ↓
GPU

走到了:

复制代码
vLLM
  ↓
Scheduler
  ↓
Continuous Batching
  ↓
PagedAttention
  ↓
GPU

甚至开始测:

复制代码
TTFT
TPOT
ITL
Throughput
Concurrency

到了这里,很容易产生一种错觉:

只要 GPU 驱动装好了,CUDA 能跑,接下来把程序塞进 Docker 就行。

但真正开始把 AI 应用容器化以后,会马上遇到一个非常经典的问题。

宿主机:

复制代码
nvidia-smi

一切正常。

RTX 3060 清清楚楚地在那里。

可是启动一个普通 Docker 容器:

复制代码
docker run --rm ubuntu:24.04 nvidia-smi

却会得到:

复制代码
nvidia-smi: command not found

或者即使自己装进去了相关工具,也可能发现:

复制代码
No devices were found

于是问题来了:

复制代码
GPU 明明插在这台机器上

为什么:

Host
 ↓
Docker Container

GPU 就消失了?

这一篇先不跑 vLLM。

也不讨论:

复制代码
PagedAttention
KV Cache
Continuous Batching

我们把整个系统再往下拆一层,专门研究:

复制代码
Docker
NVIDIA Container Toolkit
GPU Device
Runtime
CDI

然后做两个最基础的实验:

复制代码
Docker Container
      ↓
  nvidia-smi

以及:

复制代码
Docker Container
      ↓
    PyTorch
      ↓
torch.cuda.is_available()

如果这两个实验真正跑通,那么后面才有资格谈:

复制代码
vLLM Container
TensorRT-LLM Container
Kubernetes GPU Pod
GPU Operator

一、Docker 默认为什么看不到 GPU?

先暂时忘掉 NVIDIA。

我们从 Linux Container 最基本的原理开始。

Docker Container 本质上并不是:

复制代码
一台小型虚拟机

它更接近:

复制代码
Host Linux Kernel
        │
        ├── Container A
        ├── Container B
        └── Container C

多个 Container 共享:

复制代码
Host Kernel

然后通过:

复制代码
Namespace
cgroup
Mount
Capability
Seccomp
...

把进程隔离开。

所以 Container 默认看到的东西,是:

复制代码
Docker
按照 OCI Runtime 配置
允许它看到的资源

而不是:

复制代码
宿主机有什么
Container 就自动有什么

这点非常重要。

比如宿主机上可能存在:

复制代码
/dev/sda
/dev/nvme0n1
/dev/video0
/dev/dri/*
/dev/nvidia0

但 Docker 不会因为这些 Device Node 存在,就全部暴露给 Container。

否则:

复制代码
Container
   ↓
直接操作宿主机硬盘

或者:

复制代码
Container
   ↓
任意访问摄像头

显然都很危险。

所以 Docker 的基本原则实际上是:

设备访问需要显式授予。

GPU 也不例外。


二、GPU 在 Linux 里到底是什么?

我们平时看到 GPU,很容易想到:

复制代码
RTX 3060
12GB VRAM
3584 CUDA Cores

但站在 Linux Kernel 和 Container 的角度,GPU 首先表现成一些:

复制代码
Device Node

宿主机上可以看:

复制代码
ls -l /dev/nvidia*

典型会看到:

复制代码
/dev/nvidia0
/dev/nvidiactl
/dev/nvidia-modeset
/dev/nvidia-uvm
/dev/nvidia-uvm-tools

其中可以粗略理解为:

复制代码
/dev/nvidia0
    ↓
具体 GPU Device

/dev/nvidiactl
    ↓
NVIDIA Driver Control Device

/dev/nvidia-uvm
    ↓
Unified Virtual Memory

也就是说:

复制代码
PyTorch
   │
   ▼
CUDA Runtime
   │
   ▼
NVIDIA User-space Driver
   │
   ▼
/dev/nvidia*
   │
   ▼
NVIDIA Kernel Driver
   │
   ▼
GPU

于是问题已经变得非常具体了。

一个普通 Docker Container 默认并没有:

复制代码
/dev/nvidia0

因此:

复制代码
Container

自然无法直接访问:

复制代码
GPU

三、那直接 --device=/dev/nvidia0 不就行了吗?

看到这里,很容易想到:

复制代码
docker run \
    --device=/dev/nvidia0 \
    ...

Docker 本身确实支持:

复制代码
--device

把宿主机 Device Node 暴露给 Container。

那是不是 GPU 问题到这里就解决了?

并没有。

因为 CUDA 程序需要的不只是:

复制代码
/dev/nvidia0

它还可能需要:

复制代码
/dev/nvidiactl
/dev/nvidia-uvm

以及宿主机 Driver 对应的一系列:

复制代码
Driver Libraries

例如:

复制代码
libcuda.so
libnvidia-ml.so

另外还涉及:

复制代码
环境变量
Device Permission
Library Path
Driver Capability

所以真正需要做的并不是:

复制代码
把一块 Device Node 塞进去

而更接近:

复制代码
识别用户请求的 GPU
        │
        ▼
找到对应 Device Nodes
        │
        ▼
把 Device 暴露给 Container
        │
        ▼
挂载需要的 NVIDIA Driver Libraries
        │
        ▼
配置环境变量 / Hook
        │
        ▼
启动 Container

这一整套事情如果全部手工完成,会非常麻烦。

于是:

复制代码
NVIDIA Container Toolkit

出现了。


四、NVIDIA Container Toolkit 到底解决什么?

可以先画出一个非常粗略的结构:

复制代码
Application
    │
    ▼
Docker CLI
    │
    ▼
Docker Engine
    │
    ▼
Container Runtime
    │
    ▼
NVIDIA Container Toolkit
    │
    ├── GPU Device
    ├── Driver Library
    ├── Environment
    └── Runtime Configuration
    │
    ▼
OCI Runtime
    │
    ▼
Container
    │
    ▼
GPU

所以:

复制代码
NVIDIA Container Toolkit

并不是:

复制代码
CUDA

也不是:

复制代码
GPU Driver

更不是:

复制代码
Docker 的替代品

它解决的是:

怎样把宿主机已有的 NVIDIA GPU 能力正确注入 Container。

NVIDIA 当前官方架构中,nvidia-ctk 可以负责配置 Docker 等 Container Runtime,也可以生成 CDI Device Specification。

于是整个软件栈可以先记成:

复制代码
┌──────────────────────────────┐
│ Container Application        │
│ PyTorch / CUDA / vLLM        │
└───────────────┬──────────────┘
                │
┌───────────────▼──────────────┐
│ Container CUDA Libraries     │
└───────────────┬──────────────┘
                │
┌───────────────▼──────────────┐
│ NVIDIA Driver Libraries      │
│ exposed from Host            │
└───────────────┬──────────────┘
                │
┌───────────────▼──────────────┐
│ /dev/nvidia*                 │
└───────────────┬──────────────┘
                │
┌───────────────▼──────────────┐
│ Host NVIDIA Kernel Driver    │
└───────────────┬──────────────┘
                │
                ▼
              GPU

这张图很重要。

因为它解释了一个以后经常会遇到的问题:

Container 一般并不自己加载一套 NVIDIA Kernel Driver。

真正控制硬件的 Kernel Driver 仍然运行在:

复制代码
Host

Container 主要使用:

复制代码
Host Driver Interface
+
Container User-space CUDA Stack

五、Host Driver 和 Container CUDA 为什么可以不完全一样?

这里还有一个非常容易混淆的概念。

很多人会问:

复制代码
Host 装了 CUDA 12.x

Container 是 CUDA 12.y

为什么还能运行?

原因是我们需要区分:

复制代码
NVIDIA Driver

和

CUDA Toolkit / CUDA Runtime

它们不是同一个东西。

宿主机真正必须存在的是:

复制代码
NVIDIA Kernel Driver

例如:

复制代码
nvidia-smi

能够正常运行。

而 Container Image 里可以携带:

复制代码
CUDA Runtime
cuBLAS
cuDNN
PyTorch
...

所以大致变成:

复制代码
Host
────────────────────────

Linux Kernel
     │
NVIDIA Driver
     │
GPU


Container
────────────────────────

PyTorch
     │
CUDA Runtime
     │
Host Driver Interface
     │
GPU

这也是 Container 对 AI Infra 非常重要的原因之一:

复制代码
Host
尽量保持稳定 Driver

Container
携带不同版本 AI Software Stack

例如:

复制代码
Container A
PyTorch + CUDA 12.x

Container B
TensorRT + CUDA 12.y

Container C
vLLM + CUDA 12.z

底下仍然共享:

复制代码
Host NVIDIA Driver

当然,这并不代表:

复制代码
任意 CUDA Runtime
+
任意 Driver

都一定兼容。

Container Image 仍然会受到 Driver 能力和 CUDA Compatibility 要求约束。

NVIDIA 官方 Container Toolkit 也支持通过类似:

复制代码
NVIDIA_REQUIRE_CUDA

这样的约束检查 Driver 是否满足 Container 所需 CUDA 能力。


六、--gpus all 到底做了什么?

完成 NVIDIA Container Toolkit 配置以后,我们最常见的命令是:

复制代码
docker run --rm \
    --gpus all \
    IMAGE

这里最关键的是:

复制代码
--gpus all

Docker 官方当前仍然把 --gpus 作为给 Container 暴露 NVIDIA GPU 的标准方式之一。

可以粗略理解成:

复制代码
docker run
     │
     ├── Image
     │
     ├── CPU
     │
     ├── Memory
     │
     └── --gpus all
              │
              ▼
      GPU Resource Request
              │
              ▼
     NVIDIA Container Stack
              │
              ├── Device Nodes
              ├── Driver Libraries
              └── Runtime Config
              │
              ▼
          Container

这里有一个很重要的思想:

GPU 开始变成 Container 的一种显式资源。

它和:

复制代码
CPU
Memory
Disk
Network

在 Infra 思维里开始变得越来越接近。

以后到了 Kubernetes,我们看到的:

复制代码
resources:
  limits:
    nvidia.com/gpu: 1

本质上仍然是在继续解决:

复制代码
哪个 Workload
可以使用哪块 GPU?

只是系统从:

复制代码
Docker

升级成了:

复制代码
Cluster Scheduler

七、Runtime 到底在哪里?

现在再看:

复制代码
Runtime

这个词。

它特别容易和前面我们讨论过的:

复制代码
Model Runtime

混在一起。

例如:

复制代码
Ollama Runtime
llama.cpp Runtime

这里说的是:

复制代码
Model Runtime

负责:

复制代码
模型加载
推理
Tensor
Kernel
Memory

而 Docker 这里的 Runtime 是另外一层。

例如可以粗略画成:

复制代码
Docker
  │
  ▼
Container Runtime
  │
  ▼
OCI Runtime
  │
  ▼
Linux Process

NVIDIA Container Toolkit 则插入:

复制代码
GPU Device
+
Driver Capability
+
Library

等配置。

所以这一篇里需要特别区分:

复制代码
Model Runtime
≠
Container Runtime

例如:

复制代码
vLLM
     ↓
Model / Serving Runtime

Docker / containerd
     ↓
Container Runtime

runc
     ↓
OCI Runtime

它们恰好都叫:

复制代码
Runtime

但所在层级完全不同。


八、传统 NVIDIA Runtime 模式

安装 NVIDIA Container Toolkit 以后,官方当前仍然提供:

复制代码
sudo nvidia-ctk runtime configure --runtime=docker

这个命令会更新 Docker 的配置,使 Docker 能使用 NVIDIA Container Runtime,然后需要重启 Docker:

复制代码
sudo systemctl restart docker

这是 NVIDIA 当前官方安装文档给出的 Docker 配置方式。

可以粗略理解:

复制代码
Docker Engine
      │
      ▼
NVIDIA Container Runtime
      │
      ▼
OCI Runtime
      │
      ▼
Container

然后:

复制代码
docker run \
    --rm \
    --gpus all \
    ubuntu:24.04 \
    nvidia-smi

GPU 信息就可以被注入 Container。

NVIDIA 官方目前也使用类似下面的命令作为 Sample Workload:

复制代码
docker run \
    --rm \
    --runtime=nvidia \
    --gpus all \
    ubuntu \
    nvidia-smi

来验证 GPU Container 环境。

不过现代 Docker 用户平时更常见的仍然是:

复制代码
--gpus all

九、但为什么现在又出现了 CDI?

做到这里,其实已经可以用 GPU 了。

那为什么 NVIDIA 和 Container Runtime 生态后来还要推动:

复制代码
CDI

也就是:

复制代码
Container Device Interface

因为 GPU 并不是唯一需要暴露给 Container 的特殊硬件。

还可能有:

复制代码
GPU
FPGA
NIC
Accelerator
RDMA Device
AI ASIC
...

如果每一种硬件都要求:

复制代码
Container Runtime
专门认识这种设备

整个生态会越来越复杂。

于是一个非常自然的想法出现:

能不能把"怎样把某种设备注入 Container"描述成一个标准?

这就是 CDI。


十、CDI 可以先理解成"设备描述文件"

CDI 的思路可以先简化成:

复制代码
Vendor / Device Plugin
        │
        ▼
生成 CDI Spec
        │
        ▼
Container Runtime
读取 CDI Spec
        │
        ▼
知道应该注入:

Device Node
Mount
Environment
Hook
...

比如 NVIDIA GPU 会出现类似:

复制代码
nvidia.com/gpu=0
nvidia.com/gpu=all

这样的 Device Name。

Container Runtime 不需要在代码里硬编码:

复制代码
NVIDIA GPU 到底要挂哪些文件?

而只需要理解:

复制代码
CDI

这一套统一描述。

NVIDIA Container Toolkit 从较早版本就已经支持生成 CDI Specification;当前文档中,Toolkit 1.18.0 起会通过 nvidia-cdi-refresh systemd 服务自动生成和维护:

复制代码
/var/run/cdi/nvidia.yaml

并可以使用:

复制代码
nvidia-ctk cdi list

查看可用设备。


十一、CDI Spec 里面到底描述什么?

我们并不需要一开始就读完整 YAML。

只需要理解它大概解决:

复制代码
Device
应该怎样出现在 Container 里?

例如概念上类似:

复制代码
kind: nvidia.com/gpu

devices:
  - name: "0"

    containerEdits:
      deviceNodes:
        - /dev/nvidia0

真实 CDI Spec 会比这复杂。

它还可能描述:

复制代码
Device Nodes
Environment Variables
Library Mounts
Hooks

Docker 对 CDI 的定义也明确包括:

复制代码
Device
Environment Variable
Host Mount
Executable Hook

等 Container 配置。

因此 CDI 的真正价值并不是:

复制代码
换了一种 --gpus 写法

而是:

把 Hardware Device Injection 变成标准接口。

这对 Infra 特别重要。


十二、Docker 现在也原生支持 CDI 了

这个地方需要特别注意版本。

现代 Docker Engine 已经能够直接识别 CDI Device。

Docker 官方当前文档显示:

复制代码
Docker Engine 28.3.0+

CDI 默认启用。

Docker Daemon 默认读取:

复制代码
/etc/cdi/
/var/run/cdi/

中的 CDI Spec。

所以在支持的环境里,可以出现:

复制代码
docker run \
    --rm \
    --device=nvidia.com/gpu=all \
    IMAGE

这种非常有意思的写法。

整个结构开始变成:

复制代码
Docker
  │
  │ request:
  │ nvidia.com/gpu=all
  ▼
CDI
  │
  ▼
/var/run/cdi/nvidia.yaml
  │
  ├── Device Nodes
  ├── Libraries
  ├── Environment
  └── Hooks
  │
  ▼
Container

相比以前:

复制代码
Docker
   ↓
NVIDIA-specific Runtime Logic

CDI 更像:

复制代码
Docker
   ↓
Generic Device Interface
   ↓
Vendor Device Specification

这就是它真正值得学习的地方。


十三、--gpus all 和 CDI 是谁替代谁?

这里不要简单理解成:

复制代码
CDI 出现
↓
--gpus all 淘汰

现实并不是这么简单。

今天仍然大量使用:

复制代码
docker run --gpus all

Docker 官方文档也继续支持这套接口。

与此同时:

复制代码
CDI

提供了更加通用的 Device Model。

所以现阶段可以先这样理解:

复制代码
传统用户接口

docker run --gpus all
        │
        ▼
 NVIDIA GPU Support


更通用设备模型

docker run --device=nvidia.com/gpu=all
        │
        ▼
       CDI
        │
        ▼
Generic Device Injection

对于我们这篇实验:

复制代码
先把 --gpus all 跑通

这是最重要的。

然后再:

复制代码
观察 CDI

理解未来 Kubernetes、Podman 和各种 Accelerator Runtime 为什么越来越喜欢这套 Device Model。


十四、实验环境

实验继续使用前面几篇的 AI Infra 实验机:

复制代码
Ubuntu 24.04 Server

Intel Core i5-10400F

NVIDIA GeForce RTX 3060
12GB VRAM

这和前面 vLLM 实验继续使用 RTX 3060 12GB 的思路一致。

这一篇不需要:

复制代码
7B Model
vLLM
Ollama

因为我们只验证:

复制代码
Container
   ↓
GPU

所以实验链路反而非常简单:

复制代码
实验 1
Host nvidia-smi

实验 2
普通 Docker Container
默认看不到 GPU

实验 3
安装 NVIDIA Container Toolkit

实验 4
Docker + --gpus all
运行 nvidia-smi

实验 5
Docker + PyTorch
运行 torch.cuda

实验 6
观察 CDI

这一次真正的控制变量只有一个:

复制代码
GPU 有没有被显式注入 Container

十五、实验一:先确认宿主机 GPU 正常

首先:

复制代码
nvidia-smi

确认能够看到类似:

复制代码
NVIDIA GeForce RTX 3060

然后:

复制代码
ls -l /dev/nvidia*

应该能够看到 NVIDIA Device Node。

也可以:

复制代码
lsmod | grep nvidia

确认 Driver Module 已经加载。

所以这一层:

复制代码
Application
    ↓
NVIDIA Driver
    ↓
GPU

已经正常。

如果:

复制代码
nvidia-smi

在 Host 都失败,那么暂时不要碰 Docker。

因为:

复制代码
Docker GPU Support

建立在:

复制代码
Host GPU Driver 正常

之上。

NVIDIA 官方安装 Container Toolkit 的前置条件同样要求系统先安装 NVIDIA GPU Driver。


十六、实验二:普通 Docker 默认看不到 GPU

如果没有 docker,按官网提示安装:

复制代码
https://docs.docker.com/engine/install/ubuntu

先用一个普通 Ubuntu Container:

复制代码
docker run \
    --rm \
    ubuntu:24.04 \
    bash -c 'ls /dev | grep nvidia'

正常情况下:

复制代码
没有输出

这就说明:

复制代码
Host
/dev/nvidia0

并没有自动进入:

复制代码
Container

再看:

复制代码
docker run \
    --rm \
    ubuntu:24.04 \
    bash -c 'ls -l /dev'

可以看到 Container 自己拥有很多 Device:

复制代码
null
zero
random
urandom
tty
...

但是默认没有:

复制代码
nvidia0
nvidiactl
nvidia-uvm

这个实验虽然极其简单,却非常重要。

因为它证明:

Container 隔离的不只是文件系统和网络,它也控制 Process 能看到哪些 Device。


十七、安装 NVIDIA Container Toolkit

按照 NVIDIA 当前 Ubuntu / Debian 官方方式:

复制代码
https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html

先准备:

复制代码
sudo apt-get update

sudo apt-get install -y \
    ca-certificates \
    curl \
    gnupg2

加入 NVIDIA Container Toolkit Repository:

复制代码
curl -fsSL 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/stable/deb/nvidia-container-toolkit.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

这些步骤来自 NVIDIA 当前 Ubuntu / Debian 安装流程。

这里我不把 Toolkit 版本硬编码进实验命令。

因为个人实验机器更适合直接跟随:

复制代码
NVIDIA Stable Repository

安装当前稳定版本。

如果做生产环境:

复制代码
Production
CI
Cluster

则应该进一步考虑:

复制代码
Version Pinning

来保证环境可复现。


十八、让 Docker 知道 NVIDIA Runtime

接下来运行:

复制代码
sudo nvidia-ctk \
    runtime configure \
    --runtime=docker

然后:

复制代码
sudo systemctl restart docker

NVIDIA 官方说明这个命令会更新:

复制代码
/etc/docker/daemon.json

使 Docker 可以使用 NVIDIA Container Runtime。

可以检查:

复制代码
cat /etc/docker/daemon.json

再看:

复制代码
docker info

到这里可以画成:

复制代码
Before

Docker
   │
   ▼
runc
   │
   ▼
Container


After

Docker
   │
   ├── runc
   │
   └── NVIDIA Container Runtime
             │
             ▼
          Container
             │
             ▼
            GPU

十九、实验三:Container 终于看到 GPU 了

现在执行:

复制代码
docker run \
    --rm \
    --gpus all \
    ubuntu:24.04 \
    nvidia-smi

如果配置正确,应该能看到与宿主机类似的:

复制代码
NVIDIA-SMI
Driver Version
CUDA Version

GPU 0
NVIDIA GeForce RTX 3060

注意:

复制代码
Container 里显示 nvidia-smi

并不意味着:

复制代码
Container 自己加载了 NVIDIA Kernel Driver

真正发生的仍然是:

复制代码
Container
    │
    ▼
NVIDIA Driver Interface
    │
    ▼
Host Kernel Driver
    │
    ▼
RTX 3060

这就是整个实验最核心的结果。


二十、观察 /dev/nvidia* 的变化

再做一个非常直观的对比。

普通 Container:

复制代码
docker run \
    --rm \
    ubuntu:24.04 \
    bash -c 'ls -l /dev/nvidia* 2>/dev/null || true'

大概率:

复制代码
没有设备

GPU Container:

复制代码
docker run \
    --rm \
    --gpus all \
    ubuntu:24.04 \
    bash -c 'ls -l /dev/nvidia*'

现在则可以看到:

复制代码
/dev/nvidia0
/dev/nvidiactl
/dev/nvidia-uvm
...

于是:

复制代码
同一个 Image

ubuntu:24.04

在两种运行方式下:

复制代码
docker run ubuntu:24.04

和:

复制代码
docker run --gpus all ubuntu:24.04

看到的硬件世界是不一样的。

这很好地说明:

Image 并不决定 Container 能访问什么 GPU。

真正决定这一点的是:

复制代码
Container Run-time Configuration

二十一、这也解释了 Image 和 Container 的区别

这一篇顺便可以建立一个很重要的 Container 直觉。

同一个:

复制代码
Image

完全可以启动成:

复制代码
Container A
No GPU

Container B
GPU 0

Container C
GPU 1

Container D
GPU 0 + GPU 1

例如多 GPU 机器上可以指定:

复制代码
docker run \
    --rm \
    --gpus '"device=0"' \
    ubuntu:24.04 \
    nvidia-smi

或者:

复制代码
docker run \
    --rm \
    --gpus '"device=0,2"' \
    ubuntu:24.04 \
    nvidia-smi

Docker 官方当前同样支持按 GPU Index 或 GPU UUID 指定设备。

所以:

复制代码
Image

定义:

复制代码
Software Environment

而:

复制代码
docker run

定义:

复制代码
Runtime Resources

包括:

复制代码
CPU
Memory
Network
Volume
GPU

这个区别以后进入 Kubernetes 会非常重要。


二十二、实验四:真正让 PyTorch 使用 GPU

nvidia-smi 只能证明:

复制代码
GPU Device
+
NVML

已经可以在 Container 中访问。

但我们真正关心的是:

复制代码
AI Framework
      ↓
     CUDA
      ↓
     GPU

所以再跑一个 PyTorch Container。

为了避免自己在 Ubuntu Image 中重新安装一整套 PyTorch,可以直接使用带 CUDA 的 PyTorch Image。

进入 Container:

复制代码
docker run \
    --rm \
    --gpus all \
    -it \
    pytorch/pytorch:2.7.1-cuda12.8-cudnn9-runtime \
    python

具体 Tag 应以实验时 PyTorch 官方提供的版本为准。

然后:

复制代码
import torch

检查:

复制代码
print(torch.__version__)

再:

复制代码
print(torch.cuda.is_available())

期待:

复制代码
True

然后:

复制代码
print(torch.cuda.device_count())

单 GPU:

复制代码
1

再看:

复制代码
print(torch.cuda.get_device_name(0))

应该出现:

复制代码
NVIDIA GeForce RTX 3060

于是链路终于变成:

复制代码
Docker Container
      │
      ▼
    PyTorch
      │
      ▼
 CUDA Runtime
      │
      ▼
Host NVIDIA Driver
      │
      ▼
   RTX 3060

二十三、真正算一个 Tensor

只检查:

复制代码
torch.cuda.is_available()

还不够有意思。

再真正执行一次 GPU Tensor 运算:

复制代码
import torch

device = torch.device("cuda")

a = torch.randn(
    4096,
    4096,
    device=device
)

b = torch.randn(
    4096,
    4096,
    device=device
)

c = a @ b

torch.cuda.synchronize()

print(c.device)
print(c.shape)

期待:

复制代码
cuda:0
torch.Size([4096, 4096])

实验时另开一个终端:

复制代码
watch -n 0.5 nvidia-smi

就可能看到:

复制代码
python

进程出现。

这样我们就完成了从:

复制代码
Container 能看到 GPU

到:

复制代码
Container 真正在 GPU 上计算

的验证。


二十四、实验结果建议这样记录

这一篇不用像 vLLM Benchmark 那样搞几十个性能指标。

只需要一张很简单的表:

实验 GPU Device nvidia-smi PyTorch CUDA
Host ✅ ✅ ---
普通 Docker ❌ ❌ ❌
--gpus all ✅ ✅ ✅
CDI Device ✅ ✅ ✅

如果继续记录版本,可以加:

复制代码
Ubuntu Version
Docker Version
NVIDIA Driver
NVIDIA Container Toolkit
PyTorch Image
GPU

例如:

复制代码
docker --version

nvidia-smi

nvidia-ctk --version

这样以后环境变化时比较容易复现。


二十五、实验五:看看 CDI 已经生成了什么

如果使用当前较新的 NVIDIA Container Toolkit,可以执行:

复制代码
nvidia-ctk cdi list

可能看到类似:

复制代码
nvidia.com/gpu=0
nvidia.com/gpu=GPU-xxxxxxxx
nvidia.com/gpu=all

NVIDIA 当前 Toolkit 会使用:

复制代码
nvidia-cdi-refresh

维护:

复制代码
/var/run/cdi/nvidia.yaml

可以看:

复制代码
sudo cat /var/run/cdi/nvidia.yaml

不用试图一次看懂所有内容。

只关注:

复制代码
kind
devices
deviceNodes
containerEdits

就够了。

现在前面的架构终于可以落到一个真实文件:

复制代码
RTX 3060
    │
    ▼
NVIDIA Container Toolkit
    │
    ▼
/var/run/cdi/nvidia.yaml
    │
    ▼
nvidia.com/gpu=0
    │
    ▼
Container Runtime

二十六、实验六:直接用 CDI 启动 GPU Container

如果 Docker Engine 版本支持原生 CDI,可以尝试:

复制代码
docker run \
    --rm \
    --device=nvidia.com/gpu=all \
    ubuntu:24.04 \
    nvidia-smi

Docker Engine 28.3.0 起 CDI 默认启用,并且可以通过:

复制代码
--device=vendor.com/class=device

这种形式请求 CDI Device。

所以:

复制代码
--device=nvidia.com/gpu=all

已经非常接近未来更统一的 Accelerator Resource 表达方式。

这时候我们就同时跑通了两条路径:

复制代码
Path A

docker run
    │
 --gpus all
    │
    ▼
NVIDIA GPU


Path B

docker run
    │
 --device=nvidia.com/gpu=all
    │
    ▼
CDI
    │
    ▼
NVIDIA GPU

对于个人实验,两条都值得知道。


二十七、nvidia-smi 能跑,但 PyTorch 仍然说 CUDA 不可用?

这是最常见的坑之一。

比如:

复制代码
nvidia-smi

正常。

但是:

复制代码
torch.cuda.is_available()

返回:

复制代码
False

这时说明:

复制代码
Device / Driver Access

和:

复制代码
PyTorch CUDA Software Stack

并不是一个问题。

可能是:

复制代码
PyTorch CPU-only Build

例如装错:

复制代码
CPU Version

也可能是:

复制代码
Container Image
根本没有 CUDA-enabled PyTorch

所以排障顺序最好分层。


二十八、GPU Container 排障,最好按层来

不要一上来就:

复制代码
重装 CUDA
重装 Docker
重装 PyTorch

可以从底往上排。

第一层:

复制代码
Host Hardware

检查:

复制代码
lspci | grep -i nvidia

第二层:

复制代码
Host Driver

检查:

复制代码
nvidia-smi

第三层:

复制代码
Device Node

检查:

复制代码
ls -l /dev/nvidia*

第四层:

复制代码
Container Runtime / Toolkit

检查:

复制代码
nvidia-ctk --version

以及:

复制代码
docker run \
    --rm \
    --gpus all \
    ubuntu:24.04 \
    nvidia-smi

第五层:

复制代码
CUDA Framework

检查:

复制代码
import torch

print(torch.cuda.is_available())

第六层:

复制代码
Application

最后才检查:

复制代码
vLLM
TensorRT
Application Code

整个排障树其实非常像:

复制代码
Application
     │
PyTorch / Runtime
     │
CUDA
     │
Container GPU Injection
     │
NVIDIA Driver
     │
Linux Device
     │
Hardware

一层一层往下切。


二十九、坑一:宿主机 nvidia-smi 正常,不代表 Docker GPU 正常

这是这一篇最重要的坑。

复制代码
Host

nvidia-smi
   ↓
正常

只能证明:

复制代码
Host Driver
+
GPU

正常。

它不能证明:

复制代码
Docker
+
NVIDIA Container Toolkit
+
Runtime Configuration

正常。

所以必须单独测:

复制代码
docker run \
    --rm \
    --gpus all \
    ubuntu:24.04 \
    nvidia-smi

把两条路径严格区分:

复制代码
Host Test

和:

复制代码
Container Test

三十、坑二:Container 里没必要重新安装 Kernel Driver

另一个非常常见的误区是:

复制代码
Container 看不到 GPU
        ↓
是不是应该 apt install NVIDIA Driver?

一般不是。

Container 不是 VM。

NVIDIA Kernel Driver 在:

复制代码
Host Kernel

里运行。

Container 主要需要:

复制代码
User-space CUDA Stack
+
Driver Interface
+
Device Nodes

所以通常不要在普通 CUDA / PyTorch Container 里面再尝试运行:

复制代码
NVIDIA Kernel Driver Installer

否则很容易把:

复制代码
Host Driver

和:

复制代码
Container CUDA Runtime

两个层级搞混。


三十一、坑三:不要把 CUDA Version 那一行理解成"Host 装了这个 CUDA Toolkit"

nvidia-smi 顶部经常会显示:

复制代码
CUDA Version: 12.x

这个数字特别容易误导。

它主要反映:

复制代码
当前 NVIDIA Driver
最高支持到什么 CUDA Driver API / Compatibility Level

它并不简单等于:

复制代码
Host 上安装的 nvcc / CUDA Toolkit 版本

可以分别看:

复制代码
nvidia-smi

和:

复制代码
nvcc --version

它们回答的是不同问题。

甚至很多 AI Container Host:

复制代码
根本不需要安装完整 CUDA Toolkit

只需要:

复制代码
NVIDIA Driver
+
NVIDIA Container Toolkit

而 CUDA Runtime、cuDNN、PyTorch 等全部存在 Container 中。

这其实是非常干净的一种部署方式。


三十二、坑四:nvidia-smi 需要的是 utility Capability

NVIDIA Container Runtime 不只是控制:

复制代码
看哪块 GPU

还可以控制:

复制代码
Driver Capability

例如:

复制代码
compute
utility
graphics
video
display

其中 NVIDIA 官方定义:

复制代码
compute

用于 CUDA / OpenCL。

而:

复制代码
utility

则包含:

复制代码
nvidia-smi
NVML

需要的能力。

所以理论上可能出现:

复制代码
Device 在

但某种 Driver Capability 没暴露

从而造成某些工具行为不同。

普通 AI Compute 场景一般关注:

复制代码
compute
utility

即可。


三十三、坑五:CDI 文件可能过期

CDI 又增加了一个新的排障层。

例如:

复制代码
GPU / Driver
发生变化

但是:

复制代码
CDI Spec

没有同步更新。

NVIDIA 当前 Toolkit 通过:

复制代码
nvidia-cdi-refresh.path
nvidia-cdi-refresh.service

自动维护 CDI Spec。

可以检查:

复制代码
systemctl status \
    nvidia-cdi-refresh.path

以及:

复制代码
systemctl status \
    nvidia-cdi-refresh.service

如果怀疑 CDI Spec 没更新:

复制代码
sudo systemctl restart \
    nvidia-cdi-refresh.service

再看:

复制代码
nvidia-ctk cdi list

NVIDIA 也提供:

复制代码
nvidia-ctk --debug cdi list

帮助检查 CDI Spec 加载问题。


三十四、坑六:Docker 重启会影响正在运行的 Container

运行:

复制代码
sudo nvidia-ctk runtime configure \
    --runtime=docker

之后官方步骤需要:

复制代码
sudo systemctl restart docker

如果这台机器上已经运行:

复制代码
Database
Service
vLLM
Other Container

就不能把它当作完全无影响的命令。

个人实验机没什么问题。

生产服务器则一定要考虑:

复制代码
Docker Daemon Restart

对现有 Workload 的影响。


三十五、到这里,终于能解释"Docker 为什么默认看不到 GPU"

现在重新回答开头的问题。

因为 GPU 并不是:

复制代码
Container 的普通文件

而是:

复制代码
Host Kernel
管理的一种 Hardware Device

Docker 默认通过隔离机制控制:

复制代码
Container
能看到哪些 Device

所以:

复制代码
Host GPU

不会自然变成:

复制代码
Container GPU

中间必须有人完成:

复制代码
GPU Resource Request
        │
        ▼
Device Selection
        │
        ▼
Device Node Injection
        │
        ▼
Driver Library Injection
        │
        ▼
Runtime Configuration
        │
        ▼
Container

而 NVIDIA Container Toolkit 正是在做这件事情。


三十六、这一篇真正建立的不是一条 Docker 命令

如果这一篇只记住:

复制代码
docker run --gpus all

其实还不够。

真正重要的是建立这样一个 Infra 视角:

复制代码
┌────────────────────────────┐
│ AI Application             │
│ vLLM / PyTorch             │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│ CUDA Runtime               │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│ Container                  │
│                            │
│ GPU is not automatic       │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│ NVIDIA Container Toolkit   │
│ Runtime / CDI              │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│ /dev/nvidia*               │
│ Driver Libraries           │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│ Host NVIDIA Driver         │
└─────────────┬──────────────┘
              │
              ▼
             GPU

以前我们的思考主要停留在:

复制代码
Model
   ↓
Runtime
   ↓
GPU

从这一篇开始,需要再加一层:

复制代码
Model
   ↓
AI Runtime
   ↓
Container
   ↓
Container Runtime
   ↓
Device Management
   ↓
Driver
   ↓
GPU

这才越来越接近:

复制代码
AI Infrastructure

本身。


三十七、为什么 CDI 特别值得记住?

因为 CDI 背后体现的是一个更加普遍的 Infra 问题:

复制代码
如何让 Container
使用特殊硬件?

今天是:

复制代码
NVIDIA GPU

明天可能是:

复制代码
AMD GPU
Intel GPU
FPGA
SmartNIC
DPU
AI Accelerator

如果每一种 Device 都设计:

复制代码
一套 Runtime Hack

Container 生态会非常混乱。

CDI 尝试把问题变成:

复制代码
Device Vendor
      │
      ▼
CDI Specification
      │
      ▼
Container Runtime
      │
      ▼
Container

也就是:

Container Runtime 不需要理解每一种硬件,只需要理解统一的 Device Description。

这其实和很多 Infra 设计非常相似:

复制代码
NIC
   ↓
标准 Network Interface

Storage
   ↓
Block / File Interface

Accelerator
   ↓
CDI

把硬件差异隐藏到统一接口下面。


三十八、这一篇之后,我们离 Kubernetes 已经很近了

现在假设不再只有一个 Docker Container。

而是有:

复制代码
Node A
GPU 0
GPU 1

Node B
GPU 0
GPU 1
GPU 2
GPU 3

然后集群来了三个任务:

复制代码
Pod A
需要 1 GPU

Pod B
需要 2 GPU

Pod C
需要 1 GPU

新的问题马上出现:

复制代码
每台 Node 有多少 GPU?

Kubernetes 怎么知道?

Pod 怎么声明自己需要 GPU?

Scheduler 怎么选择 Node?

Container Runtime 最后又怎样
把那块 GPU 真正放进 Container?

这时:

复制代码
docker run --gpus all

已经不够了。

因为现在需要的不只是:

复制代码
Use GPU

而是:

复制代码
Discover
Advertise
Schedule
Allocate
Inject
Monitor

完整的一套 GPU Resource Lifecycle。

于是很自然就会出现:

复制代码
Kubernetes Device Plugin
NVIDIA GPU Operator
Container Runtime
CDI

这些新的组件。


三十九、总结

这一篇从一个看起来很简单的问题开始:

复制代码
为什么:

Host
nvidia-smi 正常

但是:

Docker
默认看不到 GPU?

答案最终拆成了几层。

第一层:

复制代码
Container
默认不会获得 Host 的所有 Device

GPU 首先是:

复制代码
Linux Device

第二层:

复制代码
GPU ≠ 一个 /dev/nvidia0 就结束

真正运行 CUDA 程序还涉及:

复制代码
Device Nodes
Driver Libraries
Capabilities
Environment
Runtime Configuration

第三层:

复制代码
NVIDIA Container Toolkit

负责把 NVIDIA GPU 能力正确注入 Container。

于是:

复制代码
docker run --gpus all ...

才真正能够工作。


第四层:

复制代码
Host Driver

和:

复制代码
Container CUDA Runtime

是不同层级。

因此 AI Container 可以携带自己的:

复制代码
PyTorch
CUDA Runtime
cuDNN
vLLM

同时共享宿主机:

复制代码
NVIDIA Kernel Driver

第五层:

复制代码
CDI

开始把:

复制代码
GPU Injection

从 NVIDIA 专用机制抽象成:

复制代码
Generic Container Device Interface

于是设备开始能够被描述成:

复制代码
nvidia.com/gpu=0
nvidia.com/gpu=all

最终整个链路变成:

复制代码
AI Application
      │
      ▼
PyTorch / vLLM
      │
      ▼
CUDA Runtime
      │
      ▼
Container
      │
      ▼
Container Runtime
      │
      ▼
NVIDIA Container Toolkit / CDI
      │
      ▼
Linux GPU Device
      │
      ▼
NVIDIA Driver
      │
      ▼
GPU

所以:

Docker 默认看不到 GPU,并不是 Docker"不支持 GPU",而是 Container 默认不会自动继承宿主机硬件。GPU 必须作为一种显式资源,被发现、描述、授权并注入 Container。

而这恰好是从:

复制代码
AI Application

走向:

复制代码
AI Infrastructure

非常关键的一步。


四十、下一篇为什么出现?

到这里,我们已经能运行:

复制代码
docker run --gpus all ...

让:

复制代码
PyTorch Container

使用 RTX 3060。

但是一个新的问题马上出现。

如果现在不是:

复制代码
一个 Docker Container

而是:

复制代码
几十台 GPU Server
+
几十块 GPU
+
几百个 Pod

谁来决定:

复制代码
哪块 GPU 属于哪个 Pod?

Docker 可以告诉我们:

复制代码
这个 Container 使用 GPU 0

但是 Docker 本身并不会解决:

复制代码
整个 Cluster
应该怎样调度 GPU

的问题。

于是问题自然从:

复制代码
How does a Container use a GPU?

继续变成:

复制代码
How does a Cluster manage GPUs?

这就是下一篇真正要进入的世界:

复制代码
Kubernetes
      │
      ▼
GPU Resource
      │
      ▼
Device Plugin
      │
      ▼
Container Runtime
      │
      ▼
GPU

以及一个在 AI Infra 里几乎绕不开的组件:

复制代码
NVIDIA GPU Operator

所以接下来,我们就可以继续从一块已经能被 Docker 使用的 RTX 3060 开始,看看 GPU 怎样第一次真正成为:

复制代码
Kubernetes Resource
相关推荐
小宋10211 小时前
Jev 不是另一个聊天模型:Choice、Score、Boolean 与概率决策完整实战
大数据·人工智能
染指11101 小时前
127.Agent-LangChain核心组件-模型输出后json修复
人工智能·langchain·agent·agents
xsd202411181 小时前
LingCast灵播:数字人直播技术架构全解析
人工智能
天衍四九-1 小时前
Docker Compose企业实战系列(四):Redis生产级部署|密码加固、持久化、内存优化完整方案
redis·docker·容器
Data analyse4561 小时前
数据合规的敏感数据怎么识别?
前端·人工智能·数据分析
嘿嘿-661 小时前
GPT-6.1 Sol 实战:做一个可交互的 3D 汽车展厅
人工智能·gpt·3d·chatgpt·汽车
AI你一生一世1 小时前
当广告采集器成为大模型的“眼睛“:跨站上下文注入的架构与代价
人工智能·架构·大模型·rag·隐私安全·上下文注入·跨站追踪
troy1281 小时前
如何在谷歌云(Google Cloud)上部署 Kubernetes 集群
云原生·容器·kubernetes
数字化顾问1 小时前
(136页PPT)BCGIDG资本中国某省市场Geneva项目商业尽职调查项目报告(附下载方式)
大数据·人工智能