强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

OpenGL / OpenCL 编程指南 / 第 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 章:常见问题与调试