签了约,不等于开了火: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。

一个 GPU 项目的三段式:签约时写注解并锁定显卡,发钥匙时注入可见设备与环境上限,开火时才真正分配显存,算力按时间片共享。
图 1:三段里只有最后一段会碰物理资源。点图看大图。

所以“延迟分配”这四个字,延迟的是开火,不是签约。空闲的 Pod 不端锅、不烧气,但合同一直占着。

四、明明没人做饭,为什么说没位置

回到现实里的数字。八间厨房,每间 96 口锅、100 份火力、十张门禁卡。一个“两间厨房、每间 48 口锅、五成火力”的项目,实打实只等于一间厨房的容量。八个这样的项目,正好把八间厨房的名义容量签满。第九个项目,哪怕从头到尾一行 GPU 代码都不跑,也挤不进来——因为二房东收租看的是合同,不是灶台。

这也解释了一个常见的困惑:nvidia-smi 上显存空着,调度器却说没资源。两边都没说谎,他们看的是两本账。

五、能不能超卖

能,二房东本来就有这个开关:把账本上的每间厨房从 96 口记成 192 口(deviceMemoryScaling),火力从 100 份记成 200 份(deviceCoreScaling),再决定每间厨房发多少张门禁卡(deviceSplitCount)。账本一放大,合同就能签出两倍。

左图账面每卡记 192G,八张卡可签 32 份 48G 合同;右图物理每卡 96G,只能真正端住两份,第三、第四份报 OOM。
图 2:签得多,不等于端得动。点图看大图。

但超卖的本质是赌:赌大多数租客签了不来,或者来了也吃得少。两个真开火、每人 48 口,正好;第三个也开火,就有人端不起锅,报 CUDA OOM。二房东不会赶人,也不会收锅,只会让最后伸手的那个人吃闭门羹。

所以超卖适不适合,取决于活儿的形状:

如果真要在生产上开,AI给的建议是:显存按峰值申报,只放大算力和门禁卡数量;或者显存也放大,但接受偶发 OOM,并在业务侧重试。至于“完全共享、谁也不记账”,那是另一套方案(time-slicing),代价是没有上限兜底,谁都能把整间厨房点着。

六、回到最初的问题

我理想里的“没任务就不绑卡”实现了吗?没有——卡在签约那一刻就绑了,合同上写死了是哪两间厨房。

没跑任务时占不占算力?物理上不占,账本上占。

延迟分配延迟了什么?延迟了端锅和烧气,没延迟承诺。合同和开火,是两件事——想明白这一点,很多“明明有空卡却调度不上”的疑惑,就不再像 bug 了。

留言

0/1000