外观
性能优化与容量规划
一句话定义:性能优化是"在给定的延迟预算内把吞吐做到最大",容量规划是"算出要几台机器才能扛住目标流量"——两者共享同一套指标语言:P50/P99、QPS、TTFT/TPOT。
行业洞察:性能优化最容易犯的错是单点思维——"慢就优化模型"、"卡就加机器"。真实系统的瓶颈往往在排队、在序列化、在 IO、在调度,而不是在 GPU。业界经验是先测后调、自底向上:压测定位瓶颈所在层(模型/引擎/服务/系统),一次只动一层,每动一层都要有压测数据支撑。另一个残酷事实:没有容量规划的"弹性扩缩容"等于裸奔——你不知道该扩到几个,HPA 的阈值只是拍脑袋。
一、性能指标体系
1. 延迟分布:P50/P99/P999
| 指标 | 含义 | 怎么用 |
|---|---|---|
| P50 | 一半请求在此以下 | 用户体验的中位数 |
| P99 | 99% 请求在此以下 | 服务质量承诺(SLA)的主要锚点 |
| P999 | 99.9% 在此以下 | 长尾故障排查(GC、冷启动、网络抖动) |
为什么看 P99 不看平均
平均延迟会被少量极慢请求抬高,掩盖"大多数其实很快"的真相;而 P99 反映的是最差的那 1% 用户的体验。SLO 通常写成"P99 < 100ms,99% 时间达标"。平均延迟几乎从不进 SLA。
2. 吞吐与并发
- QPS(Queries Per Second):每秒处理的请求数,容量规划的基本单位;
- 并发(concurrency):同时正在处理的请求数。两者关系:
并发 = QPS × 平均延迟(Little's Law,容量规划的核心公式,见第四节); - GPU 利用率:注意区分"算力利用率"与"带宽利用率"——推理常出现算力利用率低但带宽跑满的情况,这不叫"闲置"。
3. LLM 特有的延迟拆分
- TTFT(Time To First Token):从请求到首个 token 的时间(用户感知的"反应速度");
- TPOT(Time Per Output Token):每生成一个 token 的平均耗时(用户感知的"打字速度");
- 整体延迟 = TTFT + TPOT × 生成长度,LLM 优化必须分开看。详见 大模型推理优化。
二、瓶颈定位:先判断谁在拖后腿
1. 四象限分类
text
算力不足 带宽不足
GPU 算力利用率 90%+ GPU 算力利用率低、但带宽/显存传输高
─────────────────────────────────────────────────────────────
计算耗时随 batch 增大超线性 计算耗时随模型体积线性增长
服务端瓶颈 IO 瓶颈
请求大量排队、CPU 打满 网络/磁盘/对象存储耗时占比高
CPU 火焰图显示序列化/调度开销 nvidia-smi 显示 GPU 空闲2. 三招定位工具
| 工具 | 看什么 |
|---|---|
nvidia-smi / nvidia-smi dmon | 显存占用、SM 利用率、温度、功耗(确认 GPU 在干活) |
perf / py-spy / 火焰图 | CPU 侧在忙什么(序列化?GIL?调度?) |
| 应用侧打点(OpenTelemetry) | 各阶段延迟占比(模型 vs 排队 vs 网络,见 监控与可观测性) |
别被 nvidia-smi 骗了
nvidia-smi 的"GPU-Util"默认是 SM 利用率,推理小 batch 时可能只有 5%,但这不代表卡坏了或没优化空间——它是 memory-bound,带宽才是瓶颈。看带宽要配合 ncu(Nsight Compute)分析。
三、四层优化:模型 → 引擎 → 服务 → 系统
text
第 4 层 系统层:连接池、IO 多路复用、缓存、网卡调优
第 3 层 服务层:batching、结果缓存、并发模型、请求调度
第 2 层 引擎层:图优化、TensorRT/TVM 编译、kernel 特化
第 1 层 模型层:量化、蒸馏、剪枝、模型结构优化顺序经验:从下往上容易,从上往下便宜。模型层的量化可能带来 2~4 倍收益且改动最小,应该最先做;系统层调优收益常在 10~30%,但边际递减。下面重点讲服务层与系统层(模型层见 量化 与 蒸馏、剪枝与低秩分解,引擎层见 推理:从前向传播到推理引擎)。
1. batching:在线推理的第一大法宝
- 静态批处理:调用方攒够 N 条再发,简单但延迟不可控;
- 动态批处理(dynamic batching):服务端把几毫秒窗口内到达的请求合并成一个 batch,交给 GPU 一次算完——Triton 内置特性,小模型吞吐可提升 5~10 倍;
- 连续批处理(continuous batching):LLM 专用,逐 token 粒度调度,GPU 永不空等,详见 大模型推理优化。
text
动态 batching(非 LLM):
t=0ms 请求A │
t=2ms 请求B │──► batch(A,B,C) 一次前向 ──► 三个响应
t=4ms 请求C │ (batch_timeout=10ms)2. 结果缓存与相似请求合并
- 精确缓存:
(model, input)完全相同的请求直接返回缓存结果。推荐场景命中率可达 30~50%(热门商品/重复查询); - 相似合并(dedup):embedding 距离 < 阈值视为相似,取缓存代表值——收益大但需谨慎(影响正确性,要设业务容忍度)。
3. 并发模型:异步是王道
- 用异步 IO(asyncio)而不是"每请求一线程 + 阻塞调用";
- 推理调用尽量走服务端 batching,别让一个请求独占一次模型调用;
- 多模型共享实例时,按模型队列隔离,避免一个慢模型拖垮全部(见 NVIDIA Triton 多模型服务)。
4. 系统层:容易被忽略的 10~30%
- 连接池复用(HTTP keep-alive / gRPC channel 复用),避免每请求建连;
- 序列化优化:JSON → Protobuf 或消息压缩(gzip),大 payload 时收益显著;
- 日志/指标异步化:别让同步日志阻塞请求路径;
- 网卡/内核调优:TCP BBR、缓冲区调优,跨区域部署时网络常常是 P99 的大头。
四、容量规划:从估算到扩缩容
1. 核心公式(Little's Law)
text
并发(同时在飞请求数)= QPS × 平均延迟(秒)
例:目标 1000 QPS,平均延迟 50ms → 需要的并发 = 1000 × 0.05 = 50单实例能扛多少并发 → 反推实例数:
text
实例数 = 目标并发 / 单实例实测并发 × 1.3(安全余量 30%)2. 容量规划的完整流程
text
① 压测:对单实例加压,测出延迟-吞吐曲线(P99 达标时的最大 QPS)
(压测方法见 /practice/load-testing)
② 换算:目标 QPS ÷ 单实例最大 QPS × 1.3 = 所需实例数
③ 打点监控:实时 QPS、P99、并发,验证公式
④ 扩缩容:HPA 按 CPU/自定义指标伸缩;GPU 服务优先按 QPS 或队列深度
⑤ 预留尖峰:双十一/大促按峰值 QPS 而非均值规划压测的三个要点
- 用真实流量分布(生产流量回放),别用均匀假流量;
- 测 P99 而非平均——P99 达标才是 SLA;
- 压到"P99 开始恶化"的拐点,记录该点 QPS,那就是容量上限。完整方法见 压测与容量规划。
3. 弹性策略
| 伸缩维度 | 指标 | 建议 |
|---|---|---|
| 水平伸缩 | 请求队列深度 / QPS / 自定义指标 | 队列有界、缓慢扩容快速缩容(避免抖动) |
| GPU 专用 | vGPU 或整卡 | 模型小可用 MIG/vGPU 分卡 |
| 时间规划 | 按流量曲线预设 cron 扩缩 | 大促前 1 小时提前扩容 |
4. 优化优先级建议
text
第 1 步:压测定位瓶颈层(别猜)
第 2 步:模型层(量化)—— 收益最大、改动最小
第 3 步:服务层(batching、缓存、异步化)
第 4 步:引擎层(TensorRT 编译)—— 深度特化
第 5 步:系统层(连接池、网络、IO)
第 6 步:容量规划 + 弹性伸缩 —— 让资源跟着流量走每步之后都重新压测,量化收益;一次只动一层,否则收益归因不清。
权衡与取舍
| 决策点 | 选项 | 怎么选 |
|---|---|---|
| 延迟 vs 吞吐 | 小 batch 保延迟 / 大 batch 保吞吐 | 在线保 P99 延迟,离线放量冲吞吐 |
| 优化 vs 加机器 | 调优省钱费时 / 加机器省时费钱 | 先量化+服务层调优,再考虑扩容 |
| P50 vs P99 | 优化中位数 / 优化长尾 | SLA 锚定 P99,长尾用专项排查 |
| 缓存收益 vs 正确性 | 缓存快 / 保证新鲜 | 看业务对新鲜度容忍度 |
| 扩缩容快 vs 稳 | 激进缩容省钱 / 保守防抖动 | 缩容要慢(防震荡),扩容要快(防超时) |
一句话总结:性能是测出来的,不是调出来的——先立指标、再压测定位、按层优化、用 Little's Law 定容量,让每一分钱花在可衡量的收益上。
延伸阅读
- 压测与容量规划 —— 从压测工具到容量公式的完整实操
- 大模型推理优化 —— TTFT/TPOT 与连续批处理
- 量化 —— 模型层最大收益来源
- 监控与可观测性 —— 延迟/流量/错误/饱和度四类黄金指标
- NVIDIA Triton 多模型服务 —— 动态 batching 的生产实践