如果你接过“把一批海光 DCU 节点接进 Kubernetes,再让上层 AI 平台和 DeepSeek 推理服务跑起来”这种需求,第一反应大概率是:装个驱动、部署个 Device Plugin 不就行了?真动手之后才会发现,整卡、共享、vDCU 虚拟化是三套完全不同的调度模型,平台侧的资源配额和监控要跟着改,DeepSeek 这种大模型部署时还有一堆显存和镜像的坑。这篇文章把我这次把海光 DCU 接入 Kubernetes 和 CubeStudio 的完整实操链路拆开讲清楚,重点覆盖整卡调度、共享调度、两种 vDCU 虚拟化方案的实现差异,最后用一份 DeepSeek 7B 推理服务的部署配置收尾。正在做 AI 基础平台、或者刚拿到一批 DCU 机器准备纳入云平台的工程师,可以直接按这套思路去推。
1. 先搞清楚:K8s 里的 DCU 调度为什么这么麻烦
1.1 设备插件是 DCU 进 K8s 的唯一通道
K8s 本身并不知道 DCU 是什么,它只认识 CPU、内存、磁盘这类内置资源。异构计算设备想被 K8s 调度,必须走 Device Plugin 机制。
Device Plugin 是一个跑在每个节点上的 gRPC 服务,通过 Unix socket 跟 kubelet 通信,默认监听在/var/lib/kubelet/device-plugins/目录下。它要实现的核心接口就两个:ListAndWatch和Allocate。
ListAndWatch:启动时向 kubelet 上报“这个节点上有多少张 DCU”,平时持续上报设备健康状态。如果某张卡坏了,插件通知 kubelet 把它从可用列表里摘掉。Allocate:当调度器把 Pod 派到节点上、kubelet 准备启动容器时,插件需要返回一组信息:容器要挂载哪些设备文件、要注入哪些环境变量、要映射宿主机的哪些目录。
所以 DCU 想进 K8s,第一件事不是写调度器,而是先有一个能跟 kubelet 对话的 Device Plugin。插件注册成功后,kubelet 会把资源名写进节点状态里的 Capacity 和 Allocatable 字段,比如hygon.com/dcu: 8,kube-scheduler 只有看到这个数字,才可能在后续调度时把任务派到这个节点。
还有一个容易忽略的点:宿主机上装了驱动,容器里不一定能用。容器有独立的设备命名空间,宿主机上的/dev/kfd、/dev/dri/renderDxxx这些设备节点,容器里默认是看不到的。Device Plugin 在 Allocate 阶段干的核心事,就是把 DCU 的设备节点、依赖的驱动库、以及类似ROCR_VISIBLE_DEVICES这样的环境变量全部注入到容器 spec 里。这一步不做对,后面所有层级的资源调度全是空的。
1.2 三种调度形态,本质是三种资源粒度
标题里说的“整卡 / 共享 / 两种 vDCU 虚拟化”,落到 K8s 里其实是三套不同的资源粒度和隔离模型。
| 调度形态 | 资源命名示例 | 隔离类型 | 资源粒度 |
|---|---|---|---|
| 整卡直通 | hygon.com/dcu | 物理隔离 | 1 张卡 |
| 共享切分 | hygon.com/dcu-share | 软件切分,弱隔离 | 每张卡切成 N 个单位,可小数申请 |
| vDCU 硬件切片 | hygon.com/vdcu-physical | 驱动/固件级切片 | 按比例切分,独占算力与显存 |
| vDCU 时间片 | hygon.com/vdcu-time | 驱动级时分复用 | 按时间片轮转,可超卖 |
搞这四套资源模型的时候,我才真正理解为什么有人会说“GPU 调度难”。CPU 有 cgroup 可以做配额控制,内存有 cgroup 做硬性限制,但 DCU 的显存分配根本不经过 cgroup,算力隔离更是完全依赖驱动层的能力。你想把一张卡拆给两个任务用,不是给每个任务发一把钥匙就行,还要解决“任务 A 吃满算力把任务 B 饿死”的问题。这就是为什么整卡容易做,而共享和 vDCU 需要单独设计。
1.3 一次调度请求的完整链路
整条链路是这样的:用户提交 Pod,声明需要多少 DCU 资源 → scheduler 根据节点 Allocatable 判断哪个节点放得下 → 把 Pod 绑定到该节点 → kubelet 调用 Device Plugin 的 Allocate 接口 → 插件返回需要注入的设备节点、环境变量和挂载点 → 容器运行时按这些参数启动容器。
这里有一个关键细节:DCU 这类扩展资源和 CPU/内存不一样,K8s 要求 requests 和 limits 必须一致,不能只写 requests 不写 limits。写错了会直接导致 Allocate 阶段不触发,容器起来之后根本看不到设备。这个我后面在踩坑部分还会细说。
2. 节点侧准备:驱动、运行时与设备可见性检查
2.1 节点层面的驱动与 DTK 安装
要让 DCU 正常工作,节点上至少需要两层的软件:内核驱动和用户态 DCU Toolkit(海光那边通常叫 DTK,兼容 ROCm/HIP 生态)。
先装内核驱动。安装完之后确认模块加载正常,我习惯用lsmod | grep amdgpu这类命令检查,也可以在/dev下看有没有对应的设备节点生成。然后装 DTK,它提供 HIP 运行时、数学库和编译器,装好之后建议统一放到/opt/dtk或/usr/local这种全局目录,保证节点上所有用户都能访问。
这里最容易被坑的是版本匹配。DTK 对驱动版本有明确要求,驱动太老或太新都可能导致hipInit直接失败,或者 SMI 工具能识别到卡但容器里加载不了库。我们后来是把驱动版本和 DTK 版本锁定成一个组合,写进基线部署文档,新节点起机时按同一个组合装,才彻底消停。如果你用容器镜像来跑任务,镜像里的 DTK 版本也必须和宿主机驱动匹配,否则容器内程序调用 HIP API 时会报各种诡异的符号错误。
安装完驱动之后,一定要在节点上先确认设备可见性,别急着接 Kubernetes。跑一下驱动自带的 SMI 工具(不同版本命令名可能不一样,常见的有rocm-smi、hy-smi、dcu-smi),确认能看到卡数和显存总量。再用rocminfo这类工具看一下设备 agent 是否正常,如果列表里是空的,说明驱动栈有问题,先解决这个再往下走。
2.2 容器运行时怎么把 DCU 设备送进容器
节点驱动装好了只是第一步,还得让容器运行时知道怎么把设备送进去。现在比较推荐的是走 CDI(Container Device Interface)。
CDI 的思路是,把设备信息写成一个 JSON/TOML 描述文件放到/etc/cdi/目录,containerd 在启动容器时根据声明自动挂载对应的设备节点和库目录。相比传统方案里的 privileged 容器,CDI 的权限控制更精细,平台侧的维护成本也低。
如果设备插件本身支持在 Allocate 阶段直接返回设备节点,不依赖 CDI 也能跑通,只是后面做共享、vDCU 这种复杂分配时,插件返回的内容会很重。所以我的建议是:如果你在用 containerd,尽早把 CDI 打开,后面所有 DCU 设备的注入逻辑都统一走 CDI,kubelet 和运行时之间的配合会清爽很多。
2.3 节点是否已经“准备好”被调度
节点侧准备到什么程度算完成?我习惯按三个标准检查:
- 节点终端执行 SMI 工具能列出所有 DCU,显存容量正确;
/dev/kfd和/dev/dri设备节点存在,权限正常;- 部署完 Device Plugin 后,
kubectl describe node能在 Capacity 里看到 DCU 资源。
如果前两步正常,但kubectl describe node里没有资源,问题基本都出在 Device Plugin 没起来,或者插件与 kubelet 的 socket 通信异常。优先看插件 Pod 的日志,常见的失败原因是 socket 目录权限不对、插件 Pod 挂载的/dev不完整。
3. CubeStudio 平台适配:把 DCU 变成可申请的资源
3.1 自定义资源上报与应用感知
当 Device Plugin 正常注册后,节点上会出现类似这样的信息:
Allocatable: cpu: 128 memory: 512Gi hygon.com/dcu: 8注意,这里默认是整卡数字。也就是说,K8s 原生调度的最小单位是“一张卡”。用户想申请一张卡,Pod 里这样写:
resources: requests: hygon.com/dcu: 1 limits: hygon.com/dcu: 1这种整数卡模式是平台能感知 DCU 的最小闭环。在 CubeStudio 里,管理员只需要在资源类型配置里新增hygon.com/dcu这个资源项,用户在项目配额里申请 DCU 数量,提交训练或推理任务时选择资源类型,平台后端把这段 yaml 拼到 Pod spec 里,调度就能工作了。
但这个阶段你也会立刻遇到第一个限制:用户需要 0.5 张卡,怎么办?平台界面根本没法表达 0.5 这个数字,因为底层 Allocatable 是 8 而不是 40。这就要靠下一节的调度扩展来解决了。
3.2 调度策略:默认调度器 vs 扩展调度器
默认的 kube-scheduler 在面对 DCU 时比较“笨”,它只做数量匹配:节点剩多少张卡,够就放行,不够就等。它不管卡间拓扑,不管显存是否能容纳模型,更不管哪张卡已经满载、哪张还是空闲的。
所以像 CubeStudio 这类 AI 平台接 DCU,通常不是只用默认调度器,而是引入自定义调度能力。市面上的方案基本两类:
- scheduler extender:在默认调度器外面挂一个 HTTP 扩展服务,预选和优选阶段多问一层“这个 Pod 的 DCU 资源在哪个节点/哪张卡上最合适”。
- 平台自研调度器:直接替换 kube-scheduler,把 DCU 资源、GPU 拓扑、显存占用、任务优先级全纳入调度计算。
我们这轮用的思路是:整卡场景继续用默认调度器,共享和 vDCU 场景走扩展调度器。因为共享切分后的“资源单位”是虚拟的,默认调度器根本不知道 5 个共享单位和 1 张卡之间有什么换算关系,必须由扩展层来维护这张映射表。
3.3 CubeStudio 的资源模型对接
CubeStudio 的适配工作,主要集中在三块:
- 资源类型配置:新增 DCU 整卡、DCU 共享单位、vDCU(物理切片/时间片)这几类资源,并为每类资源定义配额单位。比如整卡按“卡”计,共享单位按“个”计,vDCU 按“实例”计。
- 任务模板透传:平台的任务模板要能把用户选择的资源类型翻译成 K8s 的 requests/limits 字段,同时把对应资源名拼对。这里最容易出错的就是资源名字符串不一致,比如平台配置里写了
hygon.com/dcu,但 Device Plugin 上报的是hygon.com/dcu-v1,两边对不上,界面会一直显示资源不足。 - 集群监控接入:让平台监控大盘能看到每张 DCU 的利用率、显存占用、温度、功耗。这个一般靠厂商提供的 DCU exporter 采集 SMI 指标,然后接到 Prometheus 体系里。监控不做的话,用户跑任务时完全是个盲盒,只能靠猜。
平台侧改造看着都是些杂活,但恰恰是上线后用户体感最明显的部分。资源模型定义清楚,后续共享和 vDCU 的推进才顺。
4. 整卡调度:一张卡完整交给一个容器的实现
4.1 部署 Device Plugin:DaemonSet 与权限细节
整卡调度是所有调度方式里最稳的,也是必须优先跑通的。我们先部署一个基础的 Device Plugin DaemonSet,类似这样:
apiVersion: apps/v1 kind: DaemonSet metadata: name: hygon-dcu-device-plugin namespace: kube-system spec: selector: matchLabels: name: hygon-dcu-device-plugin template: metadata: labels: name: hygon-dcu-device-plugin spec: hostNetwork: true containers: - name: device-plugin image: registry.example.com/hygon/dcu-device-plugin:latest securityContext: privileged: true volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: sys mountPath: /sys readOnly: true - name: dev mountPath: /dev volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins - name: sys hostPath: path: /sys - name: dev hostPath: path: /dev三个挂载点对应三类需求:
/var/lib/kubelet/device-plugins:插件注册 socket 的位置,必须和 kubelet 一致;/dev:插件需要探测 DCU 设备节点,还要在 Allocate 时把设备文件映射进容器;/sys:很多厂商的设备插件通过 sysfs 读取设备拓扑或性能属性。
插件部署起来后,去kubectl logs看插件启动日志,正常情况下会看到类似“register to kubelet success”的输出。再kubectl describe node看节点的 Allocatable,如果出现了hygon.com/dcu,说明整卡能力已经打通。
4.2 应用侧声明资源
应用侧很简单,只要在 Pod 里声明:
apiVersion: v1 kind: Pod metadata: name: dcu-test spec: restartPolicy: OnFailure containers: - name: dcu-test image: registry.example.com/hygon/pytorch-dtk:latest command: ["rocm-smi"] resources: requests: hygon.com/dcu: 1 limits: hygon.com/dcu: 1跑起来之后,如果一切正常,rocm-smi只会在容器里看到 1 张 DCU,虽然宿主机上有 8 张。这正是设备插件做了ROCR_VISIBLE_DEVICES这类环境变量隔离的结果。
4.3 整卡调度的局限
整卡好归好,但它有一个绕不开的浪费问题。一张 32GB 的卡,如果业务只是跑一个 7B 模型的推理服务,显存可能就用掉 14GB 左右,算力利用率往往只有个位数百分比。但别的任务又因为“没有整卡额度”而排队。
我见过最典型的场景:节点上 8 张卡全部分配出去了,每张卡的实际利用率不到 10%,新任务进不来。这时候就算你强行用整卡对外提供服务,用户也会疯狂吐槽资源不够。所以整卡只适合模型训练、推理并发高、显存需求接近满载的场景,一旦业务碎片化,就必须上共享或者 vDCU。
5. 共享调度:把一张卡拆成多个资源单位
5.1 共享的真正难点:隔离而不是切分
共享调度的难点不在“能不能把一张卡给多个容器用”,而在“多个任务挤在一块之后,它们之间会不会互相伤害”。
CPU 有 cgroup 做时间片控制,内存有 cgroup 做硬限制,但 DCU 的显存和算力没有 cgroup 机制可以直接管。两个容器如果被分配到同一张物理卡上,哪怕各自声明了“我只要 20% 算力”,驱动也不会真的只给它 20%——如果没有额外的虚拟化层去限制,任务 A 可能直接把算力吃满,任务 B 的推理延迟立刻飙升。
另一个风险是显存超卖。共享调度如果只切“数量”不看“显存”,两个任务可能同时申请显存,导致物理显存耗尽,驱动报 out of memory,最严重的时候整张卡上的任务全部崩掉。
所以,共享调度落地之前,必须先确认驱动或运行时插件到底提供了哪种隔离能力:是完全不隔离、只隔离设备号、还是能做到显存和算力的硬隔离。不同的隔离能力,直接决定了共享单位怎么定义。
5.2 实现共享的两种路径
路径 A 是“资源单位”模式。Device Plugin 把一张物理卡化成 M 个共享单位,比如 M=5,每个单位代表约 20% 的卡资源。Pod 申请 1 个单位,底层映射到同一张物理卡,但每个容器都会被注入同一个物理设备的ROCR_VISIBLE_DEVICES值,所以多个容器能同时看到这张卡,共享使用。
这种模式实现简单,但隔离能力基本为零,一般只适合内部开发测试环境。它解决的问题是“资源位不够分”,不解决“任务互抢”。
路径 B 是“驱动层切分”模式。让驱动或虚拟化层真正介入,每个共享单位拿到独立的显存区段和算力配额。这种情况下,上层 Device Plugin 上报的其实是“虚拟设备”,不是物理卡。这其实就是下一章要讲的 vDCU 虚拟化的一种形态。
5.3 共享调度下的显存与算力配置实战
假如你有一张 32GB 的 DCU,想用共享模式切成 4 个单位,每个单位理论显存 8GB。这时候如果业务方想跑 DeepSeek-R1-Distill-Qwen-7B,fp16 权重就要占 14GB 左右,那么 0.25 卡是绝对放不下的,0.5 卡的理论 16GB 也很勉强,因为还有 KV Cache 和激活值。这种情况我会直接建议:要么申请整卡,要么用量化的模型权重把占用压到 8GB 以内再上共享。
共享模式配置的要点是把事情说在前面:给每个容器设置显存上限,能绑就绑,绑不了宁可不超卖。宁可让每卡的单位数少一点,也不要为了报表好看去挑战物理极限。多跑几次模型加载测试,把真实显存峰值测出来再定切割比例,比拍脑袋切稳得多。
6. 两种 vDCU 虚拟化的实现差异与选型
6.1 第一种 vDCU:硬件切片隔离
第一种 vDCU 走的是硬件切片路线,类似大家熟悉的 MIG 思路。DCU 底层把计算单元、显存控制器和缓存按比例切成多个独立分区,每个分区在物理上互相隔离,拿到一个分区的任务就像独占了一张小卡。
这种模式有几点好处:
- 一个分区里的任务把算力跑满,不会影响其他分区,性能可预测;
- 显存是硬件级隔离,不会出现共享模式下的超卖 OOM;
- 一张卡可以同时跑不同类型的任务,训练和推理混部,互不干扰。
代价是分区粒度通常是固定的,比如只能按 1/2、1/4、1/8 切,不支持“30%”这种随意比例。而且在 K8s 调度上,设备插件要做的事比较多:它需要枚举出这张卡上的所有硬件切片实例,然后把每个切片当作一个独立的资源来上报和分配。
如果平台上的业务都是长期稳定跑的,比如训练任务、高并发推理服务,硬件切片 vDCU 是最理想的形态。
6.2 第二种 vDCU:时间片共享池化
第二种 vDCU 走的是时间片路线。驱动在软件层把计算单元的时间片轮流分配给多个虚拟设备,每个 vDCU 看起来都像独立设备,但底层是同一批物理计算单元的时分复用。
这种模式的优势是密度高、粒度灵活:你能在一张卡上创建出十几个 vDCU,每个 vDCU 的“大小”可以按百分比随意调整,非常适合大量轻量级推理任务、开发调试环境、以及低优先级的批处理任务。
但它的问题也很明显:当几个 vDCU 同时繁忙时,算力会被时间片稀释,任务延迟会出现毛刺。也就是说,它的隔离是“逻辑隔离”不是“物理隔离”,不适合对延迟敏感的生产业务。
6.3 两种 vDCU 的对比与选型
| 维度 | 硬件切片 vDCU | 时间片 vDCU |
|---|---|---|
| 隔离粒度 | 硬件级隔离 | 驱动软件级隔离 |
| 显存隔离 | 硬隔离,各分区独享 | 软隔离,可配置上限 |
| 算力隔离 | 强,互不干扰 | 弱,繁忙时会争抢 |
| 切分粒度 | 固定比例(1/2、1/4 等) | 任意百分比 |
| 单卡可创建数量 | 有限 | 较多,可超卖 |
| 适用场景 | 训练服务混部、SLA 明确 | 轻量推理、开发调试、短任务 |
| 复杂度 | 设备插件要枚举实例,较复杂 | 配置相对简单 |
选型上我个人的经验是:先把硬件切片 vDCU 给“吃显存、吃算力、时间长”的业务,比如模型微调、批量推理;把时间片 vDCU 给“占位小、时间短、并发多”的业务,比如 Notebook、在线 demo 服务。
6.4 在设备插件中声明 vDCU 实例
无论哪种 vDCU,接入 K8s 的本质都是把它当成一种新的扩展资源。比如硬件切片 vDCU 可以命名为hygon.com/vdcu-physical,时间片 vDCU 命名为hygon.com/vdcu-time,Device Plugin 启动时根据节点上的 vDCU 实例列表动态上报数量。
用户在 CubeStudio 里申请资源时,其实是在这两种资源之间做选择:
# 申请一个硬件切片 vDCU resources: requests: hygon.com/vdcu-physical: 1 limits: hygon.com/vdcu-physical: 1# 申请 2 个时间片 vDCU resources: requests: hygon.com/vdcu-time: 2 limits: hygon.com/vdcu-time: 2这里的数量到底代表多少算力,取决于一个 vDCU 实例的具体规格。平台需要把“vDCU 模板”的概念暴露给用户,比如“32GB 卡的 1/2 硬件切片”“一张卡的 10% 时间片”,用户看到的是几种可选的套餐,而不是原始的数字。
7. DeepSeek 推理服务在 DCU 上的落地实操
7.1 选哪个 DeepSeek 模型:显存估算思路
DeepSeek 全量版本很大,比如 DeepSeek-V3 这种 671B 级别的模型,单机根本跑不动,需要多机多卡的分布式推理,一般项目根本没必要硬上。现实的选择是 DeepSeek-R1-Distill 系列的蒸馏版。
选模型之前先做显存估算。公式很简单:权重显存 ≈ 参数量 × 2 字节(fp16 精度)。再加上 KV Cache、激活值和运行时开销,实际占用一般是权重的 1.3 到 1.5 倍。
| 模型 | 参数量 | fp16 权重 | 实际建议显存 | 卡数建议 |
|---|---|---|---|---|
| DeepSeek-R1-Distill-Qwen-7B | 7B | 约 14GB | 32GB+ | 1 张整卡 |
| DeepSeek-R1-Distill-Qwen-14B | 14B | 约 28GB | 64GB 或 2×32GB | 2 卡张量并行 |
| DeepSeek-R1-Distill-Qwen-32B | 32B | 约 64GB | 4×32GB 或 2×64GB | 多卡张量并行 |
以最常见的 32GB DCU 节点为例,首选就是 7B 版本。14B 如果想在单卡上跑,只能做量化,比如 int4 量化后显存能压到 8GB 左右,效果会损失一点,但至少能跑起来。32B 就别想着单卡了,老老实实做张量并行。
7.2 推理框架镜像准备
DCU 上跑推理,框架首选还是 vLLM,但要选支持 ROCm/DTK 的版本,海光生态通常有对应的适配包或镜像。不要浪费时间从源码编译,环境差异太多,编译问题会消耗大量精力。
我们最终用的镜像是在 DTK 基础镜像上叠加 vLLM:
FROM registry.example.com/hygon/dtk-base:24.04 RUN pip install vllm-dtk==0.7.2 COPY --from=models registry.example.com/ai/models:deepseek-7b /models ENV HIP_VISIBLE_DEVICES=0 EXPOSE 80007.3 在 CubeStudio 上发布推理服务
模型就绪后,在 CubeStudio 的在线服务模块里创建一个自定义推理服务,本质上提交的是一个 Deployment:
apiVersion: apps/v1 kind: Deployment metadata: name: deepseek-7b-dcu spec: replicas: 1 selector: matchLabels: app: deepseek-7b-dcu template: metadata: labels: app: deepseek-7b-dcu spec: containers: - name: vllm image: registry.example.com/ai/vllm-dtk:0.7.2 command: - python3 - -m - vllm.entrypoints.openai.api_server args: - --model=/models/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B - --dtype=float16 - --tensor-parallel-size=1 - --max-model-len=32768 - --gpu-memory-utilization=0.92 ports: - containerPort: 8000 resources: limits: hygon.com/dcu: 1 volumeMounts: - name: models mountPath: /models volumes: - name: models persistentVolumeClaim: claimName: model-storage这里有几个参数值得单独解释:
--tensor-parallel-size=1:单卡推理。如果业务选择多卡跑 14B,这里要改并行卡数,同时 resources 里的 DCU 配额也要相应增加。--gpu-memory-utilization=0.92:不要太贪心,留出 8% 给驱动和运行时,否则加载长文本时很容易 OOM。--max-model-len=32768:限制最大上下文长度。不设的话,vLLM 会按模型默认值申请显存,遇到长上下文任务时,KV Cache 可能把显存吃满。
另外再把 Service 暴露出来,如果是内部联调用 NodePort 就行:
apiVersion: v1 kind: Service metadata: name: deepseek-7b-dcu spec: type: NodePort selector: app: deepseek-7b-dcu ports: - port: 8000 targetPort: 8000 nodePort: 301007.4 服务验证与性能观察
服务起来之后,用 curl 验证:
curl -X POST http://<node-ip>:30100/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "/models/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", "messages": [ {"role": "user", "content": "用一句话解释 DCU 调度"} ], "max_tokens": 256 }'能正常返回就说明链路通了。接下来建议再去 vLLM 的/metrics拉一下指标,重点观察num_requests_running、gpu_cache_usage_perc和 token 生成速度。跑一段时间后你会发现,单卡服务 7B 模型,预填充长文本时显存占用会快速上升,这也是上面为什么要留出显存余量的原因。
8. 踩坑实录:接入过程里最典型的五个问题
8.1 驱动版本与 DTK 不匹配
问题现象:宿主机 SMI 工具能正常看到卡,rocminfo也正常,但容器里的程序初始化 HIP 时直接失败,报错信息语焉不详。
排查过程:先看dmesg,如果能发现 gfx ring timeout 或者驱动固件加载失败之类的日志,十有八九是驱动和 DTK 的版本组合出了问题。再把容器内的 DTK 版本和宿主机的驱动版本逐项比对,基本就是镜像里的 DTK 太新,宿主机驱动没跟上。
解决办法:建立版本基线,驱动和 DTK 用同一个发布周期的组合,镜像构建时锁定 DTK 版本。以后节点扩容、镜像更新前先对照基线检查。
8.2 容器内找不到设备节点
问题现象:Pod 起来了,但进容器一看/dev下没有kfd、没有dri,任务当然跑不起来。
排查过程:这个问题的根因往往不是设备没挂,而是 Pod 的 resources 里只写了 requests,没写 limits,导致 kubelet 认为这个 Pod 不需要做设备分配,Device Plugin 的 Allocate 根本没被调用。
解决办法:扩展资源必须 requests 和 limits 同时写,并且建议两者保持一致。统一规范后,这个坑基本不会再踩。
8.3 共享调度下的显存超卖
问题现象:两个任务被分到同一张卡的共享单位上,一个任务加载模型时,另一个任务直接报显存溢出,严重时整卡服务全部挂掉。
排查过程:一看调度记录,两个容器确实分到了不同共享单位,但设备插件只做了“数量切分”,没有做显存隔离。底层物理显存是同一块,申请总量超过物理上限,驱动只能报错。
解决办法:共享模式下必须给资源单位绑定显存上限,不做显存绑定的场景坚决控制并发数。我们后来把共享单位的定义从“百分比算力”改成“显存 + 算力上限”双约束,才好用一些。但终极方案还是迁移到 vDCU,让驱动层做真正的隔离。
8.4 平台配额和监控不感知新资源
问题现象:节点上明明有 DCU 资源,CubeStudio 提交任务时却提示资源不足,或者配额管理页面根本选不到 DCU。
排查过程:平台资源管理模块里没有注册hygon.com/dcu这个资源类型,所以配额计算时全部按 0 处理。监控大盘看不到 DCU 指标,也是因为没接 DCU exporter。
解决办法:在平台资源类型配置里逐一注册整卡、共享、vDCU 资源名,和 Device Plugin 上报的名字保持一致;监控侧接入 DCU 的 Prometheus exporter。这个环节代码量不大,但涉及平台多个模块,最好留出专门的时间做联调。
8.5 推理服务健康检查误杀
问题现象:DeepSeek 服务部署后,Pod 一直在 CrashLoopBackOff,但日志里看不到明显报错。
排查过程:看事件才发现是存活探针的问题。模型权重从 PVC 加载到初始化完成需要好几分钟,默认的 failureThreshold 几秒钟一失败,Pod 就被 kubelet 重启了。
解决办法:把 startupProbe 或 readinessProbe 的 initialDelaySeconds 调到 600 秒以上,等模型加载完成后再开始探测。如果你用的是 vLLM,健康检查路径可以用/health,比请求/v1/chat/completions要轻量得多,不会把探针请求排进推理队列。
这轮 DCU 接入做下来,我最大的体感是:整卡调度是最容易出成果的部分,但它只是起点;共享调度是平台资源利用率的分水岭,可也是最容易失控的部分;vDCU 真正解决隔离问题,但选硬件切片还是时间片,取决于业务到底怕不怕延迟毛刺。如果你也在做类似的事情,先别急着追求炫酷的虚拟化,把整卡链路打磨到稳定、把平台资源模型定义清楚,再根据业务负载特征一步步引入共享和 vDCU,这条路走起来会顺畅很多。