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.0 和 vCluster 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=true、cdi.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

前两个值必须分别为 true 和 false,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 dump 或 crio status config 检查实际生效配置。

GPU Operator 26.3 不再把 NVIDIA handler 设为默认运行时,后面的准入门禁以此为前提。如果节点被手工改成默认使用 NVIDIA handler,镜像内置的 NVIDIA_VISIBLE_DEVICES 不会出现在 Pod spec 中,准入策略就无法识别。遇到这种配置要停止,先按当前容器运行时的方式确认并恢复默认 handler。RuntimeClass 列表中如果还有 nvidia-cdi、nvidia-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 管理的 train 和 inference。任一项不符就先修正 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,在兄弟队列没有需求时最多可借用到 1;overQuotaWeight 决定同一优先级下超出保底部分的相对分配权重。队列字段 priority 控制队列间的分配顺序,不等于 Pod 的 Kubernetes PriorityClass。

应用并检查父子关系:

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

生产配置不要机械复制三等分。先根据最低吞吐、峰值时段和任务优先级确定 quota、limit、priority 与 overQuotaWeight,并把父队列 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 只读同步到租户集群;否则引用 train 或 inference 的 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-nlp、team-vision 和 team-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 必须为 false,vcluster.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.yaml 与 recommender-pod.yaml,把 Pod 名称分别改为 vision-batch-test 和 recommender-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-guard 以 Forbidden 拒绝,最后一条命令必须输出空数组或空值。若临时容器已出现,要停止上线并先修正策略。

根据 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.99。0.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-fraction 或 gpu-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 为 false、nvidia 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 的 blockNvidiaVisibleDevices 为 true,服务端试运行已将未申请 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