最近半年,我们团队一直在做云原生AI相关的工作,把大模型应用部署到Kubernetes集群上,用云原生的方式来管理和运维AI工作负载。
这半年踩了不少坑,也积累了一些实战经验。这篇文章就来总结一下,从资源调度、GPU管理、模型加载、网络存储到监控运维,聊聊我们遇到的问题和解决方案。
如果你也在做云原生AI,或者打算把AI应用迁移到K8s上,希望这篇文章能帮你少走一些弯路。
什么是云原生AI
先简单说说什么是云原生AI。
云原生AI,简单来说,就是用云原生的技术和理念(容器、Kubernetes、微服务、DevOps)来构建、部署和运行AI应用。它的核心是把AI工作负载(模型训练、模型推理、数据处理)容器化,用K8s来编排和管理。
云原生AI的好处是:弹性伸缩、资源利用率高、环境一致性好、运维自动化。特别是对于大模型应用来说,GPU资源很贵,用K8s来调度和共享GPU,能显著降低成本。
但云原生AI也有很多挑战。AI工作负载和传统的Web应用不一样,它对GPU、内存、存储、网络都有特殊要求。直接把传统的云原生方案套用到AI上,会遇到很多问题。
下面就来聊聊我们踩过的那些坑。
坑一:GPU资源调度
第一个大坑是GPU资源调度。
最开始,我们直接用K8s的nvidia-device-plugin来暴露GPU资源,然后在Pod的requests里声明nvidia.com/gpu: 1。这样确实能调度GPU,但问题很多。
第一个问题是GPU粒度太粗。一个Pod要么占一整张卡,要么不占。但很多推理服务不需要一整张卡,比如一个7B模型的推理,可能只需要20%的GPU显存。如果给它一整张卡,就浪费了80%的资源。
第二个问题是GPU共享。多个Pod想共享一张GPU,原生的K8s不支持。我们试过用时间片共享(MPS),但隔离性不好,一个Pod出问题会影响其他Pod。也试过用vGPU方案(比如NVIDIA vGPU、Aliyun GPU共享),但配置复杂,兼容性也有问题。
第三个问题是GPU型号混杂。集群里有不同型号的GPU(A100、A10、T4),不同型号的算力和显存不一样。如果调度的时候不区分,把需要大显存的任务调度到小显存的卡上,就会OOM。
我们的解决方案是:
- 用K8s的节点标签(node label)区分GPU型号,调度的时候用nodeSelector指定型号
- 引入GPU共享方案,我们最终选了NVIDIA的MIG(多实例GPU),把A100切成多个实例,每个实例有独立的显存和计算单元
- 对于小模型推理,用CPU推理或者轻量化模型,避免占用GPU
- 建立GPU资源监控,实时查看每张卡的利用率,及时调整调度策略
踩了这个坑之后,我们意识到:GPU调度不是简单地声明一下资源就行,需要根据业务场景选择合适的共享和隔离方案。
坑二:模型加载慢
第二个坑是模型加载慢。
大模型的文件很大,7B模型有十几GB,70B模型有上百GB。每次Pod启动的时候,都要从存储里加载模型到GPU显存,这个过程很慢,7B模型要几十秒,70B模型要几分钟。
对于在线推理服务来说,启动慢意味着冷启动时间长,弹性伸缩的时候,新实例不能马上处理请求,会导致请求堆积。
我们试过几种方案:
第一种是把模型文件放在镜像里。这样Pod启动的时候不需要从外部拉模型,但镜像会很大(几十GB),镜像拉取也很慢,而且每次更新模型都要重新构建镜像,不灵活。
第二种是用PVC(持久化卷)存模型。模型文件存在共享存储里,Pod启动的时候挂载PVC,然后从PVC加载模型。这样镜像小了,但从共享存储读大文件还是慢,特别是多个Pod同时加载的时候,存储IO会成为瓶颈。
第三种是用模型缓存。在节点上缓存模型文件,Pod启动的时候先检查本地有没有模型,如果有就直接从本地加载,没有再从共享存储拉。这样第二次启动就快了。但缓存管理比较复杂,需要考虑缓存淘汰、磁盘空间、模型版本等问题。
第四种是用模型预热。在节点上提前运行一个预热Pod,把模型加载到显存里,然后推理服务Pod复用这个显存。这个方案效果最好,但实现复杂,需要定制化开发。
我们最终的方案是:用PVC存模型 + 节点本地缓存 + 模型预热。大部分场景下,Pod启动时间控制在30秒以内,基本能满足弹性伸缩的需求。
坑三:显存碎片化和OOM
第三个坑是显存碎片化和OOM。
大模型推理的时候,GPU显存的使用很复杂。模型本身要占显存,KV缓存要占显存,中间计算结果也要占显存。如果显存管理不好,很容易出现OOM(内存不足)。
我们遇到的问题:
第一个是并发请求导致的OOM。推理服务支持并发处理多个请求,每个请求都要占用KV缓存。并发高的时候,KV缓存占用的显存会快速增加,导致OOM。我们的解决方案是限制最大并发数,根据模型大小和GPU显存,计算出能支持的最大并发数,超过的请求排队等待。
第二个是长文本导致的OOM。长文本的KV缓存很大,特别是上下文窗口很长的时候(比如32K、128K),一个长文本请求就能占满显存。我们的解决方案是限制最大输入长度,或者对长文本做截断和摘要处理。
第三个是显存碎片化。多次分配和释放显存之后,会产生显存碎片,虽然总显存够,但没有连续的大块显存,导致分配失败。我们的解决方案是用显存池(memory pool),预分配一大块显存,然后在池子里管理分配,避免碎片化。
第四个是多模型混布导致的OOM。一张GPU上跑多个模型,每个模型的显存使用是动态的,加起来可能超过总显存。我们的解决方案是严格限制每个模型的显存上限,用MIG或者vGPU做硬隔离,避免一个模型把显存占满影响其他模型。
踩了这些坑之后,我们总结出一条经验:大模型推理的显存管理,一定要做容量规划和限流,不能让服务无限度地使用显存。
坑四:网络和存储
第四个坑是网络和存储。
AI工作负载对网络和存储的要求,比传统Web应用高很多。
先说网络。大模型训练的时候,多机多卡之间需要大量的通信(比如AllReduce),对网络带宽和延迟很敏感。我们最开始用的是普通的万兆以太网,训练的时候通信瓶颈很明显,GPU利用率上不去。后来换成了RDMA网络(InfiniBand或者RoCE),训练速度提升了很多。
对于推理服务来说,网络要求没那么高,但如果用了模型并行(把一个大模型拆到多张卡上),卡之间的通信也很频繁,需要高速网络。
再说存储。大模型的文件很大,训练过程中还会产生大量的checkpoint和日志,对存储的容量和带宽要求很高。我们最开始用的是普通的NFS,读写大文件的时候很慢,而且多Pod并发读写的时候性能下降严重。
后来我们换成了并行文件系统(比如Lustre、BeeGFS),或者对象存储(比如S3)+ 本地缓存。并行文件系统的并发读写性能好,适合训练场景;对象存储成本低,适合长期保存模型和checkpoint。
还有一个问题是数据预处理。AI训练之前,需要对大量数据做预处理(图片解码、文本tokenize等),这个过程很耗CPU和IO。如果数据预处理和训练在同一个节点上,会争抢资源。我们的解决方案是把数据预处理做成独立的Job,提前处理好数据,存在高性能存储里,训练的时候直接用。
坑五:监控和可观测性
第五个坑是监控和可观测性。
传统的K8s监控(Prometheus + Grafana)主要监控CPU、内存、网络这些指标,但对于AI工作负载来说,这些远远不够。我们还需要监控:
- GPU利用率、显存使用率、GPU温度、功耗
- 推理延迟(首token延迟、每个token延迟)、吞吐量(tokens/s)
- 模型加载时间、缓存命中率
- 请求队列长度、超时率、错误率
- 训练的loss、学习率、梯度范数
最开始,我们用的是dcgm-exporter来监控GPU指标,用业务自定义指标来监控推理性能。但这些指标分散在不同的地方,排查问题的时候要切来切去,很不方便。
后来我们做了统一的监控大盘,把基础设施指标(CPU、内存、GPU)和业务指标(延迟、吞吐量、错误率)放在同一个大盘上,还做了告警。比如GPU利用率持续低于30%就告警(可能资源浪费),推理延迟P99超过阈值就告警(可能性能有问题),显存使用率超过90%就告警(可能快OOM了)。
还有日志。AI应用的日志很特殊,有训练日志、推理日志、框架日志(PyTorch、TensorRT),格式不一样,量也很大。我们用ELK栈收集日志,做了结构化处理,方便搜索和分析。
链路追踪也很重要。一个推理请求可能经过网关、推理服务、模型服务多个环节,出了问题需要快速定位是哪个环节慢了。我们用OpenTelemetry做链路追踪,把每个环节的耗时都记录下来。
坑六:版本管理和发布
第六个坑是版本管理和发布。
AI应用的版本管理比传统应用复杂,因为不仅有代码版本,还有模型版本。同一个代码,用不同的模型,效果可能完全不一样。模型更新的频率也很高,有时候一周要更新好几次。
我们遇到的问题:
第一个是模型和代码的版本对应关系混乱。代码更新了,模型没更新;或者模型更新了,代码没更新,导致服务出问题。我们的解决方案是在镜像里同时记录代码版本和模型版本,用统一的版本号管理,发布的时候确保代码和模型匹配。
第二个是灰度发布。传统应用的灰度发布很成熟,但AI应用的灰度发布更复杂,因为需要对比不同模型的效果。我们的解决方案是用流量镜像(traffic mirroring),把一部分请求同时发给旧模型和新模型,对比输出结果和性能,确认新模型没问题之后再全量发布。
第三个是回滚。AI应用出了问题,回滚不仅要回滚代码,还要回滚模型。我们的解决方案是保留最近几个版本的模型文件在存储里,回滚的时候快速切换模型版本。
第四个是A/B测试。不同模型的效果对比,需要做A/B测试。我们用服务网格(Istio)做流量切分,把不同比例的流量发给不同版本的模型,然后收集业务指标(用户满意度、任务完成率)来评估模型效果。
坑七:成本控制
第七个坑是成本控制。
GPU很贵,云原生AI的最大成本就是GPU。如果GPU利用率低,成本就会很高。
我们最开始的GPU利用率只有30%左右,大部分时间GPU都在空转。原因是:
- 推理服务的流量有波峰波谷,波谷的时候GPU闲置
- 每个服务独占一张GPU,即使只用了20%的显存
- 训练任务是离线的,跑完就释放,但调度不及时,GPU空等
我们做了这些优化:
第一,GPU共享。用MIG或者vGPU把一张GPU切成多个实例,让多个小模型共享一张卡,把GPU利用率提升到60-70%。
第二,弹性伸缩。根据流量自动扩缩容,波谷的时候减少实例数量,释放GPU资源。用K8s的HPA(水平Pod自动伸缩),基于GPU利用率或者请求队列长度来伸缩。
第三,混合调度。把在线推理服务和离线训练任务混合调度在同一个集群里,在线服务用稳定的GPU资源,训练任务用空闲的GPU资源,提高整体利用率。
第四, spot实例。对于容错性好的训练任务,用云厂商的竞价实例(spot instance),价格便宜很多。虽然可能被回收,但训练任务有checkpoint,可以恢复。
第五,模型优化。用模型量化、蒸馏、剪枝等技术,减小模型大小,降低推理的GPU需求。比如把FP16模型量化成INT8,显存占用减半,推理速度提升,精度损失很小。
通过这些优化,我们的GPU利用率从30%提升到了65%左右,成本降低了近一半。
一些实战经验
踩了这么多坑,我们也总结了一些实战经验。
第一,不要一开始就追求完美。云原生AI是一个复杂的系统,不要想着一步到位。先把核心流程跑通,再逐步优化。我们最开始就是用最简单的方案(独占GPU、PVC存模型、基础监控),跑通了之后再一步步优化。
第二,监控先行。在部署AI应用之前,先把监控体系建好。没有监控,出了问题都不知道哪里出了问题。特别是GPU监控和推理性能监控,一定要有。
第三,做好容量规划。GPU资源有限,要根据业务需求做好容量规划,包括需要多少张卡、什么型号、怎么分配。不要等GPU不够了才想办法,那时候业务已经受影响了。
第四,自动化。AI应用的运维很复杂,模型更新、扩缩容、故障恢复,都要尽量自动化。用K8s的Operator、CI/CD流水线、自动扩缩容,减少人工操作。
第五,关注社区。云原生AI是一个快速发展的领域,新的工具和方案层出不穷。关注Kubeflow、KServe、vLLM、TensorRT-LLM这些项目,很多问题社区已经有解决方案了,不要自己造轮子。
第六,团队协作。云原生AI需要平台团队(K8s、运维)和算法团队(模型、训练)紧密协作。平台团队要理解AI工作负载的特点,算法团队要了解云原生的最佳实践。两边多沟通,才能把系统做好。
写在最后
云原生AI是一个很有前景的方向,但也是一个充满挑战的领域。AI工作负载的特殊性,让传统的云原生方案不能直接套用,需要做很多定制化和优化。
这半年的踩坑经历,让我们对云原生AI有了更深入的理解。GPU调度、模型加载、显存管理、网络存储、监控运维、版本发布、成本控制,每一个环节都有很多学问。
但踩坑并不可怕,可怕的是踩了坑不总结。每一次踩坑,都是一次学习和成长的机会。把这些经验记录下来,分享出来,能让更多的人少走弯路。
如果你也在做云原生AI,欢迎交流。让我们一起,把AI应用做得更好、更稳、更省。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录