最近两周,我把一台装有海光 DCU 的节点成功接进了内部 Kubernetes 集群,又在我们自研的 AI 研发平台 CubeStudio 上把整卡、共享、两种 vDCU 虚拟化这几种调度形态全部打通。最后一步是在这套环境上把 DeepSeek 蒸馏模型的推理服务跑起来,用 OpenAI 兼容 API 对外提供服务。整个过程涉及的环节不少:驱动、DTK 运行时、Device Plugin、调度策略、平台资源模型、镜像适配、推理框架兼容……任何一个环节版本不对,后面全是白搭。今天把这套适配过程完整捋一遍,包括方案选型、配置文件、命令实例、踩坑记录,适合正在用海光 DCU 做容器化调度、或者正准备在国产 DCU 环境下部署推理服务的同学参考。里面很多东西是我实际试出来的,不一定全对,但一定都是能落地的。
1. 整体方案:DCU 接入 K8s 的平台视角
1.1 为什么要让 DCU 变成 K8s 的可调度资源
AI 业务不是孤立跑在一台机器上的。训练脚本、推理服务、数据预处理、模型评测这些任务都需要排队、隔离、配额管理。如果把 DCU 直接绑定在某个进程里,资源利用率很难上去,尤其是多人共用的研发环境,经常出现一个人占着卡、其他人排队等着的情况。把 DCU 抽象成 K8s 里的扩展资源,配合 CubeStudio 这类平台,用户只需要在界面上选“要几张卡、多大显存”,平台自动帮他把 Pod 创建出来,调度、回收、监控都走一套统一流程。
扩展资源的工作机制值得先说清楚:K8s 本身不认识 DCU 是什么,它只认设备插件上报的数字。Kubelet 启动后,会监听设备插件通过 Unix socket 注册的信息;设备插件告诉 Kubelet“这个节点有 4 个 hygon.com/dcu”,之后调度器在分配 Pod 时就会把这一项当成普通资源来计数。比如 Pod 声明 limits.hygon.com/dcu=1,调度器发现某个节点还剩下 1 个,就调度过去;随后 kubelet 调用设备插件的 Allocate 接口,插件再告诉 kubelet 需要把哪些设备节点、哪些环境变量注入到容器里。整个过程对外看像一个自定义资源调度器,实际核心就在那段不到 300 行的设备插件代码里。
做个不太严谨但容易理解的类比:把 DCU 接入 K8s,相当于把一个独立发电机改造进统一电网。设备插件是变压器,扩展资源是电表读数,调度器是电网调度员。变压器不工作,发电机就算转得再猛,电也送不到用户楼里;电表读数不对,调度员就会乱派任务,有的楼没电,有的楼超负荷。
1.2 两种 vDCU 虚拟化路线的选型
标题里的“两种 vDCU 虚拟化”是我在实际项目里分别验证过的两条路线,不能混着用,得先选清楚。
第一类是硬虚拟化,也就是基于 mdev 思路在驱动层切分物理卡。一张 32GB 的 DCU 卡可以根据预设的模板切成两个 16GB、或者四个 8GB 的 vDCU,每个 vDCU 在宿主机上表现为一个独立设备节点,显存隔离是硬件层保证的,一个容器把显存写满也不会影响同卡上的另一个容器。这类方案隔离性最好,适合跑生产推理服务,缺点是模板粒度固定,32GB 卡切成 4 份就只能按 8GB 申请,想申请 12GB 就没这个组合。
第二类是软虚拟化,或者叫调度级共享。设备插件把物理卡抽象成若干份“卡份”,比如 32GB 显存抽象成 8 份,每份对应 4GB 配额;多个 Pod 可以同时调度到同一张卡上,显存上限和算力份额由驱动运行时配合 MPS 机制做限制。这类方案灵活度高,可以按任意整份数申请,适合开发和批量小任务,但隔离性不如硬虚拟化,某个程序出现显存越界或驱动 hang 住,还是可能影响同卡邻居。
在实际环境里,我最后是两条路线同时保留:整卡和 vDCU 硬虚拟化提供给算法同学做模型训练和推理部署,共享软虚拟化提供给日常调试和 CI 任务使用。这样既保证了核心业务的稳定性,又不浪费碎片资源。
1.3 整体资源流转链路
一个用户从 CubeStudio 提交任务到最终在容器里看到 DCU,走的是这样一条链路,注意不是一条直线,中途有多个转换节点。
用户在 CubeStudio 界面上选择镜像、资源规格(例如“整卡 DCU x1”),平台后端把规格翻译成 Pod manifest;manifest 里的 resources 部分写入 limits.hygon.com/dcu 或者 hygon.com/vdcu,并加上对应的 nodeSelector 或者亲和性配置;K8s 调度器根据扩展资源数量决定把这个 Pod 放到哪台节点;节点上的设备插件被 kubelet 调 Allocate 后,返回设备路径(通常是 /dev/kfd 或 /dev/dri/renderDxxx、一些 vDCU 专属节点)、环境变量(比如 HIP_VISIBLE_DEVICES)、挂载点(DTK 库目录或驱动的用户态库);容器运行时把这些注入到容器里,进程起来后通过 HIP/OpenCL 接口访问 DCU。
经常有人犯一个错误:以为把 driver 装到宿主机、Pod 里手动挂一下 /dev/kfd 就能用。实际上如果没有设备插件把环境变量和资源配额注入,容器里就算看得到设备,驱动也只会把整张卡暴露给所有容器,多个 Pod 同时访问同一个计算卡,资源隔离基本不存在,显存冲突之后非常难排查。所以第一步必须先让设备插件正常工作,整条链路才有意义。
2. 环境准备与设备插件部署
2.1 硬件、驱动和 DCU 运行时
在开始之前,先把软硬件清单列清楚。我这里用的是海光深算系列 DCU,具体型号不必纠结,只要是 ROCm 兼容架构就用同一套思路。单机多卡配置,操作系统为标准的 Linux 发行版,内核启用了 amdgpu 相关模块。
安装驱动之前,有两个前置检查最常见。一个是检查 BIOS 里是否开启了 Resizable BAR / Above 4G Decoding,如果不开启,显存映射可能受限,驱动加载后会报告奇怪的问题。另一个是确认内核版本和驱动包的兼容关系,海光的 DCU 驱动安装包里一般会附带对应内核的 DKMS 模块,但如果你用的内核是自定义编译或者是太新的发行版内核,编译报错是家常便饭。
驱动装好之后,建议立刻用自带的管理工具验证一下是否能看到卡,命令名在 DTK 不同版本里可能不一样,常见的是 dcmi 或 dcu-smi。看到类似 4 张卡的列表、每张卡的显存总量,才说明驱动层正常。不要跳过这一步直接装容器,否则后面出了问题你分不清是驱动问题还是容器配置问题。
紧接着安装 DTK(DCU Toolkit),这是海光提供的一套 GPU 计算工具包,里面包含 HIP 编译器、运行库、数学库(比如 hipBLAS、hipFFT 这一系列)。DTK 默认路径一般是 /opt/dtk,装完以后需要把 /opt/dtk/bin 加入 PATH、把 /opt/dtk/lib 加进 LD_LIBRARY_PATH。这一条很多人会漏,漏了以后容器里编译 HIP 程序,第一句就会报找不到 hipcc。
2.2 K8s 集群预检与镜像准备
集群节点这边需要注意几个点。kubelet 必须启用了 DevicePlugins 特性,新版本默认开启,老版本需要加 --feature-gates=DevicePlugins=true。容器运行时这里我用的是 containerd,docker 也没有问题,但有一点要注意:如果平台或设备插件依赖 /var/lib/kubelet/device-plugins 目录,确保 kubelet 用的是默认 kubelet 目录,不要随意改。如果节点上有多个 GPU 厂商的设备插件(比如 NVIDIA 插件),建议先确认 socket 名称和端口不冲突,常见冲突是把不同厂商插件都注册成同一个 socket 名。
镜像方面,建议做一个 base 镜像,基于主流发行版,在 Dockerfile 里做几件事:安装 DTK 运行时库(也可以把宿主机 /opt/dtk 目录挂载进去,但更推荐直接打进镜像);设置 PATH 和 LD_LIBRARY_PATH;安装一些常用的运维工具(curl、gdb、strace、htop 这些,排查问题必备)。把 base 镜像准备好,后面接 CubeStudio 的时候,平台里所有任务都基于这个镜像扩展,省得每次都要重新装一遍运行库。
2.3 部署 DCU Device Plugin 并验收资源上报
设备插件是整条链路的核心。这里有几种来源:海光官方有一些版本,GitHub 社区也有一些基于 ROCm 的适配版本。我用的思路是先看官方版本,跑不起来再改社区版。部署方式通常是一个 DaemonSet,配置 YAML 里需要把以下路径挂载进插件容器:/var/lib/kubelet/device-plugins(用于插件把自己注册给 kubelet)、/dev(访问 DCU 设备节点)、/opt/dtk(DTK 运行库)。
插件启动后,通过日志能看到注册的资源名和数量,比如 “Registered device plugin with Kubelet” 或者 “Serving on /var/lib/kubelet/device-plugins/hygon-dcu.sock”。验收方法是在节点上执行:
kubectl describe node <node-name> | grep -A5 "hygon"正常情况下 Capacity 里会多出类似 hygon.com/dcu: 4 这样的记录。
如果等了半天 Capacity 里没出现,先查插件日志,再查 kubelet 日志。常见原因是插件 socket 文件名和 kubelet 期望的不一致(kubelet 只认 /var/lib/kubelet/device-plugins 下以 .sock 结尾的新 socket,注册完成后旧 socket 要清理);另一个原因是 kubelet 需要重启才能触发重新扫描,插件已经注册好了,但 kubelet 没有重新 ListAndWatch。到这里,K8s 的底层通道就打通了,接下来看三种调度形态分别怎么实现。
3. 整卡 / 共享 / vDCU 虚拟化调度实操
3.1 整卡独占调度:最稳的一档
整卡独占大概是最简单的形态。设备插件上报的总数量就是物理卡数量,Pod 里明确声明要 1 张卡,调度器保证一个 Pod 只会拿整张卡,不会有两个 Pod 同时分配到同一张卡。这个场景下,负责分配的设备插件基本不需要做太多复杂逻辑,只要对每张卡维护一个“是否被占用”的标记。
示例 manifest 我实际用的简版是这样的:
apiVersion: v1 kind: Pod metadata: name: dcu-monopoly-test spec: restartPolicy: Never containers: - name: dcu-test image: registry.com/ai/llm-base:dtk-5.1 resources: limits: hygon.com/dcu: 1 command: ["/bin/bash", "-c", "dcmi && sleep 3600"]创建后进入容器执行 dcmi,看设备列表应该是只有一张卡。这没有太多技巧,只要注意别把 requests 和 limits 写得不一致。扩展资源目前推荐 requests 和 limits 保持相同值,否则调度和创建阶段容易产生预期之外的结果。整卡模式另一个要注意的是 nodeSelector,如果集群里同时有 NVIDIA 卡和海光 DCU,必须给节点打上 GPU 厂商标签,否则调度器可能把任务派到错误的节点上。
3.2 共享调度:把一张卡拆给多个任务
共享调度的目标是把一张 32GB 的卡分配给多个小任务。原理上 K8s 扩展资源只能分配整数,所以我们必须定义“卡份”这个抽象单位。比如把一张卡抽象成 8 份,每份 4GB 显存配额,设备插件上报 hygon.com/vdcu: 32,假设 4 卡节点就是 32 份。Pod 申请 1 份,插件在 Allocate 阶段要记录这张卡的剩余份数,分配给这个 Pod 的 vDCU 份额,并返回对应的环境变量和限制参数。
这里最核心的一个问题是“怎么保证显存不超卖”。我的做法是让设备插件在 Allocate 时根据 Pod 请求的份数,调用驱动层提供的资源限制接口,不同版本 DTK 提供的限制能力不一样,有的通过 mdev 类型,有的通过环境变量加驱动改造。如果驱动层不支持严格限制,共享调度就只能靠“任务规范 + 监控告警”兜底:上限拉高一些,但实际并行任务数少一些,让 OOM 的触发概率降到可以接受的水平。
从调度的角度说,共享调度还有一个隐藏问题:K8s 只保证“总份数不超过上报值”,不保证两份请求落在同一张卡还是不同卡。所以设备插件内部得维护一张卡与剩余份数的映射,选卡逻辑要尽量让新的请求优先落到剩余空间最多的卡上,减少碎片。这个逻辑写在插件里,平台侧看不到,但它决定了碎片率和运行稳定性。
3.3 vDCU 形态一:基于 mdev 的硬虚拟化
接着说两种 vDCU 虚拟化的具体落地。第一种是硬虚拟化,它其实不依赖设备插件做显存配额,而是依赖驱动层先把物理卡切好。宿主机上一般会有一个管理接口,类似 /sys/class/mdev_bus/.../mdev_supported_types,里面能看到驱动支持切成哪几种 vDCU 类型。
实操步骤大致如下:
- 确定物理卡对应的 PCI 设备地址,用 lspci 或 dcmi 查看。
- 在宿主机上创建一个 mdev 类型的 vDCU 实例,分配一个 UUID,驱动会在 /dev/dcu_vgpu/ 下生成一个对应的设备节点。
- 把这个设备节点(而不是物理卡)暴露给容器。
- 设备插件在注册资源时,把每块物理卡上实际创建的 vDCU 实例数量作为资源总数上报。
这种方式的好处是隔离性由硬件层保障。我在一个 vDCU 里跑满显存测试,同卡另一个 vDCU 里的推理服务完全不受影响。缺点是运维维度多了一层:mdev 实例的创建、销毁、清理都要管理,节点重启之后要确保 vDCU 能自动重建。我这边是把创建逻辑做成 systemd service,结合节点启动时的脚本自动恢复;设备插件重启时也要重新把所有 vDCU 实例纳入列表,否则卡上有 vDCU 实例但插件不知道,就会把资源超卖。
3.4 vDCU 形态二:基于资源配额与 MPS 的软虚拟化
第二种软虚拟化在工程实现上更轻。设备插件在上报时,把一张卡上报成 N 份;Allocate 阶段,插件根据 Pod 申请的份数做三件事:一是给容器注入 HIP_VISIBLE_DEVICES 指向物理卡的编号;二是把显存上限通过驱动提供的接口写入该容器的 cgroup 或对应的资源控制组;三是如果并发任务需要控制算力比例,可以通过类似 ROCm MPS 的机制把多个客户端进程合并到同一个服务端,再为每个客户端分配不同的算力配额值。
我实际测试下来的感受是:软虚拟化的启动速度比硬虚拟化快得多,不需要在系统里创建虚拟设备实例,特别适合那种“一次起来几十个调试容器”的 CI 场景。但它的风险点也很明显:显存上限限制是否真的生效,完全取决于驱动和运行时是否严格配合。某些版本的 DTK 里显存上限控制并不完整,确实存在一个进程申请了 6GB、另一个申请了 8GB,加起来超过物理卡 32GB 的情况。这类问题不好复现,但一旦出现,卡上进程就会互相踩踏。
因此我给出的选择建议很简单:生产级推理服务、模型训练用硬虚拟化(mdev 型 vDCU)或整卡;开发调试、CI/CD、并发跑小脚本用软虚拟化(配额型 vDCU)。两者可以在同一套平台里共存:整卡资源一个资源名,硬虚拟化 vDCU 一个资源名,软共享 vDCU 一个资源名。用户按需求选,平台把资源规格映射到不同的资源名和 limits 上即可。
4. CubeStudio 平台侧适配:让 AI 平台认识 DCU
4.1 平台资源模型的扩展
CubeStudio 本质上是一个面向算法团队的 AI 研发平台,它把 Kubernetes 的资源申请、调度、存储挂载、日志采集都包了一层,用户不用管 YAML。所以在接入 DCU 时,平台的适配工作主要在“资源规格”这一层。
平台后端通常维护一张“资源规格表”,例如“CPU:8C / 内存:32G / GPU: 1”这样的组合。我们需要新增几种 DCU 规格,比如:
- 整卡型:CPU:16C / 内存:64G / hygon.com/dcu: 1 / 标签 dcu-mode=exclusive
- vDCU 型:CPU:4C / 内存:16G / hygon.com/vdcu: 1 / 标签 dcu-mode=hard
- 共享型:CPU:2C / 内存:8G / hygon.com/vdcu: 1 / 标签 dcu-mode=soft
平台后端在创建任务时,会把这些规格翻译成 Pod 的 resources 和 nodeSelector。如果平台之前的代码里写死了 GPU 资源名(比如 nvidia.com/gpu),就需要把资源名改成可配置项,不能把 DCU 也映射到 nvidia.com/gpu,否则调度器根本不会认。这里还有一层经验:平台层最好保留“原始资源名”和“界面展示名”两个字段,界面上可以叫“DCU 整卡”“vDCU 硬切”“vDCU 共享”,但后端真正写入 Pod 的必须是设备插件注册的资源名,两套名字对应关系要清晰。
4.2 任务镜像与运行时适配
平台默认的任务镜像如果不带 DCU 运行时,任务起来之后跑 hipcc 会直接报错。我们做了两层处理:平台提供一个系统级的基础镜像(包含 DTK 运行时、驱动运行库、常用调试工具);用户自己的项目镜像基于这个基础镜像扩展,这样算法同学完全无感。
另外,在平台的任务配置里,有几个隐藏字段容易被忽略。环境变量 HIP_VISIBLE_DEVICES 等由设备插件注入,平台不要覆盖它;大多数情况下不需要 privileged,把需要的设备节点(/dev/kfd、/dev/dri、/dev/dcu_vgpu)通过插件默认挂载即可,如果平台强制给任务容器加 privileged,建议评估一下安全影响;平台如果默认配置了 exec 健康检查,检查命令里不要依赖 nvidia-smi 这类不存在的工具,换成 dcmi 或者纯 TCP 检查。
这些看起来都是小问题,但只要踩中一个,任务创建后会花很长时间定位,而且大多数报错还不在平台日志里,而是在 kubelet 的事件里。
4.3 在 CubeStudio 上提交一个 DCU 任务
实际在平台上提交任务的过程,最核心的验证点是“平台生成的 Pod 是不是带上了正确的资源声明”。我这边踩过一个很典型的坑:平台上选了共享型规格,但生成的 Pod 里 requests 和 limits 不一致——requests 写的是 0,limits 写的是 hygon.com/vdcu: 1。这对普通资源没问题,对扩展资源来说并不推荐,因为调度器按 requests 算调度占用量,如果 requests 为 0,调度器会把多个 Pod 都看成“不占资源”,实际却都挤在一张卡上。
正确写法是 requests 和 limits 都是 hygon.com/vdcu: 1。改完之后,再提交一个简单的任务,进入容器执行 HIP 示例程序或者直接升一个 dcmi 看设备状态,能识别到设备即可。如果平台有任务事件或 Pod 详情查看,确认调度到了哪台节点、容器里注入的环境变量是什么,基本上就能做到一次通过。
5. 在适配环境上部署 DeepSeek
5.1 模型选型与下载
环境跑通之后,最后一步是验证它能真正支撑 LLM 推理。我选的是 DeepSeek 蒸馏系列:DeepSeek-R1-Distill-Qwen-7B。这个模型在半精度下大约 14GB 显存,32GB 的卡绰绰有余;如果只跑 32B 量化版,建议至少 24GB 可用显存。对大多数团队来说,先拿 7B 验证链路是最务实的做法,模型小、下载快、调环境也快。
模型文件我用 ModelScope 或内部镜像源下载,国内访问比走海外源快得多也稳得多。下载完成后,模型目录结构一般是 config.json、tokenizer.json、*.safetensors 这些文件,放到一个能被平台挂载的存储路径上,最好是 PVC 或对象存储,方便 K8s 里多个副本共享。切忌把模型打进镜像里,镜像太大会拖慢发布速度,而且模型更新频率高,放在存储里更灵活。
5.2 镜像制作与 K8s 服务编排
推理镜像在 base 镜像基础上加装推理引擎。优先试 vLLM,它提供 OpenAI 兼容 API,对 DeepSeek 蒸馏模型支持很成熟。如果当前 DTK 版本下 vLLM 编译不稳定,就降级到 llama.cpp 的推理方案,把模型转成 GGUF 后用 llama-server 起服务。镜像里需要注意把 LD_LIBRARY_PATH 指向 DTK 的 lib,否则起服务时会报找不到 libamdhip64.so 之类的共享库错误。
K8s 部署文件用一个 Deployment 一个 Service 就够。Deployment 里声明 hygon.com/vdcu: 1,我这里是给推理服务分配一个硬虚拟化 vDCU,隔离性强一点。同时注意环境变量不要设置 CUDA_VISIBLE_DEVICES,这是 DCU 环境,设置 CUDA_VISIBLE_DEVICES 反而可能干扰 HIP 运行时,应该用 HSA/HIP 可见性变量。
vLLM 启动命令参考:
vllm serve /models/DeepSeek-R1-Distill-Qwen-7B \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --port 8000如果是 llama.cpp 的方案,就是:
llama-server -m /models/DeepSeek-R1-Distill-Qwen-7B-Q4_K_M.gguf \ --host 0.0.0.0 --port 8000 \ --ctx-size 8192 --n-gpu-layers 9995.3 通过 OpenAI 兼容接口验证推理
服务起来后,先看日志确认模型加载成功,然后从另一个 Pod 或者宿主机上发请求验证:
curl http://<service-ip>:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "/models/DeepSeek-R1-Distill-Qwen-7B", "messages": [{"role": "user", "content": "用一句话介绍海光 DCU"}], "max_tokens": 128 }'返回里能看到 response 文本和 token 使用情况。这里有个小细节:model 字段在 vLLM 里默认是启动时的模型路径,如果你在前面挂了类似 harness、hermes 这类封装层或者统一模型网关,最终服务的 model 名称可能被改掉,但这不影响底层,OpenAI 兼容协议最关键的是保持 messages 和 responses 的格式一致。
怎么验证显存隔离和性能?跑一轮请求的同时,在另一个容器里执行 dcmi 看显存占用情况;再起一个相同 vDCU 规格的任务,确认两者互不干扰。这样整个链路就完整闭环了:CubeStudio 提交任务 → K8s 调度 → DCU 设备分配 → 模型加载 → 推理 API 正常响应。
6. 常见问题与排查速查
6.1 高频问题实录
以下是我实际排查过程中记录的问题和对应解决方案:
| 现象 | 根因 | 解决方式 |
|---|---|---|
| kubectl describe node 看不到 DCU 资源 | 设备插件没有向 kubelet 完成注册 | 检查插件日志,确认 socket 目录和 kubelet 一致,重启 kubelet 触发重新上报 |
| 容器里 dcmi 看不到任何设备 | Allocate 阶段没有返回设备节点和环境变量 | 手动检查 Pod 内的 /dev 和 env,确认 HIP_VISIBLE_DEVICES 存在;没有则是插件代码问题 |
| 调度失败:0/1 nodes available,Insufficient hygon.com/dcu | 资源被占用或上报数量不对 | describe node 查看 Capacity,确认资源总数;把 requests/limits 改成一致 |
| 任务启动后进程 hang 住 | vDCU 实例冲突或显存超卖 | 检查同卡上的其他 Pod 是否分配到同一个 vDCU 或物理卡,用 dcmi 看显存是否打满 |
| 编译 HIP 程序报找不到 hipcc | 镜像里没装 DTK 或 PATH 没设置 | 在镜像里安装 DTK,或构建时设置 PATH=/opt/dtk/bin |
| 启动推理引擎报 libamdhip64.so 找不到 | LD_LIBRARY_PATH 未指向 DTK 库目录 | Dockerfile 里设置 ENV LD_LIBRARY_PATH=/opt/dtk/lib:$LD_LIBRARY_PATH |
| 平台任务显示“调度成功”但实际没拿到卡 | 平台后端把资源名写成了 GPU 厂商的名字 | 检查平台生成的 Pod YAML 中 limits 字段,改成 hygon.com/dcu 或 hygon.com/vdcu |
6.2 避开版本和细节的坑
关于版本匹配,我想重点强调:海光 DCU 的驱动、DTK、设备插件、容器运行时、kubelet 这五者之间没有严格的“必须同版本”的要求,但实际中几乎每个版本差异都会以诡异的方式报错。最快的办法是把整套版本固定下来,写进项目的版本清单里,升级任何一环之前都要做全链路回归。
另一个容易被忽略的是设备插件的健康检查。插件如果只是上报资源数就不管了,某张卡掉线后 kubelet 不会感知,调度器还会把任务发过去。我用的插件实现了 ListAndWatch,卡掉线时会主动上报 Unhealthy,这样 kubelet 会自动把该卡从分配列表中移除,任务就不会再被调度到故障卡上。这一条非常关键,尤其是多卡节点长期运行时,卡掉线不一定是物理故障,也可能是热插拔、驱动重置这类事件,不及时摘除会导致一堆任务卡在 ContainerCreating。
最后再分享一个细节:如果你用的是硬虚拟化 vDCU,清理 mdev 实例时一定要先确保没有容器还在使用它,否则直接销毁实例会导致容器内驱动上下文断裂,日志里能看到类似 “device is busy” 的错误。我现在是在销毁逻辑里加了持有计数,先优雅驱逐相关 Pod,再删实例,基本不会再出这个问题。
这次适配最深的体会有两点。第一,先把“最小闭环”跑通再谈平台化。我第一次接的时候,一上来就改平台配置,结果平台生成的任务连卡都拿不到,排了两天发现是设备插件没注册成功。后来改成先在裸 K8s 上用 YAML 验证整卡和 vDCU 调度,确认无误后再让平台去生成同样的 YAML,一下就顺了。第二,别把软虚拟化的隔离性想得太美好,稳定的生产环境优先用硬隔离。K8s 和平台只是把资源分配出去,真正能否隔离干净还要看驱动层能力。如果你也在做 DCU 接入,建议按这个顺序走一遍再决定虚拟化形态,磨刀不误砍柴工。