1. AI 系统性能工程到底在解决什么问题
1.1 从一个真实场景说起
去年我接手过一个推理服务的优化项目,模型本身只有 7B 参数,单次推理在开发机上跑下来也就几百毫秒,团队里所有人都觉得“这玩意儿上线肯定没问题”。结果真到了生产环境,QPS 一上来,P99 延迟直接飙到 8 秒以上,GPU 利用率却只有 30% 出头。这个反差特别典型——模型能跑通,和模型能扛住生产流量,完全是两码事。
这就是 AI 系统性能工程要处理的核心矛盾:算法层面的指标(准确率、BLEU、F1)和系统层面的指标(吞吐、延迟、成本、稳定性)之间存在巨大的鸿沟。传统后端性能工程关注的是 CPU、内存、IO、网络这些通用资源,而 AI 系统多了一层——GPU/NPU 这类加速器,以及模型推理/训练特有的计算图、显存、批处理、算子融合等维度。你不懂这层,优化就无从下手。
我写这个系列,是想把过去几年在推理服务、训练集群、Agent 编排系统上踩过的坑和总结的方法论系统性地梳理一遍。适合谁看?如果你是把模型从 notebook 推到生产的算法工程师,或者是被拉来给 AI 服务做性能保障的后端/运维同学,再或者是在做 AI 基础设施选型的技术负责人,这个系列应该都能给你一些直接能用的东西。
1.2 性能工程和“调参”的本质区别
很多人把性能优化理解成“调调 batch size、换个大点的显卡”,这是把性能工程做成了玄学。真正的性能工程是一套可测量、可归因、可复现的方法论。我习惯把它拆成四步:
- 建立基线:在受控条件下测出当前系统的真实表现,包括吞吐、延迟分布(不只是平均值,P50/P95/P99 都要看)、资源利用率、成本。
- 定位瓶颈:用 profiling 工具找到真正的瓶颈在哪一层——是计算密集、访存密集、通信密集,还是调度/框架开销。
- 提出假设并验证:每次只改一个变量,看指标怎么动。改两个变量一起上,你永远不知道是哪个起了作用。
- 回归与固化:优化后的配置要能稳定复现,写进部署脚本,加进监控告警。
这四步听起来朴素,但我在实际项目里见过太多团队跳过第一步和第三步,直接凭感觉改配置,最后性能是上去了,但没人说得清为什么,下次遇到类似问题还是抓瞎。
提示:性能工程里最贵的成本不是显卡,是“不知道瓶颈在哪就乱改”浪费的人天。先测量,再动手。
1.3 为什么现在必须重视这件事
过去两年 AI 应用的形态发生了明显变化。以前大家关心的是“能不能训出一个效果好的模型”,现在关心的是“这个模型能不能在可接受的成本下服务百万级用户”。大模型推理的单次成本比传统模型高出一到两个数量级,一个没优化好的推理服务,账单可能是优化后的五到十倍。同时,Agent 类应用把单次请求变成了多轮模型调用加工具调用的链路,延迟是累加的,任何一个环节慢一点,端到端体验就崩了。
所以性能工程不再是“上线前临时抱佛脚”的环节,而是从架构设计阶段就要介入的一等公民。你选什么推理框架、用什么量化方案、批处理策略怎么设计、缓存放在哪一层,这些决策在写第一行代码之前就应该有性能视角的考量。
2. 核心指标体系:先把“好”定义清楚
2.1 延迟指标:别只看平均值
延迟是用户最直观的感受,但“平均延迟 200ms”这句话几乎没有信息量。真正有意义的是延迟分布。我一般会关注这几个分位数:
| 指标 | 含义 | 典型关注场景 |
|---|---|---|
| P50 | 中位数,一半请求快于此 | 整体体验基线 |
| P90 | 90% 请求快于此 | 常规 SLA |
| P95 | 95% 请求快于此 | 较严格 SLA |
| P99 | 99% 请求快于此 | 长尾体验、超时设置依据 |
| P999 | 99.9% 请求快于此 | 极端长尾,金融/实时场景 |
为什么 P99 比平均值重要?因为平均值会被大量快请求拉低,掩盖掉那 1% 的慢请求。而恰恰是这 1% 的慢请求,决定了你的超时阈值设多少、用户会不会投诉、上游服务会不会被拖垮。我见过一个服务平均延迟 150ms 看起来很漂亮,但 P99 是 6 秒,结果就是每 100 个用户里有一个要等 6 秒,投诉率居高不下。
另外要区分TTFT(Time To First Token)和TPOT(Time Per Output Token)。对于流式输出的对话类应用,用户感知的“快”主要是 TTFT——第一个字多快出来。而 TPOT 决定了后续文字吐出的流畅度。这两个指标的优化手段完全不同:TTFT 受 prefill 阶段和排队影响大,TPOT 受 decode 阶段和显存带宽影响大。
2.2 吞吐指标:QPS、TPS 和并发数
吞吐衡量的是单位时间能处理多少请求。常见的有 QPS(每秒查询数)、TPS(每秒 token 数,对大模型更贴切)。这里有个容易混淆的点:吞吐和延迟通常是矛盾的。你把 batch size 调大,吞吐上去了,但单个请求的延迟也上去了,因为要等凑批。所以不存在“吞吐又高延迟又低”的免费午餐,只有根据业务场景找平衡点。
并发数则是另一个维度。很多人以为并发数越高吞吐越高,实际上并发数超过某个点之后,吞吐不升反降,因为资源争抢和调度开销开始占主导。这个拐点在哪里,必须实测。
2.3 资源利用率:GPU 到底忙不忙
GPU 利用率是个被严重误读的指标。nvidia-smi显示的 GPU-Util 是“在采样周期内有 kernel 在执行的百分比”,它高不代表计算单元真的在满负荷干活。一个访存密集的 kernel 可以让 GPU-Util 跑到 100%,但 SM 的计算单元利用率可能只有 20%。
所以我更关注这几个:
- SM 占用率 / 计算吞吐:用 Nsight Compute 看,反映计算单元真实忙碌程度。
- 显存带宽利用率:大模型 decode 阶段通常是访存瓶颈,这个指标比算力利用率更关键。
- 显存占用:包括模型权重、KV Cache、激活值。KV Cache 在大 batch 长上下文下会吃掉大量显存,是 OOM 的常见元凶。
- SM 活跃 warp 数:反映延迟隐藏能力,太低说明并行度不够。
2.4 成本指标:每百万 token 多少钱
技术指标最终要落到钱上。我习惯算一个单位成本:每处理一百万 token 需要多少 GPU 小时,折算成电费加折旧。这个数字能直接和业务收入对比,判断这个服务是否可持续。优化性能的终极目标,很多时候就是把单位成本压到业务可承受的范围内。
3. 瓶颈定位:性能工程的手艺活
3.1 先分层,再定位
AI 系统的性能问题可能出在很多层,盲目优化等于大海捞针。我习惯按这个层次从上往下排查:
- 业务/调度层:请求排队、批处理策略、路由、重试逻辑。
- 服务框架层:推理框架(如各类 serving 框架)的调度、内存管理、并发模型。
- 模型执行层:计算图、算子、kernel 实现、量化。
- 硬件层:GPU/NPU 的计算、显存、互联带宽。
一个经验法则是:先看上层,再看下层。因为上层的问题往往更容易修,收益也更直接。比如一个服务 P99 高,可能根本不是模型慢,而是排队策略有问题——请求全挤在一个队列里,前面的大请求把后面的小请求堵死了。这种问题改调度策略就能解决,根本不用碰模型。
3.2 Profiling 工具怎么选
不同层用不同工具,我整理了一张常用工具对照表:
| 层次 | 工具 | 主要用途 |
|---|---|---|
| 服务框架 | 框架自带 metrics、Prometheus | 请求延迟分布、队列长度、批大小 |
| 模型执行 | PyTorch Profiler、Nsight Systems | 算子耗时、kernel 时间线、CPU-GPU 同步 |
| Kernel 级 | Nsight Compute | 单 kernel 的计算/访存瓶颈分析 |
| 硬件 | nvidia-smi、DCGM | 利用率、显存、功耗、温度 |
| 端到端 | 分布式追踪 | 跨服务链路延迟归因 |
我个人的习惯是:先用框架 metrics 和分布式追踪定位到“慢在哪个环节”,再用 Nsight Systems 看这个环节内部的时间线,最后如果发现是某个 kernel 慢,才上 Nsight Compute 深挖。不要一上来就 Nsight Compute,那个信息量太大,容易迷失。
3.3 一个真实的定位案例
之前有个推理服务,P99 延迟忽高忽低,波动很大。我先看框架 metrics,发现队列长度在波动,说明请求到达和处理速度不匹配。再看 Nsight Systems 的时间线,发现 GPU 上有明显的“空洞”——kernel 之间有几百毫秒的空隙,GPU 在等 CPU。
顺着这个线索查下去,发现是 tokenizer 在 CPU 上串行执行,成了瓶颈。大 batch 下 tokenizer 处理时间线性增长,GPU 只能干等。解决方案是把 tokenizer 并行化,并且和 GPU 计算流水线化。改完之后 P99 从 3 秒降到 800ms。
这个案例的教训是:GPU 服务的瓶颈经常不在 GPU 上。CPU 侧的预处理、后处理、调度逻辑,都可能是隐藏的瓶颈。只看 GPU 利用率会误导你。
提示:看到 GPU 利用率不高,先别急着说“GPU 没吃满”,要问“GPU 在等什么”。等待的原因往往在 CPU 侧或 IO 侧。
4. 优化手段:从能用到好用
4.1 批处理:最有效的杠杆之一
批处理是提升吞吐最直接的手段。原理很简单:GPU 擅长并行计算,一次处理 32 个请求和一次处理 1 个请求,耗时可能只差一点点,但吞吐差了 32 倍。但批处理有几个坑:
- 静态批 vs 连续批:静态批要等凑齐一批才处理,延迟高;连续批(continuous batching)是动态地把新请求塞进正在执行的批次,延迟和吞吐兼顾。现在主流推理框架基本都支持连续批,选型时一定要确认。
- 批大小不是越大越好:超过某个点,显存不够(尤其 KV Cache),或者计算变成访存瓶颈,吞吐不再增长。这个最优点要实测。
- 请求长度差异大时:长请求和短请求混在一起,短请求会被长请求拖累。可以考虑按长度分桶,或者用 chunked prefill 把长请求切开。
我实测过一个 13B 模型的服务,batch size 从 1 加到 16,吞吐涨了约 10 倍,延迟只涨了不到 2 倍。但从 16 加到 32,吞吐只涨了 20%,延迟却翻倍了。所以 16 就是这个场景的甜点。
4.2 量化:用精度换性能
量化是把模型权重和激活值从高精度(如 FP16)降到低精度(如 INT8、INT4),减少显存占用和计算量。常见方案:
- 训练后量化(PTQ):不需要重新训练,直接对训练好的模型量化。简单快速,但精度损失可能较大。
- 量化感知训练(QAT):训练时就模拟量化误差,精度保持更好,但需要训练资源。
- 权重量化 vs 激活量化:只量化权重实现简单,收益主要是显存;同时量化激活收益更大,但实现复杂。
量化的收益很直接:INT8 相比 FP16,显存减半,理论算力翻倍。但实际收益取决于硬件是否支持低精度加速,以及 kernel 实现质量。有些场景下量化后反而变慢,因为要插入反量化操作。所以量化一定要实测,不能想当然。
4.3 KV Cache 优化:大模型推理的显存大户
自回归生成时,每个 token 都要和之前所有 token 做注意力计算。为了避免重复计算,会把之前 token 的 Key 和 Value 缓存起来,这就是 KV Cache。它的显存占用是:
KV Cache 大小 = 2 × 层数 × 注意力头数 × 头维度 × 序列长度 × batch size × 精度字节数以一个 13B 模型为例,假设 40 层、40 头、头维度 128、FP16,序列长度 2048,batch size 16:
2 × 40 × 40 × 128 × 2048 × 16 × 2 字节 ≈ 21.5 GB光 KV Cache 就 21.5 GB,加上模型权重 26 GB,一张 48GB 的卡基本就满了。所以 KV Cache 优化是大模型推理的关键:
- PagedAttention:把 KV Cache 分页管理,减少碎片,提升显存利用率。
- KV Cache 量化:把 KV Cache 也量化到 INT8,显存减半。
- 滑动窗口 / 稀疏注意力:只保留最近 N 个 token 的 KV,长上下文场景下大幅省显存。
- 前缀共享:多个请求共享相同前缀(如 system prompt)的 KV Cache。
4.4 算子融合与计算图优化
深度学习框架执行模型时,是一堆算子按顺序跑。每个算子都有 kernel 启动开销和访存开销。算子融合就是把多个算子合并成一个 kernel,减少启动次数和中间结果的访存。
比如LayerNorm + Linear + GELU这三个算子,如果不融合,要写三次中间结果到显存再读回来;融合成一个 kernel,中间结果留在寄存器里,访存大幅减少。这类优化通常由推理框架或编译器自动完成(如 TensorRT、TorchScript、各类图编译器),但你需要知道它存在,并且在选型时关注框架的融合能力。
4.5 并行策略:单卡不够就上多卡
模型大到单卡放不下,或者单卡吞吐不够,就得上多卡。常见并行方式:
- 数据并行:每张卡一份完整模型,处理不同数据。实现简单,但显存不省。
- 张量并行:把单个算子切到多卡,层内并行。通信频繁,对互联带宽要求高。
- 流水线并行:把不同层放到不同卡,层间并行。会有流水线气泡,需要调度优化。
- 专家并行:MoE 模型专用,不同专家放不同卡。
选哪种取决于模型大小、卡间带宽、延迟要求。张量并行适合单机多卡(NVLink 带宽高),流水线并行适合跨机。实际生产里经常是几种并行混合使用。
5. 常见问题与排查速查
5.1 典型问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| GPU 利用率低 | CPU 预处理瓶颈、IO 等待、批太小 | 看 CPU 占用、Nsight 时间线空洞 |
| P99 延迟高但 P50 正常 | 长请求拖累、GC、显存碎片 | 按请求长度分组统计、看显存分配 |
| 吞吐上不去 | 批处理策略差、显存带宽瓶颈 | 调 batch、看带宽利用率 |
| OOM | KV Cache 过大、batch 太大、碎片 | 算 KV Cache 大小、开 PagedAttention |
| 多卡扩展性差 | 通信瓶颈、负载不均 | 看 NCCL 通信时间、各卡利用率 |
| 量化后变慢 | 硬件不支持、反量化开销 | 对比量化前后 kernel 时间 |
5.2 几个容易踩的坑
坑一:只看平均值。前面说过,平均值会骗人。一定要看分位数,尤其是 P99。
坑二:忽略冷启动。服务刚启动时,模型加载、CUDA 上下文初始化、kernel 编译(如果是 JIT)都要时间。如果不做预热,第一批请求会特别慢。生产环境一定要有 warmup 流程。
坑三:显存碎片。长时间运行的服务,显存分配释放频繁,容易产生碎片,导致明明总显存够却分配失败。用显存池或 PagedAttention 能缓解。
坑四:盲目追新框架。新框架可能性能好,但稳定性和生态成熟度要评估。生产环境选型,稳定优先。
坑五:不做压测就上线。开发环境的流量模式和生产的完全不同。上线前一定要用接近真实的流量做压测,包括请求长度分布、并发模式。
5.3 压测怎么做才靠谱
压测不是简单地用工具打流量。我一般会:
- 构造真实流量分布:请求长度、并发模式要贴近生产。用固定长度的请求压测,结果没有参考价值。
- 逐步加压:从低并发开始,逐步加,观察指标拐点。找到吞吐不再增长、延迟开始飙升的那个点。
- 持续足够长时间:短时间压测发现不了显存泄漏、碎片累积这类问题。至少跑几小时。
- 监控全链路:不只监控服务本身,还要看下游依赖、网络、存储。
- 记录配置:每次压测的配置、代码版本、环境都要记录,否则结果无法复现。
6. 从性能工程到成本工程
6.1 算清楚每百万 token 的成本
性能优化的最终目的是降本增效。我习惯把性能指标折算成钱:
每百万 token 成本 = (GPU 小时单价 × 处理百万 token 所需 GPU 小时)假设一张卡每小时 10 元,吞吐是每秒 2000 token,那么处理一百万 token 需要:
1,000,000 / 2000 = 500 秒 ≈ 0.139 小时 成本 = 10 × 0.139 ≈ 1.39 元这个数字能直接和业务收入对比。如果每百万 token 收费 5 元,毛利率就是 72%。优化性能就是提升这个毛利率。
6.2 性能优化的优先级排序
资源有限,优化要排优先级。我的排序原则:
- 先修正确性问题:如果服务不稳定、会 OOM、会超时,先修这些,性能其次。
- 再修高收益低成本的:比如调 batch size、开连续批、加缓存,改动小收益大。
- 然后修高收益高成本的:比如量化、换框架、上多卡,需要投入但收益可观。
- 最后修低收益的:微调 kernel、换更快的算子,收益有限,投入产出比低。
不要一上来就啃最难的骨头,先把容易的收益拿到手。
6.3 建立持续性能监控
性能工程不是一次性的项目,是持续的过程。模型更新、流量变化、依赖升级,都可能让性能退化。所以要建立持续监控:
- 核心指标看板:吞吐、延迟分位数、资源利用率、成本,实时展示。
- 性能回归测试:每次发版前跑一遍标准压测,对比基线,退化超过阈值就拦截。
- 告警:P99 超过阈值、GPU 利用率异常、成本突增,都要告警。
我在实际项目里的体会是,性能问题往往是“温水煮青蛙”——不是某次改动突然变慢,而是每次改动慢一点点,累积起来就崩了。持续监控能在早期发现这种退化。
最后分享一个我常用的技巧:给每个服务维护一份“性能档案”,记录它的基线指标、瓶颈点、优化历史、当前配置。新人接手时看这份档案,能快速理解这个服务的性能特征,少走很多弯路。这份档案不需要多正式,一个 Markdown 文件就够,关键是要持续更新。