
连续好几个团队来找我开场都是同一句海光 DCU 怎么接 Kubernetes他们手上要么已经有现成的 K8s 集群要么正在调研用 CubeStudio 搭内部 AI 平台最后都会卡在“卡怎么变成可调度资源”这一步。这次我把整个适配过程完整复盘一遍覆盖整卡调度、共享调度、两种 vDCU 虚拟化以及最后在平台上把 DeepSeek 部署起来的全链路希望能帮正准备做这件事的团队少走点弯路。1. 先搞清楚K8s 里的 DCU 调度到底难在哪1.1 “接入 K8s”其实是三个层次的问题很多人一上来就搜“DCU Kubernetes 部署教程”搜出来一堆配置片段但真正动手才发现问题的层次完全不一样。第一层是设备可见性容器里能不能看到/dev/dri/renderD128这类设备节点内核驱动装了没有用户态库能不能匹配。这个用docker run --device就能勉强解决但不解决调度问题。第二层是资源调度Kubernetes 调度器默认只知道 CPU 和内存它不知道节点上有几张 DCU、每张卡显存多大、哪些卡是健康的。想让 Pod 能申请“一张卡”“半张卡”必须先把 DCU 抽象成 K8s 认识的资源对象。第三层是平台编排有了资源调度之后还要有上层平台帮你管镜像、管作业、管队列、管 WebIDE这就是 CubeStudio 这类 AI 平台存在的意义。平台底层的资源池仍然是 Kubernetes只是把 Pod、Deployment、Service 这些抽象包装成了“创建一个推理服务”“提交一个分布式训练任务”。搞清楚这三层后面的每一步操作你都知道自己在干什么。如果你只是为了在单机上跑个 demo那一二层完全可以模糊处理但要让团队稳定用起来三层都得打通。1.2 为什么不能直接 docker run --device 一把梭单机环境最简单的做法是docker run --device /dev/dri/renderD128 --device /dev/kfd把设备塞进容器再用-v把驱动库挂进去。这套路对单卡、临时验证没问题但一上集群就崩。原因有三个。第一kubelet 不知道你用了哪张卡。同一台节点上可能插了 8 张 DCUPod 被调度到节点后容器里看到的renderD128到底是哪张卡如果多个 Pod 都被塞了同一个设备节点等于所有容器在抢同一张卡其他卡空转。第二容器调度器在调度时无法感知剩余显存。你申请了“这个容器需要 32GB 显存”但默认的 Extended Resource 只能按整数计数比如hygon.com/dcu: 1就是一张完整的卡不能申请 0.3 张。要支持小数粒度的算力和显存组合必须引入 vDCU 这类虚拟化方案。第三健康检查没有出口。裸设备方式下驱动挂没挂、卡是不是掉线了只有登录节点敲dcu-smi才知道。K8s 的 device plugin 机制有健康汇报能力能把坏卡从allocatable中摘掉这是裸设备完全做不到的。所以结论很明确K8s 集群里要规范地用 DCU必须走 device plugin不能靠 docker 参数硬塞。1.3 绕不开的三件套dcu-smi / device-plugin / vDCU海光 DCU 的软件生态虽然和 NVIDIA 的 CUDA 体系不是一套但工具形态是类似的常用三件套dcu-smi对应nvidia-smi看卡的温度、利用率、显存占用、PCIe 链路状态是排查物理卡问题最重要的入口。dcu-device-pluginK8s 的 device plugin 实现负责把节点上的 DCU 数量、健康状态上报给 kubelet让 Pod 可以声明hygon.com/dcu这种资源。vDCU实现卡资源的细粒度切分。一张 DCU 可以按显存大小、算力比例拆成多个逻辑设备分配给多个作业。如果你用的是海光官方或二次开发的 Linux 发行版驱动装好后dcu-smi应该立刻可用。装不上驱动的概率比较低反而常见的问题是用户态工具链比如 DTK 里的 HIP 相关库和驱动版本不匹配导致容器里跑模型时出现很奇怪的“找不到设备”报错。后面部署 DeepSeek 那一段我会再强调版本匹配的问题。2. CubeStudio 怎么跟 DCU 协作一套四层链路2.1 最上层CubeStudio 的作业与镜像管理CubeStudio 本质上是一个面向 AI 场景的平台层产品。用户不太需要直接写 K8s 的 YAML而是在平台界面里创建 Notebook、提交训练任务、发布推理服务。平台后端会把这些请求翻译成 K8s 的 Pod、Deployment、Service。对 DCU 适配来说CubeStudio 解决的第一个痛点是镜像管理。GPU 类容器镜像体积通常很大里面要带 DTK 用户态库、Python 环境、训练框架平台可以把这些镜像统一存起来用户不用关心 base image 从哪来。第二个痛点是作业模板。平台内置了分布式训练、模型推理这些任务模板申请资源、挂载数据集、暴露端口这些操作被固化成了表单。但要注意平台层再怎么封装最后落到节点上的还是 K8s 调度。你如果在平台里填了“需要 4 张卡”实际生成的 Pod 里一定会有类似的资源请求。所以搞清楚底层调度逻辑反而能帮你反向理解平台界面里那些参数。2.2 调度层从 kube-scheduler 到自定义扩展默认的 kube-scheduler 只认 CPU 和内存要让它认识 DCU核心工作就是注册扩展资源。dcu-device-plugin启动后会把资源名比如hygon.com/dcu注册到节点状态上kubelet 定期上报给 schedulerscheduler 就能按整卡数量做调度了。整卡场景其实不需要换成别的调度器默认的 kube-scheduler 配合nodeSelector、affinity就够用。但一旦用 vDCU事情就复杂了一张卡被切成了若干个逻辑单元scheduler 要同时考虑算力比例、显存上限、是否绑定同一张物理卡等约束这时候要么在 controller 里做二次调度要么引入调度扩展器scheduler extender让平台侧先把“哪张卡适合这个 Pod”算好再交给 kube-scheduler 落定节点。我们实际落地时没有魔改 kube-scheduler而是通过 vDCU 控制面做了预绑定控制面维护了每张物理卡当前的分配情况Pod 创建时先根据显存和算力需求挑一张或多张卡通过 nodeAffinity 把 Pod 固定到对应节点再用环境变量给容器指定 DCU 设备编号。2.3 设备层device-plugin 与 vDCU 控制面设备层是最烦的一层因为这里要把物理卡的状态翻译成 K8s 资源状态。dcu-device-plugin做的事情可以拆成两件事一是启动时扫描节点上的 DCU 设备把健康卡数量上报给 kubelet二是当 Pod 被调度到节点并申请 DCU 资源时把对应的/dev/dri/renderD*、/dev/kfd等设备节点挂载进容器同时把容器需要的用户态库目录也挂进去。vDCU 控制面则要更重一些。如果走软件 vDCU控制面需要在分配设备时创建对应的显存上限和算力比例配置并把配置通过挂载文件或环境变量的方式注入容器如果走硬件 vDCUSRIOV控制面还要负责创建虚拟功能VF、管理 VF 的启用和销毁。后面第四节我会详细讲这两种方案。2.4 驱动层DTK 与用户态 runtime驱动层虽然不在 K8s 的管辖范围内但它决定了上层能不能跑起来。DCU 的底层开发套件是 DTKDCU Toolkit里面包含了 HIP 编译器、运行时库、数学库如 hipBLAS、hipFFT。跑 PyTorch 时常见的做法是在 DTK 基础镜像上安装torch-dcu版本这个专门适配海光 DCU 的 PyTorch 分支。最容易踩坑的点是驱动版本和镜像里 DTK 版本不一致。比如宿主机内核模块版本叫driver-23.10容器里 DTK 是24.04启动模型时可能会报 HIP 初始化失败或者显存分配错误。我的建议是固定一套驱动和 DTK 组合所有容器镜像都基于同一个 DTK 版本构建不要混搭。3. 整卡调度实操先把“一张卡等于一个可调度单元”跑通3.1 节点准备用 dcu-smi 确认卡和驱动整卡调度是整个虚拟化的基础也是验证 DCU 驱动是否正常的最快路径。先在 DCU 节点上执行dcu-smi正常输出会列出每张卡的索引、型号、显存总量、当前利用率类似----------------------------------------------------------------------- | DCU 0 (PCIe 4.0 16x) 显存 64000 MiB 利用率 0 % ECC 正常 | -----------------------------------------------------------------------然后确认设备节点存在ls /dev/dri/常见的有renderD128、renderD129等每个renderD*对应一张物理卡。如果设备节点没有生成大概率是驱动没加载先查内核模块状态再查系统日志别急着往 K8s 那边排查。3.2 部署 dcu-device-plugin驱动没问题后部署 device plugin。以下是一份最简配置核心是让 DaemonSet 在每个 DCU 节点上运行apiVersion: apps/v1 kind: DaemonSet metadata: name: dcu-device-plugin namespace: kube-system spec: selector: matchLabels: name: dcu-device-plugin template: metadata: labels: name: dcu-device-plugin spec: tolerations: - operator: Exists nodeSelector: hygon.com/dcu: true hostNetwork: true containers: - name: dcu-device-plugin image: cr.hygon.cn/dcu-device-plugin:v0.5 securityContext: privileged: true volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: sys mountPath: /sys - 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部署前先在 DCU 节点上打标kubectl label node worker-dcu-01 hygon.com/dcutrue然后应用清单查看插件是否正常运行kubectl get pods -n kube-system -l namedcu-device-plugin -o wide正常情况下DaemonSet Pod 在打了标签的节点上变成 Running。这时查看节点资源会看到类似下面的内容kubectl describe node worker-dcu-01 | grep -A 10 Allocatable输出里的hygon.com/dcu: 8就表示该节点有 8 张 DCU 可以作为整卡调度。3.3 第一个申请整卡的 Pod用下面这个 yaml 验证整卡调度是否生效apiVersion: v1 kind: Pod metadata: name: dcu-test spec: restartPolicy: OnFailure nodeSelector: hygon.com/dcu: true containers: - name: dcu-check image: cr.hygon.cn/dtk/ubuntu22.04-torch-dcu:latest command: - /bin/bash - -c - dcu-smi python -c import torch; print(torch.cuda.device_count()) resources: limits: hygon.com/dcu: 1如果容器的 Python 里能看到 1 张 DCU说明整卡链路已经通了。顺手可以试一下hygon.com/dcu: 4K8s 会把 Pod 调度到一个剩余整卡数足够的节点上。3.4 整卡模式必须提前知道的 4 个坑整卡模式虽然比 vDCU 简单但坑也不少。第一个坑是节点滚动升级以后allocatable不恢复。比如节点驱逐 Pod 后要重启device plugin 是 DaemonSet 也会跟着重启如果插件在 kubelet 启动之前就绪kubelet 可能收不到设备注册。解决办法是升级节点时给个宽裕的时间等 kubelet 完全起来再恢复工作负载必要时手动重启 device plugin Pod。第二个坑是残余容器占用卡。K8s 的 Pod 删掉了但容器里的进程可能还留在显存里dcu-smi看显存是满的。这通常是进程没被回收或者有 DaemonSet 类容器挂了推理进程。整卡模式下最好在节点上定时巡检dcu-smi的显存使用把异常进程找出来。第三个坑是健康检查的滞后。device plugin 一般会定期探测卡的健康状态如果一张卡被打成“不健康”它不会立刻从allocatable中消失而是要等几个上报周期。这个时间窗口内新 Pod 仍然可能被调度到这张坏卡上。所以严重故障场景要主动摘节点 label而不是等自动检测。第四个坑是容器里没有nvidia-smi。很多团队从 NVIDIA GPU 切过来习惯性在容器里敲nvidia-smi结果报命令不存在。要么在镜像里装好dcu-smi要么直接用平台的监控页面不要在推理逻辑里写死nvidia-smi。4. 共享调度两种 vDCU 虚拟化的设计差异与实操4.1 什么时候需要共享整卡模式的问题在于浪费。一个 7B 量级的模型可能只要 12GB 显存一张 64GB 的 DCU 大部分时间都在空转。如果团队里同时有多个小模型测试、多个算法工程师要开发环境整卡分配很快就会把卡耗光8 张卡最多给 8 个人用每个人独占一张卡算力利用率却只有百分之十几。共享调度的价值就在这里把一张物理卡切成多个逻辑设备。但切法不一样效果和成本也不一样。4.2 硬件 vDCUSRIOV部署与管控思路第一种方式是硬件虚拟化通过 SRIOV 把一张物理卡虚拟出多个 VF。每个 VF 拥有独立的中断、DMA 通道、显存地址空间隔离性最好性能损耗也小。但代价是部署复杂度高。首先需要在 BIOS 里打开 SRIOV 支持驱动加载时要启用 PF物理功能和 VF 创建接口系统里会出现一批新的设备节点。其次在 K8s 里要管理这些 VF 的生命周期不能直接用默认 device plugin 上报“8 张卡”因为 VF 是动态创建销毁的。我们在实际项目中给硬件 vDCU 做的封装是自己写了一个 CRD 和 controllercontroller 负责在节点上执行创建 VF 的命令然后把 VF 对应的设备挂载到 Pod 里。资源申请时用户不直接写hygon.com/dcu: 1而是写自定义资源比如resources: limits: vdcu.hygon.io/device: 1 vdcu.hygon.io/memory: 16Gi控制面收到请求后找一张剩余显存足够的物理卡创建 VF并通过 Pod 注入完成设备映射。如果你只是做 PoC我不建议一上来就上 SRIOV。原因很简单虚拟功能的管理逻辑、故障恢复逻辑都要自己写平台层没有现成的成熟闭环。硬件 vDCU 更适合大规模、重负载、安全隔离要求高的生产场景。4.3 软件 vDCU时间切片 显存隔离部署与请求方式第二种是软件虚拟化不需要在驱动层面创建 VF而是通过运行时拦截和资源限制实现“看起来像独占”的逻辑设备。软件 vDCU 通常包含两个维度显存上限和算力时间片比例。显存上限解决的是“多个容器不能互相挤爆显存”的问题。框架在初始化 CUDA/HIP 上下文时运行时库会根据环境变量把可用显存限制成固定值。算力比例解决的是“一个容器不能占满计算单元”的问题类似把计算配额按时间片分配给不同容器。软件 vDCU 的资源请求语义很直接resources: limits: hygon.com/vdcu: 1 hygon.com/vdcu-mem: 8Gi hygon.com/vdcu-compute: 30这里vdcu-compute: 30表示最多使用单卡 30% 的计算配额。注意字段名取决于你们装的 vDCU 插件版本但语义基本是这三件事逻辑设备数量、显存上限、算力比例。软件 vDCU 的部署比硬件 vDCU 轻得多。插件要做的核心工作是在节点上维护一张卡的空闲显存和算力账本分配逻辑设备时更新账本给容器注入对应的环境变量和挂载配置。整个逻辑跑在用户态故障影响面小适合开发测试环境、小模型推理、多人共享集群这些场景。4.4 两种 vDCU 混用一个集群里的策略划分我们的生产集群里并没有只选一种 vDCU而是做了混用按业务特性分成三类资源池业务类型推荐方式原因大模型全量微调、多卡推理整卡需要多卡通信卡之间不能被打断中小模型推理服务软件 vDCU弹性好隔离需求中等多租户 SaaS、安全合规要求高硬件 vDCU隔离强避免算力侧信道问题混用时的关键是节点打标和调度策略隔离。给每个节点标识它是否支持硬件 vDCU、软件 vDCU 还是仅整卡然后在平台作业模板里让用户选择资源类型。CubeStudio 这类平台一般都能配置多个资源池底层对应不同的 nodeSelector 和资源字段。我建议把“整卡”和“软件 vDCU”作为默认选项“硬件 vDCU”作为进阶选项。这样日常开发效率高需要强隔离时再切到物理虚拟化。混用的时候要特别注意如果一个节点既允许整卡申请又允许 vDCU 申请控制面必须维护好同一张卡的分配账本避免整卡作业已经把卡占了vDCU 又把剩余算力切给别人的情况。4.5 vDCU 实测中的典型问题软件 vDCU 最常见的现象是“看起来隔离了实际没有”。比如两个容器同时跑推理一个容器申请了 8GB 显存另一个申请了 16GB但两个任务实际占用的显存都超出了限额最后触发 OOM一个容器把另一个容器的上下文冲掉。排查这种问题时先看两件事一是容器里有没有正确加载 vDCU 运行时比如环境变量是否被传递二是应用的框架是不是绕过了运行时限制。某些框架如果在启动时用了特殊的内存池配置可能不会走 vDCU 的拦截逻辑。遇到这种情况要么给应用显式设置显存上限要么把 vDCU 的校验开关打开在分配时直接拒绝超限请求。硬件 vDCU 的典型问题是 VF 创建失败后没有清理干净。要么控制面在 VF 创建失败时没有把资源释放回账本导致节点明明空闲却被判定为满的要么 VF 销毁后设备节点还残留在/dev/dri下。这个需要写一个定期巡检任务对比“驱动可见的 VF 数”和“控制面账本里的分配数”不一致时自动修复。5. 在 CubeStudio 上把 DeepSeek 跑起来5.1 模型选型R1 还是 R1-DistillDeepSeek 部署需求现在几乎每个团队都会提但大部分人没想清楚要部署哪个版本。DeepSeek-V3 和 R1 的大模型版本参数规模很大属于 MoE 架构完整权重部署需要多机多卡单机环境基本跑不动。如果只是给团队内部做代码辅助、文档问答、小规模 Agent 测试我更推荐 R1-Distill 系列比如 14B、32B 这些蒸馏版本。它们保留了推理链能力但参数量小得多单张 64GB DCU 或者两张 32GB DCU 就能带起来。从实际体验来看Distill 版本在代码生成、数学推理这类任务上能力已经相当能打部署成本却低了不止一个数量级。先把小模型跑通再根据业务量决定要不要上大版本这是风险最低的路径。5.2 镜像与启动带 vLLM 的 DCU 推理镜像模型服务我们统一用 vLLM吞吐优势明显。DCU 场景下需要选择适配海光分支的 vLLM 镜像基础环境是 DTK Python vLLM 适配版。基础镜像构建可以简化成下面几步FROM cr.hygon.cn/dtk/dtk:24.04.1-ubuntu22.04 RUN pip install torch-dcu vllm-dcu0.6.6.post2 COPY deepseek-r1-distill-14b /models/deepseek-r1-distill-14b RUN cd /models sha256sum -c deepseek-r1-distill-14b.sha256 EXPOSE 8000 CMD [vllm, serve, /models/deepseek-r1-distill-14b, --tensor-parallel-size, 4, --max-model-len, 8192]这里有两个细节。第一模型权重最好先下载并校验后再打进镜像避免服务启动时现拉权重既慢又不稳定。第二--tensor-parallel-size要根据卡数设置如果申请了 4 张整卡这里就填 4和 Pod 里的资源申请保持一致。如果要用共享 vDCU 跑模型推理命令和整卡有一点差异主要是显存上限和并发数要调低因为每一路逻辑设备能用的算力是受限的。实际测试中--max-model-len越保守越不容易在 vDCU 模式下触发显存不足。5.3 提交作业整卡和 vDCU 的资源清单差别通过 CubeStudio 提交推理服务时界面上让你填的无非也就是这些 yaml 字段。整卡模式下的资源清单很直观resources: limits: hygon.com/dcu: 4调度器会去找一台剩余整卡数不少于 4 的节点。如果你的集群节点有多张卡但 Pod 需要 4 张最好加一个反亲和性避免多个推理服务 Pod 落到同一节点后抢跨卡通信带宽。共享 vDCU 模式下的资源清单则要同时给显存和算力resources: limits: hygon.com/vdcu: 2 hygon.com/vdcu-mem: 16Gi hygon.com/vdcu-compute: 40含义是向平台申请两个逻辑设备每个最多用 16GB 显存、单卡 40% 的算力配额。需要注意如果你跑的是多路并发推理vdcu-compute给得太低会导致响应延迟明显升高这个参数要结合业务 QPS 和单请求延迟一起调。5.4 服务暴露与监控从 K8s Service 到 Grafana模型服务跑起来只是第一步让团队稳定使用还需要暴露服务和接监控。CubeStudio 平台一般会帮你创建Service但如果你想手动验证直接写一个 NodePort 或 ClusterIP 服务就行apiVersion: v1 kind: Service metadata: name: deepseek-r1-distill-14b spec: selector: app: deepseek-r1-distill-14b ports: - port: 8000 targetPort: 8000 type: ClusterIP请求体就是标准的 OpenAI 兼容格式比如curl调/v1/chat/completions。监控方面看板建议至少包含四个指标DCU 利用率、显存占用、请求延迟、请求吞吐量。前两个用dcu-smi的 exporter 打进 Prometheus后两个可以从 vLLM 内置的 Prometheus 指标里拉。Grafana 里把“DCU 显存占用超过 80% 且利用率低于 20%”这种情况拉出来看看往往会发现很多作业的显存申请都偏大可以进一步优化资源配比。6. 三个值得记录的排障过程6.1 多卡推理初始化失败共享内存的坑我们用整卡模式启动 14B 模型时vLLM 一直报初始化失败日志里提示“unable to initialize communication”但单卡模式正常。第一反应是驱动或通信库的问题查了一圈才发现是/dev/shm太小。Kubernetes 默认不给你调大容器的/dev/shm一般为几十 MB。多卡通信要共享张量数据IPC 内存不够就会初始化失败。解决方法是给 Pod 挂一个内存类型的 emptyDirspec: containers: - name: deepseek volumeMounts: - name: dshm mountPath: /dev/shm volumes: - name: dshm emptyDir: medium: Memory sizeLimit: 16Gi这个坑看起来很基础但每次换新集群都会有人踩一遍。第一时间查/dev/shm比翻通信库日志快得多。6.2 vDCU 显存隔离失效的两个原因一次测试中两个共享 vDCU 的任务各自申请了 8GB 显存结果一个任务瞬间把整张卡 64GB 全部占满另一个任务直接卡死。查下来发现是两个原因叠加了。第一个原因是 vDCU 运行时环境变量没有传进容器。推理进程读不到显存上限配置按物理卡总显存去申请自然就把卡撑爆了。解决方法是检查 vDCU 插件的注入逻辑确认在每个容器启动时都写入了正确的环境变量。第二个原因是应用的框架自己重新读取了设备属性。有些框架在初始化时会调用设备查询接口绕过 vDCU 的拦截层拿到物理卡真实显存。这时候光靠环境变量不够还需要在 vDCU 控制面把设备查询接口的结果也改了让应用看到的就是受限后的显存大小。这个问题的教训是软件 vDCU 的隔离能力取决于应用是否配合运行时拦截对不配合的应用要么做适配要么换硬件 vDCU不要指望透明生效。6.3 并发一高吞吐就崩并发参数与 NUMA 绑定部署完成后单请求延迟很漂亮但并发一高P99 直接翻了好几倍甚至出现请求超时。排查时先怀疑网络再怀疑显存位宽最后发现是 vLLM 的并发参数没调。vLLM 默认的并发能力受--max-num-seqs和--max-model-len两个参数影响。max-num-seqs控制同时处理的请求数默认值往往比较保守并发请求一多就排队延迟自然飙升。把它从默认的 256 调高同时在服务前面加一层简单的请求队列P99 立刻好看很多。另一个隐藏因素是 NUMA 绑定。DCU 是插在 PCIe 槽位上的CPU 访问不同 NUMA 节点的内存带宽差异很大。如果 Pod 没有绑 NUMA进程可能被调度到离卡很远的 CPU 核心上跨 NUMA 访问显存会吃掉不少性能。我们在容器启动命令里用numactl --cpunodebind0 --membind0把推理进程绑到离 DCU 最近的 NUMA 节点上配合 device plugin 上报的拓扑信息吞吐大约提升了 10%-15%。这次适配做完我个人最深的体会是不要被“虚拟化”“平台化”这些词吓住DCU 接 K8s 本质上就是把三件事做对一是设备节点暴露给容器二是资源逻辑映射给调度器三是隔离策略匹配业务场景。整卡模式适合当起点软件 vDCU 适合当日常工作方式硬件 vDCU 留给真正有强隔离需求的业务。对大多数团队来说先跑通整卡和软件 vDCU把 DeepSeek 这类模型稳定服务起来已经能覆盖 80% 的诉求。剩下那 20%等业务量到了再去啃也不迟。