我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

K8s环境下GPU虚拟化切分与算力调度实践指南

K8s环境下GPU虚拟化切分与算力调度实践指南 先说点实际的。很多团队买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 MPSMulti-Process Service把多个进程的计算请求聚合起来。MPS本身是NVIDIA提供的官方方案它能把多个进程的计算内核合到同一个CUDA上下文里配合CUDA_MPS_PINNED_DEVICE_MEM和CUDA_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 LTSK8s版本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 --runtimecontainerd sudo systemctl restart containerd到这里容器运行时已经具备GPU透传能力但K8s要能调度GPU还需要装Device Plugin。4.2 官方Device Plugin整卡调度验证用官方manifest部署Device Pluginkubectl 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卡。然后提交一个测试PodapiVersion: 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需要一块物理GPUhami.io/gpu-memory和hami.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卡都切完。我习惯先留一张物理卡不切分作为“安全网”专门跑那些调度不了或者镜像比较脏的实验任务。这样就算切分方案出问题调试和排查时也始终有一张干净可用的卡在等着你。这种冗余思路听上去很浪费但长远来看能省下大量排障时间。
返回列表