# Kueue 如何管理 DRA 模式下的 GPU 配额


![kueue-p3-dra.jpg](https://img.lixueduan.com/kubernetes/cover/kueue-p3-dra.jpg)

前面两篇：[Kubernetes 官方出品：一个 Controller 搞定 Job 排队和资源配额](https://www.lixueduan.com/posts/kubernetes/62-kueue-p1-intro/) 和 [终于搞懂 Kueue：5 个核心对象一次讲透](https://www.lixueduan.com/posts/kubernetes/63-kueue-p2-core-crd/) 把 Kueue 的基本玩法和核心对象走了一遍，不过 Demo 都跑在 CPU 上。

真到 GPU 集群里，大家更关心的是另一个问题：

**Kueue 能不能管理 DRA 模式下的 GPU？**

这一篇就把它跑通。NVIDIA DRA Driver 负责把整卡 GPU 发布成 `DeviceClass` / `ResourceSlice`，Kueue 在 Job 准入阶段读取 DRA 设备申请，判断这个 Job 能不能进入队列。

> 对 DRA 不熟悉的同学，可以先看这篇：[DRA P1：DRA 能解决什么问题？从部署到使用的完整体验](https://www.lixueduan.com/posts/kubernetes/54-dra-p1-quickstart/)

<!--more-->

## 1. 环境准备

环境准备分几段走：先创建 K8s 集群，再用 GPU Operator 把 GPU Driver / Container Runtime 准备好，最后装 Kueue 和 NVIDIA DRA Driver。

### 1.1 创建 Kubernetes 集群

集群使用 KubeClipper 创建。KubeClipper 1.6.0 默认支持 Kubernetes 1.36.1、containerd 2.2.4，和本文环境一致，详细步骤可以参考：[KubeClipper 1.6.0 发布：kcctl 优化与 K8s 1.36 支持](https://www.lixueduan.com/posts/kubernetes/59-kubeclipper-release-1.6.0/)。

快速创建单节点集群的命令如下：

```bash
curl -sfL https://oss.kubeclipper.io/get-kubeclipper.sh | KC_REGION=cn bash -
kcctl deploy
kcctl create cluster --name demo --master <NODE_IP> --untaint-master
```

集群起来后确认版本：

```bash
$ kubectl get node -owide
NAME            STATUS   ROLES           VERSION   CONTAINER-RUNTIME
lixd-test-gpu   Ready    control-plane   v1.36.1   containerd://2.2.4
```

### 1.2 GPU 环境准备（GPU Operator）

GPU Driver、NVIDIA Container Toolkit 等基础组件使用 GPU Operator 安装。完整说明可以参考之前这篇：[GPU 环境搭建指南：使用 GPU Operator 加速 Kubernetes GPU 环境搭建](https://www.lixueduan.com/posts/ai/02-gpu-operator/)。

本文后面会安装 NVIDIA DRA Driver，所以安装 GPU Operator 时需要关闭 DevicePlugin：

```bash
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia && helm repo update

helm upgrade --install --wait gpu-operator \
  -n gpu-operator --create-namespace \
  nvidia/gpu-operator \
  --version=v26.3.1 \
  --set driver.enabled=true \
  --set devicePlugin.enabled=false
```

> `--set devicePlugin.enabled=false`：关闭 DevicePlugin，避免与后续安装的 DRA Driver 冲突。

安装完成后，确认 GPU Operator 组件正常运行：

```bash
$ kubectl get pods -n gpu-operator
NAME                                           READY   STATUS
gpu-operator-7bcbd487f5-h8dsd                 1/1     Running
nvidia-container-toolkit-daemonset-zbpbv      1/1     Running
nvidia-operator-validator-8l2r5               1/1     Running
```

再确认节点能看到 GPU：

```bash
$ kubectl get node lixd-test-gpu -o jsonpath='{.metadata.labels.nvidia\.com/gpu\.product}{"\n"}'
Tesla-T4
```

### 1.3 安装 Kueue

Kueue 使用 0.18.1，安装方式和前两篇一样：

```bash
helm install kueue oci://registry.k8s.io/kueue/charts/kueue \
  --version=0.18.1 \
  --namespace kueue-system \
  --create-namespace \
  --wait --timeout 300s
```

如果测试环境访问 `registry.k8s.io` 不稳定，也可以用 GitHub Release 里的 chart 包：

```bash
helm install kueue \
  https://github.com/kubernetes-sigs/kueue/releases/download/v0.18.1/kueue-0.18.1.tgz \
  --namespace kueue-system \
  --create-namespace \
  --wait --timeout 300s
```

### 1.4 接入 NVIDIA DRA Driver

最后安装 NVIDIA DRA Driver 25.12.0：

```bash
helm upgrade --install nvidia-dra-driver-gpu \
  nvidia/nvidia-dra-driver-gpu \
  --version 25.12.0 \
  -n nvidia-dra-driver-gpu \
  --set gpuResourcesEnabledOverride=true \
  --create-namespace \
  --wait --timeout 300s
```

安装完成后，先看 DRA Driver 组件：

```bash
$ kubectl get pods -n nvidia-dra-driver-gpu
NAME                                                READY   STATUS
nvidia-dra-driver-gpu-controller-76864c66cb-s8tch   1/1     Running
nvidia-dra-driver-gpu-kubelet-plugin-grkbg          2/2     Running
```

再看 DeviceClass：

```bash
$ kubectl get deviceclass
NAME                                           AGE
compute-domain-daemon.nvidia.com              2m47s
compute-domain-default-channel.nvidia.com     2m47s
gpu.nvidia.com                                2m47s
mig.nvidia.com                                2m47s
vfio.gpu.nvidia.com                           2m47s
```

整卡调度用的是 `gpu.nvidia.com`。对应的 ResourceSlice 里能看到节点上的 T4：

```bash
$ kubectl get resourceslice
NAME                                 NODE            DRIVER           POOL
lixd-test-gpu-gpu.nvidia.com-p8cdq   lixd-test-gpu   gpu.nvidia.com   lixd-test-gpu
```

后面的 Job 会直接引用 `gpu.nvidia.com` 这个 DeviceClass，实际可分配设备则来自这些 ResourceSlice。

## 2. Demo：使用 Kueue 调度 DRA 模式下的 GPU

### 2.1 修改 Kueue DRA 配置

DRA 和 Kueue 使用的资源名称并不是同一个。`ResourceClaimTemplate` 里写的是 `deviceClassName: gpu.nvidia.com`，而 `ClusterQueue` 里扣配额用的是资源名，所以这里需要做一次映射：

```yaml
apiVersion: config.kueue.x-k8s.io/v1beta2
kind: Configuration
integrations:
  frameworks:
  - batch/job
resources:
  deviceClassMappings:
  - name: nvidia.com/gpu
    deviceClassNames:
    - gpu.nvidia.com
```

这段配置的意思是：只要 Workload 通过 `ResourceClaimTemplate` 申请 `gpu.nvidia.com` 这个 `DeviceClass`，Kueue 就把它折算成 `nvidia.com/gpu` 这个逻辑资源来扣配额。

![deviceClassMappings 将 DRA DeviceClass 映射为 Kueue 配额资源](https://img.lixueduan.com/kubernetes/kueue/kueue-deviceclass-mapping.jpg)

修改 Kueue manager config 后，需要重启 `kueue-controller-manager` 让新配置生效。

### 2.2 创建队列

这次只用一个队列，GPU 配额只给 1 张 T4。这样后面再提交第二个 Job 时，Pending 状态会看得很清楚。

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: p3-dra-demo
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: p3-t4
spec:
  nodeLabels:
    nvidia.com/gpu.product: Tesla-T4
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: p3-dra-cq
spec:
  namespaceSelector: {}
  resourceGroups:
  - coveredResources:
    - cpu
    - memory
    - nvidia.com/gpu
    flavors:
    - name: p3-t4
      resources:
      - name: cpu
        nominalQuota: "8"
      - name: memory
        nominalQuota: 16Gi
      - name: nvidia.com/gpu
        nominalQuota: 1
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
  name: p3-dra-queue
  namespace: p3-dra-demo
spec:
  clusterQueue: p3-dra-cq
```

注意这里的 `coveredResources` 里包含 `nvidia.com/gpu`。这是映射后的逻辑资源名，不是 Pod 里直接写的扩展资源。

### 2.3 用 ResourceClaimTemplate 申请整卡

使用 DRA 之后，Job 不再写 `resources.limits.nvidia.com/gpu: 1`，而是引用一个独立的 `ResourceClaimTemplate`。

这里 Kueue 做的事情很直接：读取 Workload 引用的 `ResourceClaimTemplate`，识别里面的 `deviceClassName` 和 `count`，再通过 `deviceClassMappings` 折算成 `ClusterQueue` 里的配额资源。

> 本文不展开 extended resource 路径，避免把两种 DRA 接入方式混在一起。

先创建 ResourceClaimTemplate：

```yaml
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: single-gpu
  namespace: p3-dra-demo
spec:
  spec:
    devices:
      requests:
      - name: gpu
        exactly:
          deviceClassName: gpu.nvidia.com
          allocationMode: ExactCount
          count: 1
```

这里有两个细节容易混：

- `deviceClassName` 是 `gpu.nvidia.com`，不是 `nvidia.com/gpu`
- `ExactCount + count: 1` 表示申请 1 张整卡

再提交 Kueue 管理的 Job：

```yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: kueue-dra-running-a
  namespace: p3-dra-demo
  labels:
    kueue.x-k8s.io/queue-name: p3-dra-queue
spec:
  parallelism: 1
  completions: 1
  suspend: true
  template:
    spec:
      resourceClaims:
      - name: gpu-claim
        resourceClaimTemplateName: single-gpu
      restartPolicy: Never
      containers:
      - name: cuda-test
        image: nvcr.io/nvidia/cuda:12.6.0-base-ubuntu24.04
        command: ["bash", "-c", "nvidia-smi -L && echo KUEUE_DRA_RUNNING_A && sleep 3600"]
        resources:
          claims:
          - name: gpu-claim
```

Job 被 Kueue 准入后，会从 `Suspended` 变成 `Running`：

```bash
$ kubectl get job,pod,workload -n p3-dra-demo
NAME                             STATUS    COMPLETIONS
job.batch/kueue-dra-running-a    Running   0/1

NAME                             READY   STATUS
pod/kueue-dra-running-a-kj7nw    1/1     Running

NAME                                                     QUEUE          RESERVED IN   ADMITTED
workload.kueue.x-k8s.io/job-kueue-dra-running-a-c3b3c    p3-dra-queue   p3-dra-cq     True
```

ResourceClaim 已经分配：

```bash
$ kubectl get resourceclaim -n p3-dra-demo
NAME                                                        STATE
kueue-dra-running-a-kj7nw-gpu-claim-c6j4j                   allocated,reserved
```

容器里能看到 T4：

```bash
$ kubectl logs -n p3-dra-demo -l job-name=kueue-dra-running-a
GPU 0: Tesla T4 (UUID: GPU-6d8fdeb7-6247-e04e-81a7-7b42dc45180c)
KUEUE_DRA_RUNNING_A
```

### 2.4 看 Kueue 是否真的扣了 GPU 配额

直接看 Workload 的准入结果：

```yaml
status:
  admission:
    clusterQueue: p3-dra-cq
    podSetAssignments:
    - count: 1
      flavors:
        nvidia.com/gpu: p3-t4
      resourceUsage:
        nvidia.com/gpu: "1"
```

这说明 `deviceClassMappings` 已经生效：用户写的是 ResourceClaimTemplate，Kueue 扣的是 `nvidia.com/gpu` 这个逻辑配额。

再看看 ClusterQueue，可以发现 GPU 配额已经被扣掉了：

```yaml
status:
  admittedWorkloads: 1
  flavorsUsage:
  - name: p3-t4
    resources:
    - name: nvidia.com/gpu
      total: "1"
```

### 2.5 第二个整卡 Job 会被 Kueue 卡住

队列里只有 1 张 GPU 配额。如果再提交一个同样申请 `single-gpu` 的 Job：

```text
kueue-dra-running-a：1 x gpu.nvidia.com
kueue-dra-pending-b：1 x gpu.nvidia.com
```

```bash
$ kubectl get job,workload -n p3-dra-demo
NAME                             STATUS
job.batch/kueue-dra-running-a    Running
job.batch/kueue-dra-pending-b    Suspended

NAME                                                     QUEUE          RESERVED IN   ADMITTED
workload.kueue.x-k8s.io/job-kueue-dra-running-a-c3b3c    p3-dra-queue   p3-dra-cq     True
workload.kueue.x-k8s.io/job-kueue-dra-pending-b-494a3    p3-dra-queue
```

再看 Workload 状态，就能发现为什么第二个 Job 一直起不来：

```yaml
status:
  conditions:
  - reason: Pending
    message: 'couldn''t assign flavors to pod set main: insufficient unused quota
      for nvidia.com/gpu in flavor p3-t4, 1 more needed'
  resourceRequests:
  - name: main
    resources:
      nvidia.com/gpu: "1"
```

## 3. 几个容易踩坑的点

### DeviceClass 和配额资源不是同一个东西

ResourceClaimTemplate 里写的是：

```yaml
deviceClassName: gpu.nvidia.com
```

ClusterQueue 里写的是：

```yaml
coveredResources:
- nvidia.com/gpu
```

两者靠 Kueue 配置里的 `deviceClassMappings` 关联起来。少了这段映射，Workload 会被标成 `Inadmissible`，原因类似：

```text
DeviceClass gpu.nvidia.com is not mapped in DRA configuration
```

### ResourceClaimTemplate 要单独创建

这两个名字最容易看混：

- `single-gpu`：前面单独创建的 `ResourceClaimTemplate`，定义“我要 1 张 `gpu.nvidia.com` 整卡”。
- `gpu-claim`：Pod 里的本地 claim 名字，后面容器通过它来使用 GPU。

Pod 里这段不是重新定义一个模板，而是引用已经存在的 `single-gpu` 模板：

```yaml
resourceClaims:
- name: gpu-claim
  resourceClaimTemplateName: single-gpu
```

容器里再通过同一个 `gpu-claim` 关联到这次申请到的设备：

```yaml
resources:
  claims:
  - name: gpu-claim
```

所以完整关系是：`ResourceClaimTemplate(single-gpu)` -> `Pod resourceClaims(gpu-claim)` -> `container resources.claims(gpu-claim)`。

### Kueue 准入不等于设备已经分配

这里要注意一点：Kueue 只是负责“准入”，真正把 GPU 分配给 Pod 的还是 kube-scheduler 和 DRA Driver。

所以生产环境里建议同时关注 `ResourceClaim` 状态和 Pod 状态。如果希望 Workload 在 Pod 长时间起不来时释放 Kueue 配额，可以结合 `waitForPodsReady` 做保护。

## 4. 小结

到这里可以看到，Kueue 并没有直接参与 GPU 分配，而是站在 Job 准入这一层，通过 `deviceClassMappings` 把 DRA 的设备申请转换成队列里的配额资源。这样既保留了 DRA 的设备模型，也让 GPU 可以继续纳入 Kueue 的统一配额管理。

| 组件 | 负责什么 |
|---|---|
| NVIDIA DRA Driver | 把 GPU 作为 `gpu.nvidia.com` DeviceClass / ResourceSlice 发布出来 |
| Kueue | 通过 `deviceClassMappings` 把 DRA 设备折算成 `nvidia.com/gpu` 配额 |
| kube-scheduler | 在 Pod 调度阶段完成 ResourceClaim 的实际设备分配 |

下一篇继续往前走一步，把 HAMi 引入进来：一张 GPU 被切成多份 vGPU 之后，Kueue 是否还能继续管理显存和算力配额。

## 5. 参考文章

- [Kueue Dynamic Resource Allocation](https://kueue.sigs.k8s.io/docs/concepts/dynamic_resource_allocation/)
- [Set Up Dynamic Resource Allocation](https://kueue.sigs.k8s.io/docs/tasks/manage/setup_dra/)
- [Run Workloads With DRA Devices](https://kueue.sigs.k8s.io/docs/tasks/run/dra/)


---

> 作者: [意琦行](https://github.com/lixd)  
> URL: https://www.lixueduan.com/posts/kubernetes/64-kueue-p3-dra-gpu/  

