资讯动态

DCU软件虚拟化从入门到落地:HAMi架构原理与K8s实践指南

发布时间:2026/10/20 2:44:27 来源:尧图企业网站定制
上个月我跑去参加了一场关于AI基础设施的线下Meetup分享嘉宾是海光信息的工程师主题是“DCU软件虚拟化从基础到实践”。说实话这两年GPU虚拟化的分享我听了不少但真正把国产DCU的虚拟化讲清楚、还带着现场demo跑通的这是头一回。整场下来信息量很大从为什么要虚拟化、到HAMi怎么做调度、再到在海光DCU这种非传统GPU架构上踩过的坑基本覆盖了从入门到落地的全链路。这篇文章算是我听完之后的一个复盘把我认为对做AI平台、K8s运维、以及算法工程化比较关键的部分整理出来给没到现场的朋友参考。先说明一下背景所谓DCU可以理解成国产加速卡里的“通用计算单元”它在软件栈上兼容ROCm和HIP生态可以跑PyTorch、TensorFlow这些主流框架。但硬件再强落到K8s平台上就绕不开一个经典问题——一张卡怎么分给多个人用。HAMi就是干这个的它在社区里被定义为异构AI计算虚拟化中间件实际能力是把物理DCU切分成多个逻辑设备让容器可以按显存、按算力去申请资源。这篇文章的核心适用人群很明确一是被“显存碎片化”折磨的平台开发二是要在国产卡上跑推理服务又不想买一堆物理卡的算法团队三是刚从CUDA切到ROCm生态、对设备管理还不太熟的人。1. 为什么一张DCU卡要被掰成八瓣用1.1 三个让人眼前一黑的真实场景先别急着谈技术我们来看看物理DCU独占模式下实际业务里会遇到什么问题。第一个场景是离线推理服务。你有一批语音识别或者图像分类任务每个请求的模型权重也就三五个GB一台机器上同时跑20个实例单实例显存占用可能只有1.5GB左右。如果按物理卡独占一张64GB的DCU卡只跑一个实例剩下的显存全空着算力也闲着。为了凑并发你就得不停地加机器、加卡成本直线上升。第二个场景是训练任务和预处理任务混部。训练任务的特点是峰谷差异大模型并行时可能要占满整卡但在数据加载、checkpoint保存、参数调优的间隙卡的使用率其实很低。而数据预处理这类任务对延迟不敏感、但需要“有块地方能跑”如果每次都让它们独占整卡一次epoch跑完可能只需要5分钟但排队等卡等了半小时。第三个场景是多租户隔离。不同团队、不同项目共用一套集群物理卡数量有限又不能让A团队的调度失败去影响B团队。最原始的方案是打标签、绑节点但这样做的结果是资源被物理隔死A团队空闲的卡B团队用不上集群整体的资源利用率惨不忍睹。这三个场景指向同一个结论物理卡粒度太粗无法匹配真实工作负载的弹性需求。你需要的是一块“刚好够用”的卡而不是一整块卡。1.2 从独占思维转向资源池化过去我们的思维是“一台机器一张卡一张卡一个任务”这在小规模时代没毛病但当你的集群有几十张卡、上百个任务时这种思维就成了瓶颈。软件虚拟化的核心思路是这样的把物理DCU看成一块“算力蛋糕”通过中间层把蛋糕切成不同大小的小块每一块叫vDCU虚拟DCU。每个vDCU拥有独立的显存空间、独立的算力配额、独立的设备索引容器视角下它就像一张真实的物理卡。K8s调度器看见的依然是“资源”只不过资源粒度从“卡”变成了“卡上的一块”。这样做的好处非常直白资源利用率上去了因为小块可以灵活拼装业务隔离性上去了因为每个vDCU的资源边界是硬的管理成本下去了因为你不需要为每个小任务单独规划物理卡。这里要插一句对比很多人会拿硬虚拟化来比较比如芯片级的物理切分。硬虚拟化的隔离性确实好但它的切分粒度通常是固定的比如一份算力、一份显存只能照着硬件规格去切不够灵活。软件虚拟化牺牲了一点隔离强度换来了极大的弹性——你甚至可以给某个Pod动态调整显存和算力配额这在硬虚拟化里很难做到。所以虚拟化方案选型的时候要搞清楚你到底要的是“安全隔离”还是“弹性分配”这两者的适用场景完全不一样。2. HAMi凭什么能做到“看起来像一张卡”2.1 三层架构拆开看HAMi的架构可以拆成三层理解K8s调度接入层、节点设备管理层、容器内注入层。第一层是调度接入层。K8s默认不认识DCU这种资源所以HAMi实现了一个自定义调度器或者说以调度扩展器Scheduler Extender的方式挂在kube-scheduler后面。Pod申请hami.io/dcu这种资源时默认调度器先做常规过滤然后把这个请求转发给HAMi扩展器。扩展器看了每个节点上真实的DCU分配情况再结合Pod申请的资源决定把任务分到哪个节点、哪块物理卡、生成什么样的虚拟设备配置。第二层是节点设备管理层。这一层是一个DaemonSet部署的组件它常驻在每个节点上通过调用DCU的管理接口比如dcmi-smi、驱动接口把节点上的DCU卡数量、每张卡的总显存、剩余可用显存、健康状态上报给调度器。同时它还要负责执行调度结果当Pod要在某节点启动时它会在节点上创建一个对应的虚拟设备文件或者设备环境并完成从物理显存到虚拟显存资源的记账。第三层是容器内注入层这一层最核心也最容易被忽略。它通过环境变量注入和动态链接库劫持的方式在容器进程真正调用ROCm运行时API时把调用拦截下来。比如进程调用hipMalloc去申请显存注入层会告诉你“好了你拿到了一个虚拟显存块”实际背后的物理显存是从一块全局池子里划出来的。进程调用内核启动、显存复制时也会被映射到正确的物理设备上。三层配合起来效果就是K8s看到的是“资源”节点看到的是“分配账单”容器看到的是一张“完整的卡”。谁都没有被欺骗只是各看各的。2.2 调度器扩展器到底在解什么题调度器扩展器不是HAMi发明的概念但HAMi把它的能力用得很充分。默认kube-scheduler做的是节点维度的调度而HAMi要解决的是“卡内维度”的调度。一句话概括就是哪个Pod该放到哪张物理卡上的哪块逻辑区域。这里面有几个关键策略值得展开。第一是拓扑亲和。HAMi要知道物理卡与CPU、NUMA节点之间的连接关系尽量把Pod调度到离它数据所在位置更近的卡上减少跨路数据传输。这个对于大模型训练尤其重要因为通信量一大PCIe或IO带宽就成了瓶颈。第二是显存碎片整理。多租户反复调度和释放之后物理卡上会留下很多显存碎片。HAMi在分配时不是简单看“剩余显存是否够”而是维护了一张“显存空洞”表尽量把新任务塞进连续的区域避免出现一堆再也拼不起来的碎片空洞。第三是超卖控制。你可以给节点设置超卖比例比如允许虚拟显存总额是物理显存的1.5倍。超卖可以进一步提升利用率但也带来了过载风险。HAMi通过“预留水位”和“驱逐优先级”来控制风险超过水位之后新任务不再调度到这个卡上已经运行的低优先级任务在物理显存吃紧时会先被挂起或迁移。2.3 容器里的显存影子和算力限流容器内注入层有两个核心机制显存影子和算力限流。显存影子理解起来很直接。进程以为自己在使用显存实际上注入层维护了一张“虚拟地址到物理地址”的映射表。当进程写一个虚拟显存块时数据被写到真实物理显存的某个位置或者在某些极端超卖场景下先落到主机内存再从主机内存同步过去。映射表让多个vDCU可以安全共享同一块物理卡你写你的区域我写我的区域互不干扰。算力限流稍微复杂一点。一块物理DCU的算力是固定的多个vDCU共享时如果不做限制某个疯狂跑算力的任务会把整张卡的算力吃干榨净。HAMi的做法是时间片轮转加上内核提交队列的按比例配额。进程发起内核启动请求时注入层会检查当前vDCU在时间窗口内已经消耗的算力配额如果配额用完了就把请求挂起排队等下一个时间窗口。这个效果类似操作系统的进程调度不过粒度更细、代价也更低。为了便于理解可以打个生活化的比方物理DCU像一个公共厨房只有一个灶台。多个vDCU就是几户人家轮流做饭。显存影子相当于每家分了一个柜子柜子上锁别人动不了。算力限流就是每家规定了做饭时长超时就得让给下一家。厨房没变但每家都觉得“灶台好像是我专用的”。3. 海光DCU上落地和普通GPU不是一回事3.1 先摸清DCU的软件栈很多人第一次接触海光DCU习惯性拿CUDA生态的思维往里面套结果碰了一鼻子灰。DCU虽然也属于GPGPU架构但它的软件生态更贴近AMD的ROCm体系。这就意味着你在CUDA里的很多概念到DCU这边需要换个名字、换套接口。我把常用到的对应关系整理成一张表方便对照功能概念CUDA生态DCU/ROCm生态编程模型CUDA C/CHIP C/C显存管理cudaMalloc / cudaMemcpyhipMalloc / hipMemcpy内核启动kernelgrid, block(args)hipLaunchKernelGGL / 设备状态查询nvidia-smidcmi-smi算子库cuBLAS / cuDNNrocBLAS / MIOpen驱动接口NVIDIA driver nvmlKFD驱动 ROCm runtime虚拟设备vGPU / MIGvDCU / MIOpen虚拟实例如果你的代码之前是CUDA写的迁移到DCU通常不是直接改个编译器就行而是要经过HIP化改造。好在很多框架已经做好了兼容层PyTorch官方版本的ROCm分支可以直接在DCU上跑省掉了最痛苦的那一步。3.2 部署前必须知道的三个坑在DCU上跑虚拟化比在普通GPU上多出几个隐藏门槛这里挑最重要的三个说。第一个坑是设备不可见。容器里默认调用dcmi-smi经常什么都看不到原因在于DCU的ROCm运行时依赖设备节点和驱动环境变量。常见做法是在Pod或容器环境变量里补一个兼容参数比如HSA_OVERRIDE_GFX_VERSION让运行时正确识别DCU的架构代号否则算子编译和加载会出现莫名其妙的“no device”错误。第二个坑是MIOpen算子库的缓存问题。MIOpen在首次运行某个卷积算子或矩阵算子时会做一次运行时调优并把调优结果写入本地的缓存目录。如果多个vDCU共享同一个Pod镜像里的缓存目录会出现并发写同一个文件的情况轻则性能退化重则进程崩溃。我见过的情况是容器里没给缓存目录权限MIOpen每次都要重新调优推理延迟直接翻了好几倍。正确做法是为每个容器设置独立的MIOPEN_USER_DB_PATH和MIOPEN_CACHE_DIR或者干脆挂一个可读写的临时卷进去。第三个坑是显存账单不透明。DCU驱动层上报的显存使用量在虚拟化之后不一定和你看到的进程显存占用完全一致。因为虚拟显存采用池化管理部分显存可能被预留在全局池中而进程自己实际只用了其中一部分。如果你对着dcmi-smi去核对每个容器的显存会发现数字对不上这很正常不代表泄漏了而是池化记账导致的偏差。理解这点排查问题的时候就不会被表面数字带偏。3.3 我在这套环境里看到的实测效果现场分享时工程师放了一个A/B对比我觉得比任何长篇大论都有说服力。测试环境是两台插着DCU卡的物理节点每张卡64GB显存。没开虚拟化时他们把20个推理Pod均匀分配到两台机器上结果因为每个Pod申请的是整块物理卡Pod一多就得排队。两卡资源利用率大概在25%到30%之间浮动剩下70%以上的算力基本是闲着的但新的Pod又因为“没有可用卡”被卡在Pending状态。开了虚拟化之后每张物理卡按显存切成若干个vDCU比如切出8个8GB的虚拟设备调度器再把20个Pod分散到这些vDCU上。同样的物理资源Pod全部在几分钟内启动峰值显存利用率冲到了80%以上算力配额还能按需调整有的Pod给30%算力、有的给15%。虽然隔离性比物理独占弱一些但在这个场景里业务本身对隔离的要求没那么苛刻利用率的大幅提升是实实在在的。这个对比揭示了一个关键结论虚拟化不是要把物理卡变快而是把物理卡的“闲置能力”重新利用起来。对AI infra团队来说省下来的每一张卡都是真金白银。4. 手把手从零搭建一套DCU虚拟化环境4.1 前置条件与组件清单这套环境我建议在一套干净的K8s集群上做验证版本不要低于1.24。为什么非要这个版本以上因为太老的K8s对设备插件机制和调度扩展器的支持有偏差排查起来非常痛苦。硬件层面节点上得能正常看到DCU卡。装好驱动后先敲一行命令确认dcmi-smi如果能看到卡列表、显存容量和健康状态说明驱动和设备节点都没问题。然后确认一下K8s的device plugin开关是开启状态默认是开的但有些云厂商的发行版会关掉它。HAMi部署需要两组组件节点设备插件是一个DaemonSet运行在每个DCU节点上负责资源上报和虚拟设备创建。调度器扩展器是一个Deployment它要能被kube-scheduler访问到所以一般放在Master或单独的调度节点上并保证网络连通。4.2 部署节点设备插件先创建命名空间再把设备插件部署进去。如果你有Helm用官方chart最省事不想引入Helm的话直接用发行版仓库里的YAML文件就行。下面是一个简化后的部署片段关键就是DaemonSet里传入的配置项kubectl create namespace hami-system kubectl apply -f https://example.org/hami/device-plugin.yaml设备插件启动后它会自动扫描本机DCU设备通过dcmi接口拿到卡数和显存然后以扩展资源的形式上报到节点。节点状态里会多出一种类似这样的资源kubectl describe node worker-1输出里会看到hami.io/dcu的数值等于该节点上报的物理卡数。如果这里没有出现资源说明设备插件没起来或者驱动有问题不要急着往下走。4.3 接入调度扩展器调度扩展器这一步是不少人的噩梦因为kube-scheduler的扩展器配置在不同版本里写法不一样。先部署扩展器服务kubectl apply -f https://example.org/hami/scheduler.yaml确认Pod起来后找到它的Service地址。然后修改kube-scheduler的启动参数加载一个Policy配置。老版本的kube-scheduler支持这种JSON策略文件{ kind: Policy, apiVersion: v1, extenders: [ { urlPrefix: http://10.96.10.1:32767/scheduler, filter: { enable: true }, bind: { enable: true }, managedResources: [ { name: hami.io/dcu } ] } ] }配置里最核心的是managedResources它宣告了hami.io/dcu这个资源由扩展器说了算。改完配置后重启kube-scheduler进程。这一步容易出错的是地址写错或者端口不通扩展器调不通的时候Pod会卡在Pending而且日志里未必直接报“extender error”有时候只显示“no nodes available”。4.4 跑起第一个虚拟DCU任务并验证环境就绪之后写一个最简Pod验证一下。这里的关键是给Pod打上HAMi的注解告诉调度器你需要多少显存和多少算力apiVersion: v1 kind: Pod metadata: name: dcu-vdcu-test annotations: hami.io/vgpu-memory: 12Gi hami.io/vgpu-percent: 30 spec: containers: - name: test image: ubuntu:22.04 command: [sleep, infinity] resources: limits: hami.io/dcu: 1 restartPolicy: Never重点看两个注解vgpu-memory是你想要的虚拟显存大小vgpu-percent是你想要的算力占比。Pod调度成功后进容器里执行kubectl exec -it dcu-vdcu-test bash dcmi-smi这时你会看到容器内出现了一个虚拟DCU设备显存容量显示为12GB而不是物理卡的64GB。这就说明虚拟化链路已经打通了。赶紧再起几个不同注解的Pod看看调度器会不会把它们错开分配到不同的物理卡上。如果所有Pod都挤到同一张卡就回到调度扩展器的配置去查亲和策略。4.5 调度策略参数调整建议跑通之后有几个参数强烈建议按自己的集群规模和业务特点调整。vgpu-percent不是越大越好。多个任务共享一张卡时算力配额的加总如果超过100%就会发生超卖高优先级任务可能被低优先级任务拖累。严谨的做法是给核心业务预留15%到20%的算力冗余。显存分配建议按“模型权重激活值冗余”三部分去估算。不要卡着模型峰值显存申请否则实际跑推理时一波动就OOM。如果混部场景多打开调度器的调度策略里的反亲和规则让同一个物理卡上的虚拟实例尽量来自不同业务团队这样单个业务爆炸时影响面可控。5. 现场答疑与故障实录全是我们踩过的坑5.1 调度不到DCU的高频原因我在现场听下来提问环节至少有三分之一的问题都卡在“Pod为什么调度不上去”。把常见原因归纳一下现象排查方向解决办法节点上没有hami.io/dcu资源设备插件没起来或驱动不可用看设备插件日志检查dcmi-smi是否正常有资源但Pod一直Pending调度扩展器地址不通或没接管资源检查扩展器Service地址确认managedResources配置正确调度成功但容器里看不到卡环境变量缺失或设备节点没映射补HSA_OVERRIDE_GFX_VERSION确认Pod挂载了设备目录多个Pod分配到了同一张物理卡没有开启反亲和或拓扑过滤调整HAMi调度策略按节点卡编号做分散排查的时候有个技巧把Pod的名字和状态对齐看调度器扩展器的日志里面会把“为什么拒绝”打印得很清楚。比起在K8s事件里瞎猜直接看扩展器日志高效得多。5.2 显存“被吃掉”和OOM怎么定位现场有个同学问了一个特别典型的问题虚拟化之后总显存感觉变小了好几百GB不翼而飞。这个问题多半不是泄漏而是“显存影子”功能把一部分显存预留在全局池里了用来处理突发超卖。你要做的不是恐慌而是通过组件提供的监控指标去看当前池子的水位和实际分配情况。如果真是某些虚拟实例泄漏导致的显存膨胀可以看每个vDCU的实际映射页数量虚拟显存的记账通常基于映射页而非进程申请大小。定位到异常实例后重启容器能快速恢复但根治还得看业务里是不是有没释放的hipFree调用或者模型服务里的缓存是否无限增长。OOM问题则要反向思考虚拟化后进程看到的显存上限是虚拟值超过了这个上限才会触发OOM。如果业务进程在物理卡明明还有空闲时也报OOM大概率是注解里的vgpu-memory写得不够把配额调上去就解决了。这一点和物理卡独占模式下的OOM排查逻辑完全不一样。5.3 算力限制不生效到底谁在偷跑有同学反映明明给Pod配了30%的算力占比实际跑起来能冲到80%感觉限制形同虚设。这个问题要分几层看。第一层算力限制基于时间片轮转但如果在某个瞬间只有一个vDCU有任务没人跟它抢那它独占的时间片就是100%统计出来自然高于30%。这种属于正常现象时间窗口拉长看平均值会收敛。第二层如果长时间平均值都偏高要检查业务里是否用了某些绕过HIP Runtime的路径。比如直接走底层库的调用、或者使用了不该被劫持的同步接口这类调用没有被注入层记录配额。第三层部分算子库比如MIOpen内部有自研的并发调度机制会一次性提交多个内核任务导致算力限制器看到的“单个请求”实际上是个“请求群”配额计算就会出现偏差。遇到这种情况需要看HAMi版本是否已经对算子库做专项适配必要时降低该vDCU的并发核数上限。5.4 遗留问题虚拟化不能解决所有事现场最后一个问答也很有意思有人问“是不是上了虚拟化我就可以彻底不用管物理卡够不够了”。答案显然是否定的。几个明显的边界第一如果你的单个模型显存需求已经超过整张物理卡虚拟化帮不了你你需要的是一整张卡或者多卡并行技术第二对延迟极其敏感的高频交易类推理虚拟化引入的调度抖动和排队延迟可能是不能接受的第三如果业务本身就是整卡打满的比如大模型预训练虚拟化反而会因为时间片切换带来少量性能损耗不如物理独占干净。搞清楚哪些场景适合虚拟化、哪些不适合比学会部署虚拟化本身更重要。我见过不止一个团队明明业务是整卡训练非要把卡切开用最后性能和稳定性双双受损回头又骂中间件不行。工具本身没有好坏用错场景才是问题。结尾最后分享一点个人体会。虚拟化这套东西底层原理讲出来并不高深无非是资源切分、调用劫持、配额控制这三板斧。但真正用起来之后会发现它考验的不是某个组件的性能而是你对业务负载的理解模型多大、峰值显存多少、算力曲线平不平、能不能接受排队这些参数组合起来决定了你该用哪种切法。我在会后也照着demo在我们自己的测试集群上复现了一遍一开始也被调度扩展器地址、MIOpen缓存这类小问题折腾得够呛。但跑通之后再回头看最值钱的不是那些YAML文件而是对“什么该虚拟化、什么不该虚拟化”的判断力。下一阶段我打算继续测试多卡之间的实例迁移以及把vDCU和自动扩缩容结合起来到时候有新结论再跟大家分享。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑