容器化你的 GPU 开发环境
第二天的目标是理解 Linux 容器隔离的底层机制, 搞懂 GPU 是如何被"穿透"进容器的, 然后亲手写出第一个能跑 CUDA 的 Dockerfile。
思维导图
Linux 隔离原语
容器不是虚拟机 — 它本质上就是一个 受 cgroups 限制资源、被 namespaces 隔离视图 的普通 Linux 进程。理解这两个原语,是理解 GPU 如何"穿透"进容器的前提。
进程管理与 NUMA 亲和性
多卡服务器上 GPU 与 CPU / 内存的物理距离不同。把进程绑到离 GPU 最近的 NUMA node, 能减少 PCIe 延迟、提高 host-to-device 传输速度。这在训练数据加载时尤为关键。
/proc 文件系统
Linux 的"万能窗口"。/proc/cpuinfo 看核数频率,/proc/meminfo 看内存,/proc/[pid]/status 看进程的 cgroup / namespace 归属。
numactl
查看 NUMA 拓扑: numactl -H。绑定进程到指定 NUMA node: numactl --cpunodebind=0 --membind=0 python train.py
taskset
CPU 绑核: taskset -c 0-7 python train.py。数据加载进程绑到离 GPU 近的核,减少跨 NUMA 访问。
| 命令 | 用途 | AI Infra 场景 |
|---|---|---|
lscpu | CPU 架构、核数、NUMA nodes | 确认 CPU-GPU 亲和关系 |
numactl -H | NUMA 拓扑 + 内存分布 | 找出 GPU 所属 NUMA node |
nvidia-smi topo -m | GPU 间拓扑 + CPU 亲和 | 最后一列 = 最近 NUMA node |
taskset -c | 绑定进程到指定 CPU 核 | DataLoader worker 绑核 |
lstopo | 可视化硬件拓扑图 | 理解 PCIe / NVLink 连接 |
Docker 与 GPU 容器
GPU 不是"天生就能进容器"的 — 需要 nvidia-container-toolkit
在容器启动时动态注入驱动库和设备节点。理解这个过程,是后面搭建 K8s GPU 集群的基础。
镜像分层 (UnionFS)
每条 Dockerfile 指令生成一层只读层,容器启动时叠加一层可写层。修改文件 = copy-on-write。镜像越小,拉取越快 — 生产环境用 nvidia/cuda:*-base 而不是 -devel。
容器内不装驱动
NVIDIA 驱动是内核模块,由宿主机提供。容器只需要 CUDA runtime 库。nvidia-container-toolkit 在启动时把宿主机的 libcuda.so bind-mount 进容器。
动手实践
4.1 · 观察 Linux 隔离原语
先在宿主机上执行这些命令,理解 cgroups 和 namespaces 的存在:
# 1. 查看当前进程所在的 namespaces ls -la /proc/self/ns/ # 2. 查看 cgroups v1 / v2 mount | grep cgroup cat /proc/self/cgroup # 3. NUMA 拓扑 numactl -H # 看 NUMA node 数量 + 内存分布 nvidia-smi topo -m # 最后一列 = GPU 最近的 NUMA node # 4. CPU 信息 lscpu | grep -E "^CPU\(s\)|NUMA|Thread"
4.2 · 安装 nvidia-container-toolkit
# Ubuntu 22.04 安装 (一次性操作) 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 sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker # 验证 docker run --rm --gpus all nvidia/cuda:12.1.0-base-ubuntu22.04 nvidia-smi
4.3 · 写你的第一个 CUDA Dockerfile
创建一个 Dockerfile.cuda,把 Day 01 的推理脚本容器化:
# Dockerfile.cuda FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 # 系统依赖 RUN apt-get update && apt-get install -y --no-install-recommends \ python3 python3-pip && \ rm -rf /var/lib/apt/lists/* # Python 依赖 RUN pip3 install --no-cache-dir \ torch --index-url https://download.pytorch.org/whl/cu121 && \ pip3 install --no-cache-dir transformers accelerate sentencepiece # 工作目录 WORKDIR /workspace COPY first_infer.py . # 默认命令 CMD ["python3", "first_infer.py"]
4.4 · 构建 & 运行
# 构建镜像 (首次较慢,PyTorch 约 2GB) docker build -f Dockerfile.cuda -t aiinfra-day02 . # 运行 — 注意 --gpus all docker run --rm --gpus all aiinfra-day02 # 进入容器交互调试 docker run --rm -it --gpus all aiinfra-day02 bash # 在容器内验证 nvidia-smi # 应该和宿主机看到一样的 GPU python3 -c "import torch; print(torch.cuda.is_available())"
镜像大小
用 docker images 查看。CUDA runtime 基础镜像 ~1.5 GB,加上 PyTorch 后总计 ~6–8 GB。生产环境要用多阶段构建瘦身。
容器内看到多少 GPU?
取决于 --gpus 参数。--gpus all = 全部GPU,--gpus '"device=0"' = 指定第 0 张卡。这由 NVIDIA_VISIBLE_DEVICES 控制。
对比宿主机的推理速度
容器内跑推理的速度应该和宿主机几乎一致 — 容器是共享内核的进程隔离,不是虚拟化,没有额外的 GPU 性能开销。
如果去掉 --gpus all?
容器内 nvidia-smi 会报错找不到设备。因为没有触发 nvidia-container-toolkit 的 OCI hook,设备节点不会被注入。
自测 · Q&A
不看答案,能讲清楚下面 6 个问题,Day 02 就算过关。
点击 + 展开参考答案。
Q.01 容器和虚拟机的本质区别是什么? +
虚拟机有独立内核(通过 hypervisor 虚拟化硬件),容器共享宿主机内核,只通过 namespaces 隔离视图、cgroups 限制资源。
因此容器启动快(毫秒级)、性能几乎无损、但隔离性不如 VM。对 GPU 而言,容器方案(nvidia-container-toolkit)远比 GPU 虚拟化(vGPU / MIG)简单高效。
Q.02 cgroups 和 namespaces 各负责什么? +
cgroups = 资源限制("你能用多少"):CPU 时间、内存上限、设备访问、进程数。
namespaces = 视图隔离("你能看到什么"):PID、网络、文件系统、主机名、IPC、用户。
GPU 容器需要两者配合:devices cgroup 允许访问 /dev/nvidia*,mnt namespace 把设备节点挂进去。
Q.03 为什么容器内不需要安装 NVIDIA 驱动? +
NVIDIA 驱动是内核模块(nvidia.ko),容器共享宿主机内核,所以驱动已经加载了。
nvidia-container-toolkit 在容器启动时把宿主机的 libcuda.so、libnvidia-ml.so 等用户态库 bind-mount 进容器。容器镜像只需要 CUDA runtime(libcudart.so)。
Q.04 Docker 镜像分层对 AI 镜像有什么实际影响? +
AI 镜像通常很大(PyTorch + CUDA ≈ 6–8 GB),镜像分层意味着:
1. 共享基础层 — 多个镜像共用 nvidia/cuda:12.1-base,只拉一次。
2. 缓存加速构建 — 如果只改了代码层,pip install 层不用重跑。所以 Dockerfile 要把依赖安装放前面、代码 COPY 放后面。
3. 生产用 multi-stage — 编译阶段用 -devel,运行阶段用 -runtime,镜像可缩小 50%+。
Q.05 NUMA 对 GPU 训练性能有什么影响? +
多 socket 服务器上,每个 GPU 通过 PCIe 连接到最近的 CPU socket(NUMA node)。如果训练进程的 DataLoader worker 跑在远端 NUMA node 的 CPU 上,host-to-device 数据传输要跨 NUMA,延迟增加 1.5–2×。
用 nvidia-smi topo -m 找到 GPU 0 对应的 NUMA node,然后 numactl --cpunodebind=0 --membind=0 python train.py 绑定。大规模训练框架(Megatron、DeepSpeed)内部已处理。
Q.06 nvidia/cuda 镜像的 base、runtime、devel 三个版本有什么区别? +
base — 只有 CUDA runtime 核心库,最小最精简(~120 MB)。适合只跑推理的场景。
runtime — base + CUDA math 库(cuBLAS、cuFFT 等)。PyTorch 推理 / 训练通常选这个。
devel — runtime + 编译工具链(nvcc、头文件)。需要编译 CUDA kernel(如 FlashAttention)时用,镜像最大(~3.5 GB)。
生产建议:编译阶段用 devel,运行阶段用 runtime(multi-stage build)。
今日复盘
在你的笔记本里新建 Day02.md,回答下面 5 个问题:
- 容器隔离靠哪两个 Linux 内核特性?它们各管什么?
- GPU 是如何"穿透"进容器的?画出 docker run --gpus all 的完整链路。
- 你的服务器有几个 NUMA node?GPU 0 连在哪个 NUMA node 上?
- nvidia/cuda 镜像的 base / runtime / devel 分别适合什么场景?
- 容器内跑推理的速度和宿主机对比如何?有没有性能损失?
完成标准 / Definition of Done
- 能口头解释 cgroups 和 namespaces 的作用,以及 GPU 容器如何利用它们
- 执行
numactl -H和nvidia-smi topo -m,记录了 NUMA 拓扑 - 安装了
nvidia-container-toolkit并通过验证 - 写出了 CUDA Dockerfile,成功在容器内跑通
nvidia-smi - 在容器内复现了 Day 01 的推理脚本,确认性能无明显损失
Day02.md笔记写完
明天预告
GPU 硬件与体系结构
深入 GPU 内部:SM、Warp、Tensor Core、HBM、L2 Cache、SM occupancy。 阅读 NVIDIA Ampere / Hopper Whitepaper,用 nvidia-smi 和 nvtop 观察 GPU 在跑模型时的行为。