8张国产GPU承载30个开发环境:基于HAMi的vGPU切分与调度落地实践
2026/9/23 4:58:45 网站建设 项目流程

从去年下半年开始,我所在的电科云团队接到不少类似的诉求:开发环境不够用、GPU卡申请排队、每张卡都被独占着但利用率惨不忍睹。核心矛盾其实就一句话——GPU的算力和显存没有被真正池化。当时我们手里正好有8张国产GPU卡,要承载30个开发环境,听起来像是“用一口锅煮30个人的饭”,但最后用HAMi做虚拟化切分,真的跑起来了,而且跑得还算稳。这篇文章就把我们整个方案设计、操作步骤、踩坑经历完整写下来,给同样在国产GPU资源池化、开发环境交付上头疼的朋友做个参考。

1. 项目背景与总体方案:为什么是8张卡扛30个环境

1.1 需求方到底在“吵”什么

先说背景。团队内部有算法工程师、训练平台研发、数据标注小组,还有做模型评测的同事,林林总总加起来30多个人,每个人都希望有一个随时能用的开发环境。所谓开发环境,通常就是一个容器或者虚拟机,里面预装了Python、PyTorch、CUDA工具链、VS Code Server或者JupyterLab,能连上GPU做调试。

以前的做法很粗暴:一人一张卡,环境互相隔离,谁也别抢谁的。但问题随之而来——绝大多数日常开发任务并不会把GPU打满。写训练脚本、调参数、看loss曲线,这些操作的GPU占用率经常就在5%到20%之间打转。8张卡如果做一对一分配,最多也就满足8个人,剩下22个人只能排队。可如果按照实际负载来算,8张卡的峰值利用率可能连30%都不到。

说白了,这不是卡不够,是资源碎得没法用。我们需要的不是“更多卡”,而是“把一张卡当成多张卡来用的能力”。

1.2 原生K8s调度国产GPU的尴尬

我们底层基础设施是Kubernetes,所以自然而然地想依赖K8s的调度体系。但K8s原生的GPU调度能力非常有限。默认情况下,NVIDIA的device plugin会把一张物理GPU上报成一个可调度的资源单位,Pod一旦申请了nvidia.com/gpu: 1,这张卡就被整个绑定了。国产GPU的device plugin也类似,基本继承了这套思路——有卡就是1,没卡就是0,完全不支持“半张卡”“四分之一张卡”这种分配方式。

这带来的直接后果是:哪怕一个环境只需要2GB显存,只要它申请了卡,整张32GB的卡就被独占,别人再也用不了。显存规格越大,浪费越明显。尤其是国产GPU,单卡显存普遍往32GB、64GB走,开发任务根本吃不满,大量显存和算力被闲置在角落里。

1.3 为什么选HAMi而不是自己造轮子

我们也评估过自己开发一套资源切分方案——通过CUDA拦截层劫持显存申请和计算流,把一张物理卡改造成多张逻辑卡。理论上可行,但工程量非常大。国产GPU的驱动栈不统一,每家的运行时环境、API和显存管理机制都有差异,自己从零适配,按团队的人力做半年都不一定稳。

HAMi是开源的GPU虚拟化中间件方案,核心卖点就两个:一是支持显存和算力的细粒度切分,二是兼容多种GPU厂商,不只是NVIDIA,还包括昇腾、寒武纪、天数智芯等国产加速卡。它天然就是为“异构GPU池化”设计的,省掉了我们大量底层适配工作。

注意:HAMi不是一个新的远程渲染协议,也不是某种带外管理系统。它本质上是Kubernetes生态里的调度扩展程序和设备管理插件,把物理GPU资源变成可切片、可配额、可调度的逻辑资源。

2. HAMi的底层逻辑:vGPU切分、显存隔离与算力分配

2.1 说人话解释HAMi的三个核心能力

我习惯把HAMi的能力拆成三块来理解,这三块也是整个方案能否成立的地基。

第一块是显存虚拟化。物理GPU上有真实的显存大小,比如32GB,HAMi会在驱动层做一个“代理”或“伪装”,让容器内的应用以为这张卡的显存上限只有8GB、6GB或者你指定的任何数值。应用申请显存的时候,会被限定在这个额度之内,超出就报OOM。这样多个Pod可以共享同一块物理显存,互不越界。

第二块是算力切分。显存隔离只是“分地盘”,算力切分则是“限速度”。HAMi可以通过控制GPU上的计算流调度,把每个Pod能使用的GPU计算核心(比如SM,或者国产卡上的计算单元)数量限制在一个比例内。这个比例可以精确到1%级别,我们用的时候一般都给到20%到40%,保证多环境并发时不会互相拖垮。

第三块是资源上报与调度。HAMi会让Kubernetes节点上的GPU资源变成类似hami.io/gpu-memhami.io/gpu-cores这样的扩展资源,K8s调度器看到这些资源后,就能按显存和算力维度去匹配Pod和节点。你可以把它理解成把一张整卡拆成了“若干个可组合的积木”,调度器根据每个环境的需求挑积木。

2.2 显存配额、算力上限和任务的对应关系

在动手配置之前,必须把显存和算力指标想清楚。我们的卡是32GB显存,目标是一个环境分配6GB到8GB显存,算力上限30%左右。为什么是这个数?这些都是根据实际负载模型算出来的。

30个开发环境如果每环境8GB显存,那么总显存需求是240GB。8张卡每张32GB,物理显存总量是256GB,剩余16GB作为系统预留和突发缓冲。地方刚好够。算力方面,如果每个环境限制30%,一张卡最多同时跑3个环境,8张卡就是24个并发位。但我们实际统计过,同时在线开发并真正执行GPU计算的环境通常不到一半,所以8张卡跑30个环境是够的,只是高峰期需要调度器排队。

资源维度物理总量单环境配额可同时承载环境数
显存8卡 × 32GB = 256GB6GB ~ 8GB32 ~ 42
算力8卡 × 100%20% ~ 30%27 ~ 40

这种配额设计给调度器留了弹性。开发环境不一定是所有Pod同时算,因此我们敢用接近满配的分配方案。

2.3 调度链路:从Pod申请到真正拿到vGPU

HAMi的调度能不能生效,关键看四件事配合:CRD资源定义、调度器扩展程序、设备插件、运行时hook。

当一个开发环境Pod被创建时,它会在资源请求里声明自己需要多少显存和算力(具体写在annotation里)。K8s调度器根据节点上的扩展资源数量,筛选出可用的节点。节点上的HAMi设备插件负责把物理卡情况登记给K8s。选完节点之后,调度器扩展程序再决定这个Pod应该绑定到哪一张物理卡的哪个切片上。

真正启动容器时,HAMi的运行时hook会注入一个CUDA库拦截层。应用发起的显存申请和计算API调用都经过这个拦截层,由它来执行显存隔离和算力限制。对用户来说,在容器里执行nvidia-smi看到的是一块“变小”的卡,显存显示为8GB,但实际上数据仍然落在物理显卡上。

2.4 兼容矩阵:为什么国产GPU要特别关注适配版本

这是我们在落地过程中最头疼的一块。国产GPU不像NVIDIA那样有统一的CUDA生态,不同厂商的驱动接口、设备枚举方式、流管理模型差异很大。HAMi确实适配了不少国产卡,但适配是分版本、分驱动版本、分卡型的。

比如我们最初拿到的卡,驱动版本比较老,HAMi的某个版本在识别卡型时会报“unsupported device”。后来升级驱动、换新版本HAMi,问题才消失。所以建议落地前一定要先对照HAMi官方的兼容矩阵表格,确认三件事:第一,卡的型号在不在支持列表里;第二,驱动最低版本是否满足;第三,容器运行时(Docker或containerd)版本是否匹配。这一套提前检查下来,后面会少走很多弯路。

3. 落地实操:8张卡部署30个开发环境全过程

3.1 环境规划与资源预算

先说我们的集群配置,给大家一个参照系。Kubernetes版本用的是1.26,容器运行时是containerd,每个GPU节点物理机上装了8张国产卡。节点内存256GB,CPU是64核。节点系统是CentOS 7系,内核版本5.4。

规划阶段我们做了三件事。第一,给GPU节点打上独立标签,比如gpu-node=true,方便调度。第二,规划显存池,把8张卡按4组划分,每两张卡一组,每组服务7到8个开发环境。第三,给开发环境设计了一个标准化资源模板:每个环境固定申请6GB显存、30%算力、4核CPU、16GB内存,保证容器本身跑得动VS Code Server、Python进程和调试工具。

提示:开发环境的资源模板不要死板。有的同事跑数据预处理,显存需求低但CPU需求高;有的同事做模型量化,显存需求高但算力需求低。标准化模板用于默认场景,同时允许用户提交时覆盖。

3.2 安装部署HAMi的完整步骤

Step 1:下载HAMi部署包。我们从GitHub上拉取了与显卡型号适配的release版,解压之后里面有helm chart和一堆CRD定义文件。如果网络环境受限,可以把整个release目录打到内网镜像仓,再pull下去。

Step 2:配置device plugin。在HAMi的values.yaml里,需要显式指定设备插件对应的GPU类型。国产卡的配置名称和NVIDIA不一样,要按官方文档映射。我们这里卡型的配置代码大致是这个样子:

devicePlugin: # 这里填写国产卡对应的设备代号 devices: - name: ascend # 示例:如果我们用的是昇腾卡 memoryUnit: MiB memoryLimit: 32768

具体卡型的代号需要根据实际型号去查。这里我最想强调的教训是:如果写错设备代号,设备插件通常不会报错,但Pod调度上去后会发现GPU资源为0。

Step 3:配置调度器扩展程序。HAMi通过调度器扩展的方式参与调度决策,我们需要修改K8s的scheduler-config.yaml,把HAMi的HTTP扩展程序挂到调度器链路上。文件看起来像这样:

apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: hami-scheduler pluginConfig: - name: HAMIPlugin args: endpoint: "hami-scheduler:443"

配置完之后重启kube-scheduler组件,让配置生效。

Step 4:安装CRD。HAMi用CRD来表示可切分的GPU设备和切片状态。执行kubectl apply -f config/crd/bases,把CRD定义装进去,然后常规的helm install安装HAMi本体。装完之后用kubectl get pods -n hami-system查看组件状态,正常情况下会有三个核心Pod:scheduler、device-plugin、admission-webhook(如果启用了)。

Step 5:验证节点资源。这是最容易忽略的一步。用kubectl describe node <node-name>查看节点上报的资源。如果看到类似hami.io/gpu-memhami.io/gpu-cores的条目,并且数值和预期一致,说明设备插件已经正常注册。此时节点上的显存总量应该等于32GB × 8,算力总量是800%(100% × 8),因为切片粒度是百分比。

3.3 给开发环境绑定vGPU的YAML示例

资源模板的核心是annotation加上资源申请。我们实际用的开发环境Deployment YAML比较长,这里给出最关键的部分:

apiVersion: v1 kind: Pod metadata: name: dev-env-user01 annotations: # 显存请求,单位是MiB hami.io/gpu-mem: "8192" # 算力请求,单位是百分比 hami.io/gpu-cores: "30" spec: containers: - name: dev-env image: registry.internal/dev-env:py3.10-torch2.1 resources: requests: # 这个资源名表示需要一张vGPU hami.io/vgpu: 1 limits: hami.io/vgpu: 1 command: ["/start.sh"]

看到这里可能有朋友会问,hami.io/vgpu: 1到底代表什么?它表示“这个Pod需要一张相当于物理卡的逻辑卡资源”,但具体这一张逻辑卡是整卡的几分之一,完全由annotation里的显存和算力决定。调度器拿到这个请求后,去寻找能满足显存8GB+算力30%切片的节点。

3.4 验证开发环境是否真的隔离

部署完成不代表大功告成,必须验证隔离是否真正生效。我们在每个开发环境容器里执行了检查命令,确认看到的显存大小是8GB而非32GB。这时候容器内的nvidia-smi输出会显示一个虚拟设备,显存总量8GB,而物理卡上的其他显存不可见。

另外,我们还做了三并发压力测试。三个环境同时跑一个简单的矩阵乘法程序,把各自的计算负载拉满,观察整卡温度和其他环境的表现。在30%算力限制下,三个环境各自完成训练一个batch的时间测试下来满足预期,没有出现某个环境把整卡资源耗尽的情况。

4. 常见问题与排查技巧实录

4.1 “Pod停留在Pending,一直调度不上去”

这是大家最常遇到的情况。我排查了几次,原因基本集中在这几个点:第一,节点上的CRD没有装好,调度器不认识新增的资源类型;第二,设备插件没有新增扩展资源上报,节点上根本没有hami.io/gpu-mem这类资源可分配;第三,annotation的key写错了。

排查技巧很简单,先看kubectl describe node确认资源,再看kubectl get crd | grep hami确认CRD存在。都正常的话,用kubectl logs查看Device Plugin的日志,里面的报错信息会直接指向原因。按我的经验,因为key拼写错误导致Pod一直Pending的案例,占了至少三成。

4.2 容器内看到的显存不对,比申请的大

有同事反映,明明申请了8GB显存,容器里看到的却是32GB。这种情况多半是HAMi的运行时hook没有注入成功。容器启动时,HAMi通过admission webhook修改Pod配置,把CUDA库的拦截层挂进去。如果webhook没生效或者配置里关掉了自动注入,容器就直接穿透到了物理卡上。

检查kubectl get pods -n hami-system看有没有webhook组件。如果没启用,需要在HAMi配置里打开admission webhook,然后重新安装。另外还有一种情况是容器里的程序自己load了绝对路径的CUDA动态库,绕过了拦截层,这个属于应用侧的问题,需要调整镜像里的CUDA库加载路径。

4.3 开发环境偶发OOM/超显存,如何定位哪个任务在消耗显存

HAMi把一张物理卡切成多块之后,显存是隔离了,但整卡的监控指标还是以物理卡为维度。一旦某张卡显存接近打满,想去查到底是哪个Pod干的,光靠nvidia-smi这类工具会看得一头雾水。

我们当时的做法是配合K8s的metrics-server和HAMi自带的监控接口。通过节点上的HAMi exporter,拉取每个vGPU的显存使用量,再做一次映射,把显存使用量对应到Pod名称,就能定位到具体的环境。实际生产中,“某个环境里跑了内存泄漏的程序导致整卡爆掉”的事故出现过好几次,所以这个监控Mapping一定要提前做。

4.4 算力限制不生效,问题反而出在驱动版本

有一次我们发现,某个环境申请了30%算力,但实际跑满之后其他环境卡顿严重,几乎等于算力限制失效。排查过程绕了好几圈,最后发现是物理卡驱动版本太老,导致HAMi在设置计算流优先级时失败,静默降级成了“不限制”。

具体到排查动作,是去HAMi组件的日志里搜“set compute flow failed”这类关键词。如果确认是这个原因,大概率需要升级厂商驱动到HMMi兼容版本,或者调整驱动参数里的资源控制开关。这个坑比较隐蔽,而且容易被人误认为是硬件性能问题,我建议在自己的环境里提前验证驱动版本。

4.5 资源碎片化:环境用完不释放导致调度越来越紧张

30个环境如果长期挂在那里,哪怕没人用,显存和算力配额也一直被占着。一开始我们不以为意,结果跑了两个星期发现新环境调度越来越困难。查了一下,发现有一大半环境是“僵尸”状态——VS Code Server还活着,但真正在跑GPU任务的一个都没有。

后来我们做了两件事:一是给开发环境加空闲自动回收策略,超过一定时间没有活跃连接就自动缩容;二是让每个环境分配的默认显存从8GB调整到6GB,释放出一部分缓冲位。碎片化问题立刻得到缓解。

5. 性能影响与容量规划:把8张卡的稳态吃透

5.1 切分带来的性能损耗到底有多少

很多人一听到虚拟化就觉得“性能一定损失很大”。HAMi在国产卡上的实际性能损耗,取决于切分深度和运行模式。显存隔离仅仅是在内存管理上做拦截,性能损耗极小,可以忽略。算力切分如果只是限制计算流数量,不做并发MPI这类操作,性能损耗也就在5%到10%之间。

如果开启更细粒度的显存分页和重映射,损耗会上升。我们的建议是:开发环境对延迟不敏感,可以开最细粒度的切分;但如果是正式的模型训练任务,尽量避免把单个任务切成并发度太高的细片,那样反而会带来额外的通信和上下文切换开销。

5.2 偷师“超卖”思路:开发环境可以比物理上限多10%

30个环境对应8张卡,这个配比本身就是一种“超卖”。在实际稳定运行后,我们发现开发环境的平均GPU占用率远低于配额上限,于是尝试把总环境数提升到33个,相当于多出10%的弹性。这10%的超卖量没有影响体验,因为同时有GPU负载的环境很少超过25个。

但超卖必须设一个安全线,不能无限制放大。我们内部的经验是:对于JupyterLab、VS Code Server这类交互式环境,超卖比例控制在20%以内,且必须配合空闲回收机制。超过这个比例,一旦出现“全员跑训练”的极端情况,整个集群的排队和卡顿会非常明显。

5.3 从8张卡到更大规模的扩容准备

这套方案验证完毕之后,团队已经在规划把它复制到更大规模的GPU资源池。8张卡只是一个起点,HAMi支持跨节点的全局调度,K8s集群上接的GPU节点越多,池化效果越明显。

扩容前要做两件事:一是把GPU节点的标签和污点设计好,让开发环境Pod和训练任务Pod调度到不同的资源池;二是做好显存配额审计,记录每个历史环境的实际资源用量,用数据来指导新环境的配额设置。如果这两件事没做,扩到几十张卡时只会把碎片化问题放大,排查成本成倍上升。

6. 后续演进:从“能用”到“好用”的三个优化方向

6.1 平台化:给用户提供自助式环境创建

HAMi本身解决的是GPU切分和调度问题,但用户侧的体验还需要一层平台来承接。我们把环境变成了模板化的自助服务,用户在内部平台上选择“PyTorch开发环境”“TensorFlow调试环境”“CUDA编译环境”等模板,系统自动生成Pod并绑定vGPU,无需手工写YAML。

这样做的好处不只是省事。模板里可以预置好镜像、启动脚本、依赖包版本,杜绝了“每个环境都不一样”的运维噩梦。用户拿到环境后可以直接干活,环境生命周期也由平台统一管理。

6.2 监控与告警要补上GPU Pod维度

K8s自带的监控体系对CPU和内存很成熟,但GPU这块必须额外补。我们在每个节点上部署了GPU监控exporter,把vGPU的显存使用量、算力利用率、温度等指标上报给Prometheus,再在Grafana里做Dashboard。

告警项我建议至少覆盖四类:vGPU显存使用率超过90%、算力配额打满持续超过10分钟、节点GPU温度过高、物理卡总显存低于某个阈值。这些告警能帮助我们在用户反馈之前就发现问题。

6.3 多卡调度策略:开发环境之外的算力复用

最后的演进方向,是把这套方案从开发环境延伸到正式训练和推理场景。训练任务的资源特征和开发环境差别很大,显存需求大、算力需要整卡或整卡MIG切分,对性能损耗敏感。我们目前正在测试把HAMi的显存切分和训练框架(比如deepmd-kit这类在GPU上部署的应用程序)结合,让不同的任务形态共享同一个GPU资源池。

我个人的建议是:如果条件允许,把资源池按“开发”“训练”“推理”三个QoS等级分层,开发池可以超卖,训练池不超卖、尽量整卡,推理池按延迟要求单独优化。三个池子底层共用HAMi,但配额策略不同,这样既保证了资源利用率,又避免了极端情况下的互相干扰。

回头再看这个项目,我觉得最有价值的不是“8张卡跑30个环境”这个数字本身,而是把“每一块国产GPU的利用率都榨干”的思路验证通了。国产卡这几年性能进步明显,但底层生态和虚拟化能力一直是短板,HAMi恰好补齐了这个缺口。真正落地之后,团队里再也没有人抱怨“申请不到卡”,管理员也不用再守着每张卡手动分配。如果你也在折腾GPU资源池化或者开发环境交付,建议先拿一块卡小范围试一下HAMi,你会发现,很多看似“不够用”的资源,其实只是缺一个称职的拆分配调机制。

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

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

立即咨询