ARTICLE DETAIL

资讯详情

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

8张国产GPU承载30个开发环境:基于HAMi的vGPU切分与调度落地实践

8张国产GPU承载30个开发环境:基于HAMi的vGPU切分与调度落地实践 从去年下半年开始我所在的电科云团队接到不少类似的诉求开发环境不够用、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上有真实的显存大小比如32GBHAMi会在驱动层做一个“代理”或“伪装”让容器内的应用以为这张卡的显存上限只有8GB、6GB或者你指定的任何数值。应用申请显存的时候会被限定在这个额度之内超出就报OOM。这样多个Pod可以共享同一块物理显存互不越界。第二块是算力切分。显存隔离只是“分地盘”算力切分则是“限速度”。HAMi可以通过控制GPU上的计算流调度把每个Pod能使用的GPU计算核心比如SM或者国产卡上的计算单元数量限制在一个比例内。这个比例可以精确到1%级别我们用的时候一般都给到20%到40%保证多环境并发时不会互相拖垮。第三块是资源上报与调度。HAMi会让Kubernetes节点上的GPU资源变成类似hami.io/gpu-mem和hami.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申请到真正拿到vGPUHAMi的调度能不能生效关键看四件事配合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张国产卡。节点内存256GBCPU是64核。节点系统是CentOS 7系内核版本5.4。规划阶段我们做了三件事。第一给GPU节点打上独立标签比如gpu-nodetrue方便调度。第二规划显存池把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查看组件状态正常情况下会有三个核心Podscheduler、device-plugin、admission-webhook如果启用了。Step 5验证节点资源。这是最容易忽略的一步。用kubectl describe node node-name查看节点上报的资源。如果看到类似hami.io/gpu-mem和hami.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你会发现很多看似“不够用”的资源其实只是缺一个称职的拆分配调机制。
返回列表