签了约,不等于开了火:GPU 虚拟化里的“延迟分配”
平台已经用 HAMi 把 GPU 虚拟化,Pod 可以按更细的颗粒度申请一张卡的一部分。但 K8s 原生只把 GPU 当整数资源,不存在requests/limits的参数概念。
我在思考一个问题:常驻的 Pod 并不是每时每刻都在跑 GPU 任务,有没有办法把一张物理卡用出超过一张卡的价值?
理想状态是这样的:Pod 调度成功,但没跑 GPU 任务时不绑卡;等真跑任务了再绑;跑完解绑。这样,一张卡上所有 Pod 的声明值加起来可以超过物理容量,而大家在资源使用上相安无事。
带着这个问题,我请教了K8s方面专业的同事,也让AI去检索HAMi 的调度和运行时的官方资料,并让AI调用集群的接口实际测试了一番。结论先放在这里:这个理想,只实现了一半;没实现的那一半,还挺要命。
先把场景摆出来。假设有一栋楼,八间大厨房,每间配 96 口锅、100 份火力,门上最多挂十张门禁卡。HAMi 是这栋楼的二房东:负责签合同、发钥匙、管超卖。
你点“新建项目”,选了“两间厨房,每间 48 口锅、五成火力”。二房东会分三步办事。
一、签约:合同一签,名额就没了
第一步发生在 Pod 被调度的那一刻。二房东拿着一张全楼卡表,逐个厨房看:还剩多少口锅、多少份火力、几张门禁卡。找到合适的位置,就把“2 号厨房和 5 号厨房各一半”这句话写进你的合同——在 Kubernetes 里,这份合同叫 Pod annotation,内容是一串“卡编号、多少 MB、百分之几”。
注意,这一步只看账本,不碰灶台。但账本上,两间厨房的名额已经划掉了。人没来,合同照样生效。
二、发钥匙:只给你那两间的钥匙
Pod 启动时,二房东把钥匙塞给你:CUDA_VISIBLE_DEVICES 里只有这两间厨房的编号,别处你也进不去。顺手再塞一张小纸条:最多 48 口锅,最多五成火。容器里预加载一个叫 libvgpu.so 的“监工”,站在灶台边上。
到这一步为止,一口锅都没被端走。
三、开火:锅是真端了,火是轮流用
真正的占用发生在程序开始调用 CUDA 的那一刻。监工拦在 cudaMalloc 前面:你伸手拿第 49 口锅,它直接告诉你 OOM——这就是显存的硬上限。
火力则是另一回事。GPU 的算力本来就是时间片轮转的:谁在炒菜谁用。而且默认策略下,合同上写的“五成火力”其实不硬性执行——整层楼没别人做饭时,你可以开满。想让“五成”真的变成五成,得把策略切成 force。
所以“延迟分配”这四个字,延迟的是开火,不是签约。空闲的 Pod 不端锅、不烧气,但合同一直占着。
四、明明没人做饭,为什么说没位置
回到现实里的数字。八间厨房,每间 96 口锅、100 份火力、十张门禁卡。一个“两间厨房、每间 48 口锅、五成火力”的项目,实打实只等于一间厨房的容量。八个这样的项目,正好把八间厨房的名义容量签满。第九个项目,哪怕从头到尾一行 GPU 代码都不跑,也挤不进来——因为二房东收租看的是合同,不是灶台。
这也解释了一个常见的困惑:nvidia-smi 上显存空着,调度器却说没资源。两边都没说谎,他们看的是两本账。
五、能不能超卖
能,二房东本来就有这个开关:把账本上的每间厨房从 96 口记成 192 口(deviceMemoryScaling),火力从 100 份记成 200 份(deviceCoreScaling),再决定每间厨房发多少张门禁卡(deviceSplitCount)。账本一放大,合同就能签出两倍。
但超卖的本质是赌:赌大多数租客签了不来,或者来了也吃得少。两个真开火、每人 48 口,正好;第三个也开火,就有人端不起锅,报 CUDA OOM。二房东不会赶人,也不会收锅,只会让最后伸手的那个人吃闭门羹。
所以超卖适不适合,取决于活儿的形状:
- 常驻但吃得少、峰值平稳的推理服务,适合超卖;
- 一上来就把显存顶满的训练,或者有显存泄漏的作业,不适合——它会连累同卡邻居。
如果真要在生产上开,AI给的建议是:显存按峰值申报,只放大算力和门禁卡数量;或者显存也放大,但接受偶发 OOM,并在业务侧重试。至于“完全共享、谁也不记账”,那是另一套方案(time-slicing),代价是没有上限兜底,谁都能把整间厨房点着。
六、回到最初的问题
我理想里的“没任务就不绑卡”实现了吗?没有——卡在签约那一刻就绑了,合同上写死了是哪两间厨房。
没跑任务时占不占算力?物理上不占,账本上占。
延迟分配延迟了什么?延迟了端锅和烧气,没延迟承诺。合同和开火,是两件事——想明白这一点,很多“明明有空卡却调度不上”的疑惑,就不再像 bug 了。
留言
虚位以待,第一条留言会被记入史册。