K8s环境下GPU虚拟化切分与算力调度实践指南
2026/9/21 2:47:13 网站建设 项目流程

先说点实际的。很多团队买GPU卡的时候,都会经历一个阶段:钱花了不少,但日常跑起来发现利用率低得可怜。我有一次在一家做AI平台的公司里看监控,那台8卡A100机器,白天只有两个任务在跑,各自占了一张卡,剩下六张卡的空闲率超过了90%。后来我们把K8s云原生环境里的GPU虚拟化切分和算力调度这套体系搭起来之后,同一批卡能同时跑十几个推理任务,才算真正把资源吃满了。

这篇文章就把我在K8s环境下做GPU虚拟化切分与算力调度的完整思路、方案选型和实操过程写出来,给同样在折腾GPU资源利用率的朋友一个参考。不管你是建设AI平台的运维,还是被多模型任务排队搞到头大的算法负责人,这篇文章都会有用。文章会覆盖为什么会需要切分、主流方案之间的区别、K8s里的Device Plugin机制、从整卡分配到MIG硬切分的完整步骤,以及一套可以落地的调度策略设计。

1. 为什么一定要在K8s里做GPU切分

1.1 一块A100解决不了的问题

很多人刚接触GPU集群时会有一个错觉:既然卡这么贵,那就一个任务占一张卡,安心跑不就行了?现实是,大模型训练确实需要一整张甚至多张卡,但日常的画像推理、小模型微调、批量特征计算这类任务,往往只吃十几GB显存。用一张80G的A100跑一个只占10G显存的小模型,剩下的70G完全闲置,这比买卡的成本还要让人心疼。

再加上K8s原生环境下,默认的Device Plugin只会把物理GPU当作一个整体来分配。也就是调度器看到的资源粒度是“一张A100”,不是“A100上的32GB显存”。一旦某个Pod申请了这张卡,其他Pod哪怕只需要一丁点显存,也进不来。结果就是一张大卡被低负载任务独占,后面真正需要资源的任务反而一直在Pending。

GPU切分要解决的就是这个粒度问题。核心思路是把物理GPU拆成更小的资源单元,让多个任务可以共享同一张卡。这里说的切分不只是显存上的划分,还要兼顾算力分配,否则就会出现“显存没爆、但两个任务互相抢计算单元导致性能雪崩”的局面。

1.2 不切分,算力浪费在哪里

算力浪费可以从两个维度看。

第一个维度是时间和空间上的浪费。绝大多数GPU推理服务有明显的波峰波谷。白天业务请求多,GPU相对紧张;凌晨几乎没有流量,整张卡却还在那里空转。如果一个推理服务独占一张卡,那么这张卡的空闲时长基本就是你对面的业务空闲时长。反过来,如果能把多个服务混合部署在同一张卡上,不同服务的波峰波谷还能相互错开,整体利用率立刻上去了。

第二个维度是任务之间天然存在资源“看得见但吃不到”的问题。典型情况是:训练任务占了一张A100的显存,但它的计算密度很低,比如在等数据加载或做验证集的评估,这个时候GPU的计算单元实际上在大量空转。如果能把这张卡剩余的计算能力切给另一个轻量任务,对整个集群来说是净赚的。

所以,GPU切分真正要解决的不只是“省显存”,而是把物理卡的显存和算力做成可以按需分配、按量计费的资源池。这也是把它放到K8s云原生这个大框架下做的事:让GPU资源成为类似CPU内存一样的可调度资源,由平台统一管理,而不是由某个人工在物理机上手工分配。

2. GPU虚拟化切分的核心方案盘点

2.1 时间共享:最简单但最不可控

时间共享大概是所有GPU切分方案里门槛最低的一种。它的思路和CPU时间片差不多:多个进程轮流使用同一块GPU,每个进程在一个时间窗口内全部占用计算单元,时间窗口按比例分配。

在K8s里,比较常见的实现方式是给Pod注入特定的CUDA环境变量,或者通过NVIDIA MPS(Multi-Process Service)把多个进程的计算请求聚合起来。MPS本身是NVIDIA提供的官方方案,它能把多个进程的计算内核合到同一个CUDA上下文里,配合CUDA_MPS_PINNED_DEVICE_MEMCUDA_MPS_ACTIVE_THREAD_PERCENTAGE这类环境变量,可以对单进程的算力占比做一个软限制。

时间共享的优点是简单,不需要改硬件配置,也不依赖特定卡型。缺点是显存隔离几乎为零,一旦某个任务的显存用量超标,整个物理卡都会被拖垮。算力限制也是软限制,多个任务同时跑的时候,可能出现实际占用超过设定比例的情况。所以它更适合内部工具类任务,不适合给外部客户做资源保障。

2.2 MIG硬切分:NVIDIA给A100后代的答案

MIG全称是Multi-Instance GPU,从Ampere架构的A100开始引入。它和前两者有本质区别:MIG是在硬件层面把GPU切成多个独立实例,每个实例拥有独立的显存、显存带宽、L2缓存和计算单元子集,实例之间互不干扰。

比如一张80G的A100,可以切成若干个10GB、20GB、40GB规格的实例。因为硬件隔离是真实存在的,所以某个实例内发生OOM或者崩溃,不会影响其他实例。这一点对稳定性要求高的线上推理服务来说非常关键。

不过MIG也有明显的限制。第一,只支持特定芯片,消费级显卡完全不支持,A30、A100、H100、A800这类数据中心卡才有。第二,一张卡上能切出的实例数量和规格都是模板化的,自由度不高。你只能按NVIDIA给定的模板来切,比如1g.10gb、2g.20gb、3g.40gb这种,不能自定义“我要33GB”。第三,配置MIG需要重启GPU,运维上会多一步操作。

2.3 兼容CUDA的vGPU共享:HAMi等开源方案

既然MIG太死板,时间共享又不够安全,开源社区开始做更灵活的用户态切分方案。HAMi是目前玩得比较多的一个,以前的思路大体相似:在用户层面拦截CUDA调用,让每个Pod以为自己独占了一张GPU,但实际上显存分配被控制在一个配额内,算力分配也可以通过MPS或自定义调度策略来限制。

这类方案的好处是,不挑卡型,NVIDIA的T4、V100、A100、4090都能用,切分粒度很细。比如你可以在K8s里给一个Pod申请“20Gi显存+50%算力”,另一个Pod申请“10Gi显存+30%算力”,完全按业务需求来定。

缺点也很明显:它本质上是软件层面的隔离,隔离强度不如MIG。如果某个Pod里跑的特权程序故意绕过CUDA拦截逻辑,理论上还是可以摸到物理卡上的其他数据温区。另外它对CUDA版本的兼容性偶尔会有坑,升级驱动或CUDA库之后要重新验证。

我在实际项目里通常把这类方案用在“内部多团队共享”的场景,对外提供资源租用服务还是优先用MIG。

2.4 方案选型对比表

对比项时间共享/MPSMIGHAMi等用户态vGPU方案
隔离级别弱,显存不隔离硬件级强隔离用户态逻辑隔离
切分粒度按算力比例配置固定模板显存、算力灵活配置
支持的卡型几乎所有NVIDIA卡A30、A100、H100等几乎所有NVIDIA卡
稳定性较差,容易互相干扰很高中等
运维复杂度中等,需重启GPU中低
适用场景内部实验、低负载混合对外资源交付、多租户隔离多团队共享、弹性平台

我个人的选型经验是:如果整个集群的卡型比较统一,而且都是A100以上,优先上MIG,省心;如果集群里什么卡都有、切分需求又五花八门,那就把HAMi这类方案作为默认选择;时间共享只适合临时应急,不建议作为长期方案。

3. K8s GPU调度的底层机制

3.1 Device Plugin与扩展资源

要理解K8s里的GPU调度,先要搞清楚一个核心组件:Device Plugin。Kubernetes本身不认GPU,它只知道CPU和内存,对于所有“不是CPU但能算资源”的硬件,都要通过Device Plugin机制来接入。

Device Plugin做的事情很简单:启动后向kubelet注册,告诉kubelet这台节点上有多少个GPU资源,并把资源名上报为nvidia.com/gpu。之后kubelet把资源数量纳入节点容量,调度器在调度Pod时,会检查哪个节点上的nvidia.com/gpu数量还够用。

这里有个常见误区:很多人以为Pod申请nvidia.com/gpu: 1以后,容器里就自动有GPU了。其实不是。Device Plugin只负责“让调度器知道资源存在并且可以分配”,真正让容器能访问GPU,需要容器运行时配合。NVIDIA官方现在的标准做法是装nvidia-container-toolkit,然后配置containerd或Docker调用它。容器启动时,toolkit会把宿主机的驱动和CUDA库挂载进容器,并自动设置好环境变量。

3.2 调度器如何感知虚拟GPU

默认调度器在处理nvidia.com/gpu时,只会把它当成一个普通的整数资源来比较。这意味着如果你不做额外处理,一个Pod申请了1个nvidia.com/gpu,它就会占用一整张物理卡。

做了GPU切分之后,情况就不一样了。比如一张A100通过HAMi变成10个“10Gi显存”的虚拟资源,调度器看到的应该是10个可分配单元,而不是1个。这就需要设备插件在上报资源时做手脚:上报的就不是物理卡数量,而是切分后的虚拟资源总数。

但光改上报数量还不够。因为调度器在把一个Pod放到某个节点后,还要知道“这张物理卡的剩余显存究竟够不够”。这类信息默认调度器是不知道的。所以大多数虚拟GPU方案的实现,都会在调度路径上加一个Admission Webhook或者调度器扩展,由这个组件实时计算每张物理卡上的剩余资源,决定Pod到底该落在哪张卡上,并且把分配结果记录在Pod的annotation里。

3.3 算力限制和显存限制的实现思路

显存限制相对容易理解。以HAMi为例,它会在Pod创建时往容器里注入一个自定义的CUDA客户端库,通过LD_PRELOAD机制拦截cuMemAlloc这类的显存分配调用。应用程序向CUDA申请显存时,这个拦截层会计算当前Pod已经用了多少显存,如果达到配额就直接返回失败。

算力限制稍微复杂一点。GPU的算力不像显存那样可以直接“划分”,本质上只能限制访问频率和控制并发块数。实际方案里多是用MPS或者CUDA的流优先级机制来实现。更直白地说,算力限制的精度取决于底层实现,MIG做到的是物理级算力隔离,HAMi这类方案更多是靠调度和限流做到“大致公平”。

硬件卡型支持MIG时,算力限制是天然的;卡型不支持MIG时,想把算力切得很好,需要做不少调参工作。这也是为什么我在方案选型里强调,对外交付稳定调度能力的平台尽量选MIG路径。

4. 实操:从整卡分配到GPU切分

4.1 集群环境准备

接下来的实操示例,我按一个常规的K8s GPU节点环境来写。假设节点是Ubuntu 22.04 LTS,K8s版本1.28以上,容器运行时是containerd,物理GPU是NVIDIA A100 80G。

第一步是安装驱动。这里有个细节容易被忽略:容器环境里跑GPU程序,需要的是宿主机驱动支持容器透传,所以驱动版本一定要和CUDA运行库兼容。安装完驱动后,用nvidia-smi验证一下,确认能看到显卡型号和驱动版本。

# 安装驱动(以535版本为例,实际根据你显卡支持列表选) sudo apt update sudo apt install nvidia-driver-535 sudo reboot # 驱动验证 nvidia-smi

第二步是安装nvidia-container-toolkit。它是容器访问GPU的桥梁,作用是在容器创建时自动注入GPU设备和驱动库。

sudo apt install nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=containerd sudo systemctl restart containerd

到这里,容器运行时已经具备GPU透传能力,但K8s要能调度GPU,还需要装Device Plugin。

4.2 官方Device Plugin整卡调度验证

用官方manifest部署Device Plugin:

kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/main/nvidia-device-plugin.yml

安装完成后,可以看节点上的可分配资源:

kubectl describe node <node-name> | grep nvidia

正常情况下会看到nvidia.com/gpu: 8这样的输出,代表这个节点被识别出了8张GPU卡。然后提交一个测试Pod:

apiVersion: v1 kind: Pod metadata: name: gpu-test spec: restartPolicy: OnFailure containers: - name: gpu-test image: nvcr.io/nvidia/cuda:12.2.0-base-ubuntu22.04 resources: limits: nvidia.com/gpu: 1 command: ["nvidia-smi"]

这个阶段跑通后,说明整卡调度链路是好的。这也是后续做切分之前必须完成的验证项,不要跳过。

4.3 用HAMi做显存与算力切分

接下来装上HAMi,实现真正的切分能力。HAMi的部署有几种方式,用Helm是最快的:

helm repo add hami-project https://project-hami.github.io/HAMi/ helm repo update helm install hami hami-project/hami --namespace kube-system

装好之后,向集群提交如下资源定义,申请20Gi显存和50%算力:

apiVersion: v1 kind: Pod metadata: name: vgpu-test spec: restartPolicy: OnFailure containers: - name: cuda-test image: nvcr.io/nvidia/cuda:12.2.0-base-ubuntu22.04 resources: limits: nvidia.com/gpu: 1 hami.io/gpu-cores: "50" hami.io/gpu-memory: "20Gi" command: ["sh", "-c", "sleep 3600"]

这个YAML里,nvidia.com/gpu: 1表示Pod需要一块物理GPU,hami.io/gpu-memoryhami.io/gpu-cores则指定了在这块物理卡上分配多少显存和多少比例的算力。提交后,进入容器执行nvidia-smi,你会看到显存显示为约20Gi,和整卡80G明显不同。

这时候再提交第二个Pod,申请10Gi显存+30%算力,它会落到同一张物理卡上。两个Pod互不知晓,但在物理机上观察nvidia-smi,能看到这张卡实际承载了两个容器,合计显存占用30Gi左右。这样一个非常自然的共享场景就搭起来了。

要注意的是,HAMi对容器镜像里的CUDA版本有要求,官方文档里列了支持的版本范围。如果跑起来发现显存限制不生效,多半是注入的库版本和镜像里的CUDA版本不匹配。

4.4 用MIG做硬切分

MIG方案在K8s里的落地路径不太一样。第一步是在物理机上开启MIG模式并创建MIG实例:

# 开启MIG模式 sudo nvidia-smi -mig 1 # 重启后查看可用模板 nvidia-smi mig -lgp # 创建1g.10gb规格的实例 sudo nvidia-smi mig -cgi 1g.10gb -C

创建完成后,用nvidia-smi能看到这张卡被切成了多个独立实例。第二步是部署支持MIG的Device Plugin。NVIDIA官方推荐用GPU Operator来管理MIG配置,也可以直接部署支持MIG的设备插件。

部署好之后,K8s节点上会出现类似nvidia.com/mig-1g.10gb的资源名。调度时直接在Pod里按资源名申请:

resources: limits: nvidia.com/mig-1g.10gb: 1

如果一台A100 80G按1g.10gb模板切,理论上可以切出7个实例(具体视GPU型号的不同模板限制),4张卡就是28个独立可调度单元。这种数量规模,对于跑几十个轻量推理服务来说已经足够了。

5. 算力调度进阶:队列、优先级与分时复用

5.1 默认调度器在GPU场景下的局限

默认K8s调度器擅长的是让Pod找到“有足够CPU和内存”的节点,但GPU切分场景下有个很别扭的地方:物理卡本身的状态是动态的。

举个例子,一张物理卡上已经跑了两个Pod,显存占用40G。这时候新来一个Pod申请30G显存,调度器知道了节点剩余资源,但它不知道这张物理卡是“剩余40G”还是“剩余0G”,因为这些信息在默认调度器眼里只是抽象的数字。

如果用HAMi这类方案,节点上报的是虚拟GPU总量,调度器看到的是“还有N个单元可用”,但不会告诉调度器这些单元分布在哪几张物理卡上。所以必须依赖额外的Webhook或调度器扩展来补足这个信息。HAMi本身实现了这部分逻辑,Volcano也提供了类似能力。

5.2 用Volcano做训练任务排队与抢占

Volcano是一个K8s原生批量调度系统,对AI训练和推理任务非常友好。它提供了队列、优先级、抢占、Gang Scheduling这些默认调度器没有的能力。

设想一个典型场景:白天推理任务多,晚上训练任务多。你想让训练任务在白天也能占用空闲的GPU,但推理任务发布后又要能及时拿到资源。

用Volcano可以这么设计:建两个Queue,一个专门放训练任务,一个放推理服务。训练任务优先级设低,推理服务优先级设高。有算力空闲时,训练任务可以补位;一旦推理服务需要资源,低优先级任务会被抢占并释放GPU。配合GPU切分后,抢占的粒度也可以很小,比如只释放20G显存,而不是整张卡。

Volcano在K8s中部署起来并不复杂,直接用Helm安装就行。配置Queue和PodGroup之后,把Pod纳入对应Queue,它才会被正确调度。对没有接触过这批调度器的朋友,我的建议是先在小集群里跑一遍排队和抢占的实验,确认行为符合预期再上生产。

5.3 动态算力分配的常见策略

在切分和调度机制都就位后,剩下的问题就是“任务来了到底给多少”。这里我分享三条实际验证过的经验策略。

一是按服务类型定配额。在线推理服务需要稳定低延迟,尽量给它保留独占的计算资源;离线批量任务可以共享,允许被抢占。二是用“显存配额做保险、算力配额做限制”。显存给足,保证任务不会OOM;算力按业务总线的峰值来给,避免两个任务同时爆发时互相拖垮。三是在集群整体占用率超过70%时,启动准入控制,拒绝新的后台任务,给线上任务留出缓冲余量。

这些策略不一定需要写代码,K8s的ResourceQuota、LimitRange加上Volcano的优先级语义就能实现一大部分。真正难的是收集线上数据,不断调整配额比例。毕竟每个业务的显存和算力比都不一样,没有银弹。

6. 实操避坑指南与排查实录

6.1 高频故障对照表

现象可能原因处理方式
Pod一直PendingDevice Plugin未部署或资源不足kubectl describe pod查看事件,确认节点GPU资源
容器内nvidia-smi不可用nvidia-container-toolkit未配置好检查/etc/containerd/config.toml的runtime配置
显存限制不生效HAMi注入的库与CUDA版本不匹配检查Pod日志和HAMi版本兼容性
MIG创建后Pod不能调度Device Plugin不支持MIG资源名部署支持MIG的Device Plugin或GPU Operator
两个Pod共享卡后性能骤降算力没有做限制,相互抢占启用MPS或配置算力配额
节点重启后MIG配置丢失GPU驱动重置了MIG模式nvidia-smi -mig 1写进开机自启

6.2 三个我踩过的坑

第一个坑是“显存切好了,但算力没有切”。当时给两个团队分配了同一张A100,显存各20G,跑起来之后发现两个任务的实际完成时间都比独占时长了一倍多。原因是显存隔离做到了,但算力还是整卡的,两个任务抢同一批SM。后来改成MPS并限制线程占比,情况才好转。

第二个坑是MIG模式下混用整卡和MIG实例。某次集群里既有需要整张卡的大训练任务,又有需要MIG实例的小推理服务。结果发现,同一节点上如果既上报了整卡资源又上报了MIG实例资源,调度器会同时把两种资源分配出去,导致物理卡上的资源冲突。最后的方案是把节点分两类管理,一类只跑整卡任务,一类只跑MIG实例。

第三个坑是升级驱动后,所有的共享切分全部失效。那次我把驱动从535升到545,没有重装nvidia-container-toolkit,结果Pod启动后容器里看不到GPU设备。后来发现是升级驱动时把CUDA库链接弄乱了,重新跑了一遍nvidia-ctk runtime configure才恢复。现在凡是涉及驱动升级,我都会先在一台测试节点上完整走一遍流程,再推到其他节点。

还有一点值得单独说:做GPU切分之前,先想清楚监控方案。切分之后,物理卡上的利用率看起来可能很高,但真正有意义的是每个Pod级别的GPU利用率和显存水位。建议提前把dcgm-exporter这类采集器部署好,按命名空间和Pod维度建立监控面板,不然切分上线之后你根本分不清是哪条业务在吃资源。

我在实际操作中的体会是,GPU虚拟化切分不是装一个组件、配几个参数就结束的事,它对业务方的工作模式也提出了新要求。以前一块卡只跑一个任务,出了问题很好定位;现在一张卡跑五六个任务,任何一个任务崩溃都可能让队友的Pod跟着遭殃。所以切分之后,需要让每个业务方明确知道自己的资源配额和实际使用量,资源使用超出配额时也要有清晰的上报和告警机制。

最后再分享一个小技巧。做MIG路径的方案时,不要一上来就把所有GPU卡都切完。我习惯先留一张物理卡不切分,作为“安全网”,专门跑那些调度不了或者镜像比较脏的实验任务。这样就算切分方案出问题,调试和排查时也始终有一张干净可用的卡在等着你。这种冗余思路听上去很浪费,但长远来看,能省下大量排障时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询