
做 LLM 性能建模最忌讳的是只盯着一个跑分数字。同一个模型在本地 Mac、单张显卡、A100 集群、不同推理框架里跑速度可能差好几倍。这个差异不是玄学而是由一组可以拆解的因素共同决定的计算峰值、显存带宽、显存容量、参数量、精度、序列长度、批量大小、解码方式。这篇文章要从第一性原理把这些因素拆开先解释一次 LLM 推理的计算和访存到底发生在哪里再给出一套可复现的估算公式最后把单请求、批量、RAG/Agent 场景的性能建模串起来。适合两类人看一类是准备本地部署或写推理调度程序的开发者另一类是想从跑分表背后看出判断依据的算法工程师。1. 先回答一个问题性能建模是在“算”什么先把结论放前面LLM 推理性能不是单一指标而是三个约束共同作用的结果。这三个约束分别是计算量、访存量、显存容量。计算量决定 GPU 算力够不够访存量决定数据搬运快不快显存容量决定中间结果放不放得下。任何一个成为瓶颈性能都会肉眼可见地变差。1.1 跑分能说明什么不能说明什么跑分是结果不是原因。比如某个模型在 RTX 4090 上单卡能跑 60 tokens/s换到 A100 上可能只有 40 tokens/s。只看这个数字你会觉得 4090 更强。但如果你继续观察显存占用和功耗就会发现 4090 的显存带宽更高而 A100 的算力优势在 decode 这种短序列阶段根本没有被发挥出来。这就是性能建模的价值它能告诉你“当前这个数字到底是哪个资源换来的”也能告诉你“如果换一种精度、换一个批大小数字大概会怎么变”。不做建模就只能靠一次次跑 benchmark 撞运气。我一般不会在拿到一个新模型后马上开 benchmark。我会先按后面的公式手算一遍把理论极限算出来。这样实测出来如果只有理论值的 30%说明配置或环境有问题如果已经到理论值的 80% 以上就不要再盲目调参了。1.2 建模的本质把一次推理拆成计算、访存、显存三件事一次 LLM 推理表面上只是“输入一段文本输出一段文本”。但在硬件层面它做了三件事把权重参数从显存中读出来做矩阵乘法这是访存和计算在每一层生成中间激活值这是显存分配把历史 token 的 K 和 V 缓存起来供后续注意力计算使用这也是显存占用。三者互相制约。比如显存不够时你可能会开启 CPU offload但 CPU 访存速度远低于 GPU 显存结果就是计算没满访存先卡住。又比如你想通过加大 batch 提升吞吐但 KV Cache 会随 batch 线性增长很快就碰到显存上限。所以性能建模的第一步不是选一个公式而是先搞清楚当前场景下哪个约束最先到顶。把这个判断做完后面的估算才有意义。Karpathy 的 LLM Wiki 里有一个很核心的思维方式把 LLM 拆回数据、参数、算力、显存这些原材料去看。性能建模走的其实是同一条路。你可以不把每个公式推到非常精确但必须知道哪些量会线性增长哪些量是常数哪些量才是真正的瓶颈。2. 第一层地基参数量、精度和计算量性能建模绕不开的第一个数字是模型参数量。它决定了两个最基本的东西权重占多少显存以及一次前向传播大概要做多少次浮点运算。2.1 模型大小不只是权重体积还包括激活和 KV Cache权重体积的估算是所有后续计算的地基。公式很简单权重显存 ≈ 参数量 × 每个参数占用的字节数以 7B 模型为例FP16 精度下每个参数占用 2 字节所以权重大约 14GB。13B 模型大约 26GB70B 模型大约 140GB。这个数字只是“模型权重”还不包括推理时的激活值和 KV Cache。很多人在本地部署时只看了 7B 模型 14GB觉得 16GB 显存够用。实际跑起来才发现激活值加上 KV Cache 很容易再吃掉 2GB 到 6GB。长上下文或多并发请求时超出显存是常态。所以正确的估算顺序是先算权重再算 KV Cache最后再留出激活值和运行时开销的余量。权重只是底线不是全部。不同模型的参数量只看名字里的 7B、13B 并不准确。同一个 7B架构可能不同层数、注意力头数、KV heads、中间维度都不一样。真正要算的时候应该打开模型的config.json把num_hidden_layers、num_attention_heads、num_key_value_heads、head_dim、hidden_size等字段拿出来再算。2.2 FP16/BF16/FP32/FP8 对速度和显存的实际影响精度直接影响每参数字节数也直接影响硬件计算效率。同样一次计算FP16 的吞吐通常比 FP32 高FP8 在支持它的硬件上又能更高。推理场景中最常用的几个精度如下精度每参数占用典型场景需要留意的点FP324 字节基线测试、部分训练场景显存开销大推理一般不用FP162 字节常规推理和训练数值范围有限但多数模型够用BF162 字节训练和推理动态范围更大大模型常用FP8约 1 字节高吞吐推理需要硬件支持质量会略有波动INT8/INT4约 1 / 0.5 字节显存受限场景通常需要量化校准输出质量要实测FP16 和 BF16 虽然都是 2 字节但对大模型来说BF16 的指数位更多不容易溢出。很多新卡会优先用 BF16 跑推理因为训练时往往也用的是 BF16权重转换成本低。把精度换成 FP8 或 INT8权重体积能直接减半decode 阶段需要从显存读取的字节数也减半。如果场景刚好卡在显存带宽瓶颈上这种精度带来的收益会非常明显。但不要只看显存下降一定要拿同样的 prompt 跑几条样本对比输出质量。有些任务对量化不敏感有些任务则会出现明显的逻辑断裂。2.3 预填充和解码为什么同一个模型有两种速度LLM 推理分两个阶段预填充和解码。这两个阶段的性能特征完全不同。预填充阶段输入 prompt 的所有 token 会一起进入模型做一次长序列的前向传播。因为一次处理很多 token矩阵乘法的规模很大计算密度高这时候主要受 GPU 算力限制。所以预填充阶段通常能跑出很高的吞吐单位时间处理的 token 数量比解码阶段高很多。解码阶段模型逐个生成新的 token。每生成一个 token都要把全部权重重新读一遍然后只产生一个 token 的输出。这个阶段的计算量相对很小数据搬运占了主导因此主要受显存带宽限制。可以用一个粗粒度的公式近似计算量前向传播计算量 ≈ 2 × 参数量 × 处理的 token 数预填充时处理的 token 数是 prompt 长度解码时每次只处理 1 个 token。所以整个生成过程的总计算量大约等于总计算量 ≈ 2 × 参数量 × (输入 token 数 输出 token 数)这个公式忽略了注意力计算、归一化层、激活函数等附加开销但用来做数量级估算已经足够。理解了这两个阶段的区别后很多现象就能解释了。比如同一个模型短 prompt 的首 token 延迟可能很快但一旦 prompt 很长TTFT 会明显增加因为预填充的计算量与输入长度成正比。又比如无论输出多长单 token 生成速度往往相对稳定因为 decode 阶段每个 token 都做差不多的事。3. 第二层地基显存带宽与算术强度算力并不是推理速度的唯一决定因素。很多时候模型跑不快是因为显存带宽不够而不是 GPU 不够强。3.1 解码阶段为什么经常卡在访存以 7B 模型 FP16 为例权重占 14GB。解码阶段每生成一个 token理论上至少要把这 14GB 从显存读一遍。如果一块显卡的显存带宽是 1TB/s那么光读取权重就需要14GB / 1000GB/s ≈ 14ms这 14ms 对应大约 71 tokens/s 的单 token 生成理论上限。如果显存带宽是 500GB/s同样的 14GB 权重需要 28ms速度上限就降到约 35 tokens/s。注意这里还没有算矩阵乘法本身的耗时。很多模型在 decode 时矩阵乘法耗时远小于权重搬运耗时所以最终瓶颈就是访存。这就是为什么一些低算力但高带宽的显卡在某些 LLM 解码任务上并不比高算力卡差太多。算力优势在长 prompt 预填充时更明显访存瓶颈在短输出 decode 时更明显。3.2 用算术强度估算单 token 的理论耗时算术强度是一个很有用的概念它表示每搬运一个字节能做多少次浮点计算算术强度 浮点计算量 / 数据搬运量decode 阶段每个 token 的计算量大约是 2 × 参数量而需要搬运的权重数据大约是权重的字节数。以 FP16 为例每参数 2 字节于是算术强度大约为2N / 2N 1 FLOP/byte这个值很低说明它属于典型的访存密集型任务。prefill 阶段就不一样了。如果一次处理 T 个 token计算量是 2NT但权重仍然只需要读一遍数据搬运量还是 2N 字节算术强度大约等于 TT FLOP/byte当 T 是几百甚至几千时算术强度显著提高系统更容易靠近算力瓶颈。这就是为什么 prefill 阶段通常能跑出更高的 token 吞吐。3.3 判断当前是算力瓶颈还是带宽瓶颈判断方法可以借用 Roofline 的思想。每台设备都有两个理论峰值算力峰值单位是 FLOPS显存带宽峰值单位是 byte/s。两者相除可以得到一个平衡点平衡点 算力峰值 / 显存带宽峰值如果算法的算术强度低于平衡点就是带宽瓶颈如果高于平衡点就是算力瓶颈。举例来说某张卡的算力峰值是 100 TFLOPS显存带宽是 2TB/s平衡点就是 50 FLOP/byte。decode 阶段算术强度约 1远低于平衡点所以瓶颈是带宽。prefill 阶段如果一次处理 1024 个 token算术强度约 1024高于平衡点所以瓶颈是算力。实际判断时不需要把数值算得特别精确重点是理解方向提高 decode 速度优先考虑减少读取权重的字节数比如量化、投机解码、KV Cache 优化提高 prefill 速度优先考虑提升矩阵乘法的算力利用率比如选择更大的 batch、调整并行策略、启用 Tensor Core。4. 第三层地基显存容量和 KV Cache性能建模时显存容量经常被放在最后看但它往往是第一个让人翻车的点。算力再高、带宽再大显存放不下就没有意义。4.1 显存拆三类权重、激活、KV Cache推理时显存主要被三类数据占用。第一类是模型权重。这个最简单参数量乘以精度字节数。第二类是激活值。每层前向传播都会产生中间张量长度越长、batch 越大激活值越占空间。不过现代推理框架会用 FlashAttention、激活重计算、内存复用等技巧来压这部分开销所以建模时可以先粗略估计。第三类是 KV Cache。这是长上下文场景下最容易超出显存的部分。注意力计算需要保存历史上所有 token 的 K 和 V每个 token 都会保留。序列越长KV Cache 越大并发越批越大KV Cache 越大。很多部署事故不是模型放不下而是连续请求一多KV Cache 把显存打满然后触发 OOM 或强制重新加载。所以 KV Cache 是容量建模的重中之重。4.2 手算一个序列的 KV Cache 占用KV Cache 的计算公式可以写成每个 token 的 KV Cache 字节数 2 × 层数 × KV 头数 × head_dim × 精度字节数其中乘 2 是因为要分别存 K 和 V。如果采用 GQA 或 MQAKV 头数会小于注意力头数KV Cache 会明显更小。举一个接近常见 8B 模型的例子。假设层数是 32KV 头数是 8head_dim 是 128精度是 FP162 × 32 × 8 × 128 × 2 131072 字节也就是大约 128KB 每 token。如果上下文长度是 8192那么单条请求的 KV Cache 大约是128KB × 8192 1GB这只是一个请求。如果 batch 是 4就是 4GBbatch 是 8就是 8GB。再加上权重和激活值显存压力会非常大。如果模型是 MHAKV 头数直接等于注意力头数比如 32 或更多KV Cache 会比 GQA 大很多。所以判断模型架构时不能只看参数量还要看num_key_value_heads。4.3 容量不够时怎么调整量化、Batch、并行和持久化上下文显存不够时有几种常见调整路径。方案收益代价权重从 FP16 换成 INT8/INT4权重显存减半或更低输出质量可能下降需要实测KV Cache 换成 INT8/FP8长上下文场景显存压力降低可能引入量化误差减小 batch sizeKV Cache 占用线性下降吞吐下降缩短 max sequence lengthKV Cache 占用线性下降无法支持长输入张量并行权重和 KV Cache 拆分到多卡卡间通信开销小模型反而不划算启用 PagedAttention减少 KV Cache 碎片化浪费需要框架支持前缀共享或持久化 KV Cache重复 prompt 不重复计算只适合有稳定前缀的场景我自己验证时有个习惯先不开任何优化用默认 FP16、batch 1 跑通再逐步加量化、加 batch。如果一上来就直接开 INT8 和连续批处理一旦出问题你很难判断是容量问题、精度问题还是调度问题。低配机器上“能跑”不代表“适合批量”。你可能用单 batch 跑通了但一旦把并发调大KV Cache 和激活值会同时上涨OOM 可能马上出现。这种情况下先算一下 KV Cache 的理论占用再决定并发上限比反复试错更有效率。5. 从单请求到批量时延、吞吐和排队单请求的性能只能说明这个模型在这块硬件上的“裸速度”。真实业务通常会同时处理多个请求所以还需要关注时延、吞吐和排队。5.1 单请求指标 TTFT 和 TPOT与 LLM 推理直接相关的两个时延指标TTFTTime To First Token从请求进入到生成第一个 token 的时间TPOTTime Per Output Token每生成一个额外 token 的耗时。总时延可以近似为总时延 ≈ TTFT 输出 token 数 × TPOTTTFT 主要反映 prefill 阶段速度受输入长度、算力和模型并行策略影响。TPOT 主要反映 decode 阶段速度受显存带宽、权重读取量、量化程度影响。如果是交互式聊天场景TTFT 很重要用户会明显感知到“等多久开始出字”。如果是离线批量生成TTFT 没那么敏感TPOT 和总吞吐更重要。5.2 批量推理的收益与代价批量推理的核心收益是权重复用。多个请求同时解码时每个 token 仍然要读取权重但权重只会从显存读一次供多个请求的矩阵乘法共用。这样总吞吐能提升很多比如单请求 50 tokens/s8 并发可能到 200 到 300 tokens/s 的总吞吐。代价是每个请求的单个 token 延迟会变长。因为共享同一个 GPU 的算力和带宽batch 越大每一轮 decode 需要处理的矩阵乘法规模越大单请求的 TPOT 可能从 20ms 涨到 60ms 甚至更高。最终要根据业务目标做取舍是要更快的单用户体验还是要更高的总吞吐。现代推理框架中的 Continuous Batching 会动态调度请求没生成完就插入新请求让 GPU 的空白槽位尽量被填满。这能显著提高吞吐但也让“并发数”和“时延”的关系变得不是固定的。建模时不能假设每个请求都会独享整块 GPU。5.3 容量规划用一个简单队列估算并发上限不需要复杂排队理论也能做一个粗略的容量判断。假设单请求的 decode 速度是 50 tokens/s平均每个请求生成 500 个 token那么单个请求的平均处理时间大约是500 / 50 10 秒如果要求系统每秒钟完成 2 个请求平均任务时长是 10 秒那么在稳定状态下系统中同时存在的请求数量大约是2 × 10 20这就是最简单的 Little’s Law并发数 吞吐率 × 平均处理时间。20 个并发请求会带来多少 KV Cache 占用要看平均上下文长度。如果每个请求峰值上下文是 4096 tokenKV Cache 就是单请求 512MB 左右20 个并发就是 10GB 以上。这个计算能快速判断一个 24GB 显卡是不是真的能支撑目标流量。实际部署时还要留出余量不能把显存用满。我一般会按理论值的 70% 到 80% 设上限。因为请求的实际 token 长度会有波动输入和输出的长度也不会完全均匀。5.4 Agent、RAG 和框架场景下的性能建模很多 LLM 应用不是单次调用而是多次调用。RAG 先检索再把检索内容拼进 prompt然后生成Agent 可能要调用工具、看工具结果、再重新生成。每一次调用都可能把历史内容和工具结果重新拼进去导致 prompt 长度不断增长。这种情况下真正耗时的不是框架本身而是多次调用累积的 prefill 开销。每次工具返回后上下文都会变长模型要重新处理新增内容KV Cache 也可能要重新计算。编排框架比如 Spring AI、MCP、LangChain 这一层主要是请求管理和协议开销通常不是主要瓶颈。但如果不把“调用次数”和“上下文增长曲线”纳入建模只按一次推理估时延实际结果会差很多。我的建议是对这类应用要单独建一个调用链清单每次调用的输入 token 数大概是多少每次调用的输出 token 数大概是多少整个任务最多串行多少次调用是否每一次都要重新传入完整历史。把这些数字加起来再回到前面的公式估算才能得到接近真实体验的时延。很多 Agent 应用看起来“慢”不是单次推理慢而是同一个任务里反复调用了 3 到 5 次模型每一次都在新增 token 上重新推理。6. 实操先跑单条再开批量最后上服务理论公式很重要但最终还是要落地。我建议把性能建模做成一张清单每个新模型、新硬件都按这个流程走一遍。6.1 建一个最小性能模型清单准备阶段先收集以下信息类型需要确认的参数硬件算力峰值、显存带宽、显存容量、CPU 内存大小模型参数量、层数、注意力头数、KV 头数、head_dim部署精度、是否量化、batch size、max_seq_len、是否启用 KV Cache 量化请求平均输入 token、平均输出 token、并发数、峰值长度然后按下面的顺序估算算权重显存算单请求 KV Cache 显存乘以预估并发得到峰值 KV Cache用带宽估算 decode 阶段的 TPOT用算力估算 prefill 阶段的 TTFT加起来得到总时延用并发和平均时长判断容量上限。这套流程不需要复杂工具一张纸、一个计算器就能完成。关键是让每个数值都有来源而不是拍脑袋。6.2 用实测数据校准估算误差估算公式是理论值实测时一定会打折扣。校准方法是先跑最小样例模型7B/8B 级别 精度FP16 或 BF16 batch1 max_new_tokens50 到 100 prompt一段 512 token 左右的文本记录四个数据首 token 延迟、每秒生成 token 数、峰值显存、总耗时。然后和理论值对比。如果 TPOT 实测比理论值差很远先看显存带宽利用率和 GPU 利用率。如果 GPU 利用率只有 30%说明模型可能没吃到足够数据量或者框架调度有问题。如果 GPU 利用率已经很高但速度还是不行那说明瓶颈不在软件而在硬件规格。实测速度接近理论值时再做批量实验。批量实验要重点观察显存是否随着并发增长单请求 TPOT 是否明显变慢总吞吐是否持续上升直到不再增长。如果总吞吐已经不再上升说明已经碰到硬件上限再增加并发只会让每个请求更慢。6.3 常见误判和排查链路性能问题很多时候不是模型问题而是前置条件没有检查干净。我习惯按下面的顺序排查先看现象。是 OOM、TTFT 慢、TPOT 慢、并发后变慢还是输出为空。再看输入。prompt 长度是否超预期是否有特别长的尾部是否因为工具结果让上下文暴增。再看环境。显存占用、GPU 利用率、CPU 负载、磁盘读写、网络传输是否异常。再看参数。精度、batch、max_seq_len、KV Cache 量化、并行度是否设置合理。最后再看框架和版本。推理框架是否支持 Continuous Batching是否启用了 PagedAttentionCUDA 或 ROCm 版本是否匹配。最常见的几个误判只看权重显存不看 KV Cache结果并发一高就 OOM只看 GPU 算力不看显存带宽以为换了旗舰卡 decode 就一定更快只测单并发直接用它估算生产环境忽略了排队和共享瓶颈看到量化后变快就以为没有质量损失没有跑足够样本对比一报错就改参数不先看日志。比如 prompt 里带非法字符或超长字段也会导致任务异常。我遇到过很多次看起来像“模型太慢”的问题最后发现是输入的 padding 太长或者框架把很多空 token 也当成有效 token 在计算。输入清洗这一步对性能的影响经常被低估。6.4 在 Agent/RAG/编排框架场景下怎么用这个模型如果使用 Spring AI、MCP、LangChain 这类编排层不要只看单个 LLM 请求的性能还要关注整条调用链。每个工具调用都会带来新输入每次拼接上下文都可能触发一次新的 prefill。对这种场景我会额外做一张调用链表第 1 次调用输入 1000 token输出 200 token第 2 次调用输入 1500 token输出 300 token第 3 次调用输入 2000 token输出 500 token。然后把每次调用的估算时延累加得到这个任务的平均总耗时。只有这样做才能判断优化点到底该放在模型侧还是应用侧。如果大部分耗时来自某一两次长上下文 prefill那么解决思路可能是压缩 prompt、做前缀缓存、或者把不必要的历史内容截断。如果耗时主要来自 decode 次数多那么优化思路可能是让模型输出更短的中间结果或者减少不必要的工具调用。这个建模清单我在实际项目里用了很多次。每次换模型、换显卡、换推理框架都先按一遍再上真实验证能省掉大量瞎试的时间。性能建模不保证每一毫秒都能预测准但它能帮你快速排除掉一大半错误的方向。最后真正决定系统能跑多快的往往不是算力表上最高的那个数字而是显存、带宽、序列长度和并发之间的四个乘数。