3096 字
约 10 分钟
20
S23-A07 · 推理容量规划:QPS、显存与降级预算

压测报告写着"单实例几十 QPS、P99 在几百毫秒",上线当天十点开始排队。差别不在压测造假,而在两边算的不是同一件事:压测用的输入长度、输出长度、并发模型与真实流量不同,而推理的成本函数对这三个量都高度敏感。同 QPS 下输入长度涨一个量级,吞吐掉一个数量级是常态。

容量规划要先把口径定死,再谈数字:请求速率、并发数、单请求成本是三个独立维度,任何一个没定义,"能跑多少"都是空话。


1. 三个维度与各自的度量口径

维度 该用的口径 常见错误口径 偏差来源
请求速率 到达率(req/s),按输入长度分层统计 平均 QPS 长短请求混算,平均值抹平尖峰
并发数 同时在处理中的请求数(in-flight) 连接数、线程数 长连接与排队中的请求被算进"处理中"
单请求成本 输入 token + 输出 token 的合计耗时 只算输出 token/s 预填充与解码是两个不同瓶颈

token/s 是吞吐量纲,不是请求量纲。用它报容量,等于用"每小时运送多少吨"回答"能发多少班车"。

# 分层统计到达率:按输入长度分桶,别只看总平均
awk '{b=int($3/1000); c[b]++} END{for(k in c) print k"k", c[k]}' access.log

2. 显存决定并发上限的机制

权重是常驻的,KV 缓存是按"序列长度 × 并发"线性增长的。可用的 KV 空间除以单请求 KV 占用,就是并发上限——这是推理容量里唯一能算准的一段。

KV 可用空间 ≈ 显存 − 权重 − 激活与临时缓冲 − 框架开销
单请求 KV  ≈ 2 × 层数 × KV头数 × 头维度 × 序列长度 × 字节数
并发上限   ≈ KV 可用空间 ÷ 单请求 KV
项 是否常驻 能否压缩 规划时怎么取
权重 常驻 量化可压 按实际精度,不按标称参数量估
KV 缓存 随并发与长度线性涨 GQA、量化 KV 可压 按 P95 序列长度算,不按均值
激活与临时缓冲 随批大小波动 有限 留固定余量,别按比例估
框架开销 常驻 有限 按引擎版本实测,不套用旧数

显存构成的细节另文展开;这里只用它反推并发上限。按目标引擎版本核对一次实际值,比套用别处的经验比例可靠。

3. 上下文长度对容量的乘性影响

上下文长度 相对容量 主要瓶颈 规划含义
短(≤1K) 基准 解码与调度 并发可开高,批处理窗口可以放大
中(4K 量级) 明显下降 KV 占比上升 按 P95 而非均值预留
长(16K 量级) 大幅下降 预填充与 KV 双重压力 长请求单独排队,不与短请求混池
超长(64K 量级) 极低并发 预填充主导,几乎串行 走异步或离线,不进在线池

乘性影响的意思是:长度翻倍不是"慢一点",而是并发上限折半,同时单请求占住槽位的时间翻倍。两个效应叠加,等效吞吐掉到四分之一。容量规划里最贵的错误,是用短请求的压测结论去接长请求的流量。

4. 量化与精度的双向影响

精度 显存 吞吐 质量风险 适用
FP16/BF16 基准 基准 无额外损失 质量敏感、显存充裕
INT8/FP8 约减半 通常上升 长尾任务上偶有退化 多数在线服务
INT4 约四分之一 上升但受限于访存 推理链、数值任务退化明显 显存硬约束下的取舍

量化换来的是并发上限提高,代价是质量分布尾部变差。规划时不能只算显存账:先确认量化后的评测集分数落在可接受区间,再把它写进容量假设。否则容量达标、效果不达标,等于重做一遍。

5. 批处理窗口与 P99 的交换

连续批处理把"等下一个请求"变成"凑一个批次",吞吐上升、尾延迟上升。窗口大小是容量与延迟之间的汇率。

# vLLM 侧与容量直接相关的三个旋钮(按版本核对参数名)
--max-num-seqs 256          # 并发序列上限:直接对应 KV 预算
--max-num-batched-tokens 8192   # 单步 token 上限:控制预填充抢占
--gpu-memory-utilization 0.90   # 留给 KV 的比例,剩余给激活与碎片
窗口设定 吞吐 P99 适用场景
极小(近似不等待) 低 最低 交互式低延迟、单请求为主
中等 高 可控 通用在线服务
极大 最高 显著变差 离线批处理、吞吐优先

调窗口的正确顺序是:先定 P99 预算,再反向找能填满吞吐的最大窗口,而不是先把窗口拉满再观察延迟。

6. 排队还是拒绝

策略 用户感知 系统后果 适用
无限排队 延迟无上限,最终超时 队列内存增长,雪崩风险 不该出现
有限排队 + 超时 等待可预期 需要队列长度与超时联动 主流选择
立即拒绝(429/降级) 快速失败,可重试 保护已有请求的 P99 有客户端重试策略时
排队 + 降级旁路 部分请求走便宜路径 需要两条路径都保活 质量可分级的业务

排队等于把延迟卖给用户,拒绝等于把可用性损失卖给用户。选哪个取决于客户端是否有退避重试:没有重试的客户端遇到 429 就是失败,有退避的客户端遇到长排队就是超时风暴。

7. 降级的四级与可感知度

级别 手段 容量释放 用户可感知度
一级 缩上下文(截断历史、减检索条数) 中 低,多数用户无感
二级 换小模型或短输出上限 高 中,答案质量可辨
三级 缓存命中(精确 + 语义) 高 低,但可能答非所问
四级 限流与排队 最高 高,直接可见
# 降级链按"可感知度从低到高"排序,触发条件写死,不靠人判断
degrade:
  - when: queue_depth > 50
    do: trim_context(max_tokens=2048)
  - when: queue_depth > 200
    do: route_small_model
  - when: error_rate > 0.05
    do: rate_limit(per_tenant)

降级链必须写上"恢复条件",否则降下去就回不来。恢复要比触发更保守,避免在临界点反复横跳。

8. 突发的三种来源与预留

来源 特征 预留方式
活动 时间已知、幅度可估 提前扩容 + 预热,按历史倍数备
重试风暴 时间不可知、放大倍数高 幂等 + 退避 + 熔断,容量只留小余量
上游批量任务 可协商、可排期 约定窗口,与在线池物理隔离

重试风暴是唯一"加容量也救不了"的一类:3 次重试等于把峰值乘 3,且全部落在同一时刻。对它的正确投入是客户端退避,不是 GPU。

9. 单位成本与容量的换算

单位成本 = 实例单价 ÷ 有效吞吐(按目标 P99 下的实际 token/s)
扩容判据 = 边际单位成本 < 业务可承受的单请求成本上限

有效吞吐必须取"目标 P99 下"的值。拿峰值吞吐算单价,会得到一张便宜但兑现不了的账单——这也是容量与成本两张表经常对不上的原因。

10. 弹性伸缩的冷启动代价

环节 典型量级 能否预热
镜像拉取 分钟级 节点侧预拉
权重量化加载 数十秒到分钟级 共享存储、内存映射
引擎初始化与图编译 秒到分钟级 保留常驻副本
首个请求的预热 秒级 启动即打一条假请求

扩容的决策延迟 = 监控滞后 + 冷启动。如果冷启动比流量爬升还慢,弹性伸缩就只是心理安慰,真正兜底的是预留容量。

11. 验收口径

项 口径 不写清楚会怎样
SLO 按输入长度分层定 P99,不用全局平均 短请求掩盖长请求的崩溃
压测 用真实长度分布与真实并发模型回放 压测通过、上线排队
灰度 按流量比例而非机器比例 大流量实例先崩
观察期 覆盖至少一个日周期峰值 漏掉晚间高峰

容量的验收不是"压测通过",而是"在真实分布下、跨过一个峰值周期、灰度全量后 P99 仍在 SLO 内"。

12. 排错

现象 常见原因 处理
压测达标、上线排队 压测长度分布与线上不同 用线上分布回放,分层看 P99
吞吐突然腰斩 长请求混入短请求池,占满 KV 按长度分池,长请求单独排队
显存够用但并发上不去 max-num-seqs 或 KV 比例限制 按 KV 预算反推,而非按显存总量
量化后容量达标、投诉变多 质量尾部退化未被评测覆盖 回到评测集,确认退化幅度
扩容后延迟仍高 冷启动慢于流量爬升 预留副本 + 预热

13. 边界

场景 为什么容量规划让位 该做什么
内部低量工具 峰值远低于单实例能力 只做成本规划与可用性
离线批处理 无 P99、可排队 按吞吐与截止时间排程
模型可替换 容量随模型变化,算不出稳定值 先定抽象层与验收口径
请求长度极短且均匀 KV 不是瓶颈 关注调度与网络开销
预算硬约束 容量上限由钱决定 先定成本,再定降级链

一句话判据:在线服务按 P99 定容量,离线任务按截止时间定容量。把两者混在一个池里,会同时得到差的延迟和差的利用率。


14. 自测

  1. 为什么用 token/s 报容量会误导?它漏掉了哪个维度?
  2. 显存够、并发却上不去,最该先查哪两个限制?
  3. 上下文长度从 4K 涨到 16K,等效吞吐为什么可能掉到四分之一?
  4. 排队与拒绝分别在卖什么?选哪个取决于客户端什么能力?
  5. 降级链为什么必须写恢复条件?恢复阈值该比触发阈值更松还是更紧?
  6. 弹性伸缩在什么情况下等于心理安慰?
S23-A07 · 推理容量规划:QPS、显存与降级预算
https://www.clxhxhhr.top/posts/6209/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。