这次我们聊一个比较硬核的话题:如何从第一性原理给 LLM 性能建模。
先解释一下什么叫“第一性原理”。说白了就是不看厂商宣传、不看云平台给的抽象参数,直接回到 LLM 推理时硬件上到底发生了什么:模型有多重、显存要多大、一次请求要算多少次浮点运算、数据搬运需要多长时间。把这些底层物理量算明白,你就能在买显卡、配服务器、调推理参数之前,先判断一套方案到底靠不靠谱。
这个思路至少能解决三类问题:
- 选型:你手上的 GPU 到底能不能跑某个模型,能跑多快,是看显存够不够,还是看算力够不够。
- 容量规划:支持多少并发、上下文要开多长、要不要开量化,这些都可以在部署前算个大概。
- 瓶颈定位:线上推理服务变慢了,到底是卡在计算,卡在显存带宽,还是卡在 KV Cache 溢出导致的显存交换。
这篇文章会从 LLM 推理的两阶段机制讲起,然后给出显存占用、推理时延、吞吐量的估算公式,再结合精度选择(FP16/BF16/INT8/INT4)、批量推理、并发设计这些工程问题做展开。最后给出一套用 Python 做性能估算的示例脚本,以及常见问题的排查思路。整个过程不需要实测数据,也能得到八九不离十的数量级判断。
如果你正在做本地部署、模型服务化、或者准备给团队做 GPU 采购评估,这篇文章建议直接收藏。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 核心目标 | 从模型参数、硬件规格推导 LLM 推理的显存占用、时延和吞吐量 |
| 适用对象 | 本地部署、推理服务化、GPU 选型、训练/推理成本评估 |
| 关键指标 | 模型参数量、上下文长度、KV Cache 大小、算力 FLOPS、显存带宽 |
| 推理阶段 | Prefill(预填充)与 Decode(逐 token 生成) |
| 精度模式 | FP32、FP16、BF16、INT8、INT4 对显存和性能的影响 |
| 可扩展方向 | 批量推理、并发服务、量化、投机采样 |
| 依赖工具 | Python、PyTorch、可选 CUDA 环境,统计脚本 |
| 是否支持一键启动 | 不涉及具体模型服务,属于理论估算方法,可直接用 Python 脚本运行 |
| 适合读者 | 算法工程师、推理工程团队、 DevOps、GPU 采购决策 |
注意,这篇文章不是某个具体开源推理框架的使用教程,而是把 LLM 性能分析的方法论拆开讲清楚。你可以把文中公式和脚本应用到自己实际的模型、显卡和场景里。
2. LLM 推理的底层机制:为什么两阶段性能表现差这么多
在写公式之前,先要建立一个基本认知:LLM 推理不是像图像分类那样“一次前向”就结束的。它分两个阶段,而且这两个阶段的瓶颈完全不同。
2.1 Prefill 阶段:计算密集型
当用户输入一段 prompt 时,模型要并行处理整个输入序列,一次性算出所有输入 token 的注意力矩阵和隐层状态。这个过程叫 Prefill。
它的特点是:
- 所有输入 token 同时参与计算,矩阵乘规模大。
- 计算量占整个请求的一大部分,但时间只占一次请求的一小部分。
- 瓶颈在 GPU 的FLOPS(每秒浮点运算次数),也就是算力够不够。
2.2 Decode 阶段:访存密集型
Prefill 结束后,模型开始一个 token 一个 token 地生成输出。每生成一个 token,都要把模型的所有权重从显存搬运到计算单元一次。
它的特点是:
- 每次只处理一个 token,计算量小。
- 权重搬运的耗时远大于计算耗时。
- 瓶颈在显存带宽(Memory Bandwidth),也就是显存到芯片之间搬运数据的速度。
这就是为什么同一个模型,在消费级显卡上跑,prefill 可能很快,但生成阶段每秒只能吐几个 token。显卡本身算力不差,但显存带宽和吞吐没有数据中心级产品那么夸张。
2.3 KV Cache:解码阶段的隐形成本
Decode 阶段每次都要重新计算当前 token 对历史 token 的注意力权重。为了不重复计算历史 token 的 K、V 向量,推理框架会把这些向量缓存下来,这就是 KV Cache。
KV Cache 的大小直接由以下因素决定:
- 模型层数。
- 注意力头数和 head 维度。
- 序列长度(prompt + 生成长度)。
- 量化精度。
KV Cache 并不是“可选项”,它甚至可能超过模型权重本身的显存占用。比如一个 7B 模型在 FP16 下权重占 14GB,但如果上下文窗口开到 32K,KV Cache 也可能占 10GB 以上。
所以,从第一性原理估算 LLM 性能时,KV Cache 永远是绕不开的重头戏。
3. 显存占用估算:模型权重 + KV Cache + 激活值
显存估算是最基础的一步。我们通常用一个总公式:
总显存 ≈ 模型权重显存 + KV Cache 显存 + 激活值显存 + 推理框架预留显存下面逐项拆解。
3.1 模型权重显存
模型权重的显存占用只取决于参数量和存储精度。
对于参数量为 N 的模型:
权重显存 = N × 每个参数占用的字节数不同精度的字节数如下:
| 精度 | 每参数字节数 | 7B 模型权重显存 | 13B 模型权重显存 | 70B 模型权重显存 |
|---|---|---|---|---|
| FP32 | 4 字节 | 28 GB | 52 GB | 280 GB |
| FP16 / BF16 | 2 字节 | 14 GB | 26 GB | 140 GB |
| INT8 | 1 字节 | 7 GB | 13 GB | 70 GB |
| INT4 | 0.5 字节 | 3.5 GB | 6.5 GB | 35 GB |
这是理论值。实际推理时还要加上 CUDA context、推理框架内部 buffer、KV Cache 等,所以实际占用要比表中数字高一些。
对于 24GB 显存的消费级显卡,FP16 下只能跑 7B 级别模型,带长上下文会更紧张。要跑 13B 模型,一个常见选择是用 INT8 或 INT4 量化。
3.2 KV Cache 显存
KV Cache 显存的公式:
KV Cache 显存 = 2(K 和 V 两份) × 层数 × 头数 × head_dim × 序列长度 × 每元素字节数这里“序列长度”指的是模型实际处理的全部 token 数,也就是输入长度加上输出长度。KV Cache 会随着生成过程动态增长,所以生成任务越长,显存占用越大。
举个例子,一个 7B 模型通常有 32 层,32 个注意力头,head_dim 为 128,FP16 精度(2 字节):
每个 token 的 KV Cache 显存 = 2 × 32 × 32 × 128 × 2 字节 = 524288 字节 ≈ 0.5 MB对于 4096 token 上下文:
KV Cache = 0.5 MB × 4096 ≈ 2 GB对于 32768 token 上下文:
KV Cache = 0.5 MB × 32768 ≈ 16 GB看出问题了吗?同一套 7B 模型,上下文从 4K 拉到 32K,KV Cache 显存从 2GB 飙升到 16GB。这在显存有限的显卡上几乎无法承受。
3.3 激活值显存
激活值是前向传播过程中每层输出的中间结果。它的大小取决于:
- 模型结构(层数、隐藏层维度)。
- 输入序列长度。
- 批量大小(batch size)。
- 是否开启梯度计算(推理时通常关闭)。
推理时激活值比训练时小很多,但在极长上下文和小显存场景下仍然不能忽略。工程上一般通过激活值重计算、优化注意力实现来降低这部分占用,但在估算时通常可以先粗略估计为权重的 10%~30%,或者通过实际运行nvidia-smi观察。
3.4 显存估算示例
用 Python 写一个简单的估算脚本:
def estimate_memory( num_layers: int, hidden_size: int, num_heads: int, head_dim: int, context_length: int, num_params_G: float, weight_bytes: int, kv_bytes: int, activation_mb: float = 256.0, ): # 权重显存 weight_gb = num_params_G * weight_bytes / 1024 # 需要转换单位 # 实际统一用 GB 计算 weight_gb = num_params_G * weight_bytes / 1024**3 # 每个 token 的 KV Cache kv_per_token_bytes = 2 * num_layers * num_heads * head_dim * kv_bytes # 单位:字节,转换为 GB kv_cache_gb = kv_per_token_bytes * context_length / 1024**3 # 激活值粗略估算,给一个上限即可 activation_gb = activation_mb / 1024 total_gb = weight_gb + kv_cache_gb + activation_gb return { "weight_gb": weight_gb, "kv_cache_gb": kv_cache_gb, "activation_gb": activation_gb, "total_gb": total_gb, } # 示例:7B 模型,FP16,4K 上下文 result = estimate_memory( num_layers=32, hidden_size=4096, num_heads=32, head_dim=128, context_length=4096, num_params_G=7, weight_bytes=2, kv_bytes=2, activation_mb=256, ) print(result)运行结果会给出一个较接近实际部署的估算值。这个脚本的价值不在于精确到 MB,而在于让你在选显卡时快速判断“这卡够不够用”。
4. 推理时延估算:算力和带宽到底谁拖后腿
显存估算解决的是“能不能跑”的问题,接下来要解决“跑多快”的问题。
4.1 计算量角度
LLM 推理的计算量主要由矩阵乘法和注意力计算组成。工程上一般用“每个 token 的计算量约等于 2 × 参数量 × 参与计算的 token 数”来近似。
- Prefill 阶段要计算所有输入 token 的前向传播,因此计算量和输入 token 总数成正比。
- Decode 阶段每次只生成一个 token,计算量约等于 2 × 参数量。
假设一个 7B 模型在 FP16 精度下,单次生成一个 token 的计算量大约是:
计算量 = 2 × 7e9 × 2 字节 = 2.8e10 FLOPS注意到这里乘了 2 是因为矩阵乘法的乘加操作计为两次浮点运算。
4.2 显存带宽角度
Decode 阶段每生成一个 token,需要把全部模型权重从显存搬运一次。搬运时间由显存带宽决定:
搬运时间 ≈ 模型权重大小 / 显存带宽以 RTX 4090 为例,显存带宽约为 1000 GB/s。FP16 权重 14GB,搬运一次需要约 14ms。这个 14ms 就是理论上的单 token 最小时延。
再看数据中心级 GPU,例如 H100 的显存带宽可达 3.35 TB/s,搬运 14GB 权重只需要大约 4ms。所以同样是 7B 模型,H100 在 decode 阶段能比 4090 快三倍以上,瓶颈大部分在带宽。
4.3 两阶段时延估算公式
Decode 阶段单 token 的理论时间:
decode_token_time ≈ max(权重搬运时间, 计算时间)通常权重搬运时间远大于计算时间,所以可以简化为:
decode_token_time ≈ 权重大小 / 显存带宽Prefill 阶段的时间更接近:
prefill_time ≈ 输入长度 × 权重大小 / 算力意思是,prefill 阶段更依赖算力。
这里的结论是:如果显存带宽高,decode 就快;如果算力高,prefill 就快。不同显卡在不同阶段有不同优势,这也是为什么有些推理框架会做“算力和带宽的联合调度”。
4.4 延迟与吞吐估算示例
用 Python 做一个粗略的吞吐估算:
def estimate_decode_speed( num_params_G: float, precision_bits: int, memory_bandwidth_GBps: float, batch_size: int = 1, ) -> float: # 权重大小,单位 GB weight_gb = num_params_G * precision_bits / 8 / 1024 # 每次 decode 全部权重过一遍 # 带宽单位换算:GB/s # 单个 token 的搬运时间(秒) token_time = weight_gb / memory_bandwidth_GBps # 实际是 batch 内多个请求共享带宽 # 简单估算:带宽利用率打折 effective_bandwidth = memory_bandwidth_GBps * 0.8 token_time = weight_gb / effective_bandwidth # 每秒生成的 token 数 tokens_per_second = batch_size / token_time return tokens_per_second # 假设:7B 模型,FP16,RTX 4090 带宽约 1000 GB/s speed = estimate_decode_speed( num_params_G=7, precision_bits=16, memory_bandwidth_GBps=1000, batch_size=1, ) print(f"单请求 decode 速度约 {speed:.1f} token/s")这里 band 利用率取 0.8,是因为实际显存访问有开销,不可能达到理论峰值。这个估算能帮你判断线上服务是不是还有优化空间,而不是在 GPU 利用率已经饱和的情况下盲目调参。
5. 精度选择与第一性原理:FP16、BF16、INT8、INT4 的实际影响
精度选择直接改变权重大小、KV Cache 大小、计算速度,甚至影响最终生成质量。从第一性原理出发,可以这样理解:
5.1 精度对显存的影响
权重显存和 KV Cache 显存都随精度线性变化。精度从 FP16 降到 INT8,权重减半;从 FP16 降到 INT4,直接减到四分之一。
5.2 精度对速度的影响
- Prefill 阶段主要看算力,低精度通常能匹配更高的算力(例如 Tensor Core 的 INT8 算力通常是 FP16 的两倍),所以 prefill 变快。
- Decode 阶段主要看带宽,低精度让权重更小,所以权重搬运时间缩短,decode 也会变快。
但要注意,INT4 和 INT8 推理通常需要反量化操作,这部分也会消耗算力。如果显卡不支持低精度快速指令集,实际收益可能被反量化开销抵消。所以低精度不一定在所有硬件上都有正收益。
5.3 精度对效果的影响
从社区实践看,FP16/BF16 效果最稳,INT8 损失很小,INT4 有可见的困惑度上升和复杂推理能力下降。如果做的是代码生成、数学推理、长文档分析这类对精度敏感的任务,INT4 要谨慎。如果是聊天助手、摘要生成等容忍度较高的场景,INT4 可以显著降低部署门槛。
5.4 精度选择建议
| 场景 | 推荐精度 | 理由 |
|---|---|---|
| 效果优先、显存充足 | FP16 / BF16 | 完全保留模型精度 |
| 效果和显存平衡 | INT8 | 损失小,显存和速度改善明显 |
| 显存紧张、消费级显卡 | INT4 | 可运行更大模型,需验证任务效果 |
| CPU 推理 | INT8 / INT4 | 降低内存带宽压力 |
6. 批量推理、并发与吞吐量:为什么显卡利用率总上不去
单请求性能只是基础,线上服务更关心并发和吞吐。这里有几个关键概念:
6.1 连续批处理(Continuous Batching)
传统批处理是等一批请求全部结束后再处理下一批。但 LLM 的 decode 阶段每个 token 生成时间不一致,如果某个请求生成了 100 个 token,另一个只生成了 5 个 token,传统批处理会拖慢平均时延。
连续批处理的做法是:一个请求生成完,立刻从等待队列里拉一个新请求补位。这样 GPU 显存带宽始终在处理活跃请求的权重,利用率更高。
6.2 批量大小对吞吐的影响
decode 阶段,批量请求共享同一份模型权重,因此只需要搬运一次权重,就能服务多个请求。批量越大,每个 token 的搬运越划算,总吞吐越高。
但这有个上限:
- 批量大会增加 KV Cache 显存占用。
- 批量过大会导致单个请求的时延变长。
- 当显存带宽完全占满后,吞吐不再线性增长,所以需要平衡。
6.3 吞吐量经验公式
总吞吐 ≈ 显存带宽 × 批量大小 / 权重大小举个例子,RTX 4090 带宽 1000 GB/s,FP16 的 7B 权重是 14GB,单请求 decode 大概 70 token/s。如果批量放大到 4,理论吞吐可以到 280 token/s,但单个请求的时延可能略有增加。实际能到多少,取决于显存剩余空间和调度开销。
6.4 并发设计思路
- 单卡部署时,先把 batch size 从 1 逐步调大,观察显存和吞吐变化。
- 多卡时优先按模型并行拆分,其次考虑请求分发。
- 如果瓶颈在显存带宽,加卡比加显存更有意义。
- 如果瓶颈在显存容量(KV Cache 溢出),减少上下文长度或降低精度更有效。
7. LLM 性能测量与验证方法
理论估算再准,也要用实际测量来校准。标准的 LLM 推理性能指标主要有三个:
7.1 TTFT(Time To First Token)
从请求发出到生成第一个 token 的时间。
TTFT 主要受 prefill 阶段影响。输入越长,TTFT 越长。用户通常希望 TTFT 控制在 1~3 秒以内。如果 TTFT 突增,常见原因有:
- 并发请求太多,排队时间长。
- 输入包含超长文本,prefill 计算量大。
- 显存带宽被其他任务抢占。
7.2 TPOT(Time Per Output Token)
每生成一个 token 的平均时间。
TPOT 是 decode 阶段的核心指标。计算公式:
TPOT = 总生成耗时 / 生成 token 数正常 7B 模型在消费级显卡上,FP16 单请求 TPOT 大约几十毫秒;在数据中心级显卡上可以到 10ms 以下。如果 TPOT 明显偏高,先看显存带宽是否打满,再看模型权重是否过大。
7.3 吞吐量
通常以 token/s 为单位。跟单请求时延不同,吞吐量关注的是单位时间内系统能处理的 token 总数,在批量推理和并发场景下更有意义。
测量时要注意:
- 纯 decode 吞吐:固定输入,让模型一直生成。
- 混合请求吞吐:模拟真实使用场景,包含不同长度的输入和输出。
- 并发吞吐:同时发起多个请求,观察总吞吐和单请求时延的平衡。
7.4 测量工具
nvidia-smi:查看显存占用、GPU 利用率、功耗和温度。- PyTorch Profiler:分析每个算子的耗时。
- 推理框架自带 benchmark 脚本,比如 vLLM 的
benchmark_serving脚本。 - 自定义计时脚本,在请求入口记录开始时间和第一个 token 返回时间。
8. 常见的性能问题与排查方向
| 问题现象 | 可能原因 | 排查方式 | 解决方向 |
|---|---|---|---|
| 显存不足,模型无法加载 | 模型权重 + KV Cache 超过显存 | nvidia-smi查看占用 | 降低精度、缩短上下文、升级显卡 |
| TTFT 过长 | prefill 阶段计算量大或排队严重 | 查看并发数和输入长度统计 | 限制并发、降低输入长度、使用投机采样 |
| TPOT 过慢,每秒生成 token 少 | 显存带宽打满或者权重过大 | 观察 GPU 利用率、显存读写 | 量化模型、增大 batch、换带宽更高的显卡 |
| 并发一高就 OOM | KV Cache 随并发线性增长 | 监控显存曲线 | 限制 batch size、使用 PagedAttention、开启 KV Cache 量化 |
| 推理速度不稳定 | 显存交换或 CPU 瓶颈 | 查看系统内存是否被大量使用 | 减少系统内存交换、优化数据预处理 |
| GPU 利用率低但速度慢 | 权重搬运占主导,算力闲置 | nvidia-smi看 GPU 利用率 | 增大 batch、使用更小精度的权重 |
9. 工程落地建议:从理论走向生产
理论估算只是第一步,真正落地时还需要注意这些细节:
9.1 先做最小验证
第一次部署时不要直接上生产配置。先用最小参数跑通一次推理,确认显存占用和时延在预期范围内,再逐步增加上下文长度、并发数、批量大小。
9.2 统一目录管理
把模型权重、输入测试集、输出结果分目录存放。设计成本估算和性能测试脚本时,建议用配置文件管理参数,而不是把路径写死在代码里。
示例配置文件:
{ "model": { "name": "7b_model", "num_params_G": 7, "precision": "fp16", "max_context": 4096 }, "hardware": { "gpu_name": "RTX-4090", "memory_bandwidth_GBps": 1000, "total_memory_GB": 24 }, "batch": { "batch_size": 1, "max_concurrent_requests": 8 } }9.3 记录推理日志
每次推理记录模型版本、输入长度、输出长度、显存占用、TTFT、TPOT、吞吐量。性能优化不能靠感觉,一组可复现的日志是排查问题的关键。
9.4 授权与合规提醒
如果你使用第三方模型权重、在真实业务中部署 LLM,请确认模型许可证和使用范围。涉及用户数据的推理服务,要注意隐私边界。涉及生成内容的场景,要在上线前做效果复核,防止低质量或违规内容输出。
9.5 不同场景的落地选择
- 个人开发测试:消费级显卡 + 4bit 量化,优先跑通功能。
- 企业内部工具:8bit 量化 + 连续批处理,兼顾效果和吞吐。
- 高并发生产:多卡部署 + 推理框架调度 + 动态 batch。
10. 总结与下一步
从第一性原理估算 LLM 性能,核心是掌握四条规则:
- 显存占用主要由模型权重、KV Cache 和激活值组成,其中 KV Cache 在长上下文场景下不可忽视。
- Prefill 阶段是计算密集型,瓶颈是算力;Decode 阶段是访存密集型,瓶颈是显存带宽。
- 精度选择直接影响显存、速度和效果,要按任务类型取舍。
- 吞吐和时延需要分开看,批量推理和连续批处理是提升 GPU 利用率的关键手段。
你不需要背公式,建议直接把文章里的 Python 脚本保存下来,以后拿到一个新模型或者新显卡,先跑一遍估算脚本,再决定要不要实际部署。这个习惯能帮你省掉很多“模型下载完、显卡跑不动”的时间。
下一步可以尝试的方向包括:把估算脚本接入自己的推理服务做容量预测,对比不同精度下的实测数据,或深入了解 PagedAttention 等显存优化机制。性能优化没有一劳永逸,但第一性原理能让你每次调优都有明确目标,而不是靠玄学或者堆硬件。