压测报告写着"单实例几十 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. 自测
- 为什么用
token/s报容量会误导?它漏掉了哪个维度? - 显存够、并发却上不去,最该先查哪两个限制?
- 上下文长度从 4K 涨到 16K,等效吞吐为什么可能掉到四分之一?
- 排队与拒绝分别在卖什么?选哪个取决于客户端什么能力?
- 降级链为什么必须写恢复条件?恢复阈值该比触发阈值更松还是更紧?
- 弹性伸缩在什么情况下等于心理安慰?