做 AI 平台的同学应该都有同感:集群从一张“通用加速卡”换成国产卡之后,最先难受的不是训练脚本,而是资源调度这一层。昆仑芯 P800 这个 AI 加速卡,单卡规格很能打,但如果只是把它当作普通设备塞进服务器,Kubernetes 根本不知道它存在。你没法在 Pod 里声明“我要一张 P800”,更别提按显存排队、隔离、计量。这篇文章就围绕“k8s 兼容昆仑芯 p800”这条主线,把设备插件、扩展资源、调度验证、常见排障整个链路讲清楚,适合正在做 AI 平台、容器云、基础设施的同学参考。
1. 为什么非要把昆仑芯P800交给K8s管
1.1 单卡上机容易,集群调度才难
很多人第一次接触昆仑芯 P800 时的直觉是:先装驱动,再用 Nvidia Container Toolkit 类似的思路把设备映射进容器。单机场景下这条路确实能跑通,插上卡、装好驱动、启动一个 docker 容器,把 /dev/xpu0 设备节点挂进去,再配一下库文件,就能跑推理了。
但到了多机集群,事情立刻变得复杂。训练团队有十几个任务要排队,有人要 1 张卡做调试,有人要 8 张卡做训练。如果靠人工去数机器、数卡,再给节点打标签、人工分配 IP,这套流程根本不具备可操作性。更麻烦的是资源碎片化:一个任务占了整台机器的卡,剩下的卡没人能用,或者另一批人不知道哪些卡是空闲的,最终结果就是大量算力闲置。
这正是 Kubernetes 要解决的问题。K8s 本身不只是容器编排,它的调度器天然支持“资源抽象”:把节点上的 CPU、内存、显存、设备卡这些资源统一上报,Pod 里声明自己需要多少资源,调度器自动找一台满足条件的节点把 Pod 放上去。昆仑芯 P800 要接入这个模型,就需要通过一种被 K8s 认可的方式把“卡”上报为资源。
1.2 用K8s管理P800到底解决了什么
第一,资源视图统一。以前算力平台可能有一批 N 卡机器,又有一批昆仑芯 P800 机器,平台上每个团队自己记着哪些机器能用。接入 K8s 之后,通过自定义资源名就能把 P800 卡数和卡型暴露给所有用户,前端展示、配额管理、排队系统都基于同一张资源列表。
第二,调度不只是“找一台机器”。K8s 调度器会同时检查 CPU、内存、加速卡数量等多个维度。比如某个训练任务 Requst 了 4 张昆仑芯 P800,同时要求内存 64GB,调度器同时满足两个条件,而不是像以前一样只分配节点再碰运气。
第三,可观测性。设备插件会上报健康状态,节点上的卡如果出现了异常,kubelet 能从资源池中移除。这个能力对生产平台太重要了,至少能让我们知道有节点上的卡掉了几张,而不是任务跑挂了才被人发现。
第四,多租户配额。K8s 有成熟的 ResourceQuota 机制,给算法团队的 namespace 设置“最多只能用 16 张 P800”,限制走 K8s 原生能力,不需要自己写一套资产记账系统。
1.3 我先给你一个结论
如果现在要在 K8s 集群里接昆仑芯 P800,标准路线是:安装厂商提供的底层驱动,再部署一个 device plugin 组件(DaemonSet),让插件通过 kubelet 上报节点上的卡数量,然后在容器运行时配合下把设备注入到 Pod 里。整个过程,和你集群里装 Nvidia Device Plugin 的体验非常类似,只是设备名、驱动路径、工具链换成昆仑芯自己的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先搞懂Device Plugin机制,后面才不会乱
2.1 扩展资源和CPU/内存有什么不一样
K8s 原生资源里,CPU 以 core 为单位,内存以字节为单位,这些资源都由操作系统内核统一管理,kubelet 通过 cgroups 就能做隔离。但昆仑芯 P800 这类设备卡,kubelet 并不知道它是什么,更不知道怎么给容器分配。为了不让 kubelet 为每一种硬件写死逻辑,K8s 从 1.8 版本开始引入了设备插件机制和扩展资源(Extended Resource)概念。
扩展资源的命名格式是 <vendor-domain>/<resource-name>,比如你的加速卡可以起名叫 kunlun.com/xpu。注意,它跟 CPU、内存这种“系统资源”有本质区别:普通资源的数量由 kubelet 自行统计,而扩展资源的数量只能由设备插件上报。调度器能根据数量来决定是否调度 Pod,但不会像对待 CPU 那样动态伸缩或者做软隔离。
你可以把扩展资源理解成一个“代金券”,Pod 声明需要几张 P800,调度器检查节点上的代金券是否充足,如果够就安排过去。但代金券怎么兑换成真正的设备访问权限,是设备插件和容器运行时共同负责的。
2.2 设备插件协议:看门人、中介和管家
设备插件本质上是一个长驻在节点上的进程,常以 DaemonSet 方式部署。它通过 Unix Socket 与 kubelet 通信,常见的协议有这么几个方法:
ListAndWatch:插件启动后,把节点上所有可用设备列表发给 kubelet,并持续监控设备健康状态。某张卡坏了,就是通过这个接口通知 kubelet,让调度器把这个资源从节点可分配数量里扣掉。Allocate:调度器把 Pod 调度到节点后,kubelet 会调用插件的 Allocate 方法,告诉它这个 Pod 需要哪几个设备 ID。插件返回一个响应,告诉 kubelet 应该给容器注入哪些环境变量、挂载哪些设备节点、需要做什么权限设置。GetPreferredAllocation:可选接口,主要用于设备选择优化。当 Pod 不指定具体设备 ID,只说要 N 张卡时,插件可以给调度器建议“优先选哪些卡”,比如避免多张卡挤在同一条 PCIe Switch 下。
用生活化的类比:插件是客栈的掌柜,掌管着楼下几间上房(P800 卡),kubelet 是账房先生。旅客(Pod)要住店,账房先生问掌柜还有几间房,掌柜用 ListAndWatch 实时报数;旅客来了,账房先生问掌柜安排哪间房,掌柜在 Allocate 里挑一间,把钥匙(设备节点)、房内设施说明(环境变量)都给准备好。
2.3 容器是怎么“摸到”那张卡的
有人以为是 K8s 把卡“切”了一块给容器,实际上昆仑芯 P800 在整个生命周期里都是一个独立的物理设备。容器拿到卡靠的是设备注入:Allocate 响应里通常会包含这样几类信息:
- 设备文件挂载:比如
/dev/xpu0、/dev/xpuctl等字符设备节点被映射进容器; - 动态库路径:比如把厂商的 driver、runtime 库目录挂载到容器
/usr/local/xxx,同时设置LD_LIBRARY_PATH; - cgroup 设备白名单:让容器具备对这些设备节点的读写权限;
- 环境变量:告诉容器内的应用它拥有几块卡、卡的设备 ID 是什么。
这些操作都是在 Pod 启动前的容器运行时配置阶段完成的。所以容器里跑的应用,不需要感知它背后是物理机还是虚拟机,它调用标准的厂商 API,就能正常使用 P800 的算力。
3. 实操:把昆仑芯P800接入K8s全流程
3.1 环境准备:先让机器“看到”卡
在动手写设备插件之前,先确认物理层驱动已经装好。这一步看着简单,实际坑不少。我用某测试集群的三台节点做验证,节点配置是单路服务器插了 8 张昆仑芯 P800。拿到机器后,先通过厂家驱动包安装驱动,如果是新卡还要注意固件版本。
安装完成后用厂商的命令行工具验证:
bash复制xpu-smi query
如果输出能列出 8 张卡,每张卡都有设备 ID、驱动版本、温度、功耗、显存使用量,说明驱动层面没有问题。如果这里就只有 0 张卡,那后面的 K8s 接入不用想,先排查硬件和驱动。
这里我特别提醒一点:不要在还没装驱动、没验卡的时候就去改 K8s。设备插件只是把驱动看到的设备上报给 K8s,它自己不会“变出”设备。我在一个模拟环境里见过某同学跳过驱动验证直接部署插件,节点始终上报 0 张卡,查了半天才发现是驱动没装好。
3.2 部署设备插件:一个DaemonSet的事
厂商通常提供已封装好的 device plugin 镜像和 K8s 部署清单。如果你所在环境拿不到官方镜像,也可以参考开源社区里自定义资源插件自己构建,但生产环境还是建议优先用厂商版本。
一个典型的部署清单长这样:
yaml复制apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kunlun-device-plugin
namespace: kube-system
labels:
app: kunlun-device-plugin
spec:
selector:
matchLabels:
app: kunlun-device-plugin
template:
metadata:
labels:
app: kunlun-device-plugin
spec:
tolerations:
- operator: Exists
hostNetwork: true
containers:
- name: device-plugin
image: registry.example.com/kunlun/k8s-device-plugin:v1.0
args:
- --enable-monitoring=true
- --resource-name=kunlun.com/xpu
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
securityContext:
privileged: true
volumeMounts:
- name: device-plugins
mountPath: /var/lib/kubelet/device-plugins
- name: xpu-driver
mountPath: /usr/local/kunlun
readOnly: true
- name: dev
mountPath: /dev
volumes:
- name: device-plugins
hostPath:
path: /var/lib/kubelet/device-plugins
- name: xpu-driver
hostPath:
path: /usr/local/kunlun
- name: dev
hostPath:
path: /dev
几个关键点:
privileged: true:设备插件需要访问宿主机设备文件和驱动目录。生产安全要求更严格的话,可以改用更细粒度的权限配置,但调试阶段先跑通。hostPath /var/lib/kubelet/device-plugins:这是插件与 kubelet 进程通信 socket 存放目录,绝不能改。/dev目录挂载:让插件在容器内看到宿主机所有设备节点。如果你担心安全问题,可以只挂载/dev/xpu*具体路径,但要注意设备节点多时容易漏。
部署命令很简单:
bash复制kubectl apply -f kunlun-device-plugin.yaml
日志怎么看?直接看 Pod 日志:
bash复制kubectl -n kube-system logs -l app=kunlun-device-plugin
正常日志会显示已经向 kubelet 注册成功,并列出检测到的设备数量。比如我实测时日志里出现了 Device count: 8,并打印出每个设备 ID。
3.3 验证节点资源上报
部署插件后,等十几秒让 kubelet 刷新节点状态:
bash复制kubectl describe node ai-node-01
会看到类似内容:
text复制Allocatable:
cpu: 96
memory: 503Gi
kunlun.com/xpu: 8
如果显示 kunlun.com/xpu: 8,说明 kubelet 已经拿到资源计数。这个数字是由插件“报”上来的,不是手动配置的。
这一步常见问题有两个:一是插件容器一直 CrashLoopBackOff,多半是 socket 目录权限或者驱动目录没挂对;二是 node 的 Allocatable 里压根没有这个资源名,大概率是插件启动时注册失败,优先看插件日志里有没有“connect kubelet socket failed”之类的关键字。
3.4 写一个测试Pod验证调度和注入
资源上报完成后,写一个最小的 Pod 来验证整个链路。测试镜像里装了厂商的 runtime 库和 python 接口。Pod 规格里声明申请一张卡:
yaml复制apiVersion: v1
kind: Pod
metadata:
name: test-xpu
spec:
containers:
- name: test-xpu
image: registry.example.com/kunlun/runtime-test:latest
command: ["bash", "-c", "xpu-smi query && sleep 3600"]
resources:
requests:
kunlun.com/xpu: 1
limits:
kunlun.com/xpu: 1
apply 之后:
bash复制kubectl apply -f test-xpu.yaml
kubectl logs test-xpu
如果日志里能看到一张卡的详细信息,说明调度、设备注入、驱动兼容性都 OK。再进一步,可以同时启动 8 个这种 Pod,每个 Pod 申请 1 张卡,观察它们是否被分布到不同或相同节点上。这个测试能检验调度器在设备数量维度上是否生效。
3.5 让 kubelet 和调度器知道“这是稀缺资源”
虽然设备和插件已经把资源上报,但 K8s 默认不会为扩展资源做复杂的拓扑感知。比如调度器发现节点有 8 张卡,它会认为这 8 张卡是扁平的,不会考虑 PCIe Switch 拓扑。如果你的训练任务追求多卡通信性能,就需要在调度层面额外引入拓扑调度插件,或者用厂商提供的 node-label 标注最佳配对。
这一步不是必须的,很多推理任务单卡就够,完全不需要多卡拓扑感知。但做大规模训练平台的话,可以提前在 DaemonSet 里加一段脚本,把节点上的卡信息转成 node label,比如 kunlun.topology.switch=switch-a,方便之后给调度器用。
4. 从“0卡”到“跑满卡”的排障实录
4.1 节点一直显示0卡:先查这三处
我刚开始接入的时候也遇到了节点 0 卡的问题,排障顺序非常关键,不要跳着查。
第一,看设备插件 Pod 是否正常运行。kubectl -n kube-system get pods | grep kunlun,如果 Pod 只是 Running 但日志里没有设备编号,检查插件是否识别到了卡。可以直接在容器里执行 xpu-smi query,如果容器里看不到卡,说明驱动目录挂载没挂全或者设备路径不对。
第二,看 kubelet 日志。journalctl -u kubelet | grep -i device-plugin。如果 socket 注册失败,日志里通常会有明确报错,比如 failed to dial device plugin socket,这时候检查 /var/lib/kubelet/device-plugins 目录权限,确保 kubelet 能读写。
第三,排查是不是节点上一次的僵尸 socket 文件导致注册失败。有时插件 Pod 被管理员用 kubectl delete 直接删掉,但 socket 文件还留在磁盘上,新的插件启动时会因 address already in use 失败。解决办法是清除 /var/lib/kubelet/device-plugins 下对应的 socket 文件,再重启插件。
4.2 调度成功但容器拿不到卡:驱动挂载和运行时的问题
资源上报正常,Pod 也能调度到节点上,但容器启动后 xpu-smi query 看不到任何卡,甚至直接报 /dev/xpu0 not found。这个问题我见了不止一次,根因基本都是驱动目录或设备节点没有正确注入。
排查方法是先进入容器看设备节点:
bash复制kubectl exec -it test-xpu -- ls -l /dev/xpu*
如果容器里不存在该设备,回到插件日志看 Allocate 步骤有没有返回挂载信息。常见的故障点是生产环境用了自定义的 runtimeClass,却把设备插件的默认 runtime 设置覆盖了,导致容器运行时没有按 Allocate 响应注入设备。
另一个常见坑是镜像基础环境缺少厂商的 runtime 动态库。如果你用了精简的 distroless 镜像,容器启动后初始化 SDK 时会报找不到 .so 文件。这时需要在 Pod 中通过环境变量指定库路径,或者把 host 上驱动库目录挂载进来。
4.3 多卡任务资源不均:把“卡数”当简单计数器的代价
设备插件工作正常后,新的问题又来了。有同学反馈,同一个训练作业申请 4 张卡,但实际两张卡利用率 99%,另外两张只有 20%,整个作业跑得很慢。这是因为 K8s 默认只保证“给你 4 张卡”,并不保证“给哪 4 张”。如果你的节点有 8 张卡,其中 4 张在 PCIe Switch A 上,4 张在 Switch B 上,跨 Switch 通信会有额外开销。
解决办法是在设备插件里实现 GetPreferredAllocation,让插件尽量把同一训练作业的多张卡分配到同一 Switch 下。如果厂商插件不支持,也可以在 Pod 上打拓扑标签,再用自定义调度器过滤。这个优化对训练任务收益明显,推理任务可以先不管。
4.4 看监控:K8s上报不等于真实利用率
接入了 K8s 以后,很多平台同学会盯着 kubectl describe node 里 kunlun.com/xpu 的数量,觉得数量少就是忙,数量多就是闲。但真实情况不完全是这样,K8s 只是记录“已分配卡数”,它不是监控系统,不会告诉你每张卡的算力利用率和显存占用。
建议在节点上部署一个 exporter,定时读取 xpu-smi query 的输出,把每张卡的显存使用率、算力使用率、温度、功率转换为 Prometheus metrics。这样平台视图上能看到节点 A 的 8 张卡虽然已经被分配出去 6 张,但实际显存占用率可能只有 45%,可以用来做超卖调度。
监控字段我用的是这些:
text复制kunlun_xpu_utilization_percent
kunlun_xpu_memory_used_bytes
kunlun_xpu_memory_total_bytes
kunlun_xpu_temperature_celsius
kunlun_xpu_power_watts
这些指标单独建一套 exporter,以 DaemonSet 方式跑在集群里,再配置 Prometheus 采集。没有这一步,设备接入只算做了一半。
4.5 常见问题速查表
| 现象 | 可能原因 | 优先排查动作 |
|---|---|---|
| 节点 Allocatable 无 kunlun.com/xpu | 插件注册失败 | 查看插件日志和 kubelet 日志 |
| 插件 CrashLoopBackOff | socket 目录权限异常 | 检查 /var/lib/kubelet/device-plugins 权限 |
| Pod 调度失败 Pending | 节点卡数不足 | 检查节点 Allocatable 和 Pod request |
| Pod 启动成功但容器无设备 | 挂载或注入失败 | 容器内 ls /dev/xpu*,看 Allocate 日志 |
| xpu-smi 报驱动不匹配 | 驱动与固件版本问题 | 宿主机直接执行 xpu-smi query |
| 多卡训练通信慢 | 拓扑感知不足 | 开启 GetPreferredAllocation 或自定义拓扑调度 |
5. 进阶:把昆仑芯P800当“一等公民”来经营
5.1 用ResourceQuota做多团队配额
设备接入只是第一步。平台团队真正要面对的问题是:几十个团队都想用 P800,谁多用谁少用?如果不想每加一个团队都手动改代码,就直接用 K8s 原生的 ResourceQuota 和 LimitRange。
给每个团队建独立 namespace,然后设置配额:
yaml复制apiVersion: v1
kind: ResourceQuota
metadata:
name: ai-team-quota
namespace: ai-team
spec:
hard:
kunlun.com/xpu: "16"
requests.cpu: "100"
requests.memory: 200Gi
这样 ai-team 的 Pod 最多累计申请 16 张 P800。谁超了谁就调度不上去,完全不需要自己去写分配逻辑。注意配额是按“累计已分配”计算的,不是按“当前占用量”。如果一个团队申请了没跑任务,配额也占着,这是 K8s 原生资源的特征,需要配合平台层清理僵尸任务。
5.2 优先级和抢占:让训练任务别堵死推理任务
如果集群里同时有训练和在线推理任务,最好给它们设置不同的 PriorityClass。推理任务延迟敏感,优先调度;训练任务是批处理,对延迟不敏感,可以被抢占。没有这层设计时,一个团队提交了 8 个训练任务把卡占满,在线推理服务只能排队等着 node 有资源,这在生产环境不可接受。
配置 PriorityClass 后,高优先级 Pod 可以驱逐低优先级 Pod。低优先级 Pod 被抢占时,K8s 会发 Event,训练任务中断。为了减少训练任务频繁被抢占导致的检查点浪费,可以让训练任务在抢占发生前自动做 checkpoint,这属于平台任务编排层的能力,但 K8s 的抢占机制至少提供了“谁该让”的规则。
5.3 从节点维护角度看设备生命周期
运行一段时间后,P800 卡也会遇到固件升级、硬件替换、驱动升级等操作。节点上的设备插件会跟着 DaemonSet 一起升级,但卡本身可能在升级过程中状态不稳定。我建议把节点的卡维护做成一个标准操作流程。
先在节点上打上维护标签:
bash复制kubectl label node ai-node-02 kunlun.com/maintenance=true
再用 nodeAffinity 避免新任务调度上去,然后把该节点上的存量任务优雅迁移走。等任务清空后再做固件刷新。完成后移除标签,让节点重新回到资源池。这套流程虽然简单,但真心建议写进运维手册,否则每次都临时找人挪任务,容易出事故。
5.4 镜像与runtime的标准化
跑 P800 的镜像最好在一开始就统一:基于厂商提供的 runtime 基础镜像,加上 Python 环境、训练框架、算子库。每个团队的镜像里都放一套厂商的动态库,会带来大量冗余,也很难排查版本不一致的问题。
我这边习惯是拉一个精简的 openEuler 或 Ubuntu 基础镜像,在 Dockerfile 里固定拷贝宿主机上的厂商库到镜像:
dockerfile复制FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 python3-pip
COPY --from=driver /usr/local/kunlun /usr/local/kunlun
ENV LD_LIBRARY_PATH=/usr/local/kunlun/lib:$LD_LIBRARY_PATH
RUN pip install kunlun-runtime
这样做的好处是镜像在不同版本驱动之间兼容,即使宿主机驱动升级了,旧任务镜像还可以继续运行一段时间。但也要定期做驱动升级同步,不然卡的新特性用不上,还可能出现 SDK 和驱动版本不匹配的警告。
6. 部分我亲测下来的体会
设备插件接入只是“K8s 兼容昆仑芯 P800”这条链路的第一公里。真正的成本在后面对卡生命周期、驱动、多队列调度、监控告警、团队配额的管理。我见过不少团队把校验 Pod 跑通就当成大功告成,结果运维同学在排障时连“为什么容器里看不到 /dev/xpu0”都要查一下午。
如果让我给后来者一个建议:先在一个只有两张卡的测试节点上,把“驱动验证 → 插件部署 → 单卡 Pod → 多卡 Pod → 节点标签 → 监控采集”这一整套流程完整跑一遍,再扩到生产集群。不要一边接入一边开发平台功能,否则问题叠加到一块儿,排障成本高得离谱。
另外,每次升级设备插件前,先在测试节点上跑一遍 xpu-smi query 和至少一个真实推理模型,确保驱动、插件、镜像三者配合正常。这套路我跑了多次,可以说在国产 AI 加速卡的 K8s 接入里,前期多花一小时做验证,后面能省下几天的排障时间。
