使用 Dynamic Resource Allocation (DRA) 进行 GPU 切片

传统的 device-plugin 模型会分配整张 GPU:Pod 请求 nvidia.com/gpu: 1,即使只是一个 LoRA 微调、从未使用超过几 GiB 显存,也会得到整块卡。在一张 24 GB 的 A30 上,这相当于作业实际所需内存的最多 20 倍,而这些资源都处于闲置状态。

Dynamic Resource Allocation (DRA) —— 一个 resource.k8s.io/v1 API,并已在 Kubernetes 1.34 中达到 GA —— 允许 Pod 改为请求 GPU 的一个切片。借助 Alauda Build of NVIDIA DRA Driver for GPUs,这个切片可以是:

  • 一个 MIG 分区 —— 由硬件隔离的卡片一部分(拥有独立的 SM 和 VRAM,由 GPU 强制隔离),或者
  • 一个 time-sliced / MPS-shared 的完整 GPU —— 多个 Pod 共享一张卡,但没有显存隔离。

本指南通过 ResourceClaimTemplate 请求一个 MIG 切片,然后在其中使用 Kubeflow Trainer v2 运行一次监督微调。所有资源都位于 assets/dra/,并且仓库中的 e2e harness 通过 c15_dra_gpu_slice.sh 用例对整条流程进行了端到端验证。

device plugin 与 DRA 的对比

传统 device pluginDynamic Resource Allocation
请求形式resources.limits."nvidia.com/gpu": 1resources.claims: [{name: gpu-slice}] + 一个 ResourceClaim
粒度整张 GPU(或像 HAMi 的 gpualloc 这样的供应商固定 vGPU)按 claim 切片:在请求时选择 MIG profile 或共享模式
形状由谁定义集群范围的 device-plugin 配置 / 节点重新标记 + 重启用户按工作负载在 ResourceClaimTemplate 中定义
隔离整卡,或供应商特定隔离使用 MIG 时为硬件隔离;使用 TimeSlicing/MPS 时为时间共享
选择逻辑节点标签 + 资源名称针对设备属性(profilememoryproductName 等)的 CEL 表达式

DRA 不会替代调度器或 Kueue —— 它改变的是设备如何被描述和声明。配额、优先级和抢占机制仍然有效(参见 将切片与 Kueue 配额结合使用)。

前提条件

要求详情
Kubernetes 1.34+已提供 resource.k8s.io/v1 —— 可通过 kubectl api-resources --api-group=resource.k8s.io 检查
Alauda Build of NVIDIA DRA Driver for GPUs已安装到集群中,并且其 kubelet-plugin 在 GPU 节点上已发布 ResourceSlice(参见 准备 GPU 节点和 DRA driver
支持 MIG 且已创建 profile 的 GPU对于 MIG 切片:A30 / A100 / H100 / H200,且已启用 MIG 模式并创建 MIG 实例(参见 在节点上准备 MIG profile)。没有 MIG 的 GPU 仍可使用 time-slicing 路径
Kubeflow Trainer v2trainer.kubeflow.org API group(参见 使用 Kubeflow Trainer v2 进行微调)。对于 KFP、Volcano 或普通 Jobs,DRA 的接入方式完全相同——参见 将同一个 claim 接入其他 orchestrator
kubectl 访问权限能在你的命名空间中管理 resourceclaimtemplates,并读取 deviceclasses / resourceslices(集群作用域)
NOTE

DeviceClassResourceSlice集群作用域资源,由 driver/admin 拥有。ResourceClaimTemplateResourceClaim命名空间作用域资源,由你创建。ResourceClaimTemplate 必须与引用它的 Pod 位于同一个命名空间。

一个切片请求中的三个对象

  cluster (driver/admin)                    namespace (you)
  ┌────────────────────┐        ┌──────────────────────────────────┐
  │ DeviceClass         │◄───────│ ResourceClaimTemplate            │
  │  mig.nvidia.com     │  refs  │  request: 1× mig.nvidia.com      │
  │  gpu.nvidia.com     │        │  selector: profile == "1g.6gb"   │
  └────────────────────┘        └──────────────┬───────────────────┘
  ┌────────────────────┐                        │ per-Pod instance
  │ ResourceSlice(s)    │  scheduler matches     ▼
  │  advertised by the  │◄──────────  Pod.spec.resourceClaims ──► container.resources.claims
  │  kubelet-plugin     │  claim & allocate a slice, inject it into the Pod
  └────────────────────┘
  • DeviceClass(由 driver 创建)定义一种设备类型和一个基础选择器。Alauda Build of NVIDIA DRA Driver for GPUs 提供 gpu.nvidia.com(完整 GPU)和 mig.nvidia.com(MIG 切片)。
  • ResourceClaimTemplate(由你创建)表示“给每个 Pod 分配一个来自该类、匹配此 CEL 选择器的设备”。调度器会基于它为每个 Pod 生成一个 ResourceClaim
  • Podspec.resourceClaims 中引用该模板,而 container 通过 resources.claims 选择使用它。完全不会使用 nvidia.com/* 限制。

第 1 步 —— 确认 driver 正在发布切片

只要 driver 安装完成,DeviceClass 就会存在;但只有当 kubelet-plugin 为节点的 GPU 发布 ResourceSlice 后,Pod 才能被调度:

kubectl get deviceclasses
# NAME              ...
# gpu.nvidia.com
# mig.nvidia.com

kubectl get resourceslices
# NAME                       NODE              DRIVER           ...
# <node>-gpu.nvidia.com-...  192.168.143.59    gpu.nvidia.com   ...

如果 get resourceslices 结果为空,说明 driver 已安装,但尚未在任何节点上发布设备——请先跳到 准备 GPU 节点和 DRA driver,然后再继续。

检查已发布的设备,以了解你的 CEL 选择器可以匹配哪些确切的属性键和值——不要假设 profile 字符串;请从你的集群中读取它们

kubectl get resourceslices -o yaml | grep -A20 'attributes:'
# ...
#   attributes:
#     gpu.nvidia.com/profile: {string: "1g.6gb"}
#     gpu.nvidia.com/productName: {string: "NVIDIA A30"}
#     gpu.nvidia.com/type: {string: "mig"}

第 2 步 —— 对切片做冒烟测试

在把 DRA 接入训练作业之前,先使用 dra-smoke-pod.yaml 证明 driver 确实会分配一个切片。它会请求一个 MIG 切片并打印 container 看到的内容:

base=https://raw.githubusercontent.com/alauda/aml-docs/master/docs/en/train/guides/assets/dra
NS=my-namespace   # namespace where your Pods run

# 1. A ResourceClaimTemplate for one 1g.6gb MIG slice.
curl -fsSL $base/mig-slice-resourceclaimtemplate.yaml | sed "s/kubeflow-admin-cpaas-io/$NS/" | kubectl apply -f -

# 2. A Pod that claims it and runs nvidia-smi + a matmul on the slice.
curl -fsSL $base/dra-smoke-pod.yaml | sed "s/kubeflow-admin-cpaas-io/$NS/" | kubectl create -f -

# 3. Watch it, then read the logs.
kubectl -n "$NS" get pods -l app=dra-smoke -w
kubectl -n "$NS" logs -l app=dra-smoke --tail=40

你应该能看到该切片降低后的显存上限——对于 1g.6gb,大约是 6 GiB,而不是整张卡的 24 GiB——这正是切片的意义所在:

===== nvidia-smi -L =====
GPU 0: NVIDIA A30 (UUID: GPU-...)
  MIG 1g.6gb  Device  0: (UUID: MIG-...)
cuda_available: True
device: NVIDIA A30 MIG 1g.6gb
slice_total_mem_GiB: 5.75
matmul_on_slice: OK

从 control plane 检查分配情况:

kubectl -n "$NS" get resourceclaims
# NAME                 STATE                ...
# dra-smoke-...-gpu-slice   allocated,reserved

第 3 步 —— 使用 Kubeflow Trainer v2 在切片内进行微调

dra-sft-trainingruntime.yaml 中的 TrainingRuntime 是一个普通的 Trainer v2 runtime,恰好包含两个 DRA 钩子,并且没有 nvidia.com/* 限制

spec:
  template:
    spec:
      replicatedJobs:
        - name: node
          template:
            spec:
              template:
                spec:
                  resourceClaims:                         # (1) Pod-level: bind a per-Pod claim
                    - name: gpu-slice
                      resourceClaimTemplateName: mig-1g-6gb
                  containers:
                    - name: node
                      resources:
                        claims:                           # (2) Container-level: consume it
                          - name: gpu-slice
                        # NOTE: no nvidia.com/gpu here — the GPU comes from the claim

它的训练脚本是自包含的——会构建一个小型 causal-LM,为其包装一个 LoRA adapter,并在合成数据集上训练,因此这次运行不需要下载模型或数据集,并且大约一分钟即可完成。这使它非常适合在离线 GPU 节点上运行,也非常适合验证切片。你可以替换为真实的基础模型和数据集,将其变成生产级微调(参见 将其转变为真实微调);DRA 相关的接入方式不变。

应用 runtime 并提交作业:

base=https://raw.githubusercontent.com/alauda/aml-docs/master/docs/en/train/guides/assets/dra
NS=my-namespace

# ResourceClaimTemplate (skip if you already applied it in Step 2).
curl -fsSL $base/mig-slice-resourceclaimtemplate.yaml | sed "s/kubeflow-admin-cpaas-io/$NS/" | kubectl apply -f -
# TrainingRuntime that consumes the slice.
curl -fsSL $base/dra-sft-trainingruntime.yaml         | sed "s/kubeflow-admin-cpaas-io/$NS/" | kubectl apply -f -
# TrainJob.
curl -fsSL $base/dra-sft-trainjob.yaml                | sed "s/kubeflow-admin-cpaas-io/$NS/" | kubectl create -f -

跟踪它直到完成:

kubectl -n "$NS" get trainjob,pods
pod=$(kubectl -n "$NS" get pods -l jobset.sigs.k8s.io/replicatedjob-name=node -o jsonpath='{.items[0].metadata.name}')
kubectl -n "$NS" logs -f "$pod"

成功时如下所示:

[slice] device=NVIDIA A30 MIG 1g.6gb total_mem=5.75 GiB
trainable params: 147,456 || all params: 4,...  || trainable%: ...
{'loss': ..., 'step': 10}
[slice] peak_mem=1.83 GiB
DRA LoRA SFT on GPU slice: OK

peak_mem 明显低于切片的 total_mem,这证明微调完全运行在其分区内。再启动第二个 TrainJob,并让第一个继续运行:如果一张卡被划分为四个 1g.6gb 切片,则最多可以在一张物理 GPU 上并发且隔离运行四个微调——每个作业都看不到其他作业的显存。

选择切片大小 / 选择 profile

修改 mig-slice-resourceclaimtemplate.yaml 中 CEL 选择器里的 MIG profile:

selectors:
  - cel:
      expression: device.attributes["gpu.nvidia.com"].profile == "2g.12gb"

常见 profile(请根据你的 ResourceSlice 确认确切字符串——几何布局因 GPU 而异):

GPUProfiles每卡最多切片数
A30 (24 GB)1g.6gb, 2g.12gb, 4g.24gb4
A100 / H100 (80 GB)1g.10gb, 2g.20gb, 3g.40gb, 7g.80gb7

选择能够轻松容纳你的模型 + LoRA + optimizer state + activations 的最小 profile。0.5–1.5B 模型的 LoRA 适合 1g.6gb;7B QLoRA 通常需要 2g.12gb 及以上。

没有 MIG?通过 time-slicing 共享整张 GPU

如果 GPU 不支持 MIG,或者你不想对其分区,可以通过一个不透明的 GpuConfig 使用 shared-gpu-resourceclaimtemplate.yaml 请求一个通过 time-slicing 共享的完整 GPU

spec:
  spec:
    devices:
      requests:
        - name: gpu-slice
          exactly:
            deviceClassName: gpu.nvidia.com
            allocationMode: ExactCount
            count: 1
      config:
        - requests: ["gpu-slice"]
          opaque:
            driver: gpu.nvidia.com
            parameters:
              apiVersion: resource.nvidia.com/v1beta1
              kind: GpuConfig
              sharing:
                strategy: TimeSlicing   # or MPS for concurrent sharing

将 runtime 指向这个模板即可(resourceClaimTemplateName: shared-gpu-timeslice)——其余部分无需更改。e2e 用例会使用 DRA_SLICE_MODE=shared 来运行这条路径。

MIG 切片Time-slicing / MPS
显存隔离 —— 硬件强制隔离无 —— 每个使用者都能看到完整 VRAM
故障隔离无 —— 一个作业可能导致其他作业 OOM
需要 MIG 模式
最适合有保证、隔离的训练切片将大量轻量 / 突发型作业塞进一张卡

将切片与 Kueue 配额结合使用

DRA 关注的是设备的形态Kueue 关注的是谁可以使用多少。Kueue 能理解 DRA DeviceClass,因此 ClusterQueue 可以像配额 nvidia.com/gpu 一样,为 mig.nvidia.com 设备设置配额——按切片预算接纳 TrainJob,并在更高优先级作业需要切片时抢占借用者。Preemptible TrainJobs with Kueue 指南中的 cohort 模式可原样使用;只需将 ResourceFlavor 中受管的资源替换为 DRA device class 即可。(Kueue 中的 DRA 支持仍在演进——在生产环境依赖之前,请先根据你的 Kueue 版本进行验证。)

将同一个 claim 接入其他 orchestrator

DRA 钩子位于 Pod spec 中,因此任何能够构造 Pod 的系统都可以请求一个切片——Trainer v2 只是其中一个调用者:

  • Kubeflow Pipelines (KFP v2): 通过 kfp-kubernetes 的平台配置(或 pod-spec patch),为某个 task 的 Pod 附加 resourceClaims / resources.claims,让 pipeline component 运行在切片上。
  • Volcano / 普通 Job / Pod 这两个钩子与 冒烟测试 Pod 在字节级别上完全一致——设置 spec.resourceClaims 和 container 的 resources.claims 即可。

冒烟测试 Pod 就是这些场景的最小模板。

将其转变为真实微调

随附脚本训练的是一个合成模型,因此可以离线验证切片。若要在切片上微调真实模型,请保留这两个 DRA 钩子,并将 container 替换为真实 recipe——例如 使用 Kubeflow Trainer v2 进行微调 中的 LlamaFactory runtime,其中 dataset-initializer / model-initializer 步骤会将基础模型和数据集下载到共享 PVC 上。该 runtime 唯一需要修改的地方,就是删除 nvidia.com/gpualloc / gpucores / gpumem 限制,并添加 spec.resourceClaims + resources.claims。根据模型大小来配置 MIG profile(0.6B LoRA 适合 1g.6gb;7B QLoRA 需要 2g.12gb 及以上)。

准备 GPU 节点和 DRA driver(管理员)

Alauda Build of NVIDIA DRA Driver for GPUs 安装了一个 controller,以及一个受节点标签控制的 kubelet-plugin DaemonSet。只要 GPU 节点还没有带上该标签,plugin 就不会发布任何 ResourceSlice,而 DRA Pod 也会一直处于 Pending 状态。

# The kubelet-plugin DaemonSet is scheduled by a node label (check its nodeSelector):
kubectl -n kube-system get ds nvidia-dra-driver-gpu-kubelet-plugin \
  -o jsonpath='{.spec.template.spec.nodeSelector}{"\n"}'
# {"nvidia-device-enable":"pgpu-dra"}

# Label the GPU node so the plugin lands and starts advertising devices:
kubectl label node <gpu-node> nvidia-device-enable=pgpu-dra

对于完整 GPU / time-sliced 方式的 claim,这就是全部需要做的事情——此时 plugin 会为每块卡发布一个 gpu.nvidia.com 设备,你可以直接跳到 对切片做冒烟测试。MIG 切片则需要下面的额外准备。

在节点上准备 MIG profile

在 driver 能够分配之前,卡上必须先存在 MIG 切片。 Alauda Build of NVIDIA DRA Driver for GPUs 不会按需创建 MIG 分区——它只会暴露已经存在的 MIG 设备。即使 MIG 模式已启用但尚未创建任何实例,kubelet-plugin 也会进入 crash-loop,并报 invalid CDI Spec: no devices。因此,管理员需要在每个 GPU 节点上只创建一次 MIG profile(所有 nvidia-smi 命令都在节点宿主机上执行——可以通过特权 Pod 或 SSH):

# 0. The GPU must be IDLE. MIG-mode changes reset GPU state and are refused while any
#    CUDA workload or monitoring client (e.g. dcgm-exporter) holds the card. Stop GPU
#    workloads and move dcgm-exporter off this node first.

# 1. Enable MIG mode on the GPU (index 0 here) and confirm it took effect.
nvidia-smi -i 0 -mig 1
nvidia-smi -i 0 --query-gpu=mig.mode.current,mig.mode.pending --format=csv
#    If mode stays "pending", a client is still attached — free it, then finalise with
#    a GPU reset (`nvidia-smi -i 0 -r`) or a node reboot.

# 2. List the GPU-instance profiles this card supports, with their IDs and how many fit.
nvidia-smi mig -lgip
#   GPU  Name          ID   Free/Total   Memory
#     0  MIG 1g.6gb    19    4/4         ~6 GiB     <- four small slices
#     0  MIG 2g.12gb   14    2/2         ~12 GiB
#     0  MIG 4g.24gb    0    1/1         ~24 GiB    <- whole card as a single MIG device

# 3. CREATE the GPU instances and their compute instances (-C). Repeat a profile ID to
#    make several slices of it. This carves the card into FOUR 1g slices (ID 19 x4):
nvidia-smi mig -cgi 19,19,19,19 -C
#    ...or two medium slices:  nvidia-smi mig -cgi 14,14 -C

# 4. Confirm the MIG devices now exist.
nvidia-smi -L
nvidia-smi mig -lgi          # list the created GPU instances
WARNING

profile 的ID、名称以及每个实例的显存都与硬件相关,而且 nvidia-smi mig -lgip 显示的名称可能与 driver 发布的 profile 属性不同——例如,A30 的一个 GPU-instance profile 在列表中显示为 2g.6gb,但在 DRA 中会暴露为 1g.6gb不要直接照抄这个示例。 请先在第 2 步读取你卡的真实 profile,然后(在下面)读取发布出来的 profile 字符串,并让你的 ResourceClaimTemplate 的 CEL 选择器与之匹配。

使用 nvidia-smi mig -cgi 创建的 MIG 实例并不总是能在 GPU 重置或节点重启后保留下来。请在重启后重新创建,或者通过 driver 的 MIG-config 工具 / 启动时 unit 自动化创建流程。

最后,重启 kubelet-plugin Pod,使其重新枚举该卡并发布 MIG ResourceSlice,然后读取 CEL 选择器必须匹配的确切 profile 字符串:

kubectl -n kube-system delete pod -l app.kubernetes.io/name=nvidia-dra-driver-gpu --field-selector spec.nodeName=<gpu-node>

kubectl get resourceslices -o json \
  | jq -r '.items[].spec.devices[].attributes."gpu.nvidia.com".profile.string' | sort -u
# 1g.6gb          <- use this exact string in mig-slice-resourceclaimtemplate.yaml
WARNING

一块物理 GPU 只能对应一个 device plugin。 NVIDIA DRA kubelet-plugin 和传统 device plugin(例如 HAMi 的 nvidia.com/gpualloc,或默认的 nvidia.com/gpu plugin)不能同时管理同一张卡——否则会造成重复分配。请为 DRA 专门保留一台节点(或特定 GPU),并确保其他 plugin 不会选择它。在另一 plugin 正在提供服务的卡上启用 MIG 模式,会干扰该 plugin 的工作负载。

故障排查

症状原因 / 修复
Pod 卡在 Pending,事件为 cannot allocate all claims没有 ResourceSlice 匹配该选择器。执行 kubectl get resourceslices -o yaml,检查 profile / 属性字符串;确认节点已打标签且 kubelet-plugin Pod 处于 Running 状态。
ResourceClaim 一直保持 pending(从未变为 allocatedGPU 没有处于 MIG 模式(针对 mig.nvidia.com),或者该 profile 的切片已经全部被预留。
kubelet-plugin CrashLoopBackOff,日志为 invalid CDI Spec: no devicesMIG 模式已开启,但没有创建 MIG 实例。请在节点上执行 nvidia-smi mig -cgi <ids> -C(见管理员部分),然后重启 plugin Pod。
nvidia-smi -i 0 -mig 1 显示 pending,模式始终无法变成 Enabled有其他进程占用了 GPU(CUDA workload,或 dcgm-exporter)。请停止它们 / 将监控 DaemonSet 移出该节点,然后重试;执行 GPU 重置(nvidia-smi -i 0 -r)或重启节点可最终完成卡住的切换。
TrainingRuntime 被拒绝:numProcPerNode ... no such overloadnumProcPerNode 必须是 autocpugpu,或者一个不加引号的整数——请使用 numProcPerNode: 1,而不是 "1"
must be no more than ... / 应用时出现 schema 错误API 版本不匹配——本指南面向 resource.k8s.io/v1(K8s 1.34 GA)。在 1.31–1.33 中,该 group 使用 v1beta1/v1beta2,其 requests 结构略有不同。
两个 GPU 资源相互争用 / 随机 CUDA 错误传统 device plugin 和 DRA plugin 都在管理这张卡。请参见上面的警告。
Trainer container 看到的是完整的 24 GiB,而不是切片你应用的是 shared-gpu-timeslice(完整 GPU)模板,或者 MIG 模式未开启。要获得真正的显存隔离切片,请在启用了 MIG 模式的 GPU 上使用 MIG 模板。

为了对整个流程进行可重复覆盖,c15_dra_gpu_slice.sh 用例会应用模板、runtime 和 TrainJob,并断言微调能够在切片内完成。如果集群中不存在 gpu.nvidia.com ResourceSlice,它会自动跳过,因此在仅使用 device-plugin 的集群上也会保持通过。