尧图精选

AI系统性能工程实战:延迟、吞吐与压测全解析

🕒 发布时间:2026/10/1 19:36:07 📁 来源:尧图网络
做AI系统的人多少都经历过这样的时刻模型在实验环境里跑得飞快一上线就被用户投诉“转圈圈”。模型本身没问题卡的是整个系统。这就是“AI 系统性能工程”这门功课的由来。前两篇我聊了性能工程的基本概念和容量规划这一篇继续往深了走把实战中真正能落地的东西拿出来讲——包括指标怎么拆、瓶颈怎么定位、压测怎么做、以及那些踩过坑之后才明白的调优细节。如果你正在做大模型推理服务、AI 图像生成平台或者任何一个带模型调用的后端系统这篇文章应该能帮你少走不少弯路。性能工程不是一个独立的技术模块它是贯穿模型上线前后的一整套思考和动作。实验环境里一个请求慢慢算没人关心延迟线上环境里几十个并发同时打过来显存、GPU 利用率、排队时间全搅在一起问题就成片地冒出来。这篇文章适合算法工程师、后端开发、平台工程师也适合那些刚把模型部署到生产环境、正准备做性能优化的团队参考。1. 先搞清楚性能工程到底在解决什么问题1.1 从“能跑起来”到“跑得好”的转变很多团队把模型跑通当成项目结束的标志这是个很危险的错觉。我在实际项目里见过太多类似场景离线推理脚本跑一次要几秒钟大家觉得“还行”上线之后要求单次响应不超过 500 毫秒结果连最理想的单请求都压不进这个阈值更别提并发场景下的排队等待。性能工程的第一步不是急着调参而是先把“跑得好”这三个字定义清楚。没有定义标准就开始优化大概率会陷入东一榔头西一棒子的状态。举个例子一个文本生成服务如果目标是“在 50 QPS 的压力下P99 延迟不超 800 毫秒”那这就是一个可量化的 SLO。后面所有优化动作都要围绕这个目标去评估收益。性能工程要解决的是系统性效率问题不是某一个函数快不快。它关心的是整条链路的配合数据怎么进来、样本怎么预处理、模型怎么推理、结果怎么返回。任何一个环节拖后腿其他环节再快都会被拉平。所以做性能工程的人思维上得从“我优化了一段代码”切换到“我在优化一条流水线”。还有个容易忽略的点是性能的稳定性。吞吐和延迟的一次性达标不算本事持续稳定才算。线上流量的波动、显存碎片的累积、宿主机其他租户的干扰都会让性能曲线出现毛刺。性能工程要做的就是把系统的性能方差压下去让它在各种场景下都保持在一个可接受的范围里。1.2 性能工程的三大目标延迟、吞吐、成本任何 AI 系统的性能优化本质上都是在延迟、吞吐和成本三者之间找平衡点。这三个目标互相牵制理解它们的关系是入门性能工程的基础。延迟指的是单次请求从进入系统到拿到结果的总时间。统计上通常关注平均值和尾部延迟P95 和 P99 尤其重要。平均值被少数慢请求影响不大但 P99 直接反映系统最差体验的那部分用户。优化延迟往往要优先解决尾部问题的来源而不是盯着平均数。吞吐量是单位时间内系统能处理的请求数通常看 QPS 或者 TPS。但这里要提醒一句孤立的吞吐数字意义不大要结合延迟约束来看。一个系统能把 QPS 打到 200但此时 P99 已经飙到 5 秒这种吞吐对生产环境没有参考价值。有效的吞吐应该是在满足 SLO 的前提下能支撑的流量。成本就更好理解了同样的服务能力用的 GPU 数量越少、资源利用率越高成本就越低。这三者的关系我常用一个简单的例子解释。扩大模型的 batch size可以提高 GPU 的计算效率从而提升吞吐但 batch 变大意味着单次请求在队列里等得更久P99 延迟就会上升。反过来加机器可以降低延迟但成本同步上升。所以性能工程不是单向度的“越快要好”而是在指定的 SLO 约束下找到资源和性能的最优组合。团队里做性能优化最忌讳的就是只盯一个指标。我见过有人把 GPU 利用率从 30% 调到 90%高兴得不行结果 P99 延迟翻了三倍业务方直接拒绝上线。这种优化就是典型的局部最优害死全局。2. 指标拆解先会看数再谈优化2.1 端到端延迟到底花在哪一次 AI 推理请求从用户点击到拿到结果时间并不是都花在模型推理上。我一般会把端到端延迟拆成六段网络传输、网关路由、排队等待、预处理、模型推理、后处理与返回。任何一段出问题整体延迟都会受影响。有次排查一个图像分类服务的延迟问题看监控面板发现 P50 才 60 毫秒P99 却高达 1.2 秒而且集中在某个时间窗口。后来把链路追踪日志拉出来发现罪魁祸首是后处理里的一个超时重试逻辑上游某个存储服务偶尔抖动客户端重试了三次每次等待 300 毫秒直接把尾部延迟拉爆。把延迟拆开之后你才会意识到模型推理在端到端时间里占据的份额可能没你想象得大。特别是在请求体较大、或者需要频繁做 RPC 调用的系统里网络和 IO 往往才是大头。所以做性能优化时我会先看链路追踪数据把时间分布摸清楚再决定从哪个环节下手。P99 这个指标还要多说两句。它不是简单的“最大延迟”而是排序后处于 99% 位置的那个值含义是 99% 的请求都能在这个时间阈值内完成。对用户体验来说P99 比平均值敏感得多。平均延迟好看的系统可能在高峰期让一大批用户卡住。有些服务端团队会把 P99 的采集方式做错比如只统计成功的请求把超时和失败排除在外这就等于把最差的那波体验全部藏掉了。建监控的时候最好把延迟按阶段维度拆开记录网关耗时、排队耗时、预处理耗时、推理耗时、后处理耗时。这样一旦延迟恶化你马上知道是哪一段出了问题不用临时去翻日志。2.2 吞吐量与资源利用率怎么看才算健康很多人对吞吐量的理解是“QPS 越高越好”我不完全赞同。QPS 必须跟延迟放在一起看才有意义。同样是 100 QPSP99 分别是 300 毫秒和 3 秒这两个服务质量天差地别。我建议团队在监控面板上同时挂上 QPS 和 P99并且把“超出 SLO 的请求”单独计数这样能快速判断系统是否处于健康状态。资源利用率这块常见的误区是只盯 GPU 平均利用率。GPU 利用率的统计口径很多有的工具报的是“有内核执行的时间占比”有的是“SM 活跃占比”这两个数值能差出百分之二三十。只看一个指标很容易被误导。我会同时看三个数值GPU 利用率、显存占用、显存带宽饱和度。利用率高但显存带宽打满说明计算效率可能不佳显存占用高但利用率很低则可能是模型太大或者动态 shape 产生了碎片。CPU 这边也不是无脑看负载。要分清楚是用户态 CPU 高、内核态 CPU 高还是上下文切换频繁。如果 CPU 一直跑在 80% 以上但任务队列没有明显堆积那可能是合理的如果 CPU 不高、负载却很高大概率是线程在等待锁或者 IO这叫“虚高负载”。整个系统的资源观察建议遵循 USE 方法对每个资源看它的利用率、饱和度、错误率。利用率是资源被使用的比例饱和度是资源还有多少余量应对突发错误率则直接反映异常。这三个维度能帮你快速定位瓶颈是在 GPU、CPU、显存、内存、磁盘还是网络。3. 全链路优化的实操步骤3.1 数据侧加载、预处理与动态 shape数据侧的优化最容易被忽视但它常常是系统性能的第一块短板。我在做一个视觉模型服务时踩过一次大坑GPU 利用率长期不到 40%但业务请求的 P99 却居高不下。排查到最后发现瓶颈根本不在 GPU而是 CPU 上的图片解码和预处理太慢生成请求的速度追不上 GPU 的推理速度GPU 一直在等数据。那次的解决办法分两步。第一步是把预处理做成流水线用多进程并行处理而不是每来一个请求现场计算第二步是引入结果缓存对相同输入直接返回缓存跳过整个预处理和推理链路。两步做完GPU 利用率从 38% 提到了 76%P99 直接砍半。数据侧的优化思路其实就三条预取、缓存、并行。把数据提前加载到内存把重复计算的结果存起来把能并行的任务拆给多进程性能就能肉眼可见地改善。动态 shape 是另一个容易被忽略的性能杀手。很多人图省事让模型支持任意尺寸的输入结果推理引擎在遇到新的输入 shape 时反复做图优化和内核编译产生的开销比推理本身还大。应对办法是限定输入尺寸到一个较小的集合或者把输入 padding 到固定大小。这里还要提一下数据增强如果放在 CPU 侧会在高并发时和预处理抢资源。我建议把能搬到 GPU 上的增强算子尽量搬过去比如在 tensor 层面直接做随机裁剪、翻转和色彩抖动CPU 侧只负责读取和解析压力会小很多。3.2 模型侧推理引擎选型与批处理策略模型推理是整个链路里最硬的一块骨头。推理引擎的选型要跟着硬件和模型结构走。NVIDIA 卡上做 CNN 类模型TensorRT 基本是首选图优化和内核自动调优做得最到位如果是 Transformer 这类生成式模型vLLM、TensorRT-LLM、LMDeploy 这类带连续批处理能力的引擎更合适。引擎选型不是越新越好而是要看它跟你的业务场景、硬件平台、依赖体系的匹配度。批处理策略值得单独讲。很多人在推理服务里把 batch size 设成一个固定值比如 4 或者 8一跑到底。这在流量平稳的时候问题不大但在线上流量波动时就会露馅。batch 设小了GPU 算力吃不满batch 设大了单个请求在队列里等太久P99 飙升。我常用一个简单的模型来估算最优 batch假设单个请求在 batch size 为 B 时的平均推理耗时为 T(B)系统最大并发请求数为 N那么请求在队列里的平均等待时间约为 N * T(B) / B。这里 T(B) 是随 B 增长而减小的但不会线性减小因为 GPU 的并行效率存在天花板。你需要做的是在 B 取不同值时分别测出有效吞吐和 P99找到那个“吞吐够高、延迟没超限”的平衡点。生成式模型还有一个特殊性自回归解码每步只产出一个 token如果每个请求独占一个 batch 槽位GPU 的算力会在解码阶段被大量浪费。连续批处理就是针对这一点设计的每步解码结束后完成的序列立刻让出槽位新的请求马上进入。这样 GPU 几乎每时每刻都在满负荷运行。如果你的服务是生成式模型我强烈建议优先选择支持连续批处理的引擎。3.3 调度侧并发模型与排队策略模型推理服务的并发模型直接决定系统的吞吐上限。Python 后端常见的坑是 GIL 限制导致多线程无法真正并行。FastAPI 这类框架能通过协程处理 IO 密集任务但如果是 CPU 密集的预处理协程也救不了你必须用多进程或者把重活扔给独立进程池。生成式模型还涉及流式输出。用户期望第一个 token 尽快到而不是等全部生成完一次性返回。流式输出能把首 token 延迟和整体生成时间解耦体验上好很多。做性能测试的时候这两种模式的数据要分开统计不能把流式场景下的总时长和非流式场景直接对比。排队策略这块我见过最经典的事故是线程池不设上限。流量高峰一来成百上千的请求同时占着线程每个线程都在等上游响应系统资源被耗光新请求进不来表现就是服务雪崩。正确的做法是限制并发数用一个有界队列承接超出并发的请求队满时快速失败或者降级。重试策略也要谨慎设计。上游抖动导致超时后如果客户端立刻重试往往会把雪上加霜。建议用指数退避加抖动jitter第一次重试等 200 毫秒第二次 400 毫秒再加上随机噪声避免所有请求在同一时刻重试形成波峰。4. 压测方法与实践记录4.1 压测场景设计与工具选型压测的目的不是把服务打挂而是找到容量拐点和瓶颈位置。场景设计我一般做四类单并发递增压测看基础延迟和多并发下的第一个拐点固定并发长时间压测看系统稳定性突发流量模拟看排队和降级机制是否生效长稳测试跑 24 小时以上排查显存泄漏和性能衰退。工具选型看团队习惯。wrk 简单直接适合快速测 HTTP 接口的极限吞吐k6 脚本能力强能模拟复杂的用户行为序列也支持分布式压测Locust 是 Python 生态的写业务逻辑方便JMeter 功能全但用起来重适合有复杂断言的场景。工具不是越强越好能快速上手、准确模拟业务流量才是关键。压测机要独立部署别跟被测服务混在同一个网络环境里。如果压测本身受网络带宽限制测出来的数据就失真了。我通常还会在压测时同时采集服务端的 CPU、内存、GPU、网络这些指标这样压测结束数据能直接对起来看。4.2 一次典型压测过程还原拿一个基于 GPU 的文本生成推理服务来举例。业务目标定的是50 QPS 压力下P99 延迟不超过 800 毫秒。第一轮压测用 20 并发持续打观察到的现象很有意思QPS 只有 38GPU 利用率只有 35%但显存占用已经到 85%P99 跳到 1.2 秒。这几组数据放在一起可以推断出两个线索GPU 没吃饱说明算力没有成为瓶颈显存占用高、P99 高说明请求可能在排队而且排队跟显存分配策略有关。进一步检查发现服务端用的是静态批处理为了攒够 batch 才执行推理导致请求排队时间过长同时每个请求预留了最大长度的显存 buffer显存被浪费了一大块。改造成动态连续批处理加最长等待时间上限之后第二轮压测结果QPS 到了 55P99 降到 720 毫秒GPU 利用率提到 68%。第三轮把并发加到 40P99 又开始抬头但 QPS 提升有限。查了日志发现此时瓶颈转移到了 CPU 侧的 tokenizer 和日志输出GPU 始终在等数据。把 tokenizer 改成缓存并加上消息队列异步化之后40 并发下 QPS 稳定在 70 左右P99 保持在 780 毫秒。这个例子想说明的就是压测要找的不是单一瓶颈而是当前压力下的最短板。每消掉一个瓶颈系统性能就会上一个台阶直到下一个瓶颈出现。性能优化本质是一个不断打破瓶颈的过程。4.3 从性能数据定位瓶颈的通用方法定位瓶颈我习惯走固定流程。第一看资源利用率CPU、GPU、显存、内存、磁盘 IO、网络谁的利用率接近饱和。第二看饱和度系统是否已经出现排队、重试、超时这比利用率更能说明问题。第三看错误率错误类型是超时、拒绝连接还是内部异常能帮助缩小范围。三个典型特征值得记住CPU 高而 GPU 低大概率是预处理或数据解析占了 CPUGPU 在等数据GPU 高而 QPS 低可能是 batch 策略太浪费或者每个请求的序列太长有效 token 产出不划算显存高而利用率低多半是显存碎片化或者预留 buffer 太大。还有一个容易忽略的点网络。如果是跨机房调用模型服务网络 RTT 的占比就不能忽视。有次我排查一个延迟问题发现服务内部处理只要 100 毫秒但端到端却要 700 毫秒一问网络链路竟然是跨城访问。对这种场景把服务搬到离用户更近的区域或者走专线效果立竿见影。5. 常见问题与排查技巧实录5.1 显存 OOM 与显存碎片显存不够用是推理服务最常碰到的问题。OOM 有三种典型情况一次性申请超限、长期运行碎片化、多模型多实例共存冲突。很多人一看到显存不足就急着调小模型或者增加机器其实先定位属于哪种情况更重要。用nvidia-smi看显存使用量再用 PyTorch 的torch.cuda.memory_summary()看分配细节能判断碎片是不是元凶。PyTorch 有自己的一套显存缓存机制它会预分配一大块显存池如果频繁申请释放各种尺寸的张量池子里会留下大量间隙新的申请无法利用这些间隙就会报 OOM但此时整体显存使用可能才 70%。解决碎片化的方向有两个一是尽量复用显存缓冲区不要频繁创建销毁不同 shape 的张量二是预先规划好最大 batch 对应的工作集直接分配一块足够大的缓冲区反复使用。对于总在高峰期 OOM 的服务还可以在代码里加一个显存水位检查超过阈值时对新的推理请求直接返回 503比眼睁睁看着进程崩溃更好。5.2 CPU 与 GPU 利用率双低的诡异现象双低的场景非常让人头疼GPU 没吃饱CPU 也没跑满但服务的吞吐就是上不去。这种“虚低”背后通常是等待——等待锁、等待 IO、等待上游返回。我踩过最典型的一个坑是高并发下业务日志量暴增日志库使用了全局锁大量线程卡在写日志的锁上CPU 看着在忙但没有线程在真正干活GPU 收不到数据自然闲置。用py-spy dump抓线程栈一眼就看到几十个线程全阻塞在日志写入的锁等待里。换成异步非阻塞日志之后吞吐直接翻倍。这类问题排查的思路是不要只看资源利用率还要看线程状态和阻塞点。抓线程栈、看监控里的锁等待时间、检查埋点上报是否有同步网络调用这些是最有效的路径。双低问题十有八九出在业务代码的串行点和隐性等待上。5.3 服务端高延迟的常见隐藏原因延迟类的隐藏问题比吞吐类更难找因为它们往往以毛刺形式出现不是持续恶化。常见的隐藏原因包括垃圾回收暂停、动态 shape 反复编译、共享宿主机的邻居干扰、热迁移导致的服务进程短暂冻结。动态 shape 引发的延迟尖峰在图像类任务里尤其常见。每次模型遇到新的 shape推理引擎就做一次内核选择可能耗时几十毫秒甚至上百毫秒直接拉爆尾部延迟。定位方法很简单在日志里打印推理引擎的编译事件如果出现频率和延迟尖峰对齐基本就坐实了。另一个经验给系统加上慢请求日志把超过 P99 两倍以上的请求详细记录下来包括当时的内存水位、正在执行的算子、前后阶段耗时。很多隐蔽问题平时看不见只有内部状态被现场记录时才露出马脚。6. 最后一点个人体会做性能工程这几年我最大的感受是它不是一个阶段性的优化任务而是一套需要持续运行的方法论。系统在上线前做一轮压测、调几个参数只能解决当时的问题流量模型一变、模型版本一换、依赖库一升级性能表现可能又完全不同。建立持续的监控、SLO 追踪和定期压测机制比任何一次性的救火优化都值钱。还有一个习惯我很推荐每次优化只改一个变量其他全部保持不变然后记录前后数据。性能优化里变量太多如果同时调整了 batch、引擎配置和并发数出了问题你根本不知道是哪个动作导致的。保持单一变量数据才具备可解释性。我自己会把这些改动和收益统一记成一个文档时间长了它就成了容量规划和性能回归预测的核心参考。如果你正准备做 AI 系统的性能优化建议从今天就开始做两件事把自己的服务端到端延迟拆开放到监控里然后跑一轮带并发的基础压测拿到基线。有了基线和拆分数据后续所有优化的方向都会清晰很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →