拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GPU虚拟化与算力调度:用HAMi实现K8s集群GPU共享

GPU虚拟化与算力调度:用HAMi实现K8s集群GPU共享

干过AI平台相关工作的同学,应该都经历过这种痛:GPU卡贵、人多、任务排队,但一到晚上又大量闲置。更气人的是,一张A100被某个推理任务“独占”了,实际显存只用了几个G、算力利用率不到20%,旁边排队的训练任务却因为没卡而干等。K8s原生的device plugin只解决“怎么把卡认出来”,并不解决“怎么把卡切开来分给多个任务”。这就是HAMi这类GPU虚拟化与算力调度中间件出现的真正原因。这篇内容我会从GPU资源浪费的根源讲起,把HAMi的架构、调度原理、部署流程和踩坑经验一次说清,适合正在搭建GPU共享平台、做K8s调用GPU,或者被大模型微调和推理任务排队折磨的工程师参考。

1. GPU争用为什么成了AI平台的日常

1.1 从一张RTX 4060 Laptop说到集群里的A100

很多开发者的电脑上有两块GPU,一块Intel UHD Graphics核显,一块NVIDIA GeForce RTX 4060 Laptop GPU。日常用核显做显示输出,独显只在跑CUDA负载时才被调用。这个组合本身就说明了GPU调度的本质问题:物理上有两块显卡,但操作系统和应用程序往往只认其中一块,资源到底谁在用、用满了没有,不是靠肉眼看就能判断的。

到了集群环境,问题被无限放大。AI训练和推理任务需要的算力波动很大,一个基于PyTorch的蒸馏模型微调任务,可能在数据加载阶段把CPU打满、GPU闲置,进入训练step之后又反过来把GPU算力吃满。而一个跑在GPU上的实时推理服务,可能只需要几百MB显存,就能稳定提供服务。这两类任务如果各自独占一张卡,浪费率非常惊人。

我在实际运营集群时见过一组数据:节点上8张A100,GPU平均利用率只有30%左右,但显存几乎全被占满。这说明大部分任务申请的是一整张卡,但真正用到的算力不到三分之一。问题不在于物理卡不够,而在于我们没有一种机制,把一张卡的显存和算力按需切成多份,分给不同任务。

1.2 一卡一容器:资源分配粒度太粗

K8s默认处理GPU资源的方式很简单:把整张GPU当作一个可调度的扩展资源。Pod申请了就是独占,调度器不会在同一张卡上安排第二个Pod。这种模式在物理机时代可以理解,但在大模型时代非常浪费。

以典型的大模型推理为例,一个7B参数模型经过INT8量化后,显存占用大约7GB到10GB。如果跑在80GB显存的A100或H100上,算力可能只用了20%。剩下的GPU资源全部空闲。如果做多路并发,传统方案只能开多张卡,或者在同一张卡上用进程内并发,但进程间缺乏隔离,一个任务的显存溢出可能会拖垮整个进程。

反过来看大模型微调,很多场景反而希望一个任务能用满整张卡甚至多张卡。训练任务和数据并行策略决定了它要的是“整个GPU的全部资源”,而不是被切成碎片。这就是调度粒度问题的两面:推理任务需要细粒度共享,训练任务则需要聚合调度。一个合格的GPU调度平台,必须同时支持这两种诉求。

1.3 原生K8s设备插件为什么解决不了

K8s的device plugin框架在2018年引入,它的模型是“节点上报资源-调度器分配资源-设备插件在Pod启动时挂载设备”。这套机制让容器能访问GPU,但也仅此而已。

问题出在几个方面:

  • 调度粒度是1,最小分配单元就是一张物理卡。你想把一个GPU分给两个Pod用,原生的Extended Resource根本不支持。
  • 调度器不具备设备内部视角。它只看得到这张卡有几个,不知道每张卡的显存容量、当前利用率、温度、功耗、健康状态。
  • 缺少策略层。用户没法说“我只要这张卡的一半算力”,也没法要求“把两个任务尽量集中在同一张卡上,剩下的卡留给大任务”。
  • 异构设备支持混乱。NVIDIA需要nvidia-device-plugin,AMD有另一套,国产加速卡每家用一套,运维起来极其痛苦。

所以很多团队开始自己写调度器、写自定义资源,核心思路就是把GPU从“整卡资源”变成“可切分资源”。HAMi就是这个方向上比较有代表性的开源实践。

2. HAMi是什么:从设备插件到算力调度中间件

2.1 项目定位与核心能力

HAMi的全称是Heterogeneous AI Computing Management Interface,早期项目名是k8s-device-plugin,后来逐渐演变成一套完整的异构算力调度中间件,目前是CNCF云原生计算基金会下的项目。它的核心价值一句话就能概括:让K8s像调度CPU和内存一样调度GPU,支持按显存和算力切分设备,并统一管理不同厂商的加速卡。

它有四个关键能力:

  • vGPU切分。一张物理GPU可以按显存和算力比例切分成多份,分给不同Pod使用。
  • 调度策略。支持binpack、spread等不同的放置策略,也支持根据显存大小、算力需求、任务类型做亲和性调度。
  • 异构加速卡统一接入。不只支持NVIDIA,还支持昇腾、寒武纪、天数智芯、沐曦等国产加速卡,通过同一套接口管理。
  • 监控与可视化。能暴露每个GPU实例的显存、算力、利用率等指标,方便接入Prometheus和Grafana。

这套能力对AI平台的价值很直接:不用再为不同厂商的GPU写不同的调度逻辑,也不用再忍受一卡一任务的低利用率。

2.2 架构拆解:三个核心组件配合

HAMi的架构并不复杂,核心由三部分组成。

第一部分是设备插件(Device Plugin)。它运行在每个GPU节点上,负责将物理GPU上报给K8s,包括整卡信息、显存大小、算力型号等。它还负责在Pod创建时注入必要的环境变量和设备路径,让容器内能访问vGPU。

第二部分是调度器组件。HAMi本身会给K8s的调度器提供扩展能力,既支持scheduler extender的形式,也有调度器插件的形式。它能读取Pod的显存需求和算力需求,在所有节点上过滤出满足条件的GPU,再做评分和优选,最终把Pod绑定到具体的GPU实例上。

第三部分是vCUDA共享库。这是最关键的部分。HAMi在容器内部注入了一个CUDA API拦截库,当应用程序调用cuMemAlloc、cuLaunchKernel这类CUDA运行库接口时,这个库会拦截调用,把它映射到物理GPU上预先分配好的显存和算力切片中。这样一来,应用完全感知不到自己用的是一张被切分的vGPU,以为自己在独占一整张卡。

这个设计比传统的“时间切片”方案高级很多。时间切片是让多个任务轮流使用整张GPU,虽然利用率看起来上去了,但一个任务的显存访问依然可能占用全卡带宽,导致另一个任务性能抖动。HAMi的vCUDA方案把显存和算力同时隔离开,天然规避了这类干扰问题。

2.3 与其他GPU共享方案的对比

很多团队在HAMi之前会尝试其他方案,这里做一个直观对比:

方案隔离粒度支持的GPU厂商K8s集成主要问题
整卡独占(原生device plugin)整卡NVIDIA为主原生严重浪费,无法切分
时间切片(time-slicing)时间片NVIDIA需额外插件显存不隔离,性能抖动
NVIDIA MIG物理分区仅NVIDIA部分型号部分支持切分固定,不支持全部卡
NVIDIA vGPU(虚拟化)虚拟设备仅NVIDIA,需License需商业套件成本高,绑定虚拟化平台
HAMi显存+算力切分多厂商深度集成需要接受共享调度的复杂性

从这张表能看出来,HAMi的核心优势不在“某一个功能做得比别人深”,而在于把“切分、调度、隔离、异构纳管”串成了一个整体,并且开源免费。对于预算有限又不想被单一厂商绑定的团队来说,这是很实际的选择。

3. 部署落地:把HAMi装进你的K8s集群

3.1 部署前的环境检查

HAMi部署本身不复杂,但前置环境一定要检查到位。先说K8s版本,建议使用1.20以上的版本,因为需要比较稳定的device plugin机制和调度器扩展机制。节点上必须有NVIDIA驱动,并确保驱动支持CUDA 11.0以上的版本,因为vCUDA库要拦截的是新版本的CUDA API。

节点上需要提前装好nvidia-container-runtime,也就是NVIDIA的容器运行时。这个组件的作用是让容器内能访问到物理GPU,不装它的话,即便K8s调度成功,容器内也找不到设备。可以用以下命令快速验证运行时是否装好:

docker run --rm --gpus all nvidia/cuda:11.8.0-base nvidia-smi

如果输出正常显示GPU列表,说明运行时没问题;如果报错找不到设备,说明nvidia-container-runtime没启用,或者Docker的默认运行时没有切换过去。

另外要注意,集群中如果已经部署了原生的nvidia-device-plugin,需要先卸载或者停止它,否则两套插件会同时上报同名的nvidia.com/gpu资源,导致调度混乱。这一点我在第一次部署时就踩过坑,调度器反复报资源冲突,查了半个小时才发现是两台DaemonSet在打架。

3.2 通过Helm安装HAMi

HAMi提供了官方的Helm Chart,安装起来比较快。典型流程如下:

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

安装完成后,检查几个关键Pod是否正常:

kubectl get pods -n kube-system | grep hami

正常情况下会看到hami-device-plugin-*运行在每个GPU节点上,hami-scheduler-*作为调度器扩展运行,hami-controller-*负责节点与设备状态的协调。

如果你想调整调度策略,可以通过values文件覆盖默认配置。比如把默认的显存切分粒度设小一点,或者开启MIG模式的支持。具体的参数名在不同版本中变化较快,建议先执行helm show values hami/hami查看当前版本支持哪些配置项,再决定怎么改。

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

kubectl describe node <your-node>

如果看到nvidia.com/gpu或hami.io/vgpu这类资源,说明设备插件已经成功上报了资源。HAMi支持一卡多切,所以这里的gpu数量是物理卡数乘以切分倍数,而不是单纯的物理卡数。

3.3 让Pod用上共享GPU

部署好HAMi之后,最直观的体验在Pod YAML里体现。同样是申请GPU,你不再只能写“给我一张卡”,而是可以写“我要多少显存、多少算力”。

下面是一个典型的共享GPU Pod声明:

apiVersion: v1 kind: Pod metadata: name: shared-gpu-pod annotations: hami.io/vgpu-memory: "10Gi" hami.io/vgpu-compute: "30" spec: containers: - name: pytorch image: nvidia/cuda:12.2-base resources: limits: nvidia.com/gpu: 1 command: ["nvidia-smi", "-L"]

这里的注解表示,我想在这张GPU上申请10GiB显存,并使用30%的算力。调度器会根据这两个数字,在当前节点上找到满足条件的vGPU切片,然后把Pod绑定上去。

这里需要特别提醒:注解的字段名和资源名在HAMi的不同版本中做过调整,有的版本用hami.io/gpu-memory,有的用hami.io/vgpu-memory。写YAML之前一定要先看当前版本官方文档里的示例,不能直接照抄网上所有版本的写法。这也是开源项目迭代快带来的常见问题。

4. 调度策略与算力隔离细节

4.1 显存隔离是怎么实现的

HAMi的vCUDA库本质上做了一次“显存中转”。当容器内的CUDA应用调用cuMemAlloc申请显存时,这个调用会被拦截,vCUDA库并不会真的在物理卡上分配用户请求的大小,而是从预先为该Pod分配好的vGPU显存块中“切一块”出来,记录好偏移量和大小,再返回一个伪地址。

之后应用读写这块显存时,实际访问的是物理GPU上的一段连续显存区间。由于每个vGPU实例的偏移区间都经过隔离,一个Pod里的应用无法越界访问另一个Pod的显存数据。这种隔离方式有点像操作系统的虚拟内存,给每个进程一个独立地址空间的错觉,底层靠页表和硬件单元做隔离。

这里有一个容易被忽略的细节:显存隔离不代表显存不会超卖。如果集群里多个Pod申请的显存总和超过了物理卡实际容量,就可能出现分配失败或者运行期OOM。HAMi支持显存超卖,但生产环境我强烈建议关闭超卖,或者只在推理场景小范围开启。训练任务一旦OOM,不仅浪费算力,还可能导致模型参数损坏。

4.2 算力切分为什么不能简单按比例算

显存隔离相对好理解,算力隔离就复杂得多了。要理解这个问题,得先知道GPU是怎么样并行执行计算任务的。

GPU的并行单位从线程开始。32个线程组成一个Warp,这是硬件调度和发射的基本单位。多个Warp再组成一个线程块(Block),线程块里的线程可以通过共享内存通信。若干个线程块组成一个Cooperative Thread Array,也就是CTA,CTA中的线程块被期望在同一GPU上同时执行。之后多个CTA构成一个Grid,对应整个kernel启动任务。

为什么要说这个?因为算力切分本质上是在问:一个kernel启动后,它发射的Warp能占用多少SM(流式多处理器)资源?最简单的做法是限制Warp的发射数量,但这样做不准,因为不同模型的kernel形状差异巨大,有的kernel占用SM高,有的占用低。纯按Warp数量限制,会导致有些任务实际算力远低于申请值,有些则超过申请值。

所以HAMi在算力切分上做了组合策略,综合使用了MPS、时间片调度和SM占有率控制。它的目标是让Pod的实际吞吐尽量贴近申请值,同时不要因为共享而明显干扰别人。这也解释了为什么GPU调度领域,算力切分永远是比显存切分更头疼的问题。

4.3 调度策略:binpack、spread与任务亲和

部署HAMi的时候,调度策略的选择直接影响集群的整体利用率。

binpack模式会把任务尽量集中放在少数几块GPU上,把剩余GPU空出来,适合“有大任务需要整卡资源”的场景。比如你有8张A100,白天跑一批小推理任务,每张卡切4份,只用了其中4张卡,另外4张整卡预留给晚上跑大模型微调任务。这种模式能显著提高整卡利用率,但代价是共享卡上的性能波动会变大。

spread模式则相反,会把任务打散放在尽可能多的GPU上,适合“尽量降低单卡压力、提升单任务稳定性”的场景,最典型的就是多个重要推理服务共存。每个推理Pod独享一段显存和算力,物理上错开,避免互相争抢。

还有一类需求是任务亲和。分布式训练任务要求多张GPU之间低延迟通信,最好能调度到同一节点上的同一物理卡或同一台机器。HAMi的调度器会检查Pod的标签和注解,根据GPU拓扑信息做聚合。如果你手动介入过分布式任务调度,应该知道这种细节有多重要。

4.4 异构算力纳入统一调度的意义

很多人忽略的一点是,AI平台扩容时不见得只会买NVIDIA显卡。国产加速卡这两年发展很快,昇腾、寒武纪等系列在特定推理场景的表现已经很有竞争力。但这些卡的内存管理、驱动接口、device plugin方式各有不同,如果每接入一种卡就写一套调度器,运维工作量会失控。

HAMi把异构设备的差异封装在设备插件和调度接口后面,上层调度逻辑保持统一。站在用户角度,我申请的还是同一类资源,只是底层节点上是NVIDIA还是昇腾,调度器自己能识别。如果你所在团队有国产化或异构算力统一纳管的需求,这项能力会比单纯的利用率提升更有价值。

5. 落地中的坑与排查建议

5.1 常见问题速查表

在实际落地过程中,我整理了一个问题速查表,遇到同类问题可以直接照着排查:

现象大概率原因排查方式
Pod一直Pending,调度器不分配GPUscheduler extender配置没生效查看kube-scheduler日志,确认extender url可访问
节点显示GPU资源为0device plugin异常或与原插件冲突检查daemonset日志,确认只有一套插件上报
容器内nvidia-smi看不到设备nvidia-container-runtime未启用用docker --gpus验证运行时是否能访问设备
CUDA报错invalid device ordinalvCUDA注入失败,应用看到了物理设备集合检查容器的LD_PRELOAD环境变量
显存频繁OOM,任务中断显存超卖或分配策略过于激进关闭超卖,降低单节点调度密度
多Pod共用一卡,性能波动大算力隔离策略不适合当前负载切换spread策略或改用MIG

这套速查表看起来简单,但每条背后都是一个真实事故。尤其是遇到“Pod Pending,报错信息却不明显”的情况,十有八九是调度器扩展器没被kube-scheduler正确加载。

5.2 版本迁移与升级的坑

HAMi从k8s-device-plugin演化过来之后,资源名和注解变化不小。如果项目从旧版本直接升级,最典型的问题是集群里存量Pod的YAML还写着旧注解,新版本不识别,Pod被调度以后,vCUDA库没有正确注入,容器内CUDA应用直接访问物理GPU,虽然能跑,但完全没隔离,等于白装了这个中间件。

我建议升级之前,先导出一份当前所有使用GPU资源的Workload清单,把注解逐一批量更新成新版本格式,再滚动升级。另外,HAMi升级时调度器组件会有一段时间不可用,最好挑业务低峰期操作。说句实话,这项目迭代速度很快,版本之间又不是完全兼容,所以生产环境尽量锁版本,不要追新。

5.3 监控指标与容量规划

部署完HAMi之后,如果不看监控,等于还在盲飞。HAMi会暴露每个vGPU实例的指标,包括显存使用量、算力占有率、任务数量等,可以把这些指标接入Prometheus,再通过Grafana做面板。

我在集群里用过一套很实用的监控组合:

  • 节点维度看物理GPU的温度、功耗、显存总量,判断是否存在超卖风险。
  • vGPU维度看每个切片的显存和算力使用率,定位哪些任务申请了资源却没用满。
  • 调度器维度看排队任务数量和等待时间,反推是否需要调整调度策略。

有个经验可以分享:算力利用率监控曲线如果长期在80%以上,说明共享已经接近极限,这时候继续塞任务,每个任务都会明显变慢,用户体验直线下降。别等告警,最好提前在利用率达到70%到75%时就开始扩容或者调度降级。

6. 从GPU争用到高效共享,还有一段路要走

HAMi解决的是“一张卡怎么切成多份再用”的问题,这已经比原生K8s机制前进了一大步。但算力调度远没有到这就算完事。大模型训练追求低延迟、高吞吐,推理场景又要求极致性价比,两种负载的调度诉求经常是冲突的。我现在的做法是用HAMi做基础共享,再结合节点池和调度策略,把训练和推理任务分到不同的GPU池子里,各自的池子内部再按需切分。

这种分层思路配合下来,集群的整体利用率确实提升明显。我这边的经验是,GPU利用率从不到30%提升到了60%左右,任务排队时间大幅缩短,夜间闲置时段还能额外塞进一批离线推理任务。提升利用率这件事,并不是单纯靠一个开源组件就能搞定,它是一个持续调整的过程。

如果团队正在做AI基础设施,我建议先小范围试点HAMi,拿几张卡跑几个真实的推理和微调任务,多观察几天的监控数据,再逐步扩大到全集群。毕竟算力调度这东西,自己踩过坑、调过参数,才能真正摸透它的脾气。

返回列表