Part 2 · Serving & Cost 服务与成本

2.4 Capacity Planning 容量规划:从流量画像到 GPU 数量

难度 ★★★ 阅读 ~18 min 面试出现频率 高(系统设计) 前置 1.2 / 2.1 / 2.2
Core Concept 核心概念

这一章是 Part 1 和 Part 2 的收口

前面几章讲的每一件事——KV cache 占多少显存、连续批处理提高多少吞吐、量化省多少空间、缓存命中多少前缀、路由分流多少请求——最终都汇聚成同一个问题:我到底需要几张卡、每月要花多少钱?

这个估算不可能精确,也不需要精确。它的目的是把数量级判断对:需要 2 张卡还是 20 张卡,月成本是 1 万还是 30 万。数量级错了,架构选型就错了;数量级对了,剩下的用压测和灰度去修正。所以本章给的是方法与假设,不是数字。

第一步:流量画像——没有它,后面全是空想

容量规划最常见的失败不是算错,而是把几个分位数当成了完整负载。P50 只说明一半样本比它短,既不是均值,也不是安全上界;输入 P50 与输出 P50 还可能来自不同请求,不能拼成一条「典型请求」。开始计算前,需要一段保留请求级关联关系与时间顺序的真实 trace;没有 trace 时,下面这些统计量只能用于量级判断:

量为什么需要它常见错误
到达时间序列保留突发、峰值窗口与请求间隔,决定排队只给「峰值 QPS」,丢掉峰内突发
输入/输出联合分布每条请求记录输入、输出、类别与终止原因;至少报告均值、P50、P95、P99拿 P50 当均值,或把两个边际分位数拼成一条请求
缓存画像记录可缓存 token、实际命中 token、共享前缀及复用时间把「可缓存比例」直接当成命中率或显存节省率
服务时间分布按请求记录排队、TTFT、TPOT、结束时间,供 Little's Law 与 SLA 校验用 SLA 上限冒充平均服务时间
负载类别与峰谷保留不同业务的占比、优先级和昼夜变化只用全日均值,掩盖长尾类别集中出现

如果这些数据还没有,容量规划的第一项工作不是计算,而是先采样并保存请求级 trace——哪怕先让小规格实例或现有 API 接真实流量跑一周。只有边际直方图时,也要按类别分别统计;不要假装自己知道输入、输出、缓存与服务时长之间的相关性。

第二步:两个独立的约束,取更紧的那个

自托管的容量由两个互相独立的瓶颈决定。很多人只算其中一个,于是要么买多了,要么上线就 OOM。

约束 A · 算力/吞吐量级
峰值输出 token 速率 ≈ 峰值窗口到达率 × E[输出 token]
理论卡数下限 ≈ 峰值输出 token 速率 ÷ 单卡在同类 trace 下的实测 goodput

约束 B · 平均显存驻留量级
平均并发数 = 峰值窗口到达率 × E[服务时间] (Little's Law)
平均 KV 需求需按请求级总序列长度、缓存共享与并发时序逐条累计

这两个结果只是诊断下限,不是采购数。
生产卡数 = 能让原始 trace 回放在故障场景下仍满足 TTFT / TPOT / goodput / OOM / 拒绝率门槛的最小配置。

其中 Little's Law(利特尔法则)是排队论里的经典结论:稳态系统中,平均在途请求数 = 到达率 × 平均停留时间。注意它给的是长期均值,不是 P95 并发或突发安全边界;把 P50 长度代进去,既不能得到平均驻留量,也不能证明长尾时不会 OOM。

同一份流量画像,两条独立的推导路径 流量画像:QPS / 长度分布 A · 算力诊断下限 峰值 QPS × 平均输出长度 ÷ 单卡实测 token/s B · 平均驻留量级 QPS × 平均时长 = 并发数 逐请求累计 KV 时间线 候选配置 → trace 回放门禁
图 1 · 两条诊断路径。哪条更紧取决于负载形态:短输入长输出更可能先碰到 decode 吞吐,长输入短输出更可能先碰到 prefill 或 KV 驻留;这只是选择候选与优化方向,最终配置仍由原始 trace 回放决定。

第三步:三个必须留的余量

解析式算出来的数字是诊断下限,真正的候选配置要逐个回放 trace。通过回放后,三处余量仍缺一不可:

① 利用率余量。系统不能跑在 100% 利用率上——排队延迟会在接近饱和时非线性爆炸(2.1 的拐点)。生产系统通常把目标利用率设在 60–75%,即实际容量要按理论值再除以 0.6–0.75。

② 故障余量。N 张卡的集群要能在挂掉 1 张时仍然扛住峰值,即按 N+1 配置。跨可用区部署时余量还要更多。

③ 增长余量。扩容不是即时的——采购、配额审批、模型加载都需要时间。按未来 1–2 个季度的预期增长预留。

API 还是自托管:一个盈亏平衡问题

这个决策通常被情绪化地讨论(「自己搭更便宜」/「API 更省心」),但它本质上是一道算术题加三个非成本因素。

盈亏平衡点(自行推导)

API 月成本 = 月 token 量 × 单价 (随用量线性增长)
自托管月成本 = GPU 台数 × 单价 + 人力 + 运维 (阶梯式固定成本)

→ 低用量时 API 稳赢;用量越过某个点后自托管的边际成本优势显现
→ 关键变量是利用率:自托管的卡闲着也要付钱,
  峰谷比越大,自托管的实际单位成本越高

峰谷比是最常被忽略的变量。一个白天峰值 100 QPS、夜间近乎为零的服务,若按峰值配置常驻 GPU,实际平均利用率可能只有 20%——这意味着你的真实单位成本是理论值的 5 倍,盈亏平衡点也随之推后 5 倍。API 的按量付费天然免疫这个问题,这往往比单价对比更重要。

另外三个不进成本表但同样决定性的因素:数据合规(数据能不能出境、能不能给第三方)、延迟确定性(自托管可控,API 受他人容量波动影响)、模型能力(前沿能力通常只有 API 才有)。如果合规要求就是不允许调外部 API,那么这道算术题根本不需要做。

Case Study 案例推演

一次完整的容量推演(匿名化)

内部知识库问答服务 · 从流量画像到卡数

场景:企业内部文档问答,接入 RAG。团队保存了连续 7 天的请求级 trace,并选取工作日最繁忙的 10 分钟原样回放。峰值窗口平均 18 QPS、1 秒桶 P99 为 31 QPS,峰谷比 9:1;每条记录都保留到达时刻、请求类别、输入/输出 token、缓存命中 token、TTFT、TPOT 与结束原因。

18 / 31
10 分钟均值 / 1 秒桶 P99 QPS
3.2K / 11.8K
输入 token P50 / P95
280 / 690
输出 token 均值 / P95
64%
请求命中共享前缀;命中 token 均值 1.1K

长度不是一条「典型请求」。trace 中 70% 是普通问答(输入均值 2.1K / P95 3.4K,输出均值 210 / P95 360),25% 是跨文档调查(输入均值 5.8K / P95 9.2K,输出均值 390 / P95 720),5% 是长报告(输入均值 15.2K / P95 23.6K,输出均值 760 / P95 1.4K)。长报告集中在整点批量到达,正是只看全局 P50 会漏掉的相关性。

候选模型:13B 级别开源模型,INT4 量化后权重示例按 7.5 GB;40 层、hidden 5120,KV cache 按 FP16 计算 per-token 约 800 KB(推导见 1.2)。候选硬件示例为单卡 24 GB。所有数字都是演示假设,不能迁移到另一个模型或框架。

先做解析式体检。峰值窗口的平均输出为 280 token,所以平均输出速率约 18 × 280 = 5,040 token/s。同一 trace 的单卡压测在目标 TPOT 下测得 1,400 token/s goodput,则算力下限约 3.6 张卡。这个数字只回答「显然少于 4 张不可能」,不回答「4 张是否够」。

显存也只先做体检。峰值窗口实测服务时间均值 12.1 秒、P95 29.4 秒,Little's Law 给出平均并发约 18 × 12.1 = 218 条。但不能再拿输入 P50 3.2K 乘这 218 条:长请求驻留更久、会在显存里被过度代表,共享前缀又可能复用物理 KV block。团队因此按每条请求的实际生命周期逐时间步累计 KV block,而不是用一个分位数代替整条 trace。

最终卡数来自完整回放门禁。团队先把门槛写全:TTFT P95 ≤ 1.5 秒、TPOT P95 ≤ 50 ms、goodput ≥ 18 QPS、OOM = 0、服务端拒绝率 ≤ 0.1%。固定模型、框架、并行方式与调度参数后,把同一峰值 trace 回放到 32 / 40 / 48 卡候选池(示例结果,不可外推):32 卡发生 0.6% OOM,goodput 只有 13.9 QPS;40 卡虽无 OOM,但 TTFT P95 为 1.8 秒;48 卡在正常场景中五项均通过。随机失去一张卡的一次实验只能算探索,不能据此声称通过 N−1;48 卡此时只是进入故障矩阵的基线候选。

优化仍由回放定位。时间线显示长报告突发时 KV block 先耗尽,于是团队依次实验 KV cache 量化、减少无关召回文档、提高稳定前缀复用(2.2),并把长报告隔离到低优先级池。每项优化都重新回放同一 trace;不能写成「per-token 减半,所以卡数必然减半」,因为调度、算力与长尾可能随即成为新瓶颈。

优化后再次测试 24 / 28 / 32 卡:24 卡仍违反 TTFT,28 卡在故障注入时不达标。32 卡先在正常回放中通过全部五项,再对每种不同的并行角色 / 故障域逐一移除一个 worker,叠加冷缓存与原始到达顺序,并重复回放;每个 N−1 场景仍通过同一组阈值,才成为本轮示例的最终候选。48 → 32 是一条特定 trace、特定实现与明确故障矩阵上的示例结果,不是容量公式。

最后一步:回头质疑自托管本身。峰谷比 9:1 意味着这 32 张卡在夜间大部分闲置,实际单位成本远高于峰值压测显示的数字。此时仍要比较定时伸缩、峰谷混部、高峰溢出到 API。容量规划的终点不总是「买多少卡」,也可能是「不要自己扛这个峰」。

说明:本案例为综合多个真实项目的匿名化推演,不含任何可识别的公司或个人信息。全部数字均为便于演算而设定的示例假设,计算方法已全部写出——请代入你自己实测的画像与单卡吞吐重算,切勿直接引用本文数字。
Tooling Sidebar 工具层
怎么把这些数量出来(会过期,正文不依赖本区块) 截至 2026-08

单卡吞吐必须实测:厂商标称与论文数字都不能直接用于容量规划。用 vllm bench serve 一类的压测工具,输入你自己的长度分布,在目标 TTFT/TPOT 约束下找出单卡的 goodput——这个数是全部推导的地基。

显存占用要实量:权重、KV cache、激活、框架开销之外,还有 CUDA context、通信缓冲等零碎开销。实际做法是把 --gpu-memory-utilization 设到目标值后启动,观察框架自己报告的 KV cache 块数,反推真实可用容量。

弹性伸缩的现实约束:GPU 实例的冷启动(调度 + 拉镜像 + 加载权重)通常是分钟级,远慢于普通无状态服务。这意味着基于实时负载的自动扩容往往来不及应对突发;更可行的是按可预测的时间规律做定时伸缩,再叠加一层 API 溢出兜底。

成本核算别漏项:除 GPU 本身,还有存储、跨可用区流量、镜像仓库、监控与日志、以及最容易被低估的工程师时间。自托管方案的隐性成本主要在最后一项。

价格属于工具层:本书不给任何具体价格。做决策时以你实际能拿到的报价(含承诺用量折扣、预留实例、竞价实例)为准,并注意这些价格的变化速度远快于本书的更新速度。

Quiz 自测

章末自测

决策题产品要上线代码助手:只知道峰值 40 QPS、输入 P50 6K token、输出均值 150 token,SLA 要求 TTFT P95 < 1.5s。能否据此给出 GPU 数与月成本?请给出可执行的容量规划方案。

不能给卡数。P50 不是平均值也不是上界;缺少输入 P95/P99、输出分布、输入输出相关性、峰内突发、缓存命中、服务时间、模型与硬件,任何精确数字都是伪精确。40 × 150 = 6,000 token/s 只能作为 decode 量级,不能推出 fleet 大小。

先补 trace。至少采集一周请求级到达时间、任务类别、输入/输出 token、缓存命中 token、TTFT/TPOT、结束原因;分别报告均值、P50、P95、P99,并保留原始顺序。若还没有生产流量,用 API 或小规模实例承接影子流量,不用合成的「典型请求」替代。

再建立候选。固定模型、量化、并行与调度配置,用同一 trace 测单卡 goodput 和逐时刻 KV 占用。解析式只用于排除明显不足的配置;候选 fleet 必须回放峰值窗口,并测 TTFT P95、TPOT P95、goodput、拒绝率和 OOM。

门禁决定卡数。对多个 fleet 大小做二分或阶梯测试,再注入 N−1、上游抖动和缓存冷启动。最小的、在正常与故障场景都满足 SLA 的配置才是容量候选。随后实验缩短代码上下文、prefix caching、KV 量化与更大显存卡,每次都重放同一 trace。

最后才算月成本。自托管用「通过门禁的卡数 × 实际报价 + 人力/网络/存储」,API 用请求级输入/输出与真实缓存计价逐条汇总;二者都用整月到达时间序列,不能用输入 P50 代替 token 总量。输出应是带敏感性分析的区间,而不是凭三项摘要统计得到的个位数卡数。

估算题某服务并发 60 条请求,平均序列长度 4K,模型 32 层、hidden 4096、FP16 服务。KV cache 需要多少显存?若要在单张 80GB 卡上跑,还剩多少给权重?

per-token KV = 2 × 32 × 4096 × 2 bytes = 524,288 B ≈ 512 KB(推导见 1.2)。

单请求 = 4096 × 512 KB ≈ 2 GB。60 条并发 = 120 GB。

结论:单张 80GB 卡根本装不下,光 KV cache 就超了 50%。

三条出路:① KV cache 量化到 INT8 → 60 GB,加上权重仍然紧张,但配合更激进的量化可行;② 降低并发到约 30 条 → 60 GB KV + 权重,勉强可行,但吞吐减半;③ 用 GQA 模型——若 KV head 数是 query head 的 1/4,per-token KV 直接降到 128 KB,60 条并发只需 30 GB,问题迎刃而解。

这道题真正想让你记住的:模型选型阶段的架构决策(是否 GQA、层数、hidden 大小)对容量的影响,远大于上线后的任何调参。容量规划应该前移到选型环节,而不是等部署时才发现装不下。

概念题为什么容量要回放峰值窗口 trace,而成本要按整月实际用量估算?两者不一致会导致什么?

容量按峰值窗口 trace:系统必须在最忙的时刻仍满足 SLA;保留秒级突发、长度与服务时间的关联,才能看见排队和 KV 峰值。一个「峰值 QPS」摘要仍不足以代替回放。

成本按整月实际用量:账单取决于实际消耗的总量。API 逐请求计量,自托管则由按峰值配置的常驻资源承担整月成本。

不一致的后果:自托管时,你按峰值门禁配卡,却按整月实际流量产生价值。峰谷比 9:1 只描述最高点与最低点,不能推出平均利用率是 11%;平均利用率必须对整月时间序列积分,并计入批处理、扩缩容和空闲保留。很多「自建比 API 便宜」的测算,正是用峰值时的利用率冒充整月利用率。

正确做法:自托管的单位成本 = 月 GPU 总费用 ÷ 月实际处理 token 数,分母用平均、分子用峰值配置。用这个口径再和 API 单价比,结论常常会反转。

面试题改编「设计一个支撑 1000 万日活的 AI 助手,需要多少 GPU?」

面试官想看的是你会不会先问问题。直接开始算的候选人已经答错了——1000 万 DAU 这个数字本身几乎不含容量信息。

必须先问清的五件事:① 人均调用次数与到达时间序列;② 请求级输入/输出联合分布;③ 缓存命中与服务时间分布;④ TTFT / TPOT / goodput SLA;⑤ 模型、硬件、容错与是否可用 API。DAU 自身不能推出其中任何一项。

然后给出推导框架(而不是一个数字):真实或影子 trace → 算力与平均驻留量级检查 → 固定候选实现 → 多种 fleet 配置回放 → SLA 与 OOM 门禁 → N−1 / 缓存冷启动故障注入 → 整月成本与敏感性分析。

加分项:主动指出优化手段会显著改变结论——prompt caching 命中率、模型路由分流比例、KV cache 量化,每一项都可能让卡数减半;以及主动质疑「是否全部自托管」,提出峰值溢出到 API 的混合方案。

减分项:给出一个精确到个位的数字。容量估算的正确输出是一个带假设的区间,以及「哪个假设变化会最剧烈地改变结论」的敏感性说明。