我在给客户做 AI 平台适配的时候,经常要回答同一个问题:手里有海光 DCU 这种国产加速卡,到底怎么接进 Kubernetes,怎么让平台上的训练和推理任务真正用起来?这个问题看着像是“装个驱动、写个 Device Plugin”的功夫活,但真做起来,牵扯到资源上报、调度策略、显存隔离、运行时注入一堆事情,尤其是 vDCU 虚拟化这块,方案选不对后面全是坑。这篇文章就用自己的实操经历来拆一遍 CubeStudio 平台接入海光 DCU 的完整链路,从整卡到共享,从两种 vDCU 虚拟化到 DeepSeek 模型部署,把每一步怎么选、怎么做、踩过什么坑都交代清楚,希望能给正在做国产算力适配的人省点弯路。
1. 整体方案拆解:DCU 进 K8s 到底要解决哪几件事
1.1 为什么不能“装个驱动就完事”
很多人刚接触海光 DCU 的时候,第一反应是“它支持 ROCm,那我把驱动装上,容器里把/dev/dri和进程库挂进去,不就能跑了吗”。单机单卡这么玩确实能跑通,但一旦进了 Kubernetes,事情就变成另一套逻辑了。K8s 的调度器根本不认识 DCU,它只知道 CPU、内存这种原生资源,设备要接入必须通过扩展资源(Extended Resource)的方式被“发现”并“上报”,然后由调度器决定把 Pod 调度到哪些节点上。
这里的核心问题有三个:一是设备发现和资源上报,节点上的 Kubelet 得知道这块节点有几张卡、每张卡多大显存,然后实时报给 API Server;二是调度感知,用户申请 1 张卡或 2 张卡的时候,调度器得能把这些请求映射到具体节点上,还得避免同一张卡被多个 Pod 同时占用;三是运行时的设备注入,Pod 真正跑到节点上的时候,容器里得能看到对应的设备文件、驱动库、环境变量,否则程序根本调不到 DCU。
海光 DCU 比较特殊的地方在于,它并不像 NVIDIA 那样有原生的 MIG 或 vGPU 方案可以直接对接 K8s,整卡模式下有成熟的社区路径,但要做共享和虚拟化,就需要一套自己的 vDCU 调度机制。对 AI 平台来说,如果只支持整卡,那么闲时算力会白白浪费,这也正是 CubeStudio 这类平台要同时支持整卡和两种虚拟化模式的原因。
1.2 CubeStudio 平台侧的适配框架
我在这个项目里用的 AI 平台是 CubeStudio,它本身是一个面向模型开发、训练和推理的云原生平台,底层依赖 Kubernetes,向上提供项目空间、资源队列、任务模板这些能力。它的资源管理层屏蔽了具体设备类型,调度的时候只认 Resource Quota 和节点 label。所以适配 DCU 的思路是:平台资源层按“抽象资源”来建模,设备类型只是资源池的一个属性。
具体的链路拆解下来是四段:
DCU 驱动和容器运行时 -> Device Plugin(资源上报与健康检查) -> 调度插件(vDCU 分配与绑卡) -> CubeStudio 业务层(队列、模板、配额)每一段都有独立的坑。驱动段要解决的是“容器里能不能看到设备”;Device Plugin 段要解决的是“K8s 怎么知道节点有卡”;调度插件段要解决的是“请求怎么映射到具体物理卡或 vDCU 实例”;业务层段要解决的是“用户提交任务时怎么声明资源、怎么选择模式”。缺任何一段,整个闭环都跑不起来。
2. 两种 vDCU 虚拟化模式:到底有什么区别,怎么选
2.1 整卡模式:最简单也最浪费
先看最基本的整卡模式。在这个模式下,资源模型就以“张”为单位,一张物理卡等于一个调度单位。用户申请hygon.com/dcu: 1,调度器就找有空闲卡的节点,然后通过 Device Plugin 在容器里注入对应的/dev/dri/renderDxxx设备节点和 ROCm 用户态库,Pod 起来之后整张卡被独占。
这种模式的优势是隔离性好,一个任务出问题不会影响同节点的其他任务,性能也能打满;缺点是资源利用率很低。我实际看过一个客户集群,跑微调任务的时候 GPU 利用率平均只有 30%-40%,但显存占着又不能给别的任务用,很浪费。如果团队规模小、任务密度低,整卡模式足够用;但如果要做多租户、模型服务混部,整卡模式就不够看了。
整卡模式的配置相对简单,发布一个 DaemonSet 形态的 Device Plugin 就行。Device Plugin 主要的逻辑是定期调用底层驱动接口,拿到每张卡的健康状态和显存容量,然后通过 ListAndWatch 接口上报给 Kubelet。我用的 DCU 驱动是 DTK 24.04.1,设备节点路径是/dev/dri/renderD128开头的几个节点,可以通过dcu-smi命令确认到底有几张卡。
2.2 vDCU 虚拟化:两条路线各有各的逻辑
到了共享模式,就需要引入 vDCU 虚拟化。所谓“两种 vDCU 虚拟化”,我理解的落地形态分别对应显存隔离型和进程虚拟化型,它们的核心逻辑和适用场景差别很大。
第一种是基于显存分片的 vDCU。这种方案在物理卡上划分出多个显存区域,每个区域配上独立的计算上下文配额,类似把一张 64G 的卡切成 4 个 16G 的 vDCU 实例。每个 vDCU 实例有独立的设备句柄、独立的显存空间,甚至可以在用户态层面模拟出独立的设备索引。这种方案的隔离性比较好,显存不会互相越界,适合跑模型推理和中小模型微调。缺点是对驱动和运行时依赖比较深,如果 DCU 驱动不提供底层的分片能力,就只能靠用户态库“伪装”显存上限,隔离强度会打折。
第二种是基于进程调度的 vDCU。这种方案把物理卡资源统一交给一个调度代理,代理按策略把计算任务轮流分发到不同计算队列上,同时用 cgroup 或在 ROCm 运行时层面做显存限制。打个比方,就像一台物理服务器上开多个虚拟机,CPU 和内存是超卖共享的,但物理机本身只有一个。这种方案的好处是密度高,可以支持几十个 Pod 共享几张卡;坏处是性能隔离弱,任务多了可能互相争抢显存带宽,导致某个 Pod 的推理延迟突然飙高。
CubeStudio 适配里这两种模式我都在用,原则是:在线推理服务和需要稳定资源的训练任务用第一种,离线批处理任务和开发调试环境用第二种。两种模式在 K8s 里的实现方式完全不同,第一种通常需要在 Device Plugin 之外再写一个“vDCU Manager”组件,负责在节点上维护 vDCU 实例的分配关系;第二种则像是一个用户态的调度器,把扩展资源请求翻译成对底层队列的配额订阅。
3. 实操:从 DCU 驱动到 CubeStudio 跑通 DeepSeek
3.1 环境准备:驱动、运行时和工具链一次性装齐
先说环境版本,避免大家照抄的时候踩版本坑。我的这套环境是:
| 组件 | 版本 |
|---|---|
| 操作系统 | Ubuntu 22.04 LTS(内核 5.15) |
| 海光 DCU 驱动 | DTK 24.04.1 |
| 容器运行时 | containerd 1.7.x |
| Kubernetes | 1.28.x |
| CubeStudio | 2.x 版本 |
| ROCm 用户态 | rocm-dev 5.x(DTK 自带适配) |
安装驱动的过程这里不展开,但有一点要强调:驱动装完之后一定要确认容器里能调 DCU。很多人栽在主机上 dcu-smi 正常,但容器里访问不了设备。确认的关键点是三样东西:一是/dev/dri/renderDxxx设备节点有没有正确注入,二是/opt/dtk或 ROCm 的librocclm.so这些用户态库能不能在容器里找到,三是/proc/driver/hygon等内核模块信息能否在特权容器和普通容器里正确读取。
我的做法是先起一个测试容器手动验证一遍:
docker run -it --rm \ --device=/dev/dri \ -v /opt/dtk:/opt/dtk \ -e LD_LIBRARY_PATH=/opt/dtk/lib:/opt/dtk/rocm/lib \ registry.example.com/dcu-runtime:base \ bash -c "rocm-smi && python3 -c 'import torch; print(torch.cuda.is_available())'"注意这里的torch.cuda.is_available(),海光 DCU 的 PyTorch 支持走的是 ROCm 后端,所以在安装了对应适配版本的 PyTorch 之后,torch.cuda层面的 API 是通用的。如果你的镜像里 CUDA 相关环境变量和 ROCm 冲突了,会出现“能 import torch,但 is_available 一直是 False”的怪问题,后面会单独说。
3.2 K8s Device Plugin 部署:让集群“看见”DCU 卡
驱动和镜像验证通过之后,第一步就是把 DCU 以扩展资源的形式注册进 K8s。这里不用自己从零写 Device Plugin,海光 DTK 有配套的 plugin 插件。但就算是现成插件,配置上还是有几个关键点。
资源名称建议统一用hygon.com/dcu,这个名称会直接成为用户在 CubeStudio 里申请资源的标识。Device Plugin 的 DaemonSet 必须配置:
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: hostNetwork: true containers: - name: dcu-device-plugin image: registry.example.com/dcu-device-plugin:dtk-24.04.1 securityContext: privileged: true volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: sys mountPath: /sys - name: dev mountPath: /dev env: - name: DCU_RESOURCE_NAME value: "hygon.com/dcu" volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins - name: sys hostPath: path: /sys - name: dev hostPath: path: /dev注意几个坑。第一,必须在 Kubelet 的启动参数里配置--feature-gates=DevicePlugins=true,一般云原生发行版默认是开着的,但自建 K8s 容易漏。第二,Device Plugin 启动顺序必须在 Kubelet 之后,如果 DaemonSet 在节点初始化阶段就跑起来了,而 Kubelet 还没注册设备插件,插件进程会自动重试,一般来说不用太担心。第三,健康检查逻辑要看日志,DCU 驱动如果加载失败,Device Plugin 会把对应设备标记为不健康。
部署完之后,检查节点资源是否注册成功:
kubectl describe node dcu-node-01 | grep -i dcu正常情况下能看到类似hygon.com/dcu: 4的 allocatable 资源。如果看不到,先看 Device Plugin 的日志,再排查设备节点的权限。
3.3 整卡模式:首个 DCU Pod 跑起来
资源注册好之后,先跑一个整卡模式的 Pod 验证链路是否通。示例 Pod 配置:
apiVersion: v1 kind: Pod metadata: name: dcu-test spec: restartPolicy: Never containers: - name: dcu-test image: registry.example.com/dcu-runtime:base command: ["/bin/bash", "-c"] args: - | rocm-smi python3 -c "import torch; print(torch.cuda.get_device_name(0))" resources: limits: hygon.com/dcu: 1调度器看到hygon.com/dcu: 1的 request 之后,只会把 Pod 调度到有 DCU 的节点。Pod 起来之后,容器里看到/dev/dri/renderD129这类节点,rocm-smi能列出一张卡,PyTorch 也能拿到设备名,说明整卡链路已经通了。
整卡模式最需要注意的其实是resources.limits和resources.requests要写一致。Device Plugin 模式下,扩展资源只有 limit 有实际意义,request 只是给调度器看的,如果你只写 requests 不写 limits,Kubelet 不会帮你在容器里注入设备节点,Pod 会因为拿不到设备而各种报错。
3.4 共享模式:两种 vDCU 的部署与调度配置
整卡通了之后,重点来了——共享模式。我这里把两种 vDCU 分别部署,对应不同的平台配额池。
先看显存分片型 vDCU。这种方案我采用的部署方式是“节点级 vDCU Manager + 独立资源名”。也就是说,每个 DCU 节点上除了 Device Plugin,还跑一个 vDCU 管理组件,它会读取一张卡的总显存,按配置切分成多份,并为每个 vDCU 实例分配独立的设备号和显存配额。资源上报的名称用hygon.com/vdcu-mem,单位是“MiB”的整数值,这样用户在 CubeStudio 里可以申请 16384MiB 这样的显存请求。
配置上,vDCU Manager 的关键参数是:
--device-ids=0-3 # 管控的物理卡范围 --slice-mode=memory # 显存分片模式 --slice-size=16384 # 每个 vDCU 的显存配额(MiB),按需调整 --resource-name=hygon.com/vdcu-mem切片的时候要留一点余量。比如 64G 的卡,标称 65536MiB,实际驱动预留之后可分配显存不到那么多,所以单卡一般切成 3 个 16G 或者 2 个 24G,而不是贪心切 4 个 16G。切太满的话,任务跑到峰值显存有 OOM 风险,调度器还需要处理“机器有卡但显存不够”的尴尬局面。
第二种进程调度型 vDCU 则不同。它不把显存硬切,而是把整张卡抽象成一个“算力池”,资源名称用hygon.com/vdcu-share,调度器根据每个 Pod 声明的算力权重来分配底层计算队列的配额。CubeStudio 里的配额池可以配置“最多提交 N 个作业共享 1 张卡”,实际落地的时候,我见过比较稳的配置是共享比控制在 1:4 以内,也就是一张卡最多同时 4 个 Pod,再高就容易出现显存带宽瓶颈。
3.5 CubeStudio 侧适配:模板和队列双管齐下
平台侧要做的事情相对聚合,主要是在 CubeStudio 的管理后台里新增资源规格、配置队列、定义模型服务模板。
首先是资源规格。CubeStudio 管理资源走的是“资源规格 -> 队列 -> 项目”的层级。我需要先定义几种规格:dcu-whole(整卡规格,hygon.com/dcu: 1)、dcu-vdcu-mem-16g(显存分片规格,hygon.com/vdcu-mem: 16384)、dcu-vdcu-share-1(进程调度规格,hygon.com/vdcu-share: 1)。然后把不同规格绑到不同队列的配额上。
其次是容器运行时配置。CubeStudio 提交的训练任务或推理服务最终都会变成 K8s 的 Deployment/Job,所以我需要在平台的“运行时模板”里把设备注入和环境变量固化下来。我给的模板核心内容如下:
resources: limits: hygon.com/dcu: 1 # 或者 hygon.com/vdcu-mem: 16384 env: - name: LD_LIBRARY_PATH value: /opt/dtk/lib:/opt/dtk/rocm/lib - name: HIP_VISIBLE_DEVICES value: "0" volumeMounts: - mountPath: /opt/dtk name: dtk volumes: - hostPath: path: /opt/dtk name: dtk这里有个容易踩的坑:HIP_VISIBLE_DEVICES和CUDA_VISIBLE_DEVICES的行为不一样。CUDA 生态里很多框架会把CUDA_VISIBLE_DEVICES当作标准环境变量,但海光 DCU 是 ROCm 生态,识别的是HIP_VISIBLE_DEVICES。如果只设了 CUDA 变量,部分框架会认为没有可用设备。最稳妥的办法是两个变量都设置,或者干脆交给 Device Plugin 去注入,不要在模板里写死。
最后是模型服务模板。因为标题里明确要部署 DeepSeek,我在 CubeStudio 里配置的是“vLLM 推理服务模板”,镜像直接选用适配 DCU 的 vLLM 版本,启动命令通过模板参数动态拼接。下文单独讲部署细节。
3.6 DeepSeek 部署:在共享和整卡上的两套方案
DeepSeek 部署这块,实际要看模型规模和可用卡的显存。满血版 DeepSeek-V3 是 671B 参数,FP16 权重光模型文件就要 1.3TB 以上,显然不是单机几卡能搞定的,需要多节点并行,那属于大规模集群部署的范畴。这次实操我主要部署的是 DeepSeek 蒸馏版模型,以 DeepSeek-R1-Distill-Qwen-14B 为例,FP16 权重约占 28GB 显存,单张 32G 或 64G 的 DCU 可以跑推理,但用共享模式跑这种规模的模型会比较吃力。
先看整卡模式部署 vLLM。在 CubeStudio 里我建了一个服务模板,核心参数如下:
apiVersion: apps/v1 kind: Deployment metadata: name: deepseek-r1-distill-qwen-14b spec: replicas: 1 selector: matchLabels: app: deepseek-14b template: metadata: labels: app: deepseek-14b spec: containers: - name: vllm image: registry.example.com/vllm-dcu:v0.6.6-dtk24.04 command: ["python3", "-m", "vllm.entrypoints.openai.api_server"] args: - --model=/models/deepseek-r1-distill-qwen-14b - --served-model-name=deepseek-14b - --tensor-parallel-size=1 - --gpu-memory-utilization=0.9 - --max-model-len=8192 - --port=8000 resources: limits: hygon.com/dcu: 1 env: - name: LD_LIBRARY_PATH value: /opt/dtk/lib:/opt/dtk/rocm/lib - name: HIP_VISIBLE_DEVICES value: "0" volumeMounts: - name: model-storage mountPath: /models - name: dtk mountPath: /opt/dtkgpu-memory-utilization=0.9的意思是让 vLLM 最多用 90% 的显存做 KV cache 和激活值,这个值不要设成 1.0,否则预分配显存的时候容易撞到驱动预留内存导致初始化失败。max-model-len=8192也要根据实际显存调整,如果业务需要更长的上下文,14B 模型跑 8192 是够用的,跑到 16384 就会开始紧张。
再看共享模式部署推理。共享模式跑在线推理的核心问题是“显存被打爆”。我用显存分片型 vDCU 的时候,会把模型压到很小的 batch size,并且把gpu-memory-utilization调到 0.7 以下,保证共享场景下多个服务实例之间不会互相挤占。如果你用的是进程调度型 vDCU,建议不要部署重量级 LLM 服务,更适合跑一些轻量的 embedding 或文生文中间件。
DeepSeek 模型跑起来之后,验证接口:
curl -s http://<service-ip>:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-14b", "messages": [{"role": "user", "content": "你好,请简单介绍下你自己"}], "max_tokens": 200 }'返回正常的 JSON 响应,说明从 DCU 驱动、Device Plugin、调度器到 CubeStudio 模板的完整链路都通了。
4. 我踩过的坑和排查经验
4.1 设备节点存在但容器内不可见
这是最常见的第一课。主机上dcu-smi能正常列卡,但容器里跑rocm-smi就是看不到。排查步骤是:
ls -l /dev/dri/renderD*看设备节点权限,owner 是不是root:video,容器进程的 GID 有没有落到video组;- 看容器运行时是不是 containerd,containerd 下要确保 Device Plugin 的 CDI 或 device 注入链路是通的,不能沿用 Docker 时代
--device那套思维; - 用特权容器做对照实验,特权容器可见就说明是设备注入问题,不是驱动问题。
我最后定位到的原因往往是 Pod 的securityContext里没有设置runAsGroup到video组(GID 通常是 44)。不加这个配置,进程虽然有设备文件,但权限不够打不开/dev/dri/renderDxxx。
4.2 vDCU 显存隔离失效
显存分片型 vDCU 最怕的事情是,一个任务把显存打满,连带把同卡其他任务一起干掉。从实际表现看,用 DTK 24.04.1 自带的 vDCU 切分功能时,显存隔离是有效的,但如果你的方案只是“改了环境变量伪装显存上限”,那不叫隔离,随时可能崩。
要验证隔离强度,我提供一个简单方法:在一个 vDCU 实例里写一个申请大显存的 PyTorch 脚本,把显存打到接近配额上限,然后在另一个 vDCU 实例里跑同样的脚本,观察是否报 OOM。如果报 OOM 说明隔离生效;如果直接把整卡搞挂,说明隔离层没有真正拦截显存申请,得换方案。
4.3 torch.cuda.is_available() 返回 False
这个问题的根因通常是镜像里的 PyTorch 与 DTK 的 ROCm 版本不匹配。海光 DCU 的官方容器镜像会配套一个指定版本的 PyTorch,如果你从社区拉一个通用的 ROCm 版 PyTorch,大概率能 import 但找不到设备。
解决的思路是确认 DTK 版本和 pytorch 的 rocm 版本对齐。比如 DTK 24.04 对应的 ROCm 5.x,那镜像里的 PyTorch 必须是针对该 ROCm 版本编译的。还有一种情况是环境变量被覆盖,容器启动时如果设置了ROCR_VISIBLE_DEVICES而没有正确指定设备索引,也可能出现 False。
4.4 调度器超卖导致节点资源瞬间打满
共享模式下,如果 CubeStudio 里配的队列配额超过物理卡总能力,就可能出现调度器把 Pod 分配到节点之后,vDCU Manager 发现没有足够资源而拒绝的情况。排查时要分清是“调度层超卖”还是“运行时拒绝”。
调度层超卖的原因在于,我把hygon.com/vdcu-mem设计成了简单的整数计数型扩展资源,K8s 调度器无法感知同一张物理卡上不同显存片之间的拓扑关系。比如一张 64G 卡按 16G 一份切,理想情况是上报 4 份资源,但如果 vDCU Manager 在创建实例的时候发现物理卡只剩 14G 可用,就会拒绝。这种问题的解法是在 CubeStudio 侧限制同一张卡上的作业数,或者让 vDCU Manager 定期反向纠正节点资源余量,标记不健康的剩余资源。
4.5 DeepSeek 加载慢、启动失败
DeepSeek 蒸馏模型的权重文件动辄几十 GB,从对象存储拉取到节点的时间可能占据整个启动流程的大头。一个实际建议是把模型权重提前预热到节点本地磁盘,用 hostPath 挂载而不是每次启动都从远端拉取。CubeStudio 里我通常会配一个“模型预热 Job”,集群空闲的时候把常用模型推到各 DCU 节点上。
启动失败的另一个原因是权重文件损坏。vLLM 启动的时候如果safetensors文件校验不过去,会直接抛异常。这种时候不要盲目重启,先把模型目录的 checksum 和源文件比对一遍,很多时候是传输中断导致的文件截断。
5. 一些实操体会
海光 DCU 接入 Kubernetes 这件事,表面上看是一个设备插件的问题,实际做下来,你会发现真正的复杂度集中在“资源语义”的设计上。整卡、显存分片、进程调度三种模式,本质上是三种不同的资源抽象:整卡是离散计数,显存分片是连续量配额,进程调度是带权重的共享池。资源语义一旦定下来,调度器、平台侧、运营运维策略全都围绕它展开,后面改起来非常伤筋动骨。
所以想给正在做类似适配的人一个建议:动手之前先想清楚你的业务场景到底需要哪几种模式。如果主要跑大模型推理,整卡加显存分片就够了;如果要做多租户开发环境,进程调度型 vDCU 的性价比会高很多。千万不要一开始就追求“全都要”,虚拟化方案越复杂,运维开销越大,出问题的时候排查链路也越长。
另外,国产加速卡的社区工具链相比 NVIDIA 还是有不小差距,遇到问题很多时候没有现成方案,需要自己通过打日志、读源码、看驱动版本来定位。在这种条件下,保持“最小化验证”的习惯很管用:每次只改一个变量,从单机到集群逐步加复杂度,能少走很多弯路。
最后分享一个我在多个项目里验证过的经验:在部署 DeepSeek 等大模型推理服务时,不要只盯着 DCU 的算力,显存带宽和模型加载路径往往是瓶颈。把模型放在本地 NVMe 上,预分配好页表,设置合理的并发数,比盲目追求更高的推理框架参数更实际。先把一个模型跑稳,再迭代优化,这条路走得最踏实。