上个月我跑去参加了一场关于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/C++HIP C/C++显存管理cudaMalloc / cudaMemcpyhipMalloc / hipMemcpy内核启动kernel<<<grid, 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和自动扩缩容结合起来,到时候有新结论再跟大家分享。
上个月我跑去参加了一场关于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/C++HIP C/C++显存管理cudaMalloc / cudaMemcpyhipMalloc / hipMemcpy内核启动kernel<<<grid, 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和自动扩缩容结合起来,到时候有新结论再跟大家分享。