多个可信的内部 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、nvidiaRuntimeClass 存在,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 资源、准入匹配和负向测试
- 组件升级后已重新执行本检查表