把海光 DCU 接进 Kubernetes,再接到 CubeStudio 这类云原生 AI 平台上,最后在平台上跑起 DeepSeek,这是一套典型的大模型基础设施落地路径。我最近把这套环境完整过了一遍,整卡、共享、两种 vDCU 虚拟化方式都体验到了,中间踩了不少坑,也把关键原理梳理清楚了。这篇就按实际操作的顺序,把 DCU 接入 K8s 的资源模型、vDCU 调度方式、CubeStudio 对接,以及最后部署 DeepSeek 推理服务的过程完整写出来。如果你手里正好有海光 DCU 的机器,或者老板给了你一个“把大模型放到平台上去”的任务,这篇文章能帮你少走一个星期的弯路。
1. 先盘清环境和资源模型
1.1 这四种接入方式的本质区别
海光 DCU 本质上是一块类似 GPU 的协处理器,Kubernetes 默认不认识它。要让 Pod 使用 DCU,不是装个驱动就能自动调度,必须先把 DCU 变成 Kubernetes 可感知、可分配的资源对象。这里说的“整卡、共享、两种 vDCU 虚拟化”,其实对应了四种不同的资源粒度。
整卡模式最简单,一个 Pod 独占一张物理 DCU。调度层面只要把资源名上报成hygon.com/dcu: 1,Kubelet 就会把这张卡分配给容器。隔离性最强,也不会出现邻居 Pod 抢占算力的问题,缺点是资源利用率不够细,跑一个小模型也占一整张卡。
共享模式是在整卡基础上做的软切分。多个 Pod 可以共用一张物理 DCU,底层由调度器和驱动按时间片把算力分给不同容器。这种模式适合推理类业务,因为推理请求本身就有空闲期,时间片共享能把碎片算力捡起来。但它的显存往往不会做严格隔离,一旦某个 Pod 的模型把显存占满,其他共享 Pod 很容易直接 OOM。
第一种 vDCU 虚拟化可以理解为“空间切片”,即把一张物理 DCU 划分成多个虚拟 DCU,每个虚拟设备拥有独立的显存区间和算力配额。这是我最推荐的模式,因为每个 Pod 拿到的是一个相对干净、容量可控的虚拟卡,互不影响。
第二种 vDCU 虚拟化偏向“细粒度配额”,它会为每个虚拟 DCU 配置算力比例和显存上限,但底层可能还是复用同一份物理执行单元。它比纯时间片共享多了显存边界,比整卡模式灵活度更高,适合平台方按资源规格售卖算力。
从平台运营角度看,这四种模式不是互斥的。CubeStudio 这类平台通常需要同时支持几种资源类型,让用户按任务选择。
1.2 硬件、驱动、K8s 版本选型
我在实际部署时用的环境如下,可以作为一个基础参考:
- 服务器:海光 DCU 节点,机器上插了 4 张 DCU 卡
- 操作系统:Ubuntu 22.04 LTS,内核保持系统默认 5.15 或以上
- DCU 驱动:安装海光 DCU 官方驱动包,里面有
rocm-smi、hiplz等工具 - Kubernetes:1.26 版本,容器运行时使用的 containerd
- 调度插件:海光 DCU Device Plugin,以 DaemonSet 方式运行在 K8s 集群中
- 存储:模型数据放在独立的 NAS PVC 里,不和系统盘混用
如果你用的 K8s 版本是 1.24 以下或者 1.28 以上,功能上没有太大差异,但 Device Plugin 的 socket 机制可能受 kubelet 启动参数影响。我建议不要一上来就用太新的 K8s 版本,除非你已经确认海光 DCU 插件兼容。
驱动装完后,第一步不是接 K8s,而是先在宿主机上确认 DCU 能被系统识别。执行:
rocm-smi如果输出里能看到设备列表、温度、显存容量,说明驱动和硬件都没问题。如果命令报错,先去排查驱动安装是不是完整,再继续后面的步骤。这个检查看起来简单,但能省掉后续一大半的排查时间。
1.3 资源命名要提前定好
接入 Kubernetes 时,资源名是个容易忽略的坑。海光 DCU Device Plugin 上报的资源名,不同版本可能不一样。我见过hygon.com/dcu、amd.com/gpu、dcu.hygon.com/vdcu几种写法。资源名并不是随便定的,它决定了后面所有 Pod 的resources.limits里的 key,也决定了 CubeStudio 资源池里需要配置的名称。
我的建议是,在最开始就统一用一个你能识别出来的名字,比如hygon.com/dcu表示整卡,hygon.com/vdcu表示虚拟化 DCU。后续所有 YAML、平台配置都用这一套命名,避免后面出现资源名对不上、调度器死活不分配的问题。
2. 整卡模式:先把 DCU 变成 K8s 可调度资源
2.1 Device Plugin 到底在做什么
Kubernetes 本身不知道什么是 DCU,它只知道 CPU 和内存。所谓“接入 DCU”,核心工作是实现 Kubelet 的 DevicePlugin 接口。这是一条本地 gRPC 通道,Device Plugin 启动后会通过/var/lib/kubelet/device-plugins/dcu.sock跟 Kubelet 握手,上报自己管理哪些设备,然后 Kubelet 把这些设备变成节点上的 allocatable 资源。
当用户创建 Pod 并声明hygon.com/dcu: 1时,调度器会把 Pod 分配到有 DCU 资源的节点上,Kubelet 再调用 DevicePlugin 的Allocate()方法。这一步相当关键,因为 DCU 不是普通存储设备,容器不能直接通过/dev/dcu0拿设备,还需要注入环境变量、挂载设备节点、可能还要复制用户态访问库。这些动作都在Allocate()阶段完成。
所以你会看到一个现象:同一个镜像,如果在宿主机上直接跑没问题,但放到 K8s 里启动就报 “DCU not found”。这往往不是镜像问题,而是 Device Plugin 没有把设备环境注入进去。
2.2 部署海光 DCU Device Plugin
拿到海光 DCU 官方的适配包之后,里面一般会带一个dcu-device-plugin.yaml。如果没有官方版本,也可以参考社区实现,核心逻辑都是一样的。下面这个是可以在大多数环境直接改改用的 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: tolerations: - operator: Exists containers: - name: dcu-device-plugin image: registry.example.com/dcu-device-plugin:v1.0 imagePullPolicy: IfNotPresent env: - name: DCU_RESOURCE_NAME value: "hygon.com/dcu" 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部署命令很简单:
kubectl apply -f dcu-device-plugin.yaml kubectl get pods -n kube-system -l name=dcu-device-plugin看到 Pod 进入 Running 状态后,检查节点资源:
kubectl describe node <node-name> | grep -A 5 "Allocatable"如果能看到类似hygon.com/dcu: 4的资源,说明整卡资源已经成功上报。
这里有个容易踩的坑:修改资源名后,必须重启 kubelet 或者重启 Device Plugin Pod,否则上报的资源名不会自动更新。有时候你改了 DaemonSet 的环境变量,但旧的 socket 连接还在,Kubelet 就只会显示旧资源名。
2.3 用整卡跑一个测试 Pod
整卡模式验证起来最简单。创建一个测试 Pod,声明hygon.com/dcu: 1,镜像用带rocm-smi的工具即可:
apiVersion: v1 kind: Pod metadata: name: dcu-test spec: restartPolicy: OnFailure containers: - name: rocm-smi image: registry.example.com/rocm-smi:ubuntu22.04 command: ["rocm-smi"] resources: limits: hygon.com/dcu: 1为什么只写limits不写requests?因为设备插件管理的扩展资源通常只按limits判定,写了requests也不会影响调度。这是 Kubernetes 扩展资源的通用逻辑,CPU、内存可以单独设 requests,但像是hygon.com/dcu这种资源,你只需要在 limits 里声明数量。
执行:
kubectl apply -f dcu-test.yaml kubectl logs dcu-test如果能正常打印 DCU 设备信息,整卡链路就通了。接下来可以进到容器里跑一个简单的 PyTorch 算子,确认矩阵乘法能正常执行。
3. 让一张卡可以多租户用:两种 vDCU 虚拟化
3.1 时间片共享 vDCU 的落地方式
整卡模式解决了“有卡可用”的问题,但利用率不够。很多模型推理场景下,单个 Pod 对算力的需求远没有跑满整卡,于是最常见的第一种 vDCU 虚拟化就出现了:时间片共享。
时间片共享的本质是让多个 Pod 交替使用同一张物理 DCU 的计算单元。它不切显存,更像 CPU 的时分复用。实现上,海光 DCU 驱动会加载一个 vDCU 控制层,由它负责把不同 Pod 的计算任务排入调度队列。
在 Device Plugin 的配置里,通常会把一张物理卡标记为共享卡,并指定最多共享给几个 Pod。假设一张卡共享给 4 个 Pod,那你可以在 configmap 里配置:
apiVersion: v1 kind: ConfigMap metadata: name: dcu-plugin-config namespace: kube-system data: config.json: | { "shared": { "enabled": true, "sharedNum": 4 } }这里的关键是理解它的边界。时间片共享对显存没有严格隔离,如果其中一个 Pod 的模型申请了超过本身共享配额的内存,驱动可能直接报 OOM,甚至影响同一个物理卡上的其他 Pod。它适合跑一些轻量模型、开发调试、短任务,不适合把大模型推理放到上面。
而且时间片共享在调度层面仍然是hygon.com/dcu: 1,只是底层由驱动做了时分复用。这意味着你无法通过limits精确控制“这半个卡给谁”,只能依赖驱动默认的时隙策略。
3.2 空间切片 vDCU:显存和算力都隔离
第二种 vDCU 虚拟化做得更“硬核”:从物理 DCU 上切出逻辑分区,每个分区有独立的显存地址空间,也分配了独立的算力配额。
我实际使用时,是通过 vDCU 管理工具创建分区的。不同版本工具名字可能不同,但流程类似,先列出物理卡索引,然后创建虚拟 DCU:
dcu-smi vdcu create -d 0 -m 16G -c 50这条命令的意思是,从物理卡 0 上创建两个可用的 vDCU,一个给 16GB 显存、算力配额 50%。具体参数要以你手上的工具为准,但核心关注两个值:
- 显存大小:决定跑什么模型
- 算力配额:决定跑多快
创建完成后,系统里会出现对应的虚拟设备节点。Device Plugin 扫描到这些虚拟节点后,会把它们作为独立资源上报到 K8s,资源名可以配置成hygon.com/vdcu。
空间切片 vDCU 最大的优点是可预期性。一个 Pod 说自己要 16GB 显存,那就真的只能拿到 16GB,不会跑到别的分区去。另一个 Pod 即使把显存占满,也影响不到你。平台做多租户隔离时,这套隔离能力是必须的。
但它的缺点也明显:分区一旦建好,物理容量就被固定了。你把 32GB 卡切成两个 16GB 后,如果后来有个任务要 24GB,就只能另想办法,需要先把分区删掉重新切。所以做容量规划时不能拍脑袋,一定先统计平台上的模型需求。
3.3 两种 vDCU 怎么选
两种 vDCU 不是替代关系,而是面向不同场景的组合。
| 对比项 | 时间片共享 vDCU | 空间切片 vDCU |
|---|---|---|
| 切分方式 | 按时间片共享执行单元 | 按显存和算力配额划分逻辑设备 |
| 显存隔离 | 弱,容易互相影响 | 强,独立显存区间 |
| 算力隔离 | 依赖调度时隙,有争抢 | 有配额限制,相对稳定 |
| 调度复杂度 | 低,一个共享卡可放多个 Pod | 高,需要预先创建分区管理 |
| 适用场景 | 轻量推理、开发调试、短任务 | 训练、大模型推理、多租户隔离 |
我个人的选择标准很简单:如果是平台给外部用户开放算力,用空间切片 vDCU;如果只是自己内部开发机,想多跑几个小服务,时间片共享会更省事。在 CubeStudio 这类平台上,通常做法是把两种 vDCU 都注册成不同资源类型,让用户在创建任务时自己选。
4. CubeStudio 接入:让平台认识 DCU
4.1 CubeStudio 的资源体系
CubeStudio 本身跑在 Kubernetes 上,它的核心能力是把繁杂的 K8s 对象包装成用户能直接使用的“资源”。我曾经一度以为它只支持 GPU,实际看下来,它所有资源类型都是可配置的。
接入海光 DCU 时,最基础的事情就是告诉 CubeStudio:平台里存在一种新的资源,它叫hygon.com/dcu,或者叫hygon.com/vdcu。
具体入口一般是在 CubeStudio 的管理后台找到“资源类型”或者“计算资源池”,把资源名填进去,再关联到对应的工作空间。如果你用的是通过 Helm 部署的 CubeStudio,也可以直接改配置中心里的资源模板。
这里有个容易踩的坑:CubeStudio 的某些版本在资源池里只识别固定的 GPU 字段。你填了hygon.com/dcu后,前端可能不显示,但提交任务时后端是能识别的。遇到这种情况不要慌,先用一个最小化训练任务验证调度,再回头处理前端展示问题。
4.2 给 CubeStudio 加一种资源模板
CubeStudio 里的训练任务最终会转换成 Kubernetes 的 Job 或 Kubeflow 的 Training Operator CRD。我建议你直接用一个 PyTorchJob 测试 DCU 调度。
下面是一个简单的PyTorchJob示例,调度器会把它放到有 DCU 资源的节点上:
apiVersion: kubeflow.org/v1 kind: PyTorchJob metadata: name: dcu-train-demo namespace: default spec: pytorchReplicaSpecs: Master: replicas: 1 restartPolicy: OnFailure template: spec: containers: - name: pytorch image: registry.example.com/pytorch:dcu command: - python - -c - "import torch; print('device ok')" resources: limits: hygon.com/dcu: 1如果你的 CubeStudio 版本比较老,不支持这种 CRD,也可以用普通 K8s Job 做验证。核心逻辑一样:在resources.limits里声明 DCU 资源,其他交给调度器。
CubeStudio 平台侧通常会提供“自定义服务”的功能,你可以在里面注册一个镜像模板,模板里预设好资源限制。比如创建一个名为py-dcu的服务模板,里面写死hygon.com/dcu: 1,之后用户创建 Notebook 或训练任务时,直接选这个模板即可。
4.3 Notebook 里验证 DCU 是否生效
在 CubeStudio 里创建一个 Notebook,资源类型选择刚才注册的 DCU 模板,镜像选择带 ROCm 支持的 PyTorch 镜像。
习惯上很多人会在 Notebook 里跑torch.cuda.is_available(),这里要特别说一下:海光 DCU 走的是 ROCm 生态,PyTorch 里虽然沿用torch.cuda的接口命名,但底层用的是 HIP。我在实际使用中更倾向于执行:
import torch print(torch.version.hip) print(torch.cuda.device_count())如果你用的是正确适配 DCU 的 PyTorch 镜像,torch.cuda.device_count()会返回可用的 DCU 卡数。如果返回 0,先检查容器有没有继承 Device Plugin 注入的HIP_VISIBLE_DEVICES环境变量。
有一次我遇到的问题是,容器能跑rocm-smi,但 PyTorch 看不到设备。排查后发现是镜像里的 ROCm 版本和宿主机驱动版本不一致。这个兼容问题在容器场景里尤其明显,因为你打包镜像时不一定会带上完整的 ROCm runtime,但宿主机驱动的接口版本可能更老或更新。最好的做法是直接使用海光官方提供的 PyTorch 镜像,或者基于官方镜像再叠加你自己的依赖。
5. DeepSeek 在 vDCU 上的部署实操
5.1 模型选型与显存估算
在 DCU 上部署 DeepSeek,我建议先从蒸馏版开始,比如 DeepSeek-R1-Distill-Qwen-7B 或 14B。如果你只有一两个虚拟 DCU,硬上 671B 的 MoE 模型不现实,基础设施层面的网络和显存都不够。
显存估算有一个简单公式:权重显存约等于参数量乘以精度字节数。以 7B FP16 为例:
- 权重部分约 14GB
- 加上 KV Cache、激活值和输入输出缓冲,保守估计至少 18GB 到 20GB
所以 24GB 的 vDCU 能比较舒服地跑 7B,但要控制最大序列长度。如果你切成 16GB 单个 vDCU,那 7B 也危险,建议量化到 INT4 后权重降到 4GB 左右,再配合 8192 的 max-model-len,勉强能跑。
我整理了一张简单的选型表:
| 模型 | 精度 | 权重显存约 | 建议 vDCU 显存 |
|---|---|---|---|
| DeepSeek-R1-Distill-Qwen-7B | FP16 | 14GB | 24GB |
| DeepSeek-R1-Distill-Qwen-7B | INT4 | 4GB | 8GB-16GB |
| DeepSeek-R1-Distill-Qwen-14B | FP16 | 28GB | 32GB 上下 |
| DeepSeek-R1-Distill-Qwen-14B | INT4 | 8GB | 16GB |
| DeepSeek-R1-Distill-Qwen-32B | FP16 | 64GB | 不建议单卡跑 |
| DeepSeek-R1-Distill-Qwen-32B | INT4 | 18GB | 32GB |
这张表只是粗略估算,实际占用还和max-model-len、gpu-memory-utilization、批次大小有关。部署时先按这个表选一个能跑的配置,再逐步调高模型长度。
5.2 用 vLLM 部署 DeepSeek 模型
vLLM 目前对 ROCm 生态支持不错,这也是我最推荐的推理框架。先准备一个带 ROCm 的 vLLM 镜像,或者在海光官方 PyTorch 镜像上自己安装 vLLM 的 ROCm 版本。
关键启动参数如下:
python -m vllm.entrypoints.openai.api_server \ --model /models/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B \ --served-model-name deepseek-r1-7b \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --port 8000如果在整卡模式下,gpu-memory-utilization可以给到 0.9 以上。但如果在 vDCU 空间切片上,我建议不要超过 0.85,因为虚拟设备的显存边界和物理卡剩余空间不一定完全对得上,留一点余量能避免莫名其妙的显存分配失败。
在 Kubernetes 里,我会用 Deployment 管理推理服务,PVC 挂载模型文件,然后声明 vDCU 资源:
apiVersion: apps/v1 kind: Deployment metadata: name: deepseek-r1-7b namespace: ai spec: replicas: 1 selector: matchLabels: app: deepseek-r1-7b template: metadata: labels: app: deepseek-r1-7b spec: containers: - name: vllm image: registry.example.com/vllm:rocm5.7 command: - python - -m - vllm.entrypoints.openai.api_server args: - --model - /models/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B - --served-model-name - deepseek-r1-7b - --tensor-parallel-size - "1" - --gpu-memory-utilization - "0.85" - --max-model-len - "8192" ports: - containerPort: 8000 resources: limits: hygon.com/vdcu: 1 volumeMounts: - name: model mountPath: /models volumes: - name: model persistentVolumeClaim: claimName: model-pvc这里有一个很多人问我的点:能不能同时申请两个 vDCU 来做张量并行?理论上可以,但实际效果不理想。因为两个虚拟 DCU 之间的互联带宽往往不如物理卡原生 NVLink 或 Infinity Fabric,张量并行会把通信延迟拉高,甚至比单卡跑还慢。我的建议是:先保证单卡能跑,再考虑多卡并行。
5.3 暴露服务并在 CubeStudio 中调用
Deployment 部署完成后,创建一个 Service 做内部负载均衡:
apiVersion: v1 kind: Service metadata: name: deepseek-r1-7b-svc namespace: ai spec: selector: app: deepseek-r1-7b ports: - port: 8000 targetPort: 8000在测试环境里,可以直接用kubectl port-forward临时验证:
kubectl port-forward -n ai service/deepseek-r1-7b-svc 8000:8000然后将 CubeStudio 的推理服务网关指向这个 Service。如果平台本身自带 API 网关,可以绑定 ClusterIP 类型 Service;如果想从集群外部访问,改成 NodePort 或者用 Ingress 都行。
测试接口:
curl http://127.0.0.1:8000/v1/models curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-r1-7b", "messages": [{"role": "user", "content": "用一句话介绍 Kubernetes"}], "max_tokens": 512, "temperature": 0.7 }'如果返回正常的 JSON 结果,DeepSeek 的推理链路就跑通了。接下来能做的扩展还有很多,比如接入 CubeStudio 的在线服务模块、配置自动扩缩容、对接日志监控等。
6. 我踩过的坑和排查速查表
6.1 DCU 明明插着,K8s 就是没有资源
遇到这种问题,先不要怀疑驱动,按顺序排查:
- 在宿主机执行
rocm-smi,确认 DCU 可见。 - 检查 Device Plugin Pod 日志。
- 检查节点上的 socket 文件是否存在。
- 查看节点 Allocatable 里的
hygon.com/dcu数量。
有一次我发现节点上根本没有hygon.com/dcu数量,查了半天发现是 Device Plugin 容器的/dev挂载缺失,容器内无法访问 DCU 设备文件,导致插件启动时扫描不到卡。把这个 hostPath 挂上就好了。
还有一次是 Kubelet 的 device plugin 目录需要重启 kubelet 才能刷新。修改了 Device Plugin 的资源名之后,旧连接不会自动断,所以我在步骤里反复强调:改完资源名记得重启 kubelet。
6.2 vDCU 的坑
vDCU 模式最常见的坑是“只切了算力,没切显存”。有些版本的 vDCU 工具,创建共享 vDCU 时如果不显式指定显存大小,默认会让容器看到整卡的显存。结果就是多个 Pod 都认为自己在独占整卡显存,一旦同时申请,直接触发驱动 OOM。
解决办法很简单:无论选哪种 vDCU,都明确配置显存上限。别写默认值,别留白。
另一个坑是 vDCU 分区重启后会丢失。物理机重启后,之前用dcu-smi vdcu create创建的分区不会自动恢复。你需要把创建命令做成开机自启,或者在 K8s 里跑一个 Job,每次启动时执行一次分区创建,然后再拉起业务 Pod。
6.3 DeepSeek 部署的坑
部署 DeepSeek 踩得最多的坑,是模型路径没挂对。vLLM 启动时如果模型路径不存在,会一直重试下载,然后在日志里刷Connection error。离线环境下这是致命的,因为根本没有机会下载权重。所以我在任何部署文档里都会强调:模型文件先通过 PVC 或镜像预置到目标节点,确保外部网络断开也能正常加载。
还有一个坑是max-model-len设置太大,导致在分配 KV Cache 时显存溢出。这个错误通常发生在有合法输出的情况下,但日志里会明显看到显存不足的报错。处理方式不是删任务,而是把max-model-len降低,或者把模型量化到更低精度。
6.4 排查速查表
| 现象 | 可能原因 | 检查方式 |
|---|---|---|
| 节点没有 DCU 资源 | Device Plugin 未上报 | kubectl describe node查看 Allocatable |
| Pod 调度失败 | 资源名写错 | 检查limits里的资源 key 是否和节点一致 |
| 容器内看不到 DCU | 设备或环境变量未注入 | echo $HIP_VISIBLE_DEVICES |
| vLLM 启动 OOM | max-model-len 过大或显存申请超限 | 调低gpu-memory-utilization |
| 推理延迟高 | 可能在时间片共享 vDCU 上 | 改用空间切片 vDCU 或整卡 |
| 模型加载卡住 | 模型路径不存在 | 查看 PVC 挂载和日志 |
最后说一点我自己的体会:把海光 DCU 接进 Kubernetes 和 AI 平台,技术链路并不玄乎,真正考验人的是细节审查。资源名要统一,驱动版本要匹配,虚拟化模式要根据场景选,模型显存要提前算清楚。我建议你从头到尾先把整卡链路跑通,再逐步上共享和 vDCU,最后再碰 DeepSeek。前面的步骤没验证完,就不要往后推进,不然问题叠着问题,排查起来非常痛苦。
另外一个很实用的小技巧:在 CubeStudio 里给不同 vDCU 类型打上 label,比如dcu-mode=shared和dcu-mode=vdcu,然后让业务 Pod 通过 NodeSelector 精确落到对应节点。这样既能利用资源共享,又能避免不同类型任务相互干扰。实测下来,对平台稳定性的提升非常大。