第 16 章:Docker 中的 GPU
第 16 章:Docker 中的 GPU
在容器化部署和 CI/CD 流水线中使用 GPU 已成为现代开发的常见需求。本章讲解如何在 Docker 容器中正确配置 GPU 访问、OpenGL 渲染和 OpenCL 计算。
16.1 为什么在 Docker 中使用 GPU?
| 场景 | 需求 |
|---|---|
| CI/CD 中的图形测试 | 无物理显示器的 GPU 渲染 |
| 服务器端渲染 | 批量生成 3D 图像/视频 |
| 机器学习推理 | GPU 加速的容器化推理服务 |
| 科学计算 | 环境一致的 GPU 计算集群 |
| 多租户隔离 | 安全隔离的 GPU 资源共享 |
16.2 NVIDIA 容器运行时
16.2.1 安装 NVIDIA Container Toolkit
# 添加 NVIDIA 仓库
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
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/$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
# 配置 Docker 运行时
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
# 验证
docker run --rm --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi
16.2.2 GPU 分配策略
# 使用所有 GPU
docker run --gpus all myimage
# 使用特定数量的 GPU
docker run --gpus 2 myimage
# 使用特定 GPU(通过 ID)
docker run --gpus '"device=0,2"' myimage
# 使用环境变量
docker run --env NVIDIA_VISIBLE_DEVICES=0,1 myimage
docker run --env NVIDIA_VISIBLE_DEVICES=all myimage
docker run --env NVIDIA_VISIBLE_DEVICES=none myimage # 禁用 GPU
16.3 Docker 中的 OpenGL 渲染
16.3.1 问题:容器没有显示器
容器环境通常没有 X11 显示器。解决方案有三种:
| 方案 | 适用场景 | 性能 |
|---|---|---|
| EGL 离屏渲染 | 服务器端渲染 | 高 |
| X11 转发 | 开发调试 | 中 |
| 软件渲染 (llvmpipe) | CI/CD 测试 | 低 |
16.3.2 EGL 离屏渲染
EGL 是 OpenGL ES 的本地平台接口,支持无显示器的 GPU 渲染:
# Dockerfile - EGL 离屏渲染环境
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
RUN apt-get update && apt-get install -y \
build-essential cmake git \
libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev \
libglew-dev libglfw3-dev libglm-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN mkdir build && cd build && \
cmake .. -DUSE_EGL=ON && \
make -j$(nproc)
CMD ["./build/render_headless"]
16.3.3 EGL 初始化代码
// egl_headless.cpp - EGL 离屏渲染
#include <EGL/egl.h>
#include <GL/gl.h>
#include <cstdio>
int main() {
// 1. 获取 EGL 显示
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
printf("Failed to get EGL display\n");
return -1;
}
// 2. 初始化 EGL
EGLint major, minor;
eglInitialize(display, &major, &minor);
printf("EGL Version: %d.%d\n", major, minor);
// 3. 选择配置
EGLint configAttribs[] = {
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_NONE
};
EGLConfig config;
EGLint numConfigs;
eglChooseConfig(display, configAttribs, &config, 1, &numConfigs);
// 4. 创建 PBuffer 表面(离屏渲染目标)
EGLint pbufferAttribs[] = {
EGL_WIDTH, 1920,
EGL_HEIGHT, 1080,
EGL_NONE
};
EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
// 5. 绑定 OpenGL API
eglBindAPI(EGL_OPENGL_API);
// 6. 创建上下文
EGLint contextAttribs[] = {
EGL_CONTEXT_MAJOR_VERSION, 4,
EGL_CONTEXT_MINOR_VERSION, 6,
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
EGL_NONE
};
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
eglMakeCurrent(display, surface, surface, context);
// 7. 现在可以正常使用 OpenGL 渲染
printf("Renderer: %s\n", glGetString(GL_RENDERER));
printf("Version: %s\n", glGetString(GL_VERSION));
// ... 执行渲染 ...
// 8. 读取渲染结果
unsigned char *pixels = new unsigned char[1920 * 1080 * 4];
glReadPixels(0, 0, 1920, 1080, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
// 9. 保存为图片(使用 stb_image_write)
// stbi_write_png("output.png", 1920, 1080, 4, pixels, 1920 * 4);
// 10. 清理
delete[] pixels;
eglDestroyContext(display, context);
eglDestroySurface(display, surface);
eglTerminate(display);
return 0;
}
16.3.4 X11 转发方式
# 允许本地 X11 连接
xhost +local:docker
# 运行容器,挂载 X11 socket
docker run -it --gpus all \
-e DISPLAY=$DISPLAY \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v $HOME/.Xauthority:/root/.Xauthority:ro \
myimage ./my_gl_app
# 用完后恢复
xhost -local:docker
16.4 Docker 中的 OpenCL
16.4.1 OpenCL 容器配置
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
RUN apt-get update && apt-get install -y \
build-essential cmake \
ocl-icd-opencl-dev \
opencl-headers \
clinfo \
&& rm -rf /var/lib/apt/lists/*
# 验证 OpenCL 可用
RUN clinfo
WORKDIR /app
COPY . .
RUN mkdir build && cd build && cmake .. && make -j$(nproc)
CMD ["./build/opencl_compute"]
# 运行 OpenCL 容器
docker run --gpus all myimage clinfo
16.5 Docker Compose 配置
# docker-compose.yml
version: '3.8'
services:
renderer:
build: .
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1 # 使用 1 个 GPU
capabilities: [gpu, compute, graphics]
environment:
- NVIDIA_VISIBLE_DEVICES=0
- LIBGL_ALWAYS_SOFTWARE=0
volumes:
- ./output:/app/output # 渲染结果输出目录
- ./assets:/app/assets # 资源文件
compute:
build:
context: .
dockerfile: Dockerfile.compute
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all # 使用所有 GPU
capabilities: [gpu, compute]
16.6 CI/CD 集成
16.6.1 GitHub Actions GPU 测试
# .github/workflows/gpu-test.yml
name: GPU Build & Test
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build with software renderer
run: |
docker build -t myapp-test .
docker run --rm \
-e LIBGL_ALWAYS_SOFTWARE=1 \
-e GALLIUM_DRIVER=llvmpipe \
myapp-test ./run_tests --headless
16.6.2 GitLab CI GPU Runner
# .gitlab-ci.yml
gpu-test:
image: nvidia/cuda:12.2.0-devel-ubuntu22.04
tags:
- gpu
script:
- apt-get update && apt-get install -y libgl1-mesa-dev
- mkdir build && cd build && cmake .. && make
- ./build/run_tests
variables:
NVIDIA_VISIBLE_DEVICES: "all"
16.7 多 GPU 容器编排
16.7.1 Kubernetes GPU 调度
# k8s-gpu-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: gl-renderer
spec:
replicas: 4 # 4 个渲染实例
template:
spec:
containers:
- name: renderer
image: myregistry/gl-renderer:latest
resources:
limits:
nvidia.com/gpu: 1 # 每个 Pod 1 个 GPU
env:
- name: NVIDIA_VISIBLE_DEVICES
value: "all"
16.8 完整 Dockerfile 示例
# Dockerfile - 完整的 OpenGL/OpenCL 开发环境
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
# 避免交互式安装
ENV DEBIAN_FRONTEND=noninteractive
# 系统依赖
RUN apt-get update && apt-get install -y \
build-essential cmake git wget \
# OpenGL
libgl1-mesa-dev libglu1-mesa-dev \
libegl1-mesa-dev libgles2-mesa-dev \
libglew-dev libglfw3-dev libglm-dev \
# OpenCL
ocl-icd-opencl-dev opencl-headers \
# 工具
clinfo mesa-utils \
# 图片库
libpng-dev libjpeg-dev \
&& rm -rf /var/lib/apt/lists/*
# stb 头文件
RUN mkdir -p /usr/local/include/stb && \
wget -O /usr/local/include/stb/stb_image.h \
https://raw.githubusercontent.com/nothings/stb/master/stb_image.h && \
wget -O /usr/local/include/stb/stb_image_write.h \
https://raw.githubusercontent.com/nothings/stb/master/stb_image_write.h
WORKDIR /app
COPY CMakeLists.txt .
COPY src/ src/
COPY libs/ libs/
COPY shaders/ shaders/
COPY assets/ assets/
RUN mkdir build && cd build && \
cmake .. -DCMAKE_BUILD_TYPE=Release && \
make -j$(nproc)
# 验证
RUN nvidia-smi && clinfo | head -20
CMD ["./build/MyApp"]
16.9 注意事项
⚠️ NVIDIA 驱动版本兼容性:容器内的 CUDA 版本必须 ≤ 主机驱动支持的版本。使用
nvidia-smi查看主机支持的最高 CUDA 版本。
⚠️ DISPLAY 环境变量:使用 EGL 离屏渲染时不需要设置 DISPLAY。如果设置了 X11 转发但没有 X 服务器,OpenGL 初始化会失败。
⚠️ 权限问题:Docker 容器中的
/dev/dri设备需要适当的权限。使用--device /dev/dri或--privileged(不推荐)。
⚠️ 共享内存大小:Docker 默认的
/dev/shm只有 64MB,某些 GPU 应用需要更大的共享内存。使用--shm-size=1g增大。
16.10 业务场景
场景 1:服务器端 3D 渲染服务
将 OpenGL 渲染引擎容器化,通过 REST API 接收渲染请求,返回渲染结果图片。
场景 2:GPU 计算集群
使用 Kubernetes + NVIDIA device plugin 管理数百个 GPU 节点的 OpenCL 计算任务。
场景 3:自动化 3D 内容生成
CI/CD 流水线中自动生成产品 3D 预览图、数据可视化图表。
16.11 扩展阅读
| 资源 | 说明 |
|---|---|
| NVIDIA Container Toolkit | 官方文档 |
| EGL Native Platform Interface | EGL API 参考 |
| Docker GPU 支持 | Docker GPU 文档 |
| Kubernetes GPU 调度 | K8s GPU 文档 |
本章小结
- NVIDIA Container Toolkit 是 Docker GPU 支持的基础
- EGL 离屏渲染是服务器端 OpenGL 渲染的标准方案
- X11 转发适合开发调试,但不适合生产环境
- CI/CD 中可使用 Mesa 软件渲染进行基础编译测试
- 多 GPU 环境通过环境变量或 K8s 调度控制 GPU 分配
- 务必注意 CUDA 驱动版本兼容性
上一章:第 15 章:Vulkan 入门 下一章:第 17 章:常见问题与调试