最近在做一个云原生AI推理平台的项目,需要把多个AI模型部署到Kubernetes集群上,提供高并发、低延迟的推理服务。

刚开始的时候,服务的性能很差:推理延迟高、吞吐量低、GPU利用率上不去、高峰期经常OOM。经过一系列的优化,最终把延迟降低了60%,吞吐量提升了3倍,GPU利用率从30%提升到了80%以上。

这篇文章就来分享一下云原生AI性能优化的实战经验,从资源调度、模型优化、服务架构、自动扩缩容四个维度,聊聊怎么把一个慢的AI推理服务一步步优化成快的。

背景和问题

先简单说一下项目背景。

我们的平台需要部署多个AI模型,包括图像分类、目标检测、自然语言处理等。这些模型用PyTorch或TensorFlow训练,需要部署成在线推理服务,接收HTTP请求,返回推理结果。部署环境是Kubernetes集群,有CPU节点和GPU节点。

刚开始的时候,我们用最简单的方式部署:每个模型一个Deployment,用Docker镜像打包模型和推理代码,暴露一个HTTP接口,用Nginx做负载均衡。

上线之后,问题很快就来了。

第一个问题是延迟高。一个简单的图像分类请求,端到端延迟要500毫秒以上,其中模型推理只需要50毫秒,剩下的时间都花在了请求排队、数据传输、模型加载上。

第二个问题是吞吐量低。单实例的QPS只能到10左右,远远达不到要求。增加副本数之后,QPS没有线性提升,因为GPU节点的资源有限,多个实例共享GPU,互相竞争。

第三个问题是GPU利用率低。GPU的平均利用率只有30%左右,大部分时间GPU在等待数据,而不是在计算。GPU很贵,利用率低就是在浪费钱。

第四个问题是扩缩容不及时。流量高峰的时候,Pod启动慢(因为模型大,加载需要时间),等新Pod起来的时候,高峰已经过去了,请求已经超时了。

第五个问题是资源浪费。不同模型的资源需求差异很大,有的模型需要大显存,有的模型需要高算力。我们统一给每个Pod分配相同的资源,导致有的模型资源不够用,有的模型资源用不完。

这些问题加在一起,导致服务的性能很差,成本很高。我们花了大概一个月的时间,从各个方面进行优化,最终解决了这些问题。下面就来详细说说优化的过程。

第一步:模型本身的优化

AI推理服务的性能,基础是模型本身的性能。如果模型本身很慢,再怎么优化架构也没用。所以第一步是优化模型。

第一个优化是模型量化。我们把FP32的模型量化成FP16或INT8。FP16在现代GPU上几乎没有精度损失,但推理速度能提升一倍左右,显存占用减少一半。INT8的速度更快、显存更少,但可能会有一定的精度损失,需要评估。我们大部分模型用了FP16,一些对精度要求不高的模型用了INT8。

量化之后,单模型的推理延迟从50毫秒降到了20毫秒左右,显存占用减少了一半,同样的GPU可以部署更多的实例。

第二个优化是模型编译优化。我们用了TensorRT和TorchScript来优化模型。TensorRT是NVIDIA推出的推理优化引擎,能对模型进行层融合、内核自动调优、精度校准等优化,推理速度比原生PyTorch快2到3倍。TorchScript是PyTorch自带的模型序列化格式,能把Python代码转换成静态图,减少Python解释器的开销。

我们把PyTorch模型导出成TorchScript,然后用TensorRT进行编译优化。优化之后,推理延迟又从20毫秒降到了8毫秒左右。

第三个优化是模型剪枝和蒸馏。对于一些特别大的模型,我们用了模型剪枝(去掉不重要的参数)和知识蒸馏(用大模型指导小模型训练)的方法,在精度损失不大的前提下,把模型体积和计算量减少了一半以上。

第四个优化是预处理和后处理的优化。很多人只关注模型推理的性能,忽略了预处理和后处理。比如图像的解码、缩放、归一化,结果的后处理(NMS、排序等),这些操作如果在CPU上做,可能比模型推理还慢。我们把一些预处理操作移到了GPU上,用CUDA加速,或者用更高效的库(比如OpenCV的GPU版本、DALI)。预处理的时间从100毫秒降到了10毫秒以内。

经过模型层面的优化,单请求的推理时间(包括预处理、推理、后处理)从160毫秒降到了30毫秒左右,为后续的架构优化打下了基础。

第二步:推理服务架构优化

模型优化之后,接下来是推理服务的架构优化。

第一个优化是用专用的推理服务器。我们一开始用的是Flask+Gunicorn这种通用的Web框架来做推理服务,但这种框架不适合AI推理。它是同步阻塞的,一个请求在推理的时候,worker就被占住了,不能处理其他请求。而且Python的GIL限制了并发。

我们换成了Triton Inference Server(NVIDIA推出的开源推理服务器)。Triton专门为AI推理设计,支持多种模型格式(PyTorch、TensorFlow、ONNX、TensorRT等),支持动态批处理、模型并发、多GPU管理、模型热更新等功能。用了Triton之后,推理服务的吞吐量提升了2倍以上,延迟也更稳定了。

Triton最有用的功能是动态批处理(dynamic batching)。它会把一段时间内的多个请求攒成一个batch,一起送给模型推理。因为GPU处理大batch的效率比小batch高很多,动态批处理能在增加少量延迟的情况下,大幅提升吞吐量。我们设置了一个合理的批处理延迟(比如10毫秒),在这个时间内到达的请求会被合并成一个batch处理。

第二个优化是请求队列和限流。在高并发的时候,如果所有请求都直接打到推理服务,会导致服务过载,延迟急剧上升,甚至OOM。我们在推理服务前面加了一个请求队列和限流层,控制同时处理的请求数,超过的请求在队列里等待。这样虽然会增加少量排队延迟,但能保证服务的稳定性,避免雪崩。

第三个优化是模型的多实例和GPU共享。一个GPU上可以同时运行多个模型实例,只要显存够。我们根据每个模型的显存占用和计算需求,在一个GPU上部署多个实例,提高GPU的利用率。同时,用NVIDIA MPS(Multi-Process Service)或者MIG(Multi-Instance GPU)技术,让多个进程安全地共享GPU,避免互相干扰。

第四个优化是gRPC替代HTTP。HTTP/1.1的开销比较大,特别是对于小请求。我们把推理服务的接口从HTTP改成了gRPC,用protobuf序列化,减少了网络传输的开销,延迟又降低了一些。对于需要传输大文件(比如图片、视频)的场景,gRPC的优势更明显。

第三步:Kubernetes资源调度优化

架构优化之后,接下来是Kubernetes层面的资源调度优化。

第一个优化是GPU资源的合理分配。Kubernetes对GPU的调度是按"卡"来的,一个Pod要么用一整张卡,要么不用。但很多AI模型不需要一整张卡的算力,这样会造成浪费。

我们用了两种方案来解决这个问题。一种是用NVIDIA MIG技术,把一张物理GPU切分成多个逻辑GPU,每个逻辑GPU有独立的显存和计算单元,可以分配给不同的Pod。这样一张卡可以同时服务多个模型,提高了利用率。

另一种方案是在一个Pod里运行多个模型实例(前面提到的多实例),共享一张卡。这种方式不需要MIG支持,更灵活,但需要自己管理实例之间的资源隔离。

第二个优化是节点亲和性和污点容忍。我们把GPU节点打上特定的标签和污点,只有AI推理的Pod才能调度到GPU节点上,避免其他Pod占用GPU资源。同时,根据模型的GPU型号需求(比如有的模型需要A100,有的模型T4就够了),用节点亲和性把Pod调度到合适的节点上。

第三个优化是资源请求和限制的合理设置。每个模型的CPU、内存、GPU需求都不一样,我们根据实际测试的结果,给每个Pod设置合理的resources.requests和resources.limits。requests用于调度,保证Pod能调度到有足够资源的节点上;limits用于限制,防止单个Pod占用过多资源影响其他Pod。

设置合理的资源请求和限制之后,集群的资源利用率提升了很多,不会出现有的节点过载、有的节点空闲的情况。

第四个优化是Pod的优先级和抢占。对于重要的、延迟敏感的模型,我们设置了高优先级;对于不重要的、可以批量处理的模型,设置了低优先级。当集群资源不足的时候,Kubernetes会优先调度高优先级的Pod,必要时抢占低优先级的Pod。这样能保证核心服务的稳定性。

第四步:自动扩缩容优化

AI推理服务的流量波动很大,有时候很闲,有时候突然来一波高峰。自动扩缩容是应对流量波动的关键。

Kubernetes默认的HPA(Horizontal Pod Autoscaler)是基于CPU或内存利用率来扩缩容的,但对于AI推理服务来说,CPU和内存不是瓶颈,GPU利用率和请求延迟才是关键指标。所以我们用了自定义指标的HPA,基于GPU利用率和请求的P95延迟来扩缩容。

比如,当GPU利用率超过70%,或者P95延迟超过100毫秒的时候,自动增加副本数;当GPU利用率低于30%,且P95延迟低于50毫秒的时候,自动减少副本数。

但默认的HPA有一个问题:扩容太慢。AI推理的Pod启动需要加载模型,大模型的加载时间可能要几十秒甚至几分钟。等新Pod启动完成,流量高峰可能已经过去了。

为了解决这个问题,我们做了几个优化。

第一个是预热和镜像优化。把模型打包到Docker镜像里,而不是启动的时候再下载。镜像尽量小,用多阶段构建,去掉不必要的依赖。这样Pod的启动时间主要就是模型加载的时间,而不是镜像拉取的时间。

第二个是预留缓冲副本。对于核心服务,我们保持一定数量的"热"副本,即使在低峰期也不缩到零。这样当流量突然增加的时候,有现成的Pod可以处理请求,不会因为启动慢而导致请求超时。

第三个是预测式扩缩容。我们分析了历史流量数据,发现流量有明显的时间规律(比如白天高、晚上低,工作日高、周末低)。基于这个规律,我们用了定时扩缩容(CronHPA),在高峰到来之前提前扩容,在高峰过去之后再缩容。这样比反应式扩缩容更及时。

第四个是基于队列长度的扩缩容。除了GPU利用率和延迟,我们还把请求队列的长度作为扩缩容的指标。当队列里等待的请求超过阈值的时候,说明当前的处理能力不够了,需要扩容。这个指标比GPU利用率更灵敏,能更早地发现容量不足。

第五步:监控和持续优化

性能优化不是一次性的工作,需要持续的监控和迭代。

我们建立了一套完善的监控体系,覆盖了从基础设施到应用性能的各个层面。

基础设施层面:监控GPU的利用率、显存使用、温度、功耗,CPU和内存的使用,网络的带宽和延迟。用DCGM(NVIDIA Data Center GPU Manager)来采集GPU的指标,用Prometheus+Grafana来展示和告警。

应用层面:监控每个模型的QPS、延迟(P50/P95/P99)、错误率、排队时间、推理时间。用Triton自带的metrics接口,加上自定义的业务指标,全面了解推理服务的性能。

业务层面:监控每个API的调用量、成功率、用户反馈。把技术指标和业务指标关联起来,知道性能问题对业务的影响。

有了监控数据之后,我们定期做性能分析,找出瓶颈,针对性地优化。比如发现某个模型的GPU利用率低,就优化它的批处理策略;发现某个接口的延迟高,就分析是预处理慢还是推理慢;发现某个时间段经常超时,就调整扩缩容策略。

我们还建立了性能基准测试的流程。每次模型更新、代码变更、配置调整之后,都跑一遍性能测试,和基准做对比,确保优化有效,没有引入性能回退。

优化成果

经过这一系列的优化,我们的云原生AI推理平台的性能有了显著提升。

延迟方面:单请求的端到端延迟从500毫秒降到了80毫秒左右,P95延迟从1秒降到了150毫秒。

吞吐量方面:单GPU节点的吞吐量从每秒10个请求提升到了每秒50个请求,提升了5倍。

资源利用率方面:GPU的平均利用率从30%提升到了80%以上,CPU和内存的利用率也提升到了60%以上。

成本方面:因为利用率提升了,同样的流量需要的GPU节点减少了一半,基础设施成本降低了40%。

稳定性方面:有了完善的监控、限流、扩缩容机制,服务的可用性达到了99.9%,高峰期也很少出现超时和错误。

这些成果不是一蹴而就的,而是经过了反复的测试、分析、优化、验证得来的。性能优化是一个系统性的工作,需要从模型、架构、调度、扩缩容、监控等多个方面入手,每一个环节都可能成为瓶颈。

一些经验和教训

最后,分享一些在优化过程中总结的经验和教训。

第一,先测量,再优化。不要凭感觉去优化,要用数据说话。先建立监控和基准测试,找出真正的瓶颈,再针对性地优化。很多时候,你以为的瓶颈并不是真正的瓶颈。

第二,模型优化是基础。模型本身的性能决定了推理服务的上限。如果模型很慢,再怎么优化架构和调度也没用。先把模型优化好,再做架构层面的优化。

第三,不要过早优化。在项目初期,先让服务跑起来,满足基本的功能需求,再逐步优化性能。不要一开始就追求极致的性能,那样会拖慢开发进度。等服务上线了,有了真实的流量和数据,再做针对性的优化。

第四,注意优化的成本。有些优化能带来很大的性能提升,但实现成本很高(比如自定义CUDA内核)。要权衡优化的收益和成本,优先做那些投入产出比高的优化。

第五,稳定性比极致性能更重要。一个稳定的、延迟可预测的服务,比一个偶尔很快但经常超时的服务要好。在优化性能的时候,不要牺牲了稳定性。限流、降级、熔断这些机制,和性能优化一样重要。

第六,关注成本。性能优化不只是为了快,也是为了省。GPU很贵,提高利用率就是在省钱。在优化的时候,要同时考虑性能和成本,找到性价比最高的方案。

写在最后

云原生AI性能优化是一个很有挑战性但也很有价值的工作。当你看到一个原本很慢、很耗资源的服务,经过你的优化,变得又快又省,那种成就感是很难用语言形容的。

这篇文章分享的是我个人的实战经验,不同的模型、不同的集群、不同的业务场景,优化的方法可能会有所不同。但核心思路是一样的:测量瓶颈、针对性优化、持续迭代。

随着AI应用的普及,云原生AI推理会越来越重要,性能优化也会成为一个越来越关键的技能。希望这篇文章能给正在做或者将要做AI推理服务优化的朋友一些参考。

如果你也有云原生AI性能优化的经验或者问题,欢迎交流讨论。