1. 基准测试只能告诉你“现在多快”,不能告诉你“为什么快”
如果你跑过一个参数规模稍大的模型推理服务,应该碰到过这样的场景:同一个模型,在显卡 A 上速度不错,换到显卡 B 上却慢了一截;同一个服务,前几个请求响应很快,上下文变长之后却肉眼可见地变慢;官方文档里写的吞吐数字,在自己的业务数据上一测,差了不止一倍。
这些问题如果只是靠反复测试去定位,效率会非常低。因为你每次调整参数、切换框架、换部署环境,都得重新跑一遍压力测试。测试结果不稳定,还会遇到网络波动、并行任务互相抢占、数据前后处理耗时等干扰因素。就算你把面板数据截图截下来,也只能证明“这一秒钟它跑成了这样”,说明不了为什么,更预测不了下一次调整之后会变成什么样。
这就是我强调“从第一性原理建模”的原因:性能建模不是给你一个精确的数字,而是给你一条从底层机制推导到上层指标的计算链条。
链条大致长这样:
- 模型要执行哪些计算;
- 每个 token 经过模型时,理论上消耗多少浮点运算量;
- 这些计算在硬件上分发时,瓶颈是算力、显存带宽,还是内存容量;
- 自回归解码的串行依赖,是怎么把理论算力拖到实际水位之下的;
- 模型规模、上下文长度、批处理大小,分别会推高哪一项成本。
只要链条理解对了,很多性能现象不需要实测就能解释个七八成。比如“长上下文后变慢”,这几乎不可能是算力不够,更像是注意力计算量随长度平方增长,加上 KV 缓存挤占了显存带宽。这两个原因在模型里是分开的,但表现都是“变慢”。如果你不建模,只盯着压力测试数字去看,很容易把两个问题当成一个处理。
所以我的第一个建议是:不要丢掉基准测试,但也不要把它当成第一工具。基准测试是用来“验证模型预测”的,不是用来“发现规律”的。真正能帮你做选型和调优的,是一个能解释因果的计算模型。
当然,第一性原理建模不是万能药。要把它用起来,你得先理解几个核心概念:FLOPs、HBM 带宽、自回归延迟、KV 缓存、缩放规律。下面逐个展开,讲清楚它们分别卡住的是哪一段性能。
2. 先把计算成本算清楚:一个 token 经过模型时到底消耗了多少 FLOPs
2.1 Transformer 的两大部分:注意力层和前馈层
要给 LLM 性能建模,第一步是准确估算计算量。虽然现代 LLM 在结构上各有各的变体,比如不同的归一化方式、不同的注意力变体、不同的激活函数,但绝大多数主流模型仍然基于 Transformer 的基本架构。一个 Transformer 层基本由两部分构成:
- 自注意力模块:将输入向量投影成 Q、K、V,然后做注意力计算,再投影输出;
- 前馈网络模块:先升维,再激活,最后降维。
从矩阵乘法的角度看,这两部分的计算量大头都比较好统计。对于一个隐藏维度为 d 的模型,单个 token 经过单个 Transformer 层时的乘法次数是:
- 自注意力的 Q/K/V 投影和输出投影:4 个 d x d 的矩阵乘法;
- 前馈网络:通常从 d 升维到 4d,再降维回 d,相当于两个 d x 4d 和 4d x d 的矩阵乘法;
- 加上嵌入层映射和最后的输出映射,也可以摊到每个 token 上。
把这些系数合并起来,会出现一个被反复引用的近似结果:一个基于 Transformer 的稠密语言模型,每个 token 的前向传播大约需要 2N 次浮点运算(FLOPs),后向传播大约是它的 2 倍,也就是 4N。训练一个 token 的总计算量约为 6N。这里的 N 是模型参数数量。
2.2 为什么“每 token 约 2N FLOPs”有参考价值
这个公式很粗,但在工程估算阶段非常有效。它把模型结构、层数、头数等细节全部折叠成一个参数总数 N,让不同模型之间可以直接对比。
举例来说,一个 7B 参数量模型,每处理一个 token 的前向推理,理论计算量约是 14G FLOPs。如果显卡能持续提供 30TFLOPs 的有效算力,那理想情况下每秒钟约能处理 2000 个 token。真实场景当然达不到这个值,因为还有访存、调度、内核启动、PCIe 传输等各种开销。但是用这个数量级去反推,你至少能判断一个推理框架的性能到底还有多少提升空间。
很多人在部署模型时看到一个“每秒生成 20 个 token”的数字,觉得已经不错了。但用上面的方法一算,就会发现真实利用率可能只有百分之几。这不是“模型太大跑不动”,而是瓶颈根本没有被定位到正确的资源上。
需要说明的是,这个近似公式对 MoE 模型要分开处理。MoE 的参数量里只有部分参数在每次前向推理中被激活,所以严格来说:N 应该替换成“单次推理实际激活的参数数”,而不是总参数量。否则算出来的理论吞吐会严重偏低。
2.3 注意力计算:一个容易忽略的平方项
除了粗粒度的大头 FLOPs,注意力模块内部还有一项二次复杂度:序列长度 T 的平方。标准的自注意力机制需要为每个 token 计算与其他所有 token 之间的相似度。对于一个长度为 T 的序列,注意力分数矩阵的大小是 T x T,计算量随 T 的平方增长。
在短序列场景下,这个平方项不算什么。但序列长度从 2K 涨到 8K、再从 8K 涨到 32K 时,注意力部分的计算量就变成不可忽略了。这也是为什么近两年的推理框架和模型架构都在想办法缓解长上下文开销,比如各种线性注意力方案、稀疏注意力、KV 缓存复用、分页 KV 等。
如果想把模型性能建模得更准,建议把注意力计算单独列出来:
- 对于标准多头注意力,注意力分数与权重矩阵的计算量约是
4 * L * d * T的线性项,以及2 * L * d * T^2的平方项,其中 L 是层数,d 是隐藏维度; - 在实际工程中,真正更容易成为瓶颈的其实是 KV 缓存带来的显存占用,这个放在后面说。
写代码的时候,你可以用一个简单的函数快速估算不同配置下的理论 FLOPs:
def estimate_training_flops(num_params: int, num_tokens: int) -> float: """粗略估算稠密 Transformer 训练一个 token 集所需的 FLOPs""" return 6.0 * num_params * num_tokens def estimate_inference_flops_one_token(num_params: int) -> float: """估算一次前向解码耗时, 单位是 FLOPs/token""" return 2.0 * num_params这段代码不是为了做精确仿真,而是为了让“性能预测”变成可以用数字快速比较的东西。后面所有经验法则,几乎都能挂在这两个公式之上。
3. 从 FLOPs 到真实耗时:内存带宽常常才是真正的瓶颈
3.1 为什么算力那么高,模型却跑不快
理论 FLOPs 只是第一步。真正部署时要回答的问题是:这个计算量需要多少时间?
如果模型权重完全驻留在显存里,并且每一步计算都只跟算力相关,那问题很简单:耗时 = FLOPs / 算力。但现实中一个被广泛忽略的因素是:模型每前向一次,都要把所有参与计算的权重从显存搬到计算单元里。搬移速度受显存带宽限制,而带宽通常远低于算力的增长速率。
所以推理性能的下限往往由访存决定:
单 token 理论最小耗时 ≈ 参数量 × 每个参数字节数 / 显存带宽举例来说,一个 7B 模型如果使用 FP16 权重,占用约 14GB。假设显存带宽为 800GB/s,那么每生成一个 token 最少需要14GB / 800GB/s ≈ 17.5 毫秒,换算下来上限大约为 57 token/s。如果实际推理速度离这个上限还很远,说明主要问题不在权重访存,而可能在注意力计算、内核启动、框架调度或显存容量不足导致的缓存抖动上。
现实情况是,很多推理框架已经能逼近这个带宽上限。如果模型是 70B 级别,FP16 权重约 140GB,单 GPU 的带宽再高也难以承载,必须做多卡张量并行。此时每块卡处理一部分权重,总带宽增加,但还要额外考虑卡间通信。通信比例会随着并行度提高而上升,到了一定规模后,“多卡一起算”带来的收益会被通信开销抵消,继续加卡并不能线性提升速度。
3.2 自回归解码:一次只能生成一个 token 的串行约束
另一个关键约束是自回归解码。LLM 推理是一个“逐步生成”的过程,每一步生成一个 token,前一步的结果是后一步的输入。这个串行依赖关系使得你没办法像训练那样,把一批短序列整体并行算完。
它的影响体现在两个方面:
- 延迟变高:第一个 token 需要处理完整的输入上下文,之后每新增一个 token,都需要重新计算整个上下文上的注意力分布。理论上每一步的 QKV 投影可以复用前一步的中间结果,但实际工程要做缓存和优化,复杂度不低;
- 吞吐受限:如果单次只处理一个请求,GPU 的算力利用率会非常低。要提高吞吐,通常要引入批处理。批处理能让 GPU 同时在多个请求上执行矩阵乘法,从而摊薄固定开销,提高算力利用率和吞吐量。
批处理也不是越大越好。batch size 增大后,KV 缓存的大小会线性增长,显存占用快速上升。一旦超过显存容量,性能会断崖式下跌。所以,在做容量规划时,有两个数一定要先算:
- 单条序列在给定上下文长度下的 KV 缓存大小;
- 批处理 N 条序列时 KV 缓存总占用。
KV 缓存的估算公式大概是:2(K和V) × 层数 × 注意力头数 × 头维度 × 序列长度 × 每个元素字节数。折叠成参数语言,它跟模型层数、头数、隐藏维度、上下文长度都相关,不能只用参数量 N 估算。
3.3 算力、带宽、容量三者决定的性能区间
把 FLOPs、带宽、KV 缓存放在一起,一个实际推理场景的性能边界就比较清楚了:
| 资源维度 | 关键问题 | 典型瓶颈表现 |
|---|---|---|
| 计算量(FLOPs) | 单 token 计算量大不大 | 算力利用率长期很低 |
| 带宽 | 权重和 KV 缓存加载快不快 | 单 token 耗时逼近带宽上限 |
| 显存容量 | KV 缓存和并发请求会不会爆显存 | 批处理一加大就 OOM 或急剧变慢 |
调优时先判断当前最接近哪个边界,再决定优化方向。如果接近带宽边界,可以考虑量化、权重共享、减少 KV 缓存占用,例如用 INT8/INT4 量化或滑动窗口注意力;如果还有大量算力余量,则可以做更大的批处理或提升并发;如果显存容量不足,则需要调整上下文长度、批处理大小,或用更高效的位置编码和缓存淘汰策略。
这也是为什么“第一性原理建模”能帮助定位瓶颈:它先把资源维度拆开,再根据实际测试数据做判断。你不需要在 GPU 面板上盯着一堆看起来很复杂的指标,只需要回答三个问题:它的理论上限多少?当前距离哪个上限最远?现在是在浪费算力还是在逼近带宽?
4. 缩放规律:模型能力与性能预测之间的桥
4.1 损失函数与参数、数据之间的幂律关系
前面讲的都是“跑得快不快”,但性能建模还有一个更重要的维度:这个模型到底能学到什么程度?如果在部署前无法预估更大模型带来的收益,你就很难回答“该选 7B 还是 70B”的问题。
缩放规律来自 Google 等在 2020 年前后做的系统研究。核心观点是:在计算预算固定的前提下,模型参数量 N、训练数据量 D、最终的交叉熵损失 L 之间存在近似幂律关系:
- 参数规模越大,损失越低;
- 训练数据越多,损失越低;
- 两者之间存在一个平衡点,而不是简单地“一味加大模型或一味加数据”。
这个规律在学术界已经有较多验证,常被引用的结论是:模型参数量和训练数据应该按大致相同的比例增长。对于一个受限的计算预算,更合理的做法通常是同时扩大参数和数据,而不是只堆其中一个。
4.2 从缩放规律得到三条实用判断
把这个规律落实成工程实践,能得到三条值得记住的判断:
- 小模型在小数据上训练,效果不会因为模型足够小而忽略。小模型需要足够的数据才能发挥出自身容量。数据不够的时候,损失下降会非常慢;
- 大模型如果数据不够,多出的参数会形成冗余,不会等比例转化为能力。所以直接买一个已经用大量数据训好的大模型 API,往往比自己在小数据上微调一个大模型更靠谱;
- 推理成本和模型能力的权衡是显式的。参数扩大一倍,前向推理的 FLOPs 大致翻倍,吞吐也会明显下降;但如果缩放规律显示能力提升不显著,那不如选已经充分训练的小模型。
这也是很多项目在实践中采用“小模型优先”的原因。不是小模型比大模型强,而是小模型在有限成本和有限算力约束下,更容易达到“够用”的水平。判断“够不够用”,当然需要基准测试;但判断“提升多少才算值得”,就需要缩放规律给出的趋势作为参考。
4.3 数据质量:缩放规律没说清楚的部分
这里要特别指出缩放规律的边界。它假设训练数据是从同一分布中充分采样,但真实场景几乎不会满足这个假设。数据质量、重复度、领域覆盖、去重策略、预处理管线、课程学习顺序等,都会让实际的 loss 曲线偏离理论规律。换句话说,用缩放规律做“能力天花板”估算可以,用它预测“某个数据清洗方案能带来多少提升”,就超出了它的适用范围。
更稳妥的说法是:缩放规律适合做规模化判断,不适合做具体训练计划的精度预测。先用它选一个参数规模的档位,再用小规模实验来验证数据和超参策略,最终上线前用目标评估集做一次完整测评。
这样组合起来的做法,本质上就是“第一性原理趋势 + 小样本验证”的闭环。它对性能建模的价值,不只是预测,更是帮助团队在决策之前就砍掉明显不合理的选项,避免把大量预算花在大概率无效的路线上。
5. 拿一张纸搭起性能模型的骨架:从单次请求到批处理
5.1 三步建模法:先定输入,再定瓶颈,最后实测校准
当你要给自己手头的应用做一个性能模型时,我建议先用三步法搭一个粗糙版本,而不是一上来就问“用什么工具做压力测试”。
第一步:定义任务形态。是单轮问答、多轮对话,还是长文档摘要?输入 token 和输出 token 的典型数量各是多少?这些数字直接决定计算量和 KV 缓存占用。
第二步:写出瓶颈等式。根据当前目标平台,先分别估算:在给定参数量和量化位宽下,单 token 的前向耗时是多少;在给定序列长度下,KV 缓存需要多少显存;在给定 batch size 下,注意力计算量会不会超过权重访存的时间。把这三个数算出来,就能知道瓶颈大概率落在哪里。
第三步:在机器上做一次最小验证。不用压测工具跑复杂场景,只跑一个几十条请求的样例集,记录真实的输入 token 数、输出 token 数、时延。把实测值跟模型预测值放在一起看,差值太大的部分就是值得深入研究的地方。
一个常见的建模表格可以这样设计:
| 配置项 | 模型参数 | 量化位宽 | 输入长度 | 输出长度 | batch size | 预测时延 | 实测时延 | 差异原因 |
|---|---|---|---|---|---|---|---|---|
| 本地小模型 | 1.5B | FP16 | 512 | 200 | 1 | 待估 | 待测 | 待定位 |
| 本地大模型 | 7B | INT8 | 512 | 200 | 1 | 待估 | 待测 | 待定位 |
| 服务端并发 | 7B | FP16 | 1024 | 512 | 16 | 待估 | 待测 | 待定位 |
这张表的价值不在于第一行算得准,而在于它逼着你在测试之前明确边界条件。很多人做性能测试时,只记录“速度是多少”,遗漏了输入长度、输出长度、batch size、上下文窗口等关键信息,结果测试数据完全不可复用。
5.2 单 token 验证法:最稳定、最该先做的小样本测试
性能建模有一个容易出错的地方:一上来就测“每秒处理多少个请求”这种大吞吐指标。这个指标受并发数、排队、前后处理影响很大,波动也大,不适合作为第一轮的判断依据。
我更建议先测单 token 延迟。做法很简单:固定一个请求,输入固定的 prompt,连续生成固定数量的 token,记录每次生成一个 token 的平均耗时。这个数字主要反映模型前向推理的底层速度,受调度影响小,多跑几次就能得到一个比较稳定的参考值。
拿到单 token 延迟后,再乘以输出长度,得到纯模型生成耗时;然后加上输入 prompt 的 prefill 耗时,得到一个近似单请求总时延。最后再加入并发和批处理的影响,估算整个服务的吞吐能力。这个顺序是从稳定、可控的小指标逐步扩大到复杂指标。如果一开始就测复杂指标,定位问题会难得多。
5.3 小样本验证后,需要检查哪些环节
以下是我在实际项目中常用的一组检查顺序,适合作为排查链路使用:
- 看现象:是速度慢、延迟高、吞吐低,还是显存不稳定;
- 看输入:输入 token 数是多少,上下文长度有没有隐性增长,prompt 里是否包含超长文档;
- 看配置:批处理大小、并发数、量化位宽、精度设置;
- 看资源:显存占用是否接近上限,GPU 利用率是否长期很低,CPU 是否有瓶颈;
- 看日志:是否有重算、重复加载、缓存未命中、频繁重新分配显存;
- 看框架版本:同一模型在不同推理框架或不同 CUDA 版本下的性能差异可能很大。
其中容易出问题的是第一和第二步。很多“突然变慢”的现象,其实是输入 prompt 变长导致的注意力计算量增加,而不是模型或框架出了问题。遇到这种情况,先查输入长度和 KV 缓存,再考虑优化结构。
5.4 一个可复用的性能估算函数
到这里,你可以把前面所有概念浓缩成一个非常简单的估算流程。以推理单 token 为例:
def estimate_one_token_latency_ms( params_b: float, # 参数量,单位十亿 precision_bytes: float, # 每个权重占用的字节数 bandwidth_gbs: float # 显存带宽,单位GB/s ) -> float: weight_size = params_b * 1e9 * precision_bytes / 1024**3 latency_ms = weight_size / bandwidth_gbs * 1000 return latency_ms # 示例:7B模型,FP16,A100约1.5TB/s print(estimate_one_token_latency_ms(7, 2, 1500)) # 约9.3ms这个函数没有处理注意力计算、层数、序列长度,所以不要拿它做精确预测。它真正的价值在于给“模型能不能跑得动”做一个快速判断。把精度从 2 字节降到 0.5 字节,单 token 耗时能大幅下降;把参数量从 7B 提到 70B,耗时也会倍增。模型部署前的选型讨论,用这个数量级先对齐预期,会顺畅很多。
6. 建模的边界与长期维护:不要把近似公式当成万能法则
6.1 哪些情况会让模型预测失效
第一性原理建模在不同场景下的准确度差别很大。在单 GPU、单请求、模型权重能够完全放入显存、没有复杂并行依赖的场景下,预测能相当精准。但在下面这些情况下,模型会失效或者至少偏差很大:
- 多卡并行时,通信开销占比可能比计算出力还高;
- 使用投机解码、KV 缓存复用、前缀缓存、动态批处理等优化手段后,实际 token 生成流程已经不再是简单的前向计算;
- MoE 模型的实际激活参数远小于总参数,直接用总参数量会严重高估延迟;
- 服务端有排队、限流、多租户共享资源时,单个请求的时延和吞吐都不能从单次前向推导出来;
- 不同框架对算子融合、内存分配、内核编译的优化程度差异大,同一模型在不同框架下耗时可能差好几倍。
所以,能建模是好事,但建模前一定要确认边界条件。更安全的做法是:把它当成“估算上限和数量级”的工具,不要当成精确仿真器。
6.2 性能模型需要持续校准,而不是一次性建完
很多团队第一次建性能模型时热情很高,模型跑几轮,数据表也整理好了,但一个月后就过期了。原因是模型换了、推理框架升级了、数据分布变了、硬件环境调整了,所有实测数据都失去参考意义。性能建模不是一次性任务,它是一个需要持续维护的“活文档”。
我的建议是:
- 把关键配置记录在版本管理工具里,包括模型版本、框架版本、CUDA 版本、量化位宽、batch size、输入长度分布;
- 每次模型或环境变化后,至少重新跑一次最小样例,更新性能基线表;
- 不只记录平均值,还要记录 P95 和 P99,因为 LLM 推理服务的 tail 延迟经常比平均值重要得多;
- 把建模用的脚本和配置也纳入版本管理,避免下次重建时找不到计算口径。
从工程经验看,真正能长期用下去的性能模型,靠的不是一次做多精细,而是有了一套可重复、可更新、可对照数据变化的流程。单次测量结果会过期,但一套标准化流程不会。
6.3 最终判断:把模型当作你的“决策前试验场”
回到开头的问题:为什么值得从第一性原理建模?
因为 LLM 在应用层的性能问题,绝大多数都不是某个单独环节的简单故障,而是多个变量相互作用的结果。只靠基准测试和面板截图,你很难快速回答“如果我把 7B 换成 13B,并发从 4 调到 8,输入从 512 扩展到 2048,最终会给用户带来怎样的延迟变化”。这类问题要等实际部署之后测量才知道,代价很高。
而第一性原理建模给出的,是一个可以在部署前做沙盘推演的框架。你可以在 Excel、脚本或者脑内,先把这些变量按资源维度换算成数量级。它不会给出精确数字,但能帮你排除明显不可能的选项,定位最值得优化的方向,并在小样本验证后逐步逼近真实值。
用一句话来总结这篇文章的核心观点:别把第一性原理建模当成一个精确预测工具,而要把它当成一种压缩问题的方式。它把“LLM 性能为什么是这样”压缩成几条资源约束和几个可以计算的公式,让你在做选型、调优、容量规划时,不再依赖反复跑测试去碰运气。这不是最炫酷的技术,但大概率是长期最省力的那一种。