AI Tools
教程19 分钟2026年8月4日作者:AIGCDev

多个团队如何用 KAI Scheduler 和 vCluster 共享一张 GPU

多个可信的内部 AI 团队需要独立 Kubernetes API Server、基于角色的访问控制(RBAC)和自定义资源定义(CRD),同时共享同一张 GPU 时,可以用 vCluster 隔离团队控制面,用 KAI Scheduler 分配 GPU 调度配额。NVIDIA 在 2026 年 8 月 3 日发布的官方教程中,用 1 张 L40S 同时运行 3 个租户集群,演示了这条实施路径。

shared nodes 只提供逻辑隔离:租户仍共享主机内核、物理网络、存储设施和 GPU。启用真实 Node 同步后,选定 GPU 节点的 Node 对象会出现在每个 vCluster 中,因此不能把它描述成节点信息隔离。租户不可信时应从一开始选择 vCluster private nodes;它需要 vCluster Platform(免费模式可用),已创建的 shared nodes vCluster 不能原地迁移。NVIDIA L40S 规格页的“Multi-Instance GPU (MIG) Support”一项为“No”;必须硬隔离显存时,要先更换为支持 MIG 的 GPU,再重做资源和准入配置。

接受这些边界后,先验证 GPU 运行时,再安装 KAI、创建队列和 vCluster。准入、网络、回收或显存测试有一项失败,就停止上线。

shared nodes 只适合可信内部团队

NVIDIA 示例中的三个团队共享同一物理节点和 GPU,只隔离虚拟控制面与各自的命名空间工作负载视图。

需求 选择
可信内部团队需要独立 API Server、RBAC、CRD 和 cluster-admin vCluster shared nodes 可作为起点
团队需要保底 GPU 配额,空闲时允许其他团队借用 KAI Scheduler 分层 Queue
可信团队仍需限制租户间网络流量 启用 vCluster 在主集群管理的 NetworkPolicy,并验证容器网络接口(CNI)确实执行
租户之间不互信,需要节点、内核、网络和存储隔离 新建 vCluster private nodes;需要 vCluster Platform,不能从 shared nodes 原地切换
需要硬件级显存隔离 L40S 无法满足;更换为支持 MIG 的 GPU,再由调度器分配 MIG 资源
可以接受多个 CUDA 进程在同一设备上竞争算力,且不要求份额对应性能服务级别协议(SLA) 可评估 GPU sharing

上线前要证明以下结果:

  • 每个团队只能从自己的 vCluster 看到本团队的命名空间工作负载
  • 选定 Node 的可见字段符合已批准的脱敏规则
  • 三个队列获得约定配额,并能按策略借用和回收
  • 主集群 NetworkPolicy 能阻断跨租户和未批准的公网流量
  • 没有 RuntimeClass、GPU 环境变量或临时容器可以绕过 KAI Queue
  • 每个 GPU 测试容器中的 nvidia-smi 实际成功,不只是 Pod 显示 Running
  • L40S 的显存超限由应用限额或 HAMi 软件限额处理;需要硬隔离时已更换 GPU

固定复现版本和 CDI/NRI 契约

NVIDIA 2026-08-03 的教程使用以下组合:

组件 已验证版本
操作系统 Ubuntu 24.04.4 LTS
Kubernetes 发行版 MicroK8s 1.36.2
GPU 1 张 NVIDIA L40S
GPU Operator 26.3.3
KAI Scheduler 0.16.4
vCluster CLI 0.35.1

复现时固定使用这组版本。截至 2026-08-04,项目最新发布版已是 KAI Scheduler 0.17.0vCluster 0.36.1。“已验证组合”不等于“最新版”;升级前要阅读发布说明并在预生产环境回归。KAI 0.17.0 新增抢占延迟和基于动态资源分配(DRA)的扩展资源,升级后必须重新验证回收时序和主集群准入范围。

检查 CLI、节点、存储类和 GPU Operator Pod:

vcluster --version
kubectl get nodes -o wide
kubectl get storageclass
kubectl get pods -n gpu-operator-resources

vcluster --version 必须显示 0.35.1。不同发行版的 GPU Operator 命名空间可能是 gpu-operator。进入下一步前,GPU 节点必须为 Ready,vCluster 可用的默认 StorageClass 必须存在,NVIDIA device plugin(设备插件)、container toolkit(容器工具包)和 validator(验证器)不能持续失败。

这条复现路径固定使用 GPU Operator 26.3.3,运行时契约是 cdi.enabled=truecdi.nriPluginEnabled=false,并保留 nvidia RuntimeClass。GPU Operator 25.10.0 及以上默认使用容器设备接口(Container Device Interface,CDI),节点资源接口插件(Node Resource Interface plugin,NRI)仍是可选项。执行:

kubectl get clusterpolicy
kubectl get clusterpolicy cluster-policy \
  -o jsonpath='{.spec.cdi.enabled}{"\t"}{.spec.cdi.nriPluginEnabled}{"\n"}'
kubectl get runtimeclass \
  -o custom-columns=NAME:.metadata.name,HANDLER:.handler

前两个值必须分别为 truefalse,RuntimeClass 清单必须包含 nvidia。若 ClusterPolicy 名称不是 cluster-policy,用第一条命令返回的实际名称替换。CDI 为 false 时,先按 GPU Operator 26.3 CDI 文档修正并重新运行 validator。

固定的 MicroK8s 环境还要登录每个 GPU 节点,读取 containerd 合并后的生效配置:

sudo /snap/microk8s/current/bin/containerd \
  --config /var/snap/microk8s/current/args/containerd.toml \
  config dump | grep -E 'default_runtime_name[[:space:]]*='

输出的默认 runtime 必须是 runc 或另一个已审计的非 NVIDIA handler。命令无输出或显示 nvidia 时都要停止,核对节点实际的 containerd 路径和导入文件。其他 Kubernetes 发行版按 GPU Operator 故障排查文档使用 containerd config dumpcrio status config 检查实际生效配置。

GPU Operator 26.3 不再把 NVIDIA handler 设为默认运行时,后面的准入门禁以此为前提。如果节点被手工改成默认使用 NVIDIA handler,镜像内置的 NVIDIA_VISIBLE_DEVICES 不会出现在 Pod spec 中,准入策略就无法识别。遇到这种配置要停止,先按当前容器运行时的方式确认并恢复默认 handler。RuntimeClass 列表中如果还有 nvidia-cdinvidia-legacy 或自定义名称,也要记入变更清单;后面的策略会把任意显式 RuntimeClass 都送入队列门禁,避免别名漏网。

NRI 为 true 时要停在这里。NRI 会删除 nvidia RuntimeClass,而 KAI Scheduler 0.16.4 的 admission.gpuFractionRuntimeClassName 默认值正是 nvidia;如果继续,准入会给分片 GPU Pod 注入一个不存在的 RuntimeClass。当前设计还依赖“无 NRI 时 GPU 管理容器必须使用 nvidia RuntimeClass”这一条件关闭未记账访问,因此不能只把 KAI 的 RuntimeClass 设为空。必须使用 NRI 的集群需要另外的宿主机准入或运行时策略,不在这条固定路径的范围内。

NRI 若已开启,可以在确认没有其他工作负载依赖它后,用固定版本的 ClusterPolicy 关闭:

kubectl patch clusterpolicy cluster-policy --type merge \
  -p '{"spec":{"cdi":{"enabled":true,"nriPluginEnabled":false}}}'
kubectl get clusterpolicy cluster-policy \
  -o jsonpath='{.spec.cdi.enabled}{"\t"}{.spec.cdi.nriPluginEnabled}{"\n"}'
kubectl get runtimeclass nvidia

等待 container toolkit、device plugin 和 validator 恢复健康后再继续。KAI binder 将在安装时显式设置 cdiEnabled=true;GPU reservation Pod 仍不需要 binder.runtimeClassName。这两个 RuntimeClass 字段用途不同,不要混用。后续日志中的 NVML 指 NVIDIA Management Library(NVIDIA 管理库)。

若使用 MicroK8s,还要启用 DNS 和供 vCluster PVC 使用的存储:

microk8s enable dns
microk8s enable hostpath-storage

hostpath-storage 只适合这类单节点复现;多节点或生产集群应使用能满足故障恢复要求的动态 StorageClass,并跳过第二条命令。

安装启用 GPU sharing 的 KAI Scheduler

按照上面的运行时契约,通过开放容器倡议(Open Container Initiative,OCI)格式的 Helm Chart 安装 KAI Scheduler。除了 GPU sharing,还要阻止普通容器自行设置 NVIDIA_VISIBLE_DEVICES,并显式固定 CDI 与 RuntimeClass:

helm upgrade -i kai-scheduler \
  oci://ghcr.io/kai-scheduler/kai-scheduler/kai-scheduler \
  -n kai-scheduler --create-namespace \
  --version v0.16.4 \
  --set "global.gpuSharing=true" \
  --set "global.blockNvidiaVisibleDevices=true" \
  --set "binder.cdiEnabled=true" \
  --set "admission.gpuFractionRuntimeClassName=nvidia" \
  --set "defaultPriorityClasses.enabled=true"

安装后检查部署和 KAI Config:

kubectl get pods -n kai-scheduler
kubectl get configs.kai.scheduler kai-config -o yaml
kubectl get configs.kai.scheduler kai-config \
  -o jsonpath='{.spec.admission.gpuSharing}{"\t"}{.spec.admission.blockNvidiaVisibleDevices}{"\t"}{.spec.admission.gpuFractionRuntimeClassName}{"\t"}{.spec.binder.cdiEnabled}{"\n"}'
kubectl get priorityclass train inference

JSONPath 命令必须依次输出 true true nvidia true,最后一条命令必须找到 Chart 管理的 traininference。任一项不符就先修正 Helm values,不得用后面的 Pod 测试替代配置验收。

输出中应包含以下控制组件:

路径 组件
准入与分组 admission、pod-grouper、podgroup-controller
调度与绑定 scheduler、binder
配置与队列 operator、queue-controller

所有长期运行组件应为 Running。reservation Pod 会在共享 GPU 请求出现后创建,届时不应因 RuntimeClass 或 NVML 反复失败。工作负载若没有设置 schedulerName: kai-scheduler,会继续由默认 kube-scheduler 处理,不会进入 KAI 队列调度路径。

为组织设置保底配额和硬上限

KAI Scheduler 中,quota 是保证分配量,limit 是硬上限,limit: -1 表示不设上限。NVIDIA 示例把父队列配置为 quota: 1, limit: -1;单 GPU 环境会被物理容量自然限制,但复制到多 GPU 集群后,父队列可能使用超过 1 张 GPU。为明确限制组织最多使用 1 张 GPU,以下 create-queues.yaml 把父队列的 limit 设为 1

apiVersion: scheduling.run.ai/v2
kind: Queue
metadata:
  name: ml-org
spec:
  resources:
    gpu: { quota: 1, limit: 1, overQuotaWeight: 1 }
---
apiVersion: scheduling.run.ai/v2
kind: Queue
metadata:
  name: team-nlp
spec:
  parentQueue: ml-org
  priority: 100
  resources:
    gpu: { quota: 0.33, limit: 1, overQuotaWeight: 1 }
---
apiVersion: scheduling.run.ai/v2
kind: Queue
metadata:
  name: team-vision
spec:
  parentQueue: ml-org
  priority: 100
  resources:
    gpu: { quota: 0.33, limit: 1, overQuotaWeight: 1 }
---
apiVersion: scheduling.run.ai/v2
kind: Queue
metadata:
  name: team-recommender
spec:
  parentQueue: ml-org
  priority: 100
  resources:
    gpu: { quota: 0.33, limit: 1, overQuotaWeight: 1 }

按照 KAI 0.16.4 Queue 文档,子队列各保底 0.33,在兄弟队列没有需求时最多可借用到 1overQuotaWeight 决定同一优先级下超出保底部分的相对分配权重。队列字段 priority 控制队列间的分配顺序,不等于 Pod 的 Kubernetes PriorityClass

应用并检查父子关系:

kubectl apply -f create-queues.yaml
kubectl get queues
kubectl describe queue ml-org

生产配置不要机械复制三等分。先根据最低吞吐、峰值时段和任务优先级确定 quotalimitpriorityoverQuotaWeight,并把父队列 limit 设置为组织实际允许使用的硬上限。

创建 vCluster 并限制 Node 信息暴露

vCluster 0.35.1 的 setOwner 只控制是否把主集群同步对象挂到 vCluster Service 下便于垃圾回收,不会把虚拟 Job、Deployment 或 ReplicaSet 的所有者链重建到主集群。当它为 true 时,KAI 可能把 vCluster Service 误当成物理 Pod 的上层工作负载;vcluster.yaml 把它设为 false,让后面的裸 Pod 各自作为独立调度单元。

同一份配置还使用 vCluster 的主集群管理型隔离策略。Pod Security Standard(Pod 安全标准,PSS)的 baseline 级别拒绝特权容器和 hostPath 等越界能力;NetworkPolicy 默认只允许同一 vCluster 的工作负载、虚拟控制面和 DNS 流量,这里另外关闭工作负载公网出站。ResourceQuota 和 LimitRange 限制 CPU、内存、临时存储和对象数,避免 KAI 只限 GPU 时出现另一类噪声邻居。将以下配置保存为 vcluster.yaml

experimental:
  syncSettings:
    setOwner: false
policies:
  podSecurityStandard: baseline
  networkPolicy:
    enabled: true
    workload:
      publicEgress:
        enabled: false
  resourceQuota:
    enabled: true
    quota:
      requests.cpu: "4"
      requests.memory: 8Gi
      requests.storage: 20Gi
      requests.ephemeral-storage: 20Gi
      limits.cpu: "8"
      limits.memory: 16Gi
      limits.ephemeral-storage: 40Gi
      services.nodeports: 0
      services.loadbalancers: 0
      count/pods: 40
      count/services: 30
      count/secrets: 100
      count/configmaps: 100
      count/persistentvolumeclaims: 10
  limitRange:
    enabled: true
    default:
      cpu: "2"
      memory: 2Gi
      ephemeral-storage: 4Gi
    defaultRequest:
      cpu: 100m
      memory: 256Mi
      ephemeral-storage: 1Gi
sync:
  toHost:
    networkPolicies:
      enabled: false
    resourceClaims:
      enabled: false
  fromHost:
    runtimeClasses:
      enabled: false
    nodes:
      enabled: true
      clearImageStatus: true
      selector:
        labels:
          nvidia.com/gpu.present: "true"
    priorityClasses:
      enabled: true

上面的数字只是单节点复现上限,不是根据当前机器推导出的生产容量。ResourceQuota 会同时计入 vCluster 控制面 Pod 和 PVC。创建前先查看 GPU 节点的 allocatable 与现有用量,为系统组件和故障恢复保留空间,再按团队实际需求修改每个 quota:

kubectl get nodes -l nvidia.com/gpu.present=true \
  -o custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory,EPHEMERAL:.status.allocatable.ephemeral-storage
kubectl top nodes

kubectl top 依赖 Metrics Server;不可用时要从现有监控系统取同等数据。三个租户的请求上限加上主集群系统余量,不得超过节点可分配容量。

这里不用 NVIDIA 示例的 selector.all: true。根据 vCluster Node 同步文档all: true 会让每个租户看到主集群全部 Node;上面的标签选择器只同步 GPU 节点,并把同一选择器加到同步至主集群的 Pod。clearImageStatus: true 会清除 status.images,但选中 Node 的名称、调度标签与污点、容量和部分运行时信息仍可能对所有三个 vCluster 可见。若这些信息也不能暴露,应使用 patch 进一步脱敏、关闭真实 Node 同步,或改用 private nodes。

priorityClasses.enabled: true 把主集群 PriorityClass 只读同步到租户集群;否则引用 traininference 的 Pod 无法同步到主集群。它默认同步全部 PriorityClass;需要缩小范围时按 vCluster PriorityClass 文档增加 selector,并确认两个 PriorityClass 仍在选择范围内。

sync.fromHost.runtimeClasses.enabled: false 不向租户暴露主集群 RuntimeClass。分片 GPU Pod 的 nvidia RuntimeClass 由主集群 KAI 准入注入,租户无需直接选择宿主机 handler。RuntimeClass 绕过由后面的主集群服务端试运行验证。

policies.networkPolicy 由 vCluster Helm release 在主集群管理,不依赖租户管理员保留某个租户内对象。sync.toHost.networkPolicies.enabled: false 阻止租户创建的 NetworkPolicy 同步到主集群;这些虚拟对象不能被当成已生效的实际网络边界。主集群策略默认允许同一 vCluster 的工作负载互通;Kubernetes NetworkPolicy 是叠加允许模型,即使开启同步,一个租户内 default-deny 也不能取消已有的主集群管理型允许规则。需要租户内应用分段时,应使用 CNI 支持的管理员策略或重新设计主集群规则,不要在当前配置上直接开启 NetworkPolicy 同步。

保存配置后创建三个租户集群:

vcluster create team-nlp \
  --namespace vcluster-team-nlp \
  --values vcluster.yaml --connect=false
vcluster create team-vision \
  --namespace vcluster-team-vision \
  --values vcluster.yaml --connect=false
vcluster create team-recommender \
  --namespace vcluster-team-recommender \
  --values vcluster.yaml --connect=false
vcluster list
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl get priorityclass train inference
kubectl get resourcequota,limitrange,networkpolicy -n vcluster-team-nlp
kubectl get resourcequota,limitrange,networkpolicy -n vcluster-team-vision
kubectl get resourcequota,limitrange,networkpolicy -n vcluster-team-recommender

setOwner: false 不是通用默认建议。vCluster 会把虚拟 Pod 的 owner references 序列化到物理 Pod 注解,但 KAI 0.16.4 pod-grouper 遍历的是主集群中的真实 metadata.ownerReferences 和上层对象。验收只覆盖裸 Pod,不能把结果外推到 Job、Deployment、PyTorchJob 或 gang scheduling。这些工作负载需要单独设计并验证主集群 PodGroup 契约。

关闭 owner reference 也会失去部分垃圾回收保障。组件升级、vCluster 删除或重建后,要检查主集群对应命名空间是否存在孤立 Pod 或 PodGroup,并重新验证裸 Pod 仍没有物理 owner reference:

kubectl get pods,podgroups -n vcluster-team-nlp
kubectl get pods,podgroups -n vcluster-team-vision
kubectl get pods,podgroups -n vcluster-team-recommender

再从 NLP vCluster 提交一个特权 hostPath Pod,验证 baseline 确实阻止它同步到主集群。将以下清单保存为 invalid-privileged-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: invalid-privileged-hostpath
spec:
  containers:
    - name: test
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      securityContext:
        privileged: true
      volumeMounts:
        - name: host-dev
          mountPath: /host-dev
  volumes:
    - name: host-dev
      hostPath:
        path: /dev
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl get namespace default \
  -L pod-security.kubernetes.io/enforce
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl apply -f invalid-privileged-pod.yaml
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl get pod invalid-privileged-hostpath
kubectl logs -n vcluster-team-nlp statefulset/team-nlp \
  -c syncer --since=5m
kubectl get pods -n vcluster-team-nlp -o name | \
  grep -F invalid-privileged-hostpath
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl delete -f invalid-privileged-pod.yaml

第一条命令必须显示 baseline。特权 Pod 通常会被租户 API 的 Pod Security Admission 直接拒绝,此时租户侧 kubectl get 应返回 NotFound。如果租户 API 先接受了对象,syncer 必须记录 Pod Security Standard 拒绝。无论走哪条路径,主集群 grep 都必须无输出并返状态码 1;若物理 Pod 已创建,立即停止上线。租户 API 直接拒绝时,最后的删除命令返回 NotFound 属于预期结果。

验证主集群 NetworkPolicy 确实隔离租户

vCluster shared nodes 指南提醒,部分 CNI 会接受 NetworkPolicy 对象却不执行。下面用 Vision 内部访问作为可达基线,再从 NLP 访问同一 Pod IP,证明跨 vCluster 流量被主集群策略阻断:

vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl run np-web --image=nginx:1.29-alpine --port=80
vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl run np-client --image=busybox:1.36 --restart=Never -- sleep 3600
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl run np-client --image=busybox:1.36 --restart=Never -- sleep 3600
vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl wait --for=condition=Ready pod/np-web pod/np-client --timeout=120s
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl wait --for=condition=Ready pod/np-client --timeout=120s
VISION_POD_IP="$(kubectl get pods -n vcluster-team-vision \
  -l vcluster.loft.sh/managed-by=team-vision,run=np-web \
  -o jsonpath='{.items[0].status.podIP}')"
test -n "$VISION_POD_IP"
vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl exec np-client -- wget -qO- -T 3 "$VISION_POD_IP"

同租户基线必须返回 NGINX 页面。检查公网出站前,先证明 NLP Pod 可以通过 vCluster DNS 解析公网域名;否则 wget 失败只能说明 DNS 坏了,不能证明 egress 被拦截:

vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl exec np-client -- nslookup example.com
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl exec np-client -- wget -qO- -T 3 "$VISION_POD_IP"
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl exec np-client -- wget -qO- -T 3 http://example.com

nslookup 必须返回地址,后两条命令则必须无内容且以非零状态结束,通常表现为超时。任一 wget 返回内容,都说明 CNI 没有执行预期策略或 egress 配置被改写,shared nodes 的网络边界不能通过验收。工作负载需要访问模型仓库或外部 API 时,要在 policies.networkPolicy.workload.egress 中按目标 CIDR 和端口增加最小允许,不要把 publicEgress 整体打开。镜像拉取由节点容器运行时执行,不受 Pod egress 规则影响。

验证完成后删除三个测试 Pod:

vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl delete pod np-client
vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl delete pod np-web np-client

在主集群强制绑定租户与队列

拥有 vCluster cluster-admin 权限的团队可以自行填写队列标签,所以队列约束必须由主集群执行。MicroK8s 1.36.2 已支持稳定版 ValidatingAdmissionPolicy(验证准入策略)。以下策略把 GPU 访问定义为任一 KAI 分片注解、整卡或 MIG 资源、显式 RuntimeClass,或 NVIDIA_VISIBLE_DEVICES 环境变量;它还拦截 GPU Pod 的临时容器子资源。任意 RuntimeClass 都纳入门禁,是为了失败关闭未知的 NVIDIA handler 别名。需要 Kata 或 gVisor 等非 GPU RuntimeClass 时,应先审计 handler,再为明确的安全名称设计例外。vCluster 控制面不使用这些路径,因此不会被匹配。

vCluster 0.35.1 会把实际 vCluster 名写入同步对象的管理标签。本例的标签值分别是 team-nlpteam-visionteam-recommender,不是固定值 vcluster。将策略和 Binding 保存为 queue-guard.yaml

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: kai-vcluster-queue-guard
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods", "pods/ephemeralcontainers"]
  matchConditions:
    - name: gpu-access
      expression: >-
        (has(object.metadata.annotations) &&
         ('gpu-fraction' in object.metadata.annotations ||
          'gpu-memory' in object.metadata.annotations)) ||
        object.spec.?runtimeClassName.orValue('') != '' ||
        object.spec.containers.exists(c,
          (has(c.resources.limits) && c.resources.limits.exists(r,
            r == 'nvidia.com/gpu' || r.startsWith('nvidia.com/mig-'))) ||
          (has(c.resources.requests) && c.resources.requests.exists(r,
            r == 'nvidia.com/gpu' || r.startsWith('nvidia.com/mig-'))) ||
          c.env.exists(e, e.name == 'NVIDIA_VISIBLE_DEVICES')) ||
        (has(object.spec.initContainers) && object.spec.initContainers.exists(c,
          (has(c.resources.limits) && c.resources.limits.exists(r,
            r == 'nvidia.com/gpu' || r.startsWith('nvidia.com/mig-'))) ||
          (has(c.resources.requests) && c.resources.requests.exists(r,
            r == 'nvidia.com/gpu' || r.startsWith('nvidia.com/mig-'))) ||
          c.env.exists(e, e.name == 'NVIDIA_VISIBLE_DEVICES'))) ||
        (has(object.spec.ephemeralContainers) &&
          object.spec.ephemeralContainers.exists(c,
            c.env.exists(e, e.name == 'NVIDIA_VISIBLE_DEVICES')))
  validations:
    - expression: >-
        has(object.metadata.labels) &&
        'vcluster.loft.sh/managed-by' in object.metadata.labels &&
        ((object.metadata.namespace == 'vcluster-team-nlp' &&
          object.metadata.labels['vcluster.loft.sh/managed-by'] == 'team-nlp') ||
         (object.metadata.namespace == 'vcluster-team-vision' &&
          object.metadata.labels['vcluster.loft.sh/managed-by'] == 'team-vision') ||
         (object.metadata.namespace == 'vcluster-team-recommender' &&
          object.metadata.labels['vcluster.loft.sh/managed-by'] == 'team-recommender'))
      message: "GPU Pod 必须带有与主集群命名空间一致的 vCluster 管理标签"
      reason: Forbidden
    - expression: >-
        has(object.metadata.labels) &&
        'kai.scheduler/queue' in object.metadata.labels &&
        ((object.metadata.namespace == 'vcluster-team-nlp' &&
          object.metadata.labels['kai.scheduler/queue'] == 'team-nlp') ||
         (object.metadata.namespace == 'vcluster-team-vision' &&
          object.metadata.labels['kai.scheduler/queue'] == 'team-vision') ||
         (object.metadata.namespace == 'vcluster-team-recommender' &&
          object.metadata.labels['kai.scheduler/queue'] == 'team-recommender'))
      message: "GPU Pod 的 kai.scheduler/queue 必须与所属 vCluster 一致"
      reason: Forbidden
    - expression: "object.spec.?schedulerName.orValue('') == 'kai-scheduler'"
      message: "GPU Pod 必须设置 schedulerName: kai-scheduler"
      reason: Forbidden
    - expression: >-
        !has(object.spec.ephemeralContainers) ||
        size(object.spec.ephemeralContainers) == 0
      message: "GPU Pod 禁止添加临时容器;请创建单独的调试 Pod"
      reason: Forbidden
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: kai-vcluster-queue-guard
spec:
  policyName: kai-vcluster-queue-guard
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: In
          values:
            - vcluster-team-nlp
            - vcluster-team-vision
            - vcluster-team-recommender

租户身份映射现在位于 validations,不是 matchConditions。标签缺失、vCluster 改名或值漂移时,GPU 访问会被拒绝,而不是因匹配条件为 false 跳过整个策略。修改 vCluster 名或主集群命名空间时,要同步更新 managed-by 值、object.metadata.namespace 和 Binding 的 values。Kubernetes 低于 1.30 时,应改用 admission webhook、Kyverno 或 Gatekeeper 在主集群提供同等约束。

固定路径中 NRI 必须为 falsevcluster.yaml 也显式把 sync.toHost.resourceClaims.enabled 固定为 false。策略预先匹配 nvidia.com/mig-* 只是为了防止该资源无门禁进入,不代表 L40S 可用 MIG。更换 GPU、开启 ResourceClaim 或升级到 KAI 0.17.0 的 DRA 扩展资源前,要同时重做 vCluster 同步、Queue 资源、GPU 访问匹配和负向测试。

这个策略只在 Pod 新建或更新时生效,不会追溯修复已有对象。在主集群策略、Binding 和下面的负向测试全部通过前,不要向团队交付 vCluster kubeconfig;上线时还要查找三个主集群命名空间里的已有 GPU Pod。ValidatingAdmissionPolicy 和 Binding 是主集群对象,应用 RBAC 限制修改和删除权限,并为这两类操作保留审计日志。

再将以下六个负向用例保存为 invalid-gpu-pods.yaml,分别测试错队列、缺队列、默认调度器、缺租户身份、显式 RuntimeClass 和 GPU 环境变量绕过:

apiVersion: v1
kind: Pod
metadata:
  name: invalid-wrong-queue
  namespace: vcluster-team-nlp
  labels:
    vcluster.loft.sh/managed-by: team-nlp
    kai.scheduler/queue: team-vision
  annotations:
    gpu-fraction: "0.10"
spec:
  schedulerName: kai-scheduler
  containers:
    - name: test
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
---
apiVersion: v1
kind: Pod
metadata:
  name: invalid-missing-queue
  namespace: vcluster-team-nlp
  labels:
    vcluster.loft.sh/managed-by: team-nlp
  annotations:
    gpu-fraction: "0.10"
spec:
  schedulerName: kai-scheduler
  containers:
    - name: test
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
---
apiVersion: v1
kind: Pod
metadata:
  name: invalid-default-scheduler
  namespace: vcluster-team-nlp
  labels:
    vcluster.loft.sh/managed-by: team-nlp
    kai.scheduler/queue: team-nlp
  annotations:
    gpu-fraction: "0.10"
spec:
  containers:
    - name: test
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
---
apiVersion: v1
kind: Pod
metadata:
  name: invalid-missing-managed-by
  namespace: vcluster-team-nlp
  labels:
    kai.scheduler/queue: team-nlp
  annotations:
    gpu-fraction: "0.10"
spec:
  schedulerName: kai-scheduler
  containers:
    - name: test
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
---
apiVersion: v1
kind: Pod
metadata:
  name: invalid-runtime-bypass
  namespace: vcluster-team-nlp
  labels:
    vcluster.loft.sh/managed-by: team-nlp
    kai.scheduler/queue: team-nlp
spec:
  runtimeClassName: nvidia
  containers:
    - name: test
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
---
apiVersion: v1
kind: Pod
metadata:
  name: invalid-visible-devices-bypass
  namespace: vcluster-team-nlp
  labels:
    vcluster.loft.sh/managed-by: team-nlp
    kai.scheduler/queue: team-nlp
spec:
  containers:
    - name: test
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      env:
        - name: NVIDIA_VISIBLE_DEVICES
          value: all

应用策略后,先检查 CEL 类型,再做主集群服务端试运行:

kubectl apply -f queue-guard.yaml
kubectl get validatingadmissionpolicy kai-vcluster-queue-guard -o yaml
kubectl apply --dry-run=server -f invalid-gpu-pods.yaml

输出中必须出现 status.typeChecking,表示类型检查已经完成;expressionWarnings 应缺省或为 []。如果 status 还没生成,等待 API Server 完成检查后重新执行 kubectl get,不要直接进入 Pod 测试。任何警告都必须先修正。六个 Pod 都应被 kai-vcluster-queue-guard 拒绝,服务端试运行不会留下对象。invalid-runtime-bypass 不设置 GPU 资源或环境变量,用它单独证明 RuntimeClass 本身就会进入门禁。

还要证明 KAI 会把已进入它准入路径、但没有申请 GPU 的容器改为 NVIDIA_VISIBLE_DEVICES=void。将以下清单保存为 blocked-visible-devices.yaml

apiVersion: v1
kind: Pod
metadata:
  name: blocked-visible-devices
  namespace: vcluster-team-nlp
  labels:
    vcluster.loft.sh/managed-by: team-nlp
    kai.scheduler/queue: team-nlp
spec:
  schedulerName: kai-scheduler
  runtimeClassName: nvidia
  containers:
    - name: test
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      env:
        - name: NVIDIA_VISIBLE_DEVICES
          value: all
kubectl apply --dry-run=server -f blocked-visible-devices.yaml \
  -o jsonpath='{.spec.containers[0].env[?(@.name=="NVIDIA_VISIBLE_DEVICES")].value}{"\n"}'

输出必须是 void。如果仍是 all、请求被接受却没有发生改写,或 KAI webhook 没有响应,都不能继续。

主集群试运行只能验证准入对物理 Pod 的处理,还要从真实 vCluster 提交错队列和 GPU 环境变量绕过两个用例。将以下清单保存为 invalid-tenant-pods.yaml,不要手工添加 vCluster 管理标签:

apiVersion: v1
kind: Pod
metadata:
  name: invalid-tenant-wrong-queue
  labels:
    kai.scheduler/queue: team-vision
  annotations:
    gpu-fraction: "0.10"
spec:
  schedulerName: kai-scheduler
  containers:
    - name: test
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
---
apiVersion: v1
kind: Pod
metadata:
  name: invalid-tenant-visible-devices-bypass
  labels:
    kai.scheduler/queue: team-nlp
spec:
  containers:
    - name: test
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      env:
        - name: NVIDIA_VISIBLE_DEVICES
          value: all

从 NLP vCluster 提交后,租户 API 可能先接受对象;syncer 会添加 managed-by: team-nlp,主集群策略随后分别因队列不符和缺少 KAI scheduler 拒绝物理 Pod。检查控制面日志,并确认主集群没有对应 Pod:

vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl apply -f invalid-tenant-pods.yaml
kubectl logs -n vcluster-team-nlp statefulset/team-nlp \
  -c syncer --since=5m
kubectl get pods -n vcluster-team-nlp -o name | \
  grep -E 'invalid-tenant-(wrong-queue|visible-devices-bypass)'
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl delete -f invalid-tenant-pods.yaml

等待 syncer 完成一次同步后,日志中必须能找到两个 Pod 各自对应的 kai-vcluster-queue-guard 准入拒绝,grep 应无输出并以状态码 1 结束。syncer 重试时可能产生多条同名记录,不要用日志总数做断言。租户侧 kubectl apply 返回成功不代表主集群已经创建物理 Pod。

提交用于借用和回收验证的 GPU 测试 Pod

以下 Pod 只测试调度、借用和回收,不是生产推理服务。它使用可抢占的 train PriorityClass(值为 50)。生产推理应使用非抢占的 inference(值为 125),并把请求控制在队列保底内;KAI 0.16.4 规定优先级值不低于 100 的工作负载不能借用超额配额,详见工作负载优先级文档。清单中的公开镜像标签用于方便复现;生产环境要换成已扫描、按 digest 锁定的内部镜像。将清单保存为 nlp-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: nlp-batch-test
  labels:
    kai.scheduler/queue: team-nlp
  annotations:
    gpu-fraction: "0.33"
spec:
  schedulerName: kai-scheduler
  priorityClassName: train
  tolerations:
    - key: nvidia.com/gpu
      operator: Exists
      effect: NoSchedule
  containers:
    - name: cuda-test
      image: nvidia/cuda:12.4.0-base-ubuntu22.04
      command: ["bash", "-c", "nvidia-smi -L && nvidia-smi && sleep infinity"]
  nodeSelector:
    nvidia.com/gpu.present: "true"

复制 nlp-pod.yaml 生成 vision-pod.yamlrecommender-pod.yaml,把 Pod 名称分别改为 vision-batch-testrecommender-batch-test,并修改 kai.scheduler/queue,再提交到对应 vCluster:

vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl apply -f nlp-pod.yaml
vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl apply -f vision-pod.yaml
vcluster connect team-recommender --namespace vcluster-team-recommender -- \
  kubectl apply -f recommender-pod.yaml

三个 Pod 不仅要进入 Ready,日志中的两次 nvidia-smi 也必须成功。固定环境只有一张 L40S,所以三份 nvidia-smi -L 输出还应显示同一 GPU UUID。任一命令超时、Pod 进入 CrashLoopBackOff、日志无 GPU 清单或出现 NVML 错误,都算验收失败:

vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl wait --for=condition=Ready pod/nlp-batch-test --timeout=180s
vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl wait --for=condition=Ready pod/vision-batch-test --timeout=180s
vcluster connect team-recommender --namespace vcluster-team-recommender -- \
  kubectl wait --for=condition=Ready pod/recommender-batch-test --timeout=180s
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl logs pod/nlp-batch-test
vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl logs pod/vision-batch-test
vcluster connect team-recommender --namespace vcluster-team-recommender -- \
  kubectl logs pod/recommender-batch-test

接着在主集群确认真实管理标签和队列标签同时存在,并证明裸 Pod 没有物理 owner reference:

kubectl get pods -n vcluster-team-nlp \
  -l vcluster.loft.sh/managed-by=team-nlp --show-labels
kubectl get pods -n vcluster-team-nlp \
  -l vcluster.loft.sh/managed-by=team-nlp,kai.scheduler/queue=team-nlp \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.ownerReferences}{"\n"}{end}'

第二条命令中,nlp-batch-test 后的 owner references 必须为空。出现 vCluster Service 或其他 owner 时,setOwner: false 没有按预期生效;裸 Pod 的调度结论也随之失效。

物理 GPU Pod 已存在后,再直接调用主集群的 ephemeralcontainers 子资源,验证准入策略不允许在 GPU Pod 中新增 KAI 0.16.4 没有处理的调试容器:

HOST_NLP_POD="$(kubectl get pods -n vcluster-team-nlp \
  -l vcluster.loft.sh/managed-by=team-nlp,kai.scheduler/queue=team-nlp \
  -o jsonpath='{.items[0].metadata.name}')"
test -n "$HOST_NLP_POD"
kubectl debug -n vcluster-team-nlp "$HOST_NLP_POD" \
  --image=nvidia/cuda:12.4.0-base-ubuntu22.04 \
  --target=cuda-test -- nvidia-smi
kubectl get pod -n vcluster-team-nlp "$HOST_NLP_POD" \
  -o jsonpath='{.spec.ephemeralContainers}{"\n"}'

kubectl debug 必须被 kai-vcluster-queue-guardForbidden 拒绝,最后一条命令必须输出空数组或空值。若临时容器已出现,要停止上线并先修正策略。

根据 KAI 0.16.4 GPU sharing 文档gpu-fraction: "0.33" 表示调度器按设备显存的 33% 记录请求,并允许总请求不超过设备容量的 Pod 共置。KAI 默认不强制实际显存上限,Pod 仍可能使用超过请求值的显存。

gpu-fraction 不保证 33% 的 GPU 核心、时间片、吞吐或延迟,也不是性能 SLA。NVIDIA 的 GPU Operator 26.3 时间切片文档同样说明,共享请求数量不代表等比例算力。

验证租户视图、队列回收和显存边界

租户视图和 Node 暴露范围

三个 vCluster 应各自只显示本团队的命名空间工作负载:

vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl get pods -A -o wide
vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl get pods -A -o wide
vcluster connect team-recommender --namespace vcluster-team-recommender -- \
  kubectl get pods -A -o wide

Node 不是租户私有对象。每个 vCluster 都应只看到标签选择器选中的 GPU 节点,且 status.images 为空;逐项评估其他标签、污点、容量和运行时字段是否可以暴露:

vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl get nodes -o yaml
vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl get nodes -o yaml
vcluster connect team-recommender --namespace vcluster-team-recommender -- \
  kubectl get nodes -o yaml

只要任一敏感 Node 字段超出已批准范围,就停止上线并调整 patch、关闭真实 Node 同步或切换 private nodes。

主集群落点和队列分配

检查物理 Pod、vCluster 管理标签和三个队列:

kubectl get pods -A -o wide --show-labels
kubectl get pods -n kai-resource-reservation
kubectl describe queue team-nlp
kubectl describe queue team-vision
kubectl describe queue team-recommender

NVIDIA 官方示例中,三个队列各显示 330m GPU requested 和 allocated。不要把 330m 当成固定结果;实际 Pod 注解、Queue 状态、调度事件和 reservation Pod 一致才算通过。

用额外请求验证借用和回收

额外请求才能证明队列确实借用了空闲配额。先将以下可抢占 Pod 保存为 nlp-burst.yaml

apiVersion: v1
kind: Pod
metadata:
  name: nlp-burst
  labels:
    kai.scheduler/queue: team-nlp
  annotations:
    gpu-fraction: "0.66"
spec:
  schedulerName: kai-scheduler
  priorityClassName: train
  tolerations:
    - key: nvidia.com/gpu
      operator: Exists
      effect: NoSchedule
  containers:
    - name: nlp-burst
      image: nvidia/cuda:12.4.0-base-ubuntu22.04
      command: ["bash", "-c", "nvidia-smi -L && nvidia-smi && sleep infinity"]
  nodeSelector:
    nvidia.com/gpu.present: "true"

停止另外两个团队的 0.33 Pod,再提交这个 0.66 Pod。只停止其他 Pod 不会让原有 0.33 请求自动扩大,不能单独作为借用证据。

vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl delete -f vision-pod.yaml
vcluster connect team-recommender --namespace vcluster-team-recommender -- \
  kubectl delete -f recommender-pod.yaml
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl apply -f nlp-burst.yaml
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl wait --for=condition=Ready pod/nlp-burst --timeout=180s
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl logs pod/nlp-burst
kubectl describe queue team-nlp

突发 Pod 也必须进入 Ready,且日志显示 GPU 清单和完整状态。借用成功时,NLP 队列的总请求约为 0.99,明显高于 0.33 保底但不超过 1 上限。接着恢复 Vision 的 0.33 Pod:

vcluster connect team-vision --namespace vcluster-team-vision -- \
  kubectl apply -f vision-pod.yaml
kubectl get pods -A -o wide
kubectl describe queue team-nlp
kubectl describe queue team-vision
kubectl get events -A --sort-by=.lastTimestamp

低于保底的 Vision 队列应在团队事先约定的调度服务目标(SLO)内重新获得 0.33,NLP 也不应长期保持 0.990.66 Pod 是不可拆分的调度单元,调度器可能驱逐它,也可能让它重新等待;验收对象是 Queue 状态、Pod 状态和事件,不是某个固定受害 Pod 或通用秒数。KAI 的回收依据与层级例外见 fair-share 文档

Vision 通过后,恢复 Recommender 并重复检查。Recommender 也应在约定 SLO 内获得 0.33;若任一保底队列持续等待,回收或饥饿测试失败。最后删除突发 Pod,恢复三队列各 0.33 的基线:

vcluster connect team-recommender --namespace vcluster-team-recommender -- \
  kubectl apply -f recommender-pod.yaml
kubectl describe queue team-nlp
kubectl describe queue team-recommender
kubectl get events -A --sort-by=.lastTimestamp
vcluster connect team-nlp --namespace vcluster-team-nlp -- \
  kubectl delete -f nlp-burst.yaml

显存和故障边界

运行一个逐步增加显存占用的测试程序,记录应用超过约定值时的行为。KAI 默认配置只做调度记账,不会阻止进程超过 gpu-fractiongpu-memory 请求;同卡工作负载可能因此显存不足(OOM)或互相影响。

可信团队若需要独立于应用配置的显存软件限额,可评估 KAI 0.16.4 的 HAMi 集成。它通过 kai-resource-isolator 注入 HAMi-core,以 CUDA 调用拦截方式限制容器显存;上线前必须验证 CUDA、镜像和 LD_PRELOAD 兼容性。HAMi 仍是软件隔离,不提供 MIG 的硬件故障域或安全边界。

这套 L40S 环境不存在“原地切换 MIG”的选项。不接受跨租户显存和算力干扰时,只能改为独占 L40S,或更换为支持 MIG 的 GPU。更换硬件后,要重新定义 GPU Operator/MIG Manager、Queue 资源、工作负载请求、vCluster 同步和主集群准入;当前准入策略只是预先拦截 nvidia.com/mig-*,没有验证 MIG 路径。

上线前检查表

  • 租户是彼此信任的内部团队;否则已通过 vCluster Platform 新建 private nodes 集群
  • GPU Operator 正常运行;CDI 为 true、NRI 为 falsenvidia RuntimeClass 存在,NVIDIA handler 不是节点默认运行时,reservation Pod 无 RuntimeClass 或 NVML 错误
  • 上表列出的 KAI 控制组件全部健康,固定版本及升级目标已在预生产验证
  • 父队列 limit 不超过组织实际 GPU 预算,子队列有明确的保底、上限、权重和优先级
  • vCluster 已启用 baseline、ResourceQuota、LimitRange 和主集群管理的 NetworkPolicy,数值已按节点容量重算
  • setOwner: false 已经用物理 owner references 验证;结论仅适用于裸 Pod,删除或升级后无孤立 Pod/PodGroup
  • 主集群 CNI 能保持同 vCluster 基线可达,并阻断跨 vCluster 和未批准的公网流量
  • 准入策略已覆盖队列、身份、调度器、RuntimeClass 别名、GPU 资源、环境变量和临时容器;六个主集群与两个 vCluster 负向用例均被拒绝
  • KAI 的 blockNvidiaVisibleDevicestrue,服务端试运行已将未申请 GPU 容器的可见设备值改为 void
  • 每个团队只能看到自己的命名空间工作负载;共享 Node 的可见字段和 status.images 脱敏符合约定
  • 三个基线 Pod 和突发 Pod 的 nvidia-smi 日志均成功;借用、回收与饥饿场景有 Queue、Pod、Event 三类记录
  • 团队理解 gpu-fraction 不代表算力份额或性能 SLA
  • L40S 的应用限额或 HAMi 超限测试已通过;需要硬隔离时已改用独占 GPU 或更换硬件并重做 MIG 验收
  • ResourceClaims/DRA 保持关闭;若启用,已重做 vCluster 同步、Queue 资源、准入匹配和负向测试
  • 组件升级后已重新执行本检查表
gpu-infrastructurekuberneteskai-schedulervclusternvidia