DRA P2---理解 DRA:ResourceSlice、Claim、Class 三角关系

dra-p2.png

在上一篇 DRA P1:DRA 能解决什么问题?从部署到使用的完整体验 中,我们部署了 DRA 并成功运行了第一个 GPU Pod。DRA 相比 DevicePlugin 最大的区别在于:资源展示更详细,资源申请也更灵活。靠三个 API 对象来实现:ResourceSlice、DeviceClass、ResourceClaim。

那么这三个对象各自干什么,它们之间怎么配合?本篇就来回答这个问题。

1. 从 CSI 看 DRA:同一套设计思路

如果对 K8s 比较熟悉的同学就会发现,DRA 的设计思路和 CSI 有点类似,DRA 把异构设备的管理拆成了同样的三层:

CSI vs DRA 三层架构对比


存储 (CSI)设备 (DRA)谁创建
供给PVResourceSlice驱动 (Driver)
分类StorageClassDeviceClass管理员 (Admin)
需求PVCResourceClaim用户 (User)

ps:严格来说 ResourceSlice 和 PV 不能完全等同——PV 是一块具体的存储卷,ResourceSlice 更像是节点上设备的目录清单。但从供需关系看,模式是一样的:

  • CSI:PV 代表可用存储 → StorageClass 分类 → PVC 申请。
  • DRA:ResourceSlice 代表可用设备 → DeviceClass 分类 → ResourceClaim 申请。

DRA 三者之间的关系,按数据流方向串起来就是:

1
2
ResourceSlice ──→ DeviceClass ──→ ResourceClaim ──→ 调度器分配
  (供给)            (分类)          (需求)           (决策)
  • ResourceSlice:DRA 驱动发布节点上的设备信息,包括型号、显存、驱动版本等属性,以及可用容量
  • DeviceClass:管理员用 CEL 表达式从 ResourceSlice 的设备属性中筛选出一类设备形成用户友好的分组,比如"所有 NVIDIA GPU"或"仅 A100"
  • ResourceClaim:用户通过 deviceClassName 引用某个 DeviceClass,声明需要几个该类设备
  • 调度器:综合 ResourceSlice 的库存和 ResourceClaim 的需求,选出满足条件的节点和具体设备,完成分配

逐个来看。

2. ResourceSlice:资源提供方(Driver 视角)

2.1 从 DevicePlugin 到 ResourceSlice

ResourceSlice 是 DRA 驱动向集群"公告"设备信息的方式, 可以理解为是对 DevicePlugin 提供的 nvidia.com/gpu:4 这个信息的扩展。

1
2
3
4
5
6
7
8
9
root@GB200-POD2-F06-Node05:~# kubectl get node gb200-pod2-f06-node05 -oyaml|grep "capacity:" -A 7
  capacity:
    cpu: "144"
    ephemeral-storage: 1840577300Ki
    hugepages-2Mi: "0"
    hugepages-16Gi: "0"
    hugepages-512Mi: "0"
    memory: 1002716864Ki
    nvidia.com/gpu: "4"

HAMi 提供的 hami-device-plugin-nvidia 为了能够感知 GPU 显存信息,也是只能通过给节点打 Annotations 方式实现,DRA 中的 ResourceSlice 则是原生支持了这些。

推荐阅读:HAMi VGPU 原理分析 Part1:hami-Device-Plugin-Nvidia 实现

2.2 结构

一个标准的 ResourceSlice 内容如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: resource.k8s.io/v1
kind: ResourceSlice
metadata:
  name: gb200-pod2-f06-node05-gpu.nvidia.com-j7ggv
  ownerReferences:
  - apiVersion: v1
    controller: true
    kind: Node
    name: gb200-pod2-f06-node05
spec:
  devices:
  - attributes:
      addressingMode:
        string: ATS
      architecture:
        string: Blackwell
      brand:
        string: Nvidia
      cudaComputeCapability:
        version: 10.0.0
      driverVersion:
        version: 580.126.20
      productName:
        string: NVIDIA GB200
      type:
        string: gpu
      uuid:
        string: GPU-137d7996-cb8e-7683-e47d-9c99e6f49eb5
    capacity:
      memory:
        value: 189471Mi
    name: gpu-0
  # ... 省略 gpu-1, gpu-2, gpu-3 的类似内容
  driver: gpu.nvidia.com
  nodeName: gb200-pod2-f06-node05
  pool:
    name: gb200-pod2-f06-node05

可以看到,相比 DevicePlugin 只能报一个 nvidia.com/gpu:4 数量信息,ResourceSlice 把 GPU 架构、型号、显存、驱动版本都暴露出来了。有了这些信息,“只要 A100”、“显存大于 40Gi 的 GPU"这类调度策略才能做到。

  • driver:DRA 驱动

  • nodeName:资源所属节点

  • pool:资源池,对于本地资源,名称一般是节点名

  • attributes:描述设备固有特性的键值对,用于标识和选择设备

    • 比如 GPU 架构、型号等
  • capacity:设备可分配的资源量(如 GPU 显存),DRAConsumableCapacity 特性启用后可被多个 ResourceClaim 按容量共享消耗

3. DeviceClass:资源分类标准(Admin 视角)

DeviceClass 定义了"什么样的设备属于这一类”,一般由 DRA Driver 或者集群管理员创建。

一个完整的 DeviceClass 内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
root@GB200-POD2-F06-Node05:~# k get deviceclass gpu.nvidia.com -oyaml
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  annotations:
    meta.helm.sh/release-name: nvidia-dra-driver-gpu
    meta.helm.sh/release-namespace: nvidia-dra-driver-gpu
  creationTimestamp: "2026-04-22T02:08:27Z"
  generation: 1
  labels:
    app.kubernetes.io/managed-by: Helm
  name: gpu.nvidia.com
  resourceVersion: "52725555"
  uid: 2e700a15-2392-4c7f-9467-26bf6249b71a
spec:
  selectors:
  - cel:
      expression: device.driver == 'gpu.nvidia.com' && device.attributes['gpu.nvidia.com'].type
        == 'gpu'

3.1 CEL 选择器

DeviceClass 的关键是 CEL 选择器:

1
2
3
4
spec:
  selectors:
  - cel:
      expression: device.driver == 'gpu.nvidia.com' && device.attributes['gpu.nvidia.com'].type == 'gpu'

这表示:驱动为 gpu.nvidia.com 且类型为 gpu 的设备才属于这个 Class

CEL 表达式可以组合任意属性条件。比如管理员可以创建一个只包含高端 GPU 的 DeviceClass:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: gpu.nvidia.com-a100
spec:
  selectors:
  - cel:
      expression: |
        device.driver == 'gpu.nvidia.com' &&
        device.attributes['gpu.nvidia.com'].type == 'gpu' &&
        device.attributes['gpu.nvidia.com'].productName == 'NVIDIA A100'

这样用户只需要在 ResourceClaim 中指定 deviceClassName: gpu.nvidia.com-a100,就能精确地申请到 A100,而无需关心 CEL 表达式的写法。

DevicePlugin 只能通过 gpu.nvidia.com:1 指定数量,GPU 型号管不了,得靠 nodeSelector 之类的方式绕路。DRA 直接在 Claim 里选 Class 就行。

说白了,DeviceClass 就是把 CEL 的复杂性藏起来——管理员写规则,用户选 Class。

3.2 config:设备级配置

DeviceClass 还可以在 config 字段中定义传递给 DRA 驱动的配置参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
spec:
  selectors:
  - cel:
      expression: device.driver == 'gpu.nvidia.com' && device.attributes['gpu.nvidia.com'].type == 'gpu'
  config:
  - opaque:
      driver: gpu.nvidia.com
      parameters:
        apiVersion: v1
        kind: GPUConfig
        spec:
          enablePersistence: true

当 Kubelet 调用驱动的 NodePrepareResources 准备设备时,会将这些配置传递给驱动。管理员在 Class 级别统一设置设备参数,不用每个用户单独配。

目前 NVIDIA DRA 驱动对 config 的支持还在完善中,具体可配置项参考驱动文档。

3.3 extendedResourceName:兼容模式

extendedResourceName 是 DeviceClass 中的一个字段,开启后匹配该 Class 的设备可以通过 Pod 的扩展资源请求(resources.requests/limits)来使用,不用写 ResourceClaim。调度器会负责资源核算,确保不会超分配。

DeviceClass 中指定 extendedResourceName

1
2
3
4
5
6
7
8
9
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: nvidia-gpu-class
spec:
  extendedResourceName: nvidia.com/gpu
  selectors:
  - cel:
      expression: 'device.driver == "gpu.nvidia.com"'

Pod 按传统方式申请即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: container
    resources:
      requests:
        nvidia.com/gpu: 1
      limits:
        nvidia.com/gpu: 1

extendedResourceName 是 Alpha 特性,需要启用 DRAExtendedResource feature gate 才能生效。该特性在 Kubernetes 1.34/1.35 默认关闭,1.36 开始默认开启。

4. ResourceClaim:资源需求方(User 视角)

ResourceClaim 是用户对资源的"采购订单",描述需要什么设备、需要多少。

4.1 两种声明方式

4.1.1 ResourceClaimTemplate:Pod 级别声明

上一篇我们用了 ResourceClaimTemplate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: single-gpu
spec:
  spec:
    devices:
      requests:
      - name: gpu
        exactly:
          deviceClassName: gpu.nvidia.com
          allocationMode: ExactCount
          count: 1

随 Pod 创建而自动生成 ResourceClaim,Pod 删除时自动回收。适合"一个 Pod 用一份资源"的场景。

4.1.2 ResourceClaim:独立声明(支持跨 Pod 共享)

单独创建的 ResourceClaim 有独立的生命周期,可以跨 Pod 共享。适合"多个 Pod 共用同一份资源"的场景(如 GPU 共享)。

是的,DRA 默认就支持 GPU 共享。多个 Pod 同时引用同一个 Claim,获得同一个设备的访问权,本质是共享同一块 GPU。效果和 DevicePlugin 的 TimeSlicing 方案类似。

关于 TimeSlicing 方案推荐阅读这篇文章:一文搞懂 GPU 共享方案: NVIDIA Time Slicing

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 共享的 ResourceClaim
apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
  name: shared-gpu
spec:
  devices:
    requests:
    - name: gpu
      exactly:
        deviceClassName: gpu.nvidia.com
        allocationMode: ExactCount
        count: 1
---
# Pod A 引用共享 Claim
apiVersion: v1
kind: Pod
metadata:
  name: pod-a
spec:
  resourceClaims:
  - name: gpu
    resourceClaimName: shared-gpu   # 直接引用独立 Claim
  containers:
  - name: app
    image: nvidia/cuda:12.1.0-base-ubuntu22.04
    command: ["nvidia-smi", "-L"]
    resources:
      claims:
      - name: gpu
---
# Pod B 引用同一个 Claim
apiVersion: v1
kind: Pod
metadata:
  name: pod-b
spec:
  resourceClaims:
  - name: gpu
    resourceClaimName: shared-gpu   # 同一个 Claim
  containers:
  - name: app
    image: nvidia/cuda:12.1.0-base-ubuntu22.04
    command: ["nvidia-smi", "-L"]
    resources:
      claims:
      - name: gpu

Pod A 和 Pod B 共享同一个 GPU 设备,调度器会将它们调度到同一节点,分配同一个设备。

结果如下:

1
2
3
4
root@GB200-POD2-F06-Node05:~/lixd/dra/test# k logs -f pod-a
GPU 0: NVIDIA GB200 (UUID: GPU-f6c15dc5-4024-fb95-39e6-5089c0c100c8)
root@GB200-POD2-F06-Node05:~/lixd/dra/test# k logs -f pod-b
GPU 0: NVIDIA GB200 (UUID: GPU-f6c15dc5-4024-fb95-39e6-5089c0c100c8)

4.2 分配模式

ResourceClaim 支持两种分配模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 模式一:精确数量
exactly:
  deviceClassName: gpu.nvidia.com
  allocationMode: ExactCount
  count: 2              # 恰好需要 2 个 GPU

# 模式二:全部满足
all:
  deviceClassName: gpu.nvidia.com
  allocationMode: All   # 需要节点上该 Class 下的全部设备

All 模式本质是:我要这个节点这个资源池里的所有设备。

只有当该节点上该 Class 下的所有设备均未被占用时,分配才会成功;否则分配失败(除非设置了 adminAccess)。适合分布式训练这类需要独占节点全部 GPU 的场景。

4.3 额外选择条件

除了通过 deviceClassName 限定设备范围,ResourceClaim 还可以添加额外的 CEL 选择条件,进一步缩小匹配范围:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
spec:
  devices:
    requests:
    - name: training-gpu
      exactly:
        deviceClassName: gpu.nvidia.com
        selectors:
        - cel:
            expression: device.attributes['gpu.nvidia.com'].productName == 'NVIDIA GB200'
        allocationMode: ExactCount
        count: 2

这里虽然引用的是通用的 gpu.nvidia.com Class,但通过额外选择条件限定只匹配 GB200 GPU。

4.4 跨设备约束

当请求多个设备时,可以用 constraints 定义跨设备的约束条件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
spec:
  devices:
    requests:
    - name: gpu
      exactly:
        deviceClassName: gpu.nvidia.com
        allocationMode: ExactCount
        count: 2
    constraints:
    - matchAttribute: productName
    - matchAttribute: cudaComputeCapability

matchAttribute: productName 表示:分配的 2 个 GPU 必须是相同型号。

matchAttribute: cudaComputeCapability 则是确保 CUDA 兼容性。

matchAttribute 配置主要匹配 ResourceSlice 中的 devices.attributes 参数,保证所有设备都有同样的参数。

5. 三角协作:从设备发布到 Pod 启动

三个对象各自干什么搞清楚了,接下来看它们怎么配合完成一次资源分配。以下流程以上篇的 gpu-test-pod 为例。

DRA 资源分配流程

5.1 阶段一:设备注册(DRA Driver → ResourceSlice)

1
2
3
4
5
DRA Driver 启动
    ├─ 1. 扫描节点上的 GPU 设备
    ├─ 2. 收集设备属性(型号、显存、驱动版本等)
    └─ 3. 创建/更新 ResourceSlice

DRA 驱动以 DaemonSet 方式运行在每个节点上,持续 watch 设备状态。设备变化时(新增、移除、健康状态变化),驱动更新对应的 ResourceSlice 并递增 pool.generation

5.2 阶段二:分类定义(Admin → DeviceClass)

1
2
3
4
5
6
管理员创建 DeviceClass
    ├─ 1. 确定设备筛选规则(如"所有 NVIDIA GPU")
    ├─ 2. 编写 CEL 表达式
    │     例如:expression: device.driver == 'gpu.nvidia.com'
    └─ 3. 创建 DeviceClass,供用户引用

DeviceClass 是管理员对 ResourceSlice 中设备的分类标准。驱动把原始设备信息发布到集群后,管理员通过 DeviceClass 告诉用户"有哪些设备类别可用"。

例如创建一个通用的 NVIDIA GPU 类别:

1
2
3
4
5
6
7
8
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: gpu.nvidia.com
spec:
  selectors:
  - cel:
      expression: device.driver == 'gpu.nvidia.com'

用户不需要理解 CEL 语法,只需知道 deviceClassName: gpu.nvidia.com 代表"NVIDIA GPU"。管理员写好规则,用户选 Class 就行——跟 CSI 里选 StorageClass 一个道理。

5.3 阶段三:用户声明需求(ResourceClaim / Template)

用户根据 DeviceClass 创建 ResourceClaim,声明需要什么设备、需要多少。上一篇我们用的是 ResourceClaimTemplate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: single-gpu
spec:
  spec:
    devices:
      requests:
      - name: gpu
        exactly:
          deviceClassName: gpu.nvidia.com
          allocationMode: ExactCount
          count: 1

也可以直接创建独立的 ResourceClaim(适合跨 Pod 共享的场景)。

无论哪种方式,都是通过 deviceClassName: gpu.nvidia.com 引用 DeviceClass,告诉调度器"我需要这个类别的设备"。

与 DevicePlugin 只能指定数量不同,ResourceClaim allocationMode 除了 ExactCount 之外还支持 All 模式,不指定数量,直接占用该节点该 class 下的全部设备。适合分布式训练等需要独占所有 GPU 的场景。

5.4 阶段四:创建 Pod 并触发调度

用户创建 Pod,通过 resourceClaims 引用 Template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
  name: gpu-test-pod
spec:
  containers:
  - name: cuda-container
    image: nvidia/cuda:12.1.0-base-ubuntu22.04
    resources:
      claims:
      - name: gpu-claim
  resourceClaims:
  - name: gpu-claim
    resourceClaimTemplateName: single-gpu

Pod 提交后,调度流程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Pod 提交到 API Server
    ├─ 1. ResourceClaim Controller 根据 Template 创建 ResourceClaim
    ├─ 2. 调度器 Filter
    │     → 遍历节点,基于 ResourceSlice 判断:
    │        - 先通过 DeviceClass CEL 筛选符合 Class 的设备子集
    │        - 再结合 Claim selector 进一步过滤
    │        - 容量是否满足
    │        - 约束是否满足
    ├─ 3. 调度器 Reserve + Bind
    │     → 选定节点 + 选定具体设备
    │     → 写入 ResourceClaim.status.allocation(driver/pool/device)
    │     → 写入 ResourceClaim.status.reservedFor
    │     → 绑定 Pod 到节点
    └─ 4. Kubelet 启动 Pod

调度器不仅选节点,还选定了具体设备。Reserve 阶段调度器在内部缓存中完成设备选择,Bind 阶段将分配结果(哪个节点、哪个设备)写入 ResourceClaim.status.allocation,后续 Driver 和 Kubelet 都基于这个结果工作。

三个组件的分工:

组件职责
Scheduler选节点 + 选具体设备,写入 allocation
Driver根据 allocation 结果准备设备(NodePrepareResources)
Kubelet调用 Driver + 将设备注入容器

5.5 阶段五:设备准备与 Pod 启动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Kubelet 收到绑定的 Pod
    ├─ 1. 查找 Pod 引用的 ResourceClaim
    │     → gpu-test-pod-gpu-claim-7rqsj (已 allocated + reserved)
    ├─ 2. 调用 DRA Plugin 的 NodePrepareResources
    │     ├─ 传入 ResourceClaim 的分配结果
    │     ├─ 驱动准备设备(配置 GPU、创建 CDI 描述文件)
    │     └─ 返回 CDI 设备 ID
    ├─ 3. Kubelet 将 CDI ID 传递给容器运行时
    └─ 4. 容器运行时根据 CDI 描述注入设备到容器
          → Pod 启动,nvidia-smi 可见 GPU

最终我们看到 P1 中的运行结果:

1
2
3
root@GB200-POD2-F06-Node05:~# kubectl logs gpu-test-pod
GPU 0: NVIDIA GB200 (UUID: GPU-137d7996-cb8e-7683-e47d-9c99e6f49eb5)
GPU allocation successful.

5.6 ResourceClaim 状态流转

ResourceClaim 状态流转

整个过程中 ResourceClaim 的状态变化:

状态触发时机含义
(创建)Pod 创建时由 Controller 生成等待调度器处理
Allocated调度器完成分配已选定具体设备,写入 allocation 结果
ReservedPod 绑定到节点设备已预留给该 Pod,其他 Pod 不可使用

P1 中我们看到的 allocated,reserved 就是这两个状态的组合:

1
2
NAME                           STATE                AGE
gpu-test-pod-gpu-claim-7rqsj   allocated,reserved   37s

Pod 删除后,基于 Template 创建的 ResourceClaim 会随 Pod 一起被回收,设备回到可分配状态。若是独立创建的 ResourceClaim,Pod 删除后 allocation 被清除、Claim 回到 Pending,但 Claim 本身不会被删除,可被其他 Pod 重新引用。

6. 小结

概念职责
ResourceSlice设备目录,驱动发布设备信息
DeviceClass分类标准,封装选择规则
ResourceClaim资源订单,描述用户需求

协作流程:

  1. 驱动发布 ResourceSlice(设备注册)
  2. 管理员定义 DeviceClass(分类筛选)
  3. 用户创建 ResourceClaim/Pod(资源申请)
  4. 调度器综合三者的信息做分配决策
  5. Kubelet 调用驱动准备设备,容器获得 GPU

下一篇来看 DRA 的完整工作流和源码实现——调度器怎么分配、Kubelet 怎么准备设备,以及 NVIDIA DRA Driver 的源码。

0%