原力注入

Container Device Interface (CDI) 技术介绍

原文地址:https://github.com/ForceInjection/kubernetes-hands-on-course/blob/master/Advanced-Topics/CDI/cdi_intro.md

相关文章:

Nvidia K8s Device Plugin 原理解析和源码分析

NVIDIA Container Toolkit 原理分析与代码深度解析

Kubernetes Dynamic Resource Allocation 权威指南

1. 容器设备访问现状与挑战

1.1 传统容器设备访问的挑战

在传统的容器化环境中,容器运行时对硬件设备的访问主要依赖于简单的设备节点暴露机制。然而,随着现代硬件设备复杂性的不断增长,这种传统方式面临着诸多挑战:

1.1.1 设备隔离与访问控制挑战

以 NVIDIA GPU 为例,传统容器化 GPU 计算面临严重的设备隔离和访问控制问题:

# 传统 Docker 方式:需要手动处理复杂的设备文件挂载和权限控制
docker run -it \
  --device=/dev/nvidia0 \
  --device=/dev/nvidia-uvm \
  --device=/dev/nvidia-uvm-tools \
  --device=/dev/nvidiactl \
  --volume=/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1 \
  --volume=/usr/lib/x86_64-linux-gnu/libcuda.so.1:/usr/lib/x86_64-linux-gnu/libcuda.so.1 \
  --volume=/usr/lib/x86_64-linux-gnu/libcudart.so.11.0:/usr/lib/x86_64-linux-gnu/libcudart.so.11.0 \
  --env NVIDIA_VISIBLE_DEVICES=0 \
  --env NVIDIA_DRIVER_CAPABILITIES=compute,utility \
  ubuntu:20.04

主要挑战:

  • • 设备文件安全挂载:需要确保容器只能访问分配的 GPU 设备(如 /dev/nvidia0),防止恶意容器访问宿主机的其他 GPU 资源
  • • 权限精确控制:通过 Linux 权限机制(chmod、chown)精确控制对 GPU 设备文件的访问权限
  • • 多容器资源隔离:通过 cgroups 等机制确保每个容器只能访问其分配的 GPU 资源,防止资源冲突

1.1.2 驱动程序依赖与兼容性挑战

驱动程序版本兼容性问题:
GPU 应用需要特定版本的驱动程序和运行时库,这些组件通常安装在宿主机上,容器化时面临复杂的依赖管理:

# 不同 CUDA 版本的兼容性问题
# 宿主机:CUDA 11.8,容器应用:需要 CUDA 12.0

docker run --gpus all \
  -e CUDA_VERSION=12.0 \
  nvidia/cuda:12.0-runtime-ubuntu20.04 \
  # 可能因驱动版本不匹配导致运行失败

运行时差异导致的兼容性问题:
不同容器运行时对同一设备的处理方式存在显著差异:

# Kubernetes Pod 规范(使用 containerd + device plugin)
apiVersion: v1
kind: Pod
spec:
  containers:
  -
 name: gpu-container
    image: nvidia/cuda:11.0-base
    resources:
      limits:
        nvidia.com/gpu: 1  # 该资源键需由 NVIDIA Device Plugin 注册,Kubernetes 本身并未内置 GPU 资源类型。
# Podman 方式(需要 CDI 或 nvidia-container-toolkit)
podman run --security-opt=label=disable \
  --hooks-dir=/usr/share/containers/oci/hooks.d/ \
  --device nvidia.com/gpu=0 \
  nvidia/cuda:11.0-base

主要挑战:

  • • 驱动版本兼容性管理:确保不同容器运行的应用程序与宿主机驱动程序版本兼容,避免版本冲突
  • • 运行时库动态链接:容器内应用程序需要动态链接宿主机上的运行时库(如 libcuda.so、libnvidia-ml.so)
  • • 多 CUDA 版本共存:在同一宿主机上支持多个不同版本的 CUDA 应用程序

1.1.3 资源管理与调度挑战

GPU 资源分配与调度问题:
多个容器之间的 GPU 资源分配和调度需要精确的管理机制,传统方式缺乏标准化的设备生命周期管理:

// 传统方式:容器运行时需要手动处理 GPU 资源管理
// 注意:以下是伪代码,仅用于说明传统方式的局限性,不可直接运行

// 实际环境中,GPU 内存分配主要依赖于 GPU 厂商提供的工具或 Kubernetes 等上层调度机制

func manageGPUResource(containerID string, memoryMB int) error {
    // 伪代码:展示传统方式缺乏统一接口来进行 GPU 内存配额管理

    // 在实际环境中,无法通过简单命令直接控制 GPU 内存分配

    // 这里仅为说明传统方式在 GPU 内存管理上的局限性

    cmd := exec.Command("docker", "run", "--gpus", "1",
                       "nvidia/cuda:11.0-base"
, "nvidia-smi")
    if
 err := cmd.Run(); err != nil {
        return
 fmt.Errorf("failed to allocate GPU resource: %v", err)
    }

    // 资源回收问题:容器退出时可能无法可靠地回收资源

    defer
func() {
        cmd := exec.Command("docker", "stop", containerID)
        cmd.Run() // 依赖容器停止时自动释放资源,可能存在资源泄漏问题
    }()

    return
 nil
}

// 缺乏公平调度机制,容器间可能出现资源竞争

func scheduleGPUResources(containers []Container) error {
    // 简单的先到先得调度,缺乏公平性保证

    for
 _, container := range containers {
        if
 isGPUAvailable() {
            assignGPU(container.ID, 0) // 硬编码 GPU 0
        } else {
            return
 fmt.Errorf("no GPU available for container %s", container.ID)
        }
    }
    return
 nil
}

// 设备冲突检测需要手动实现,容易出错

func checkDeviceConflict(deviceID string, containers []string) error {
    for
 _, container := range containers {
        if
 isUsingDevice(container, deviceID) {
            return
 fmt.Errorf("device %s is already in use by container %s", deviceID, container)
        }
    }
    return
 nil
}

主要挑战:

  • • GPU 内存分配与回收:每个容器只能访问其分配的 GPU 内存,需要防止资源冲突和内存泄漏
  • • 计算资源公平调度:确保所有容器都能获得公平的 GPU 资源分配,避免资源浪费
  • • GPU 利用率监控:实时监控 GPU 利用率,动态调整资源分配以提高资源利用率

1.1.4 性能开销挑战

容器化引入的性能损失:
传统容器化方式可能引入显著的性能开销,特别是在 GPU 计算场景下:

// 传统方式:通过多层抽象访问 GPU,增加延迟
func accessGPUTraditional() {
    // 容器运行时 -> OCI Runtime -> runc -> 系统调用 -> GPU 驱动

    // 每一层都可能引入额外的延迟和开销


    // 内存拷贝开销:CPU <-> GPU 数据传输

    data := make([]float32, 1024*1024) // 4MB 数据

    // 传统方式需要多次内存拷贝

    hostBuffer := allocateHostMemory(len(data))
    copy
(hostBuffer, data)                    // 第一次拷贝:用户空间 -> 主机内存

    deviceBuffer := allocateGPUMemory(len(data))
    cudaMemcpy(deviceBuffer, hostBuffer)      // 第二次拷贝:主机内存 -> GPU 内存

    // 系统调用开销:频繁的内核态/用户态切换

    for
 i := 0; i < 1000; i++ {
        syscall.Syscall(SYS_IOCTL, gpuFd, cmd, uintptr(unsafe.Pointer(&args)))
    }
}

主要挑战:

容器抽象层开销:

  • • 容器运行时调用链:容器运行时 -> OCI Runtime -> runc -> 系统调用 -> GPU 驱动的多层调用链
  • • 系统调用转换:容器内系统调用需要转换为主机系统调用,增加延迟
  • • 运行时隔离机制:容器安全隔离机制引入的额外检查和验证

GPU 硬件层次固有限制:

  • • PCIe 总线带宽限制:GPU 与 CPU 之间的数据传输受 PCIe 总线带宽限制
  • • 内存拷贝开销:数据在 CPU 和 GPU 之间的必要拷贝操作
  • • 零拷贝机制缺失:缺乏直接访问 GPU 内存的零拷贝机制

1.1.5 供应商生态碎片化挑战

厂商锁定与维护成本:
每个硬件供应商需要为不同运行时维护专门的集成代码,导致严重的生态碎片化:

// NVIDIA Container Toolkit 的发展历程体现了这一挑战:
// nvidia-docker v1 (2016) -> nvidia-docker v2 (2018) -> NVIDIA Container Toolkit (2019-至今)

// 每个版本都需要重新适配不同的容器运行时


// 传统方式:每个供应商为每个运行时单独开发

type
 NVIDIADeviceManager struct {
    dockerImpl     *DockerNVIDIADevice     // nvidia-docker2
    containerdImpl *ContainerdNVIDIADevice // nvidia-container-runtime
    podmanImpl     *PodmanNVIDIADevice     // nvidia-podman-plugin
    crioImpl       *CRIONVIDIADevice       // nvidia-crio-hook
}

// Intel 设备的类似实现

type
 IntelDeviceManager struct {
    dockerImpl     *DockerIntelDevice
    podmanImpl     *PodmanIntelDevice
    containerdImpl *ContainerdIntelDevice
    k8sImpl        *K8sIntelDevicePlugin  // 需要额外的 Device Plugin
}

func (m *IntelDeviceManager) ConfigureDevice(runtime string, deviceID string) error {
    switch
 runtime {
    case
 "docker":
        return
 m.dockerImpl.Configure(deviceID)
    case
 "podman":
        return
 m.podmanImpl.Configure(deviceID)
    case
 "containerd":
        return
 m.containerdImpl.Configure(deviceID)
    case
 "kubernetes":
        return
 m.k8sImpl.Configure(deviceID) // 完全不同的实现方式
    default
:
        return
 fmt.Errorf("unsupported runtime: %s", runtime)
    }
}

// AMD GPU 的又一套实现

type
 AMDDeviceManager struct {
    rocmDockerImpl     *ROCmDockerDevice
    rocmContainerdImpl *ROCmContainerdDevice
    // 每个供应商都需要重复开发相似的功能

}

主要挑战:

  • • 开发成本高:每个硬件供应商需要为每个容器运行时单独开发和维护集成代码
  • • 用户体验差:不同供应商和运行时的使用方式差异巨大,学习成本高
  • • 生态碎片化:缺乏统一标准,导致容器生态系统的碎片化

1.1.6 传统方式问题总结

基于上述分析,传统容器设备访问面临的主要挑战可以总结如下:

问题类别
具体问题
技术影响
NVIDIA GPU 示例场景
设备隔离与访问控制
设备文件安全挂载
安全风险,配置复杂
需要手动挂载 /dev/nvidia0 等多个设备节点
权限精确控制
权限管理困难
GPU 设备文件的 chmod/chown 权限控制
多容器资源隔离
资源冲突风险
通过 cgroups 隔离 GPU 资源访问
驱动程序依赖与兼容性
驱动版本兼容性
版本冲突,运行失败
CUDA 11.8 vs CUDA 12.0 兼容性问题
运行时库动态链接
库文件管理复杂
libcuda.so
、libnvidia-ml.so 动态链接
多版本共存
环境管理困难
同一主机支持多个 CUDA 版本
资源管理与调度
GPU 内存分配回收
内存泄漏风险
GPU 内存的精确分配与清理
计算资源公平调度
资源利用率低
多容器间的 GPU 资源公平分配
设备冲突检测
资源竞争问题
多容器争用同一 GPU 设备
性能开销
GPU 设备访问延迟
性能损失
PCIe 访问延迟优化问题
内存拷贝开销
数据传输效率低
CPU ↔ GPU 数据传输的多次拷贝
系统调用优化
启动延迟高
容器启动时的频繁系统调用
供应商生态碎片化
开发成本高
重复开发工作
NVIDIA 需为 Docker/Podman/containerd 分别开发
用户体验差
学习成本高
不同运行时的 GPU 使用方式差异巨大
生态碎片化
标准化缺失
缺乏统一的设备访问标准

1.2 现有解决方案的局限性

为了解决上述挑战,业界出现了多种解决方案,但都存在一定的局限性:

1.2.1 供应商特定解决方案

NVIDIA Container Toolkit:

  • • 专门针对 NVIDIA GPU 设计,无法支持其他厂商设备
  • • 需要为每个容器运行时单独适配

详细请参考:NVIDIA Container Toolkit 原理分析与代码深度解析

Intel GPU Device Plugin:

  • • 仅支持 Kubernetes 环境
  • • 与其他容器运行时兼容性差
  • • 缺乏统一的配置标准

1.2.2 Kubernetes Device Plugin 机制

详细请参考:Nvidia K8s Device Plugin 原理解析和源码分析

优势:

  • • 提供了相对标准化的设备管理接口
  • • 支持资源调度和分配

局限性:

  • • 仅限于 Kubernetes 环境,无法在其他容器运行时中使用
  • • 开发复杂度高,需要实现 gRPC 接口
  • • 缺乏设备生命周期的精细化管理

1.3 行业标准化需求

基于上述分析,容器设备访问领域迫切需要一个标准化解决方案,这正是 Container Device Interface (CDI) 诞生的背景和意义。


2. CDI 解决方案概述

2.1 CDI 的解决方案

Container Device Interface (CDI) 是一个用于容器运行时的标准化规范,专门设计用来解决传统容器设备访问面临的挑战。以 NVIDIA Container Toolkit 的 CDI 支持 为例,CDI 通过以下方式有效解决了上述问题:

统一设备描述标准:

  • • 通过 JSON/YAML 格式的设备规范文件,标准化描述 GPU 等复杂硬件设备
  • • 消除了不同供应商和运行时之间的配置差异
  • • 提供版本化的设备描述,确保向后兼容性

运行时无关的设备管理:

  • • CDI 为 Docker、Podman、containerd、CRI-O 等运行时提供统一接口
  • • NVIDIA Container Toolkit 通过 CDI 实现一次开发,多运行时支持
  • • 简化了 Kubernetes 等编排系统的设备集成

智能设备生命周期管理:

  • • 自动化的设备注入和清理机制
  • • 内置的设备冲突检测和资源管理
  • • 标准化的设备权限和安全策略

2.2 CDI 的核心理念

CDI 引入了设备作为资源的抽象概念。每个设备都通过一个完全限定名称(Fully Qualified Name)来唯一标识,该名称由以下部分构成:

vendor.com/class=unique_name

其中:

  • • vendor.com/class 被称为设备种类(device kind)
  • • unique_name 是在特定供应商 ID 和设备类别组合下的唯一名称

2.3 CDI 的核心设计理念

CDI 通过引入标准化的设备描述和注入机制,从根本上解决了传统容器设备访问方式的局限性。其核心设计理念包括:

  • • 设备抽象与标准化:将各类硬件设备抽象为统一的资源模型
  • • 运行时中立性:设计独立于特定容器运行时的接口规范
  • • 完整生命周期管理:覆盖设备的全生命周期
  • • 可扩展的插件架构:支持供应商自定义的设备处理逻辑
  • • 安全与隔离保障:提供细粒度的权限控制和设备隔离机制

2.4 CDI 与传统方案的对比分析

CDI 相比传统方案在以下关键方面具有显著优势:

对比维度
传统方案
CDI 方案
改进效果
设备描述
手动配置设备节点和挂载
统一的 JSON/YAML 配置格式
显著降低配置错误率
运行时支持
每个运行时单独适配
统一接口,一次开发
大幅降低开发成本和维护工作量
设备管理
手动资源管理和清理
自动化生命周期管理
有效减少资源泄漏问题
冲突检测
手动实现冲突检测
内置冲突检测机制
明显降低隔离失败率
性能优化
多层抽象,性能损失
直接设备访问优化
设备发现过程更高效
容器启动流程更加简化
并发处理机制更加优化
安全性
粗粒度控制
细粒度权限控制
有效降低安全风险
生态兼容性
每个供应商重复开发
标准化接口,统一开发
大幅减少兼容性问题
显著降低供应商开发成本
明显缩短新手上手时间

2.5 CDI 的安全性与可扩展性

2.5.1 安全性设计

CDI 在安全性方面提供了多层次的保障机制:

  • • 细粒度权限控制:通过 deviceNodes 配置,可以精确控制容器对设备的访问权限,包括读写权限和所有权
  • • SELinux/AppArmor 集成:支持与主流 Linux 安全模块集成,提供额外的安全隔离层
  • • 最小权限原则:容器只能访问显式声明的设备,避免权限过度问题
  • • 安全的设备发现机制:规范文件扫描过程中进行严格的权限检查,防止恶意规范注入
// 细粒度权限控制示例
"deviceNodes": [
  {
    "path": "/dev/nvidia0",
    "type": "c",
    "major": 195,
    "minor": 0,
    "permissions": "rw",
    "uid": 1000,
    "gid": 1000
  }
]

2.5.2 可扩展性设计

CDI 的可扩展性体现在多个方面:

  • • 多设备类型支持:除 GPU 外,CDI 已支持 FPGA、RDMA、AI 加速卡等多种设备类型
  • • 供应商扩展机制:通过 annotations 字段,供应商可以添加自定义元数据和配置
  • • 插件化架构:支持通过插件扩展 CDI 的功能,如自定义设备发现和资源管理逻辑
  • • 版本兼容性:严格的版本控制确保向后兼容性,同时允许规范演进

Intel FPGA 设备示例:

以下配置可放置于 /etc/cdi/intel-fpga.yaml 文件中,容器运行时将自动发现并加载该配置:

{
  "cdiVersion": "0.8.0",
  "kind": "intel.com/fpga",
  "devices": [
    {
      "name": "arria10",
      "annotations": {
        "intel.com/fpga.family": "arria10",
        "intel.com/fpga.interface": "opae"
      },
      "containerEdits": {
        "deviceNodes": [
          {
            "path": "/dev/intel-fpga-port.0",
            "type": "c"
          }
        ],
        "mounts": [
          {
            "hostPath": "/opt/intel/fpga/bitstreams",
            "containerPath": "/bitstreams"
          }
        ]
      }
    }
  ]
}

2.6 CDI 快速上手指南

以下是一个快速上手 CDI 的实操指南,帮助读者快速体验 CDI 的功能:

2.6.1 环境准备

# 安装 NVIDIA Container Toolkit(以 Ubuntu 为例)
sudo
 apt-get update
sudo
 apt-get install -y nvidia-container-toolkit

# 配置 Docker 支持 NVIDIA GPU

sudo
 nvidia-ctk runtime configure --runtime=docker
sudo
 systemctl restart docker

2.6.2 生成并验证 CDI 规范

# 生成 CDI 规范文件
sudo
 nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml

# 注意:根据环境配置,CDI 文件可能生成在 /etc/cdi/ 或 /var/run/cdi/ 目录

# 可以通过以下命令确认文件位置

find /etc/cdi /var/run/cdi -name "*.yaml" 2>/dev/null

# 查看生成的 CDI 设备列表

nvidia-ctk cdi list
# 输出示例:nvidia.com/gpu=0, nvidia.com/gpu=1, nvidia.com/gpu=all

2.6.3 使用 Docker/Podman 运行 CDI 容器

# 使用 Docker 运行带 CDI 设备的容器
docker run --rm --device nvidia.com/gpu=0 nvidia/cuda:12.0-base nvidia-smi

# 使用 Podman 运行带 CDI 设备的容器

podman run --rm --device nvidia.com/gpu=0 nvidia/cuda:12.0-base nvidia-smi

2.6.4 在 Kubernetes 中使用 CDI

创建一个使用 CDI 注解的 Pod:

注意:CDI 注解方式是 Kubernetes 的实验性机制,目前尚未完全稳定。在生产环境中,请根据集群配置选择合适的设备分配方式。

apiVersion: v1
kind: Pod
metadata:
  name: cdi-gpu-pod
  annotations:
    # 使用 CDI 注解指定设备(实验性功能)

    cdi.k8s.io/nvidia-gpu: "nvidia.com/gpu=0"
spec:
  containers:
  -
 name: cuda-container
    image: nvidia/cuda:12.0-base
    command: ["nvidia-smi", "-L"]
    resources:
      limits:
        # 传统 GPU 分配方式,依赖 Device Plugin 注册

        nvidia.com/gpu: 1

说明:上述示例同时使用了两种设备分配机制:

  1. 1. CDI 注解方式(cdi.k8s.io/nvidia-gpu):Kubernetes 新增的实验性机制,直接通过 CDI 规范分配设备
  2. 2. 传统资源限制方式(resources.limits.nvidia.com/gpu):依赖 Device Plugin 注册的资源分配方式

在实际生产环境中,可能只会启用其中一种方式,具体取决于集群配置和稳定性需求。

应用 Pod 配置:

kubectl apply -f cdi-gpu-pod.yaml
kubectl logs cdi-gpu-pod

通过这个快速上手指南,读者可以在几分钟内体验 CDI 的基本功能,了解其在不同容器运行时中的使用方式。


3. CDI 规范详解

3.1 CDI 规范文件结构

CDI 规范文件采用 JSON 或 YAML 格式,包含以下主要字段:

{
    "cdiVersion": "0.8.0",
    "kind": "vendor.com/class",
    "annotations": {
        "key": "value"
    },
    "devices": [
        {
            "name": "device-name",
            "annotations": {
                "key": "value"
            },
            "containerEdits": {
                // 设备特定的容器修改

            }
        }
    ],
    "containerEdits": {
        // 全局容器修改

    }
}

3.2 CDI 规范文件发现机制

CDI 使用预定义的目录结构来发现和加载规范文件:

3.2.1 默认目录配置

// 默认静态目录 - 用于手动配置的 CDI 规范
DefaultStaticDir = "/etc/cdi"

// 默认动态目录 - 用于自动生成的 CDI 规范

DefaultDynamicDir = "/var/run/cdi"

// 默认目录优先级配置

DefaultSpecDirs = []string{DefaultStaticDir, DefaultDynamicDir}

3.2.2 规范文件扫描过程

CDI 通过 scanSpecDirs 函数扫描指定目录:

func scanSpecDirs(dirs []string, scanFn scanSpecFunc) error {
    for
 priority, dir := range dirs {
        err = filepath.Walk(dir,func(path string, info os.FileInfo, err error) error {
            // 跳过子目录

            if
 info.IsDir() {
                if
 path == dir {
                    return
 nil
                }
                return
 filepath.SkipDir
            }

            // 只处理 .json 和 .yaml 文件

            if
 ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" {
                return
 nil
            }

            // 加载并处理规范文件

            spec, err := ReadSpec(path, priority)
            return
 scanFn(path, priority, spec, err)
        })
    }
    return
 nil
}

3.3 当前版本信息

当前 CDI 规范版本为 0.8.0。

NVIDIA Container Toolkit 从 v1.12.0 版本开始支持生成 CDI 规范,最新版本为 v1.17.5。规范采用语义化版本控制,任何对规范的修改都会导致至少一个次版本号的增加。

3.4 版本发布历史

以下版本历史信息来源于 CDI 官方 GitHub 仓库 container-orchestrated-devices/container-device-interface:

版本
主要变更
v0.3.0
规范的初始标记发布
v0.4.0
为 Mount 规范添加 type 字段
v0.5.0
为 DeviceNodes 添加 HostPath;允许设备名称以数字开头
v0.6.0
为 Spec 和 Device 规范添加 Annotations 字段;允许在 Kind 字段的名称段中使用点号
v0.7.0
添加 IntelRdt 字段;为 ContainerEdits 添加 AdditionalGIDs
v0.8.0
从 specs-go 包中移除 .ToOCI() 函数

3.5 CDI JSON 规范结构

CDI 规范使用 JSON 格式定义,主要包含以下字段:

{
    "cdiVersion": "0.8.0",
    "kind": "<vendor.com/device-class>",
    "annotations": {
        "key": "value"
    },
    "devices": [
        {
            "name": "<device-name>",
            "annotations": {
                "key": "value"
            },
            "containerEdits": { ... }
        }
    ],
    "containerEdits": {
        "env": ["<envName>=<envValue>"],
        "deviceNodes": [...],
        "mounts": [...],
        "hooks": [...],
        "additionalGIDs": [...],
        "intelRdt": {...}
    }
}

3.5.1 必需字段说明

  • • cdiVersion(字符串,必需):必须符合语义化版本 2.0 规范,指定供应商使用的 CDI 规范版本
  • • kind(字符串,必需):指定唯一标识设备供应商的标签
  • • devices(对象数组,必需):供应商提供的设备列表

3.5.2 Kind 字段规则

kind 标签包含两个部分:前缀和名称,用斜杠(/)分隔:

  • • 名称段:必需,最多 63 个字符,以字母数字字符开头和结尾,中间可包含破折号、下划线、点号和字母数字字符
  • • 前缀:必须是 DNS 子域,由点号分隔的 DNS 标签系列,总长度不超过 253 个字符

有效示例:vendor.com/foo、foo.bar.baz/foo-bar123.B_az
无效示例:foo、vendor.com/foo/、vendor.com/foo/bar

3.6 容器编辑(Container Edits)

containerEdits 字段描述对 OCI 规范的编辑操作,支持以下类型的编辑:

3.6.1 环境变量(env)

"env": ["VARNAME=VARVALUE"]

3.6.2 设备节点(deviceNodes)

"deviceNodes": [
    {
        "path": "/dev/card1",
        "hostPath": "/vendor/dev/card1",
        "type": "c",
        "major": 25,
        "minor": 25,
        "fileMode": 384,
        "permissions": "rw",
        "uid": 1000,
        "gid": 1000
    }
]

3.6.3 挂载点(mounts)

"mounts": [
    {
        "hostPath": "/usr/lib/libVendor.so.0",
        "containerPath": "/usr/lib/libVendor.so.0",
        "type": "bind",
        "options": ["ro"]
    }
]

3.6.4 钩子(hooks)

"hooks": [
    {
        "hookName": "createContainer",
        "path": "/bin/vendor-hook",
        "args": ["arg1", "arg2"],
        "env": ["ENV_VAR=value"],
        "timeout": 30
    }
]

4. CDI 工作原理

4.1 整体流程图

CDI 与容器运行时的交互涉及多个组件和步骤,下图展示了完整的工作流程:

┌──────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   容器运行时       │    │   CDI 缓存      │    │   CDI 规范文件    │
│  (Docker/Podman) │    │                 │    │   (/etc/cdi)    │
└──────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         │ 1. 启动容器请求         │                       │
         │ --device nvidia.com/gpu=0                     │
         ├──────────────────────→│                       │
         │                       │ 2. 扫描规范文件         │
         │                       ├──────────────────────→│
         │                       │                       │
         │                       │ 3. 加载规范内容         │
         │                       │←──────────────────────┤
         │                       │                       │
         │ 4. 返回设备配置         │                       │
         │←──────────────────────┤                       │
         │                       │                       │
         │ 5. 注入设备到容器       │                       │
         │                       │                       │

CDI 与容器运行时交互完整流程图:

阶段 1: 设备供应商准备
┌─────────────────────┐    安装    ┌──────────────────────┐
│    设备供应商         │ ────────► │   CDI 规范文件        │
│                     │           │                      │
│ • 创建 CDI 规范文件   │           │ • /etc/cdi/*.json    │
│ • 定义设备配置        │           │ • /var/run/cdi/*.yaml│
└─────────────────────┘           └──────────────────────┘
                                            │
                                            │ 读取
                                            ▼

阶段 2: 容器运行时初始化
┌─────────────────────┐  初始化  ┌─────────────────────┐  扫描   ┌─────────────────────┐
│    容器运行时         │ ──────► │  CDI 缓存初始化      │ ─────► │     设备发现          │
│                     │         │                     │        │                     │
│ • containerd/CRI-O  │         │ • 扫描规范目录        │        │ • 解析规范文件        │
│ • 启动时初始化        │         │ • 加载所有规范        │        │ • 构建设备索引        │
└─────────────────────┘         └─────────────────────┘        └─────────────────────┘
         │                                │                              │
         │ 接收请求                        │ 提供设备信息                   │
         ▼                                ▼                              │
                                                                         │
阶段 3: 容器请求处理                                                        │
┌─────────────────────┐ 请求设备 ┌─────────────────────┐                  │
│   容器创建请求        │ ──────► │   设备注入处理        │ ◄────────────────┘
│                     │         │                     │
│ • --device          │         │ • InjectDevices()   │
│   nvidia.com/gpu=0  │         │ • 查找匹配设备        │
│ • 指定所需设备        │         │                     │
└─────────────────────┘         └─────────────────────┘
                                          │
                                          │ 生成修改
                                          ▼

阶段 4: OCI 规范修改
┌─────────────────────┐ 启动容器 ┌──────────────────────┐
│   OCI 规范修改       │ ──────► │     容器启动          │
│                     │         │                     │
│ • ContainerEdits.   │         │ • 使用修改后的        │
│   Apply()           │         │   OCI 规范           │
│ • 应用设备配置        │         │                     │
└─────────────────────┘         └─────────────────────┘
         │
         │ 详细操作
         ▼

ContainerEdits 详细操作
╔═══════════════════════════════════════════════════════════════════════════════╗
║                          ContainerEdits 包含的修改操作                          ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  • 环境变量 (Env):                                                             ║
║    └─ 设置 CUDA_VISIBLE_DEVICES 等环境变量                                      ║
║                                                                               ║
║  • 设备节点 (DeviceNodes):                                                     ║
║    └─ 挂载 /dev/nvidia0 等设备文件到容器                                         ║
║                                                                               ║
║  • 挂载点 (Mounts):                                                            ║
║    └─ 挂载驱动库和工具目录到容器文件系统                                            ║
║                                                                               ║
║  • 生命周期钩子 (Hooks):                                                        ║
║    └─ 容器启动前后执行的自定义脚本                                                 ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

从图中可以看出,CDI 的工作流程主要包括以下四个阶段:

  1. 1. 阶段 1: 设备供应商准备:设备供应商创建 CDI 规范文件并安装到系统指定目录(如 /etc/cdi/ 和 /var/run/cdi/)
  2. 2. 阶段 2: 容器运行时初始化:容器运行时启动时初始化 CDI 缓存,扫描规范目录,解析规范文件并构建设备索引
  3. 3. 阶段 3: 容器请求处理:当用户请求创建容器并指定设备时,CDI 进行设备注入处理,查找匹配的设备配置
  4. 4. 阶段 4: OCI 规范修改:通过 ContainerEdits.Apply() 方法修改 OCI 规范,应用设备配置,最终启动容器

每个阶段都有明确的职责分工,确保了 CDI 系统的模块化和可维护性。底部的详细操作框展示了 ContainerEdits 包含的具体修改类型,包括环境变量、设备节点、挂载点和生命周期钩子等。

4.2 详细工作流程

CDI 的详细工作流程可以分为两个主要阶段:

4.2.1 设备安装阶段

  1. 1. 设备驱动安装:用户在机器上安装第三方设备驱动程序和设备
  2. 2. CDI 规范创建:设备驱动程序安装软件在已知路径(如 /etc/cdi/vendor.json)写入 JSON 格式的 CDI 规范文件
  3. 3. 规范文件验证:系统验证 CDI 规范文件的格式和内容是否符合规范要求

4.2.2 容器运行时阶段

  1. 1. 设备请求:用户使用 --device 参数运行容器,指定所需的设备名称(如 nvidia.com/gpu=0)
  2. 2. 规范文件读取:容器运行时扫描并读取 CDI 规范目录中的 JSON/YAML 文件
  3. 3. 设备验证:容器运行时验证请求的设备在 CDI 规范文件中是否有相应的描述
  4. 4. 镜像拉取:容器运行时拉取容器镜像(与设备配置并行进行)
  5. 5. OCI 规范生成:容器运行时生成初始的 OCI 运行时规范
  6. 6. 设备注入:根据 CDI 规范文件中的 containerEdits 指令修改 OCI 规范,注入设备相关配置
  7. 7. 容器启动:使用修改后的 OCI 规范启动容器,确保设备正确挂载和配置

4.3 CDI 缓存机制

CDI 实现了一个高效的缓存系统来管理规范文件和设备信息:

4.3.1 缓存结构

// 必要的导入包
import
 (
    "encoding/json"

    "fmt"

    "os"

    "path/filepath"

    "strings"

    "sync"

    "time"

    "log"


    "github.com/fsnotify/fsnotify"

    oci "github.com/opencontainers/runtime-spec/specs-go"
    "sigs.k8s.io/yaml"

    cdi "tags.cncf.io/container-device-interface/specs-go"
)

// Cache 结构体定义

type
 Cache struct {
    sync.Mutex
    specDirs  []string              // CDI 规范目录列表
    specs     map[string][]*Spec    // 按供应商分组的规范
    devices   map[string]*Device    // 设备名称到设备对象的映射
    errors    map[string][]error    // 规范文件错误记录
    dirErrors map[string]error      // 目录访问错误

    autoRefresh bool                // 自动刷新开关
    watch       *watch              // 文件系统监控
}

4.3.2 缓存刷新机制

func (c *Cache) refresh() error {
    var
 (
        specs      = map[string][]*Spec{}
        devices    = map[string]*Device{}
        conflicts  = map[string]struct{}{}
        specErrors = map[string][]error{}
    )

    // 收集每个规范文件的错误

    collectError :=func(err error, paths ...string) {
        for
 _, path := range paths {
            specErrors[path] = append(specErrors[path], err)
        }
    }

    // 基于设备规范优先级解决冲突

    resolveConflict :=func(name string, dev *Device, old *Device) bool {
        devSpec, oldSpec := dev.GetSpec(), old.GetSpec()
        devPrio, oldPrio := devSpec.GetPriority(), oldSpec.GetPriority()
        switch
 {
        case
 devPrio > oldPrio:
            return
 false
        case
 devPrio == oldPrio:
            devPath, oldPath := devSpec.GetPath(), oldSpec.GetPath()
            collectError(fmt.Errorf("conflicting device %q (specs %q, %q)",
                name, devPath, oldPath), devPath, oldPath)
            conflicts[name] = struct{}{}
        }
        return
 true
    }

    // 扫描所有 CDI 目录

    _ = scanSpecDirs(c.specDirs,func(path string, priority int, spec *Spec, err error) error {
        path = filepath.Clean(path)
        if
 err != nil {
            collectError(fmt.Errorf("failed to load CDI Spec %w", err), path)
            return
 nil
        }

        vendor := spec.GetVendor()
        specs[vendor] = append(specs[vendor], spec)

        // 处理设备冲突

        for
 _, dev := range spec.devices {
            qualified := dev.GetQualifiedName()
            other, ok := devices[qualified]
            if
 ok {
                if
 resolveConflict(qualified, dev, other) {
                    continue

                }
            }
            devices[qualified] = dev
        }

        return
 nil
    })

    // 删除冲突的设备

    for
 conflict := range conflicts {
        delete
(devices, conflict)
    }

    c.specs = specs
    c.devices = devices
    c.errors = specErrors

    return
 nil
}

4.3.3 自动监控机制

CDI 支持文件系统监控,当规范文件发生变化时自动刷新缓存:

type watch struct {
    watcher *fsnotify.Watcher
    tracked map[string]bool
}

func (w *watch) watch(fsw *fsnotify.Watcher, m *sync.Mutex, refreshfunc() error, dirErrors map[string]error) {
    for
 {
        select
 {
        case
 event, ok := <-fsw.Events:
            if
 !ok {
                return

            }
            // 检测到文件变化,触发缓存刷新

            m.Lock()
            refresh()
            m.Unlock()

        case
 err, ok := <-fsw.Errors:
            if
 !ok {
                return

            }
            // 处理监控错误

        }
    }
}

4.4 设备发现和加载机制

CDI 系统通过扫描指定目录来发现和加载设备规范:

4.4.1 规范文件扫描

func (c *Cache) scanSpecDirs() (map[string]*Spec, error) {
    specs := make(map[string]*Spec)

    for
 _, dir := range c.specDirs {
        files, err := filepath.Glob(filepath.Join(dir, "*.json"))
        if
 err != nil {
            continue

        }

        yamlFiles, err := filepath.Glob(filepath.Join(dir, "*.yaml"))
        if
 err == nil {
            files = append(files, yamlFiles...)
        }

        for
 _, file := range files {
            spec, err := ReadSpec(file, priority)
            if
 err != nil {
                continue

            }
            specs[spec.GetQualifiedName()] = spec
        }
    }

    return
 specs, nil
}

4.4.2 规范文件解析

// ReadSpec 读取指定的 CDI 规范文件
// path: 规范文件路径

// priority: 规范优先级

func ReadSpec(path string, priority int) (*Spec, error) {
    data, err := os.ReadFile(path)
    switch
 {
    case
 os.IsNotExist(err):
        return
 nil, err
    case
 err != nil:
        return
 nil, fmt.Errorf("failed to read CDI Spec %q: %w", path, err)
    }

    raw, err := ParseSpec(data)
    if
 err != nil {
        return
 nil, fmt.Errorf("failed to parse CDI Spec %q: %w", path, err)
    }
    if
 raw == nil {
        return
 nil, fmt.Errorf("failed to parse CDI Spec %q, no Spec data", path)
    }

    spec, err := newSpec(raw, path, priority)
    if
 err != nil {
        return
 nil, err
    }

    return
 spec, nil
}

// ParseSpec 解析 CDI 规范数据,支持 JSON 和 YAML 格式

func ParseSpec(data []byte) (*cdi.Spec, error) {
    var
 spec cdi.Spec
    var
 err error

    // 尝试解析为 JSON

    if
 err = json.Unmarshal(data, &spec); err != nil {
        // 如果 JSON 解析失败,尝试 YAML

        err = yaml.Unmarshal(data, &spec)
    }

    return
 &spec, err
}

4.5 规范验证机制

CDI 实现了严格的规范验证:

func validateSpec(raw *cdi.Spec) error {
    validatorLock.RLock()
    defer
 validatorLock.RUnlock()

    if
 specValidator == nil {
        return
 nil
    }
    err := specValidator.Validate(raw)
    if
 err != nil {
        return
 fmt.Errorf("Spec validation failed: %w", err)
    }
    return
 nil
}

4.5.1 目录优先级和文件发现

CDI 使用标准化的目录结构和优先级机制:

  • • 默认目录:/etc/cdi 和 /var/run/cdi
  • • 文件格式:支持 .json 和 .yaml 格式
  • • 自动发现:容器运行时启动时自动扫描这些目录
  • • 缓存机制:提高设备查找和注入的性能
  • • 文件监控:支持规范文件的动态更新
// 默认 CDI 目录配置
var
 (
    DefaultSpecDirs = []string{
        "/etc/cdi"
,      // 系统级配置,优先级最高
        "/var/run/cdi"
,  // 运行时配置,优先级较低
    }
)

// 获取 CDI 规范目录

func GetSpecDirs() []string {
    specDirs := os.Getenv("CDI_SPEC_DIRS")
    if
 specDirs != "" {
        return
 strings.Split(specDirs, string(os.PathListSeparator))
    }
    return
 DefaultSpecDirs
}

4.6 CDI 性能优化机制

CDI 通过多种优化策略来提升设备管理性能:

4.6.1 缓存优化策略

  • • 多层缓存架构:规范缓存、设备索引、LRU 缓存
  • • 智能缓存策略:避免重复解析,提供快速设备查找
  • • 缓存统计监控:实时监控缓存命中率和性能指标
// 高效的设备查找
func (c *Cache) GetDevice(name string) *Device {
    // 检查 LRU 缓存

    if
 device, ok := c.lruCache.Get(name); ok {
        return
 device.(*Device)
    }

    // 检查设备索引

    if
 device, ok := c.deviceIndex[name]; ok {
        c.lruCache.Add(name, device)
        return
 device
    }

    return
 nil
}

4.6.2 并发处理优化

  • • 工作协程池:使用协程池并发处理规范文件加载
  • • 读写锁机制:保证并发访问的安全性
  • • 异步文件监控:实时监控规范文件变化

4.6.3 内存和 I/O 优化

  • • 对象池技术:重用设备和规范对象,减少内存分配
  • • 批量文件读取:批量处理规范文件,减少 I/O 操作
  • • 内存监控:实时监控内存使用情况,及时触发垃圾回收
  • • 异步文件监控:使用缓冲通道避免阻塞

4.6.4 性能监控和调优

  • • 性能指标收集:监控设备查找时间、缓存命中率、内存使用等关键指标
  • • 自动调优机制:根据性能指标自动调整缓存大小和触发垃圾回收
  • • 实时监控:提供实时的性能监控和告警机制
// CDI 性能监控示例
type
 PerformanceMetrics struct {
    CacheHitRate    float64
    DeviceLookupTime time.Duration
    MemoryUsage     uint64
    RefreshCount    int64
}

// 收集性能指标

func (c *Cache) GetMetrics() *PerformanceMetrics {
    return
 &PerformanceMetrics{
        CacheHitRate:    c.calculateHitRate(),
        DeviceLookupTime: c.avgLookupTime,
        MemoryUsage:     c.getMemoryUsage(),
        RefreshCount:    atomic.LoadInt64(&c.refreshCount),
    }
}

5. 容器运行时集成

5.1 NVIDIA Container Toolkit 中的 CDI 支持

NVIDIA Container Toolkit 支持生成和管理 CDI 规范。CDI 规范可以通过以下方式生成:

# 生成 CDI 规范
nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml

# 列出可用的 CDI 设备

nvidia-ctk cdi list

CDI 与 Multi-Instance GPU (MIG) 完全兼容,支持 MIG 设备的自动发现和配置。

5.2 CDI 缓存配置

CDI 缓存是容器运行时与 CDI 规范交互的核心组件。以下是 CDI 缓存的配置和使用示例:

package main

import
 (
    "github.com/container-device-interface/cdi/pkg/cdi"

)

func main() {
    // 创建 CDI 缓存

    cache := cdi.GetRegistry(
        cdi.WithAutoRefresh(true),
    )

    // 配置缓存

    err := cache.Configure(
        cdi.WithSpecDirs("/etc/cdi", "/var/run/cdi"),
    )
    if
 err != nil {
        panic
(err)
    }

    // 刷新缓存

    err = cache.Refresh()
    if
 err != nil {
        panic
(err)
    }
}

5.3 缓存刷新机制

// 缓存刷新函数
func (c *Cache) refresh() error {
    var
 (
        specs      = map[string][]*Spec{}
        devices    = map[string]*Device{}
        conflicts  = map[string]struct{}{}
        specErrors = map[string][]error{}
    )

    // 扫描所有 CDI 目录

    _ = scanSpecDirs(c.specDirs,func(path string, priority int, spec *Spec, err error) error {
        if
 err != nil {
            specErrors[path] = append(specErrors[path], err)
            return
 nil
        }

        vendor := spec.GetVendor()
        specs[vendor] = append(specs[vendor], spec)

        // 处理设备冲突

        for
 _, dev := range spec.devices {
            qualified := dev.GetQualifiedName()
            devices[qualified] = dev
        }
        return
 nil
    })

    c.specs = specs
    c.devices = devices
    c.errors = specErrors
    return
 nil
}

5.4 NVIDIA GPU CDI 实际使用案例

5.4.1 基础 GPU 访问

# 生成 NVIDIA CDI 规范
nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml

# 查看生成的 CDI 设备

nvidia-ctk cdi list
# 输出示例:

# nvidia.com/gpu=0

# nvidia.com/gpu=1

# nvidia.com/gpu=all

使用 CDI 设备运行容器:

# 使用单个 GPU
podman run --rm --device nvidia.com/gpu=0 nvidia/cuda:12.0-base-ubuntu20.04 nvidia-smi

# 使用所有 GPU

podman run --rm --device nvidia.com/gpu=all nvidia/cuda:12.0-base-ubuntu20.04 nvidia-smi

5.4.2 MIG (Multi-Instance GPU) 支持

# 启用 MIG 模式
nvidia-smi -mig 1

# 创建 MIG 实例

nvidia-smi mig -cgi 1g.5gb,2g.10gb -C

# 重新生成 CDI 规范以包含 MIG 设备

nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml

# 查看 MIG 设备

nvidia-ctk cdi list | grep mig
# 输出示例:

# nvidia.com/gpu=0:0

# nvidia.com/gpu=0:1

使用 MIG 设备:

# 使用特定的 MIG 实例
podman run --rm --device nvidia.com/gpu=0:0 nvidia/cuda:12.0-base-ubuntu20.04 nvidia-smi

5.4.3 GPU 共享和资源限制

{
  "cdiVersion": "0.8.0",
  "kind": "nvidia.com/gpu",
  "devices": [
    {
      "name": "shared-gpu-0",
      "annotations": {
        "nvidia.com/gpu.memory": "4096",
        "nvidia.com/gpu.compute-capability": "8.6"
      },
      "containerEdits": {
        "env": [
          "NVIDIA_VISIBLE_DEVICES=0"
,
          "NVIDIA_MPS_ACTIVE_THREAD_PERCENTAGE=50"

        ],
        "deviceNodes": [
          {
            "path": "/dev/nvidia0",
            "type": "c",
            "major": 195,
            "minor": 0
          }
        ]
      }
    }
  ]
}

5.4.4 Kubernetes 集成示例

在 Kubernetes 环境中,CDI 可以通过以下步骤与 Pod 集成:

5.4.4.1 步骤 1: 生成 CDI 规范文件

首先,在 Kubernetes 节点上生成 CDI 规范文件:

# 在每个 GPU 节点上执行
# 生成 CDI 规范文件到标准位置

sudo
 nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml

# 验证生成的 CDI 设备

nvidia-ctk cdi list
# 输出示例:

# nvidia.com/gpu=0

# nvidia.com/gpu=1

# nvidia.com/gpu=all
5.4.4.2 步骤 2: 选择 CDI 集成方式

Kubernetes 中有两种主要的 CDI 集成方式:

  1. 1. 使用 CDI 注解方式:通过 Pod 的 annotations 字段指定 CDI 设备
    apiVersion: v1
    kind: Pod
    metadata:
    name: gpu-workload-annotations
    annotations:
        # 使用 CDI 注解指定设备

        cdi.k8s.io/nvidia-gpu: "nvidia.com/gpu=0"
    spec:
    containers:
    -
     name: cuda-container
        image: nvidia/cuda:12.0-base-ubuntu20.04
        command: ["nvidia-smi"]
        resources:
        limits:
            # 仍然需要声明资源限制以触发调度

            # 注意:该资源键需由 NVIDIA Device Plugin 注册,Kubernetes 本身并未内置 GPU 资源类型

            nvidia.com/gpu: 1
  2. 2. 使用 Device Plugin 与 CDI 结合方式:通过 NVIDIA Device Plugin 与 CDI 结合使用
    # 首先部署支持 CDI 的 NVIDIA Device Plugin
    apiVersion: apps/v1
    kind: DaemonSet
    metadata:
    name: nvidia-device-plugin-daemonset
    namespace: kube-system
    spec:
    selector:
        matchLabels:
        name: nvidia-device-plugin-ds
    template:
        metadata:
        labels:
            name: nvidia-device-plugin-ds
        spec:
        containers:
        -
     name: nvidia-device-plugin-ctr
            image: nvcr.io/nvidia/k8s-device-plugin:v0.14.0
            args: ["--use-cdi=true"]
            volumeMounts:
            -
     name: device-plugin
                mountPath: /var/lib/kubelet/device-plugins
            -
     name: cdi-spec
                mountPath: /etc/cdi
        volumes:
            -
     name: device-plugin
            hostPath:
                path: /var/lib/kubelet/device-plugins
            -
     name: cdi-spec
            hostPath:
                path: /etc/cdi

然后创建使用 CDI 设备的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: gpu-workload-device-plugin
spec:
  containers:
  -
 name: cuda-container
    image: nvidia/cuda:12.0-base-ubuntu20.04
    command: ["nvidia-smi"]
    resources:
      limits:
        # Device Plugin 会自动将此资源映射到对应的 CDI 设备

        # 注意:该资源键需由 NVIDIA Device Plugin 注册,Kubernetes 本身并未内置 GPU 资源类型

        nvidia.com/gpu: 1
5.4.4.3 步骤 3: 高级 CDI 配置示例

使用 MIG 设备和资源限制:

# 在节点上启用 MIG 模式
sudo
 nvidia-smi -mig 1

# 创建 MIG 实例

sudo
 nvidia-smi mig -cgi 1g.5gb -C

# 重新生成包含 MIG 设备的 CDI 规范

sudo
 nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
apiVersion: v1
kind: Pod
metadata:
  name: mig-workload
  annotations:
    # 指定使用 MIG 实例

    cdi.k8s.io/nvidia-mig: "nvidia.com/gpu=0:0"
    # 设置 GPU 内存限制

    cdi.k8s.io/nvidia-gpu.memory: "4Gi"
    # 设置计算能力要求

    cdi.k8s.io/nvidia-gpu.compute: "8.6"
spec:
  containers:
  -
 name: cuda-container
    image: nvidia/cuda:12.0-base-ubuntu20.04
    command: ["nvidia-smi"]
    resources:
      limits:
        nvidia.com/mig-1g.5gb: 1

上述示例展示了如何在 Kubernetes Pod 中使用 MIG 设备,通过 CDI 注解指定具体的 MIG 实例、内存限制和计算能力要求。这种方式使得 Kubernetes 能够更精细地管理 GPU 资源,提高资源利用率。

5.4.5 多 GPU 工作负载配置

# 为多 GPU 训练任务配置 CDI
nvidia-ctk cdi generate \
  --output=/etc/cdi/nvidia-multi-gpu.yaml \
  --device-name-strategy=index

# 运行多 GPU 容器

podman run --rm \
  --device nvidia.com/gpu=0 \
  --device nvidia.com/gpu=1 \
  --device nvidia.com/gpu=2 \
  --device nvidia.com/gpu=3 \
  nvcr.io/nvidia/pytorch:23.12-py3 \
  python -c "import torch; print(f'GPUs available: {torch.cuda.device_count()}')"

5.4.6 性能监控和调试

# 生成包含调试信息的 CDI 规范
nvidia-ctk cdi generate \
  --output=/etc/cdi/nvidia-debug.yaml \
  --nvidia-ctk-path=/usr/bin/nvidia-ctk \
  --ldconfig-path=/sbin/ldconfig

# 验证 CDI 规范

nvidia-ctk cdi validate --input=/etc/cdi/nvidia-debug.yaml

# 查看详细的设备信息

nvidia-ctk cdi list --verbose

5.5 自动监控机制

type watch struct {
    watcher *fsnotify.Watcher
    tracked map[string]bool
}

// start 启动文件系统监控

func (w *watch) start(m *sync.Mutex, refreshfunc() error, dirErrors map[string]error) {
    if
 w.watcher != nil {
        go
 w.watch(w.watcher, m, refresh, dirErrors)
    }
}

// watch 监控文件系统事件并触发缓存刷新

func (w *watch) watch(fsw *fsnotify.Watcher, m *sync.Mutex, refreshfunc() error, dirErrors map[string]error) {
    for
 {
        select
 {
        case
 event, ok := <-fsw.Events:
            if
 !ok {
                return

            }
            // 处理文件系统事件

            if
 event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Remove|fsnotify.Rename) != 0 {
                m.Lock()
                refresh()
                m.Unlock()
            }
        case
 err, ok := <-fsw.Errors:
            if
 !ok {
                return

            }
            // 记录监控错误

            log.Printf("CDI watcher error: %v", err)
        }
    }
}

5.6 设备注入机制

// InjectDevices 将指定的 CDI 设备注入到 OCI 规范中
// 返回未解析的设备列表和可能的错误

func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, error) {
    var
 (
        unresolved []string
        edits      = &ContainerEdits{}
    )

    if
 ociSpec == nil {
        return
 nil, fmt.Errorf("can't inject devices, OCI Spec is nil")
    }

    for
 _, device := range devices {
        d := c.GetDevice(device)
        if
 d == nil {
            unresolved = append(unresolved, device)
            continue

        }

        // 应用设备的容器编辑

        edits.Append(d.GetContainerEdits())
    }

    // 将编辑应用到 OCI 规范

    if
 err := edits.Apply(ociSpec); err != nil {
        return
 unresolved, fmt.Errorf("failed to apply container edits: %w", err)
    }

    return
 unresolved, nil
}

6. CDI 使用示例

6.1 完整的 CDI 规范示例

{
  "cdiVersion": "0.8.0",
  "kind": "vendor.com/device",
  "devices": [
    {
      "name": "myDevice",
      "containerEdits": {
        "deviceNodes": [
          {
            "hostPath": "/vendor/dev/card1",
            "path": "/dev/card1",
            "type": "c",
            "major": 25,
            "minor": 25,
            "fileMode": 384,
            "permissions": "rw",
            "uid": 1000,
            "gid": 1000
          }
        ]
      }
    }
  ],
  "containerEdits": {
    "env": [
      "FOO=VALID_SPEC"
,
      "BAR=BARVALUE1"

    ],
    "deviceNodes": [
      {
        "path": "/dev/vendorctl",
        "type": "b",
        "major": 25,
        "minor": 25,
        "fileMode": 384,
        "permissions": "rw",
        "uid": 1000,
        "gid": 1000
      }
    ],
    "mounts": [
      {
        "hostPath": "/bin/vendorBin",
        "containerPath": "/bin/vendorBin"
      },
      {
        "hostPath": "/usr/lib/libVendor.so.0",
        "containerPath": "/usr/lib/libVendor.so.0"
      }
    ],
    "hooks": [
      {
        "hookName": "createContainer",
        "path": "/bin/vendor-hook"
      },
      {
        "hookName": "startContainer",
        "path": "/usr/bin/ldconfig"
      }
    ]
  }
}

6.2 使用设备的命令行示例

# 使用 podman
$ podman run --device vendor.com/device=myDevice ...

# 使用 docker

$ docker run --device vendor.com/device=myDevice ...

6.3 带注解的设备示例

{
  "cdiVersion": "0.8.0",
  "kind": "vendor.com/device",
  "devices": [
    {
      "name": "myDevice",
      "annotations": {
        "whatever": "false",
        "whenever": "true"
      },
      "containerEdits": {
        "deviceNodes": [
          {
            "path": "/dev/vfio/71"
          }
        ]
      }
    }
  ]
}

7. 生态与发展现状

7.1 业界采用情况

CDI 作为一个相对较新的规范,已经获得了多家主要硬件厂商和容器技术提供商的支持:

厂商/项目
支持状态
实现方式
NVIDIA
全面支持
NVIDIA Container Toolkit 提供完整 CDI 支持,包括 GPU 和 MIG 设备
Intel
积极采用
为 GPU、FPGA 和 AI 加速器提供 CDI 支持
AMD
初步支持
为 ROCm 平台提供实验性 CDI 支持
Mellanox
积极采用
为 RDMA 设备提供 CDI 支持
Kubernetes
集成中
通过 KEP-3063 提案集成 CDI 支持
containerd
原生支持
从 v1.6.0 开始提供 CDI 支持
Podman
原生支持
从 v4.0.0 开始提供 CDI 支持
CRI-O
原生支持
已集成 CDI 支持
Docker
计划中
通过 Moby 项目计划集成 CDI

目前,除 NVIDIA 外,Intel(GPU、FPGA)、AMD(ROCm)、Mellanox(RDMA)等厂商也在积极推进对 CDI 的支持,这表明 CDI 正逐渐成为容器设备访问的行业标准。随着更多硬件厂商的加入,CDI 生态系统将更加丰富和完善,为用户提供更加一致和简化的设备访问体验。

7.2 实际应用案例

以下是一些 CDI 在实际环境中的应用案例:

  1. 1. 大规模 AI 训练集群:使用 CDI 管理数百个 GPU 节点,简化了设备配置和资源管理
  2. 2. 混合云环境:通过 CDI 实现跨云平台的一致设备访问体验
  3. 3. 边缘计算:在资源受限的边缘设备上,CDI 提供了轻量级的设备管理解决方案
  4. 4. 多厂商硬件环境:在同时使用 NVIDIA、Intel 和 AMD 硬件的环境中,CDI 提供统一接口

7.3 未来发展趋势

CDI 规范仍在积极发展中,未来有望在以下方面取得更大进展:

  1. 1. 更广泛的设备支持:扩展到更多类型的硬件设备,如 TPU、DPU、SmartNIC 等专用加速器
  2. 2. 更深入的运行时集成:与更多容器运行时和编排系统实现原生集成
  3. 3. 更完善的资源调度:提供更精细的设备资源分配和调度能力
  4. 4. 更强大的监控能力:增强对设备使用情况的监控和指标收集
  5. 5. 更丰富的生态系统:吸引更多硬件厂商和软件供应商加入 CDI 生态

8. 总结

Container Device Interface (CDI) 为容器生态系统提供了一个强大而灵活的第三方设备支持标准。通过标准化设备访问方式,CDI 简化了供应商的开发工作,提高了容器运行时的互操作性,并为复杂设备的容器化使用提供了完整的解决方案。

8.1 主要优势

  1. 1. 标准化:为容器运行时提供统一的第三方设备支持标准
  2. 2. 简化开发:减少供应商为不同运行时编写和维护多个插件的需要
  3. 3. 灵活性:允许运行时和编排器具有很大的灵活性
  4. 4. 透明性:对集群管理员和应用程序开发人员透明
  5. 5. 可扩展性:支持复杂的设备初始化和供应商特定软件的自动注入

8.2 当前限制

  1. 1. 规范稳定性:规范仍在积极开发中,可能引入破坏性更改
  2. 2. 生态系统成熟度:作为相对较新的标准,生态系统支持仍在完善中
  3. 3. 复杂性管理:对于简单设备访问场景,CDI 规范可能引入额外的配置复杂性,但这种标准化带来的长期收益通常超过初期的复杂性成本
  4. 4. 平台限制:CDI 在某些平台和配置上存在限制,需要根据具体环境进行兼容性测试
  5. 5. 运行时兼容性:不同容器运行时对 CDI 的支持程度可能存在差异

9. 参考资料

  1. 1. NVIDIA GPU Operator CDI 支持
  2. 2. NVIDIA Container Toolkit CDI 支持
  3. 3. CDI 官方规范
  4. 4. Container Device Interface GitHub 仓库
  5. 5. NVIDIA GPU Operator 发布说明
  6. 6. 容器运行时接口 (CRI) 规范
  7. 7. OCI 运行时规范
  8. 8. Kubernetes Enhancement Proposal: CDI Support (KEP-3063)
  9. 9. Intel GPU CDI 支持
  10. 10. AMD ROCm CDI 支持