LLM性能建模:从第一性原理估算显存与延迟

发布时间:2026/8/30 15:36:26
LLM性能建模:从第一性原理估算显存与延迟 部署一个大语言模型到 GPU 服务器之前最常见的踩坑路径是什么不是模型下载慢也不是 prompt 写不好而是模型快下载完了才发现显存不够服务刚跑起来才发现单请求延迟高到没法用压测一上并发一拉满QPS 直接归零。这些问题其实都可以在动手前用一张纸、一个脚本算清楚。但很多团队的现状是等 GPU 账单出来之后才反过来研究估算公式。LLM 性能建模简单说就是只靠模型参数量、精度、输入输出 token 数和 GPU 规格在不上模型、不跑 benchmark 的前提下推算出延迟、吞吐量和显存占用。这件事不需要什么玄学用的都是公开的物理常识和几个非常稳定的工程公式。这篇文章我想把整套推算思路拆开讲一遍先讲基础概念再给出可直接复制的 Python 脚本最后说明怎么用实际跑批结果回推你的估算偏差。1. 这篇文章真正要解决的问题很多工程师提到性能第一反应是上 nvidia-smi 看利用率或者跑一个 benchmark 脚本测测 tokens/s。这当然没错但这是验证阶段不是规划阶段。真正高效的流程应该是先算再猜最后跑。从第一性原理建模 LLM 性能解决的是下面四类问题部署前选型一个 13B 模型要跑在 24G 显存的卡上勉强能装下但 KV Cache 够不够并发 8 会不会爆显存延迟预估用户输入 1000 token期望生成 500 token大概要等多少秒这个数字能不能写进产品体验文档吞吐量规划一张卡每秒能出多少 token要不要上量化要不要用 vLLM 这类推理框架成本评估一个日均 100 万请求的服务到底要租多少张卡用 A10 还是 A100差几倍价格在 AI 应用开发、RAG 应用、Agent 应用越来越普及的背景下这类问题几乎每个 LLM 应用团队都会遇到。纯靠试错成本太高纯靠 benchmark又只能覆盖已经跑通的模型。所以我们需要回到 LLM 推理的物理本质用第一性原理建立一个粗略但有效的性能模型。读完这篇文章你会得到三样东西一套手工估算 LLM 显存和延迟的方法一个几十行的 Python 性能估算脚本一份用来对照排查的常见问题清单。2. 基础概念参数、精度、算力与显存在进入公式前先统一几个概念。LLM 性能建模绕不开这些词但很多文章默认读者已经懂了我在这里用最直白的方式讲清楚。2.1 参数量与精度字节数模型的大小通常以参数量Parameters来衡量。7B、13B、70B指的就是模型有 70 亿、130 亿、700 亿个参数。参数在显存里占用多少空间取决于精度精度每个参数占用字节数常见场景FP324训练、梯度累积中间过程FP162推理、训练数值范围有限BF162推理、训练动态范围更稳INT81量化推理INT40.5量化推理质量和稳定性要看模型一个 7B 模型用 FP16 加载权重本身大约占 14GB7B * 2 bytes 14 GB如果是 FP32就是 28GB。这就是为什么很多 7B 模型能跑在 16G~24G 显存的消费级显卡上而 70B 模型必须在多卡或大显存服务器上。关于 FP16、BF16、FP32 的精度问题实际项目中不仅要看显存占用还要看数值稳定性。BF16 只有 8 位指数位、7 位尾数位FP16 是 5 位指数位、10 位尾数位虽然都是 2 字节但 BF16 的动态范围更接近 FP32更不容易在训练和推理中出现溢出。2.2 Prefill 和 Decode 两个阶段LLM 推理不是输入完整句子直接输出完整句子它分为两个阶段Prefill预填充把用户输入的 prompt 一次性并行处理生成第一个 token。这个阶段计算密集GPU 算力利用率通常比较高。Decode解码一个一个地生成后续 token每生成一个 token 都要做一次完整的前向计算。这个阶段是访存密集GPU 算力利用率往往很低。理解这两个阶段的差异是性能建模的关键。很多人用同一个指标评估整个推理过程结果误差很大。实际上 Prefill 的延迟主要受算力影响Decode 的延迟主要受显存带宽影响。2.3 FLOPs 与算力利用率FLOPs 是浮点运算次数。GPU 的算力通常用 TFLOPS 表示比如 A100 的 FP16 算力大约是 312 TFLOPSH100 的 BF16 算力接近 1000 TFLOPS 级别。这里的 T 是 10^12。但实际跑模型时不可能跑到理论峰值。算力利用率MFUModel FLOPs Utilization衡量的是实际有效 FLOPs 占理论峰值的比例。Transformer 推理中 MFU 通常在 10%~40% 之间取决于模型、框架、批大小和显存带宽。2.4 KV Cache 与显存带宽KV Cache 是解码阶段为了加速生成而缓存的 Key 和 Value 矩阵。每处理一个 token模型的每一层注意力都要保存一份 KV所以 KV Cache 大小随序列长度线性增长。它和模型权重不一样是动态分配的最容易导致线上 OOM。显存带宽Memory Bandwidth决定了 Decode 阶段的速度。因为生成每个 token 都要把模型权重从头到尾读一遍GPU 的算力再高如果权重读取速度跟不上token 生成速度也会被卡住。3. LLM 性能建模的三个核心公式有了上面的概念接下来就是推算框架。实际工程中不需要精确到每个算子三个公式就够用。3.1 推理总计算量估算在训练场景下一个 token 的前向反向计算量大约是 6N其中 N 是模型参数量。推理只有前向单 token 的计算量大约是 2N。假设你有模型参数量 N输入 prompt 长度 P生成 token 数量 G那么总计算量Total FLOPs ≈ 2N * (P G)这个估计很粗糙没有考虑注意力矩阵的计算但在参数量几百亿以内、序列长度几千以内时2N 是工程上足够用的近似。Karpathy 在他的 LLM 相关材料和 wiki 风格文章里也多次提到这个估算思路用 2N/6N 先算一个数量级再根据实际利用率打折。3.2 显存预算公式推理时显存分成三块最少显存 模型权重 激活值 KV Cache 框架开销模型权重的估算很简单权重显存 参数量 * 每参数字节数KV Cache 的估算需要一些模型内部信息。以 MHA标准多头注意力为例每个 token 的 KV Cache 大小每 token KV Cache 字节数 2 * 层数 * 隐藏维度 * 每元素字节数如果模型用了 GQA分组查询注意力KV 头部数量更少实际 KV Cache 会小很多。这也是为什么新出的模型越来越倾向于用 GQA。对于 7B 级别的常见模型KV Cache 在序列长度 2048~8192 时通常占 1~8GB。框架开销通常包括 CUDA context、运行时缓冲、中间结果等一般预留 1~2GB 比较稳妥。3.3 延迟下界估算延迟的估算分两个阶段Prefill 耗时 ≈ 总计算量 / (GPU 算力 * 利用率) Decode 单 token 耗时 ≈ 权重大小 / 显存带宽Decode 阶段每生成一个 token原则上要把全部权重加载一遍所以 Decode 的瓶颈是显存带宽。比如 14GB 权重在带宽 2TB/s 的情况下理论单 token 最短耗时约为14GB / 2000GB/s ≈ 7 毫秒也就是说理想情况下每秒最多生成 100 多个 token。这是纯物理下限任何推理框架都没办法突破。如果实际测试到的速度更慢要查框架开销和批处理配置。4. 环境准备与前置条件这篇文章的核心脚本不需要 GPU不需要安装大型深度学习框架有一个 Python 环境就能跑。建议使用 Python 3.8 以上版本依赖只需要标准库最多用到json和math。如果你打算在真实环境验证需要准备一块可用的 NVIDIA GPU并已安装 NVIDIA 驱动和 CUDA 运行环境一个推理服务可以是 vLLM、TGI、TensorRT-LLM 自带的 OpenAI 兼容接口命令行工具nvidia-smi用来观察显存和 GPU 利用率我这里不会把某些版本写死因为不同模型的推理框架要求差异很大。性能估算脚本和框架无关只依赖 GPU 规格参数。5. 完整示例代码实现下面我会写一个从第一性原理估算 LLM 性能的 Python 脚本。它有三部分核心估算函数、GPU 配置表和输出主流程。5.1 核心估算脚本文件路径llm_perf_model.py LLM 推理性能第一性原理估算器 只依赖标准库无需 GPU无需深度学习框架 import math import json # 字节换算常量 GB 1024 ** 3 def estimate_weights_mem(params_b, precision_bytes2): 估算模型权重显存 :param params_b: 模型参数量单位亿 :param precision_bytes: 每个参数字节数FP16/BF16 为 2INT8 为 1 :return: 显存大小单位 GB total_params params_b * 1e8 return total_params * precision_bytes / GB def estimate_kv_cache_mem(layers, hidden_dim, seq_len, precision_bytes2, num_kv_headsNone, head_dimNone): 估算 KV Cache 显存简化估算按 MHA 计算GQA 模型请传入 num_kv_heads 和 head_dim :param layers: Transformer 层数 :param hidden_dim: 隐藏维度 :param seq_len: 序列长度prompt 生成 :param precision_bytes: KV 缓存精度字节数通常与权重一致 :param num_kv_heads: KV 头数MHA 场景可设为 None :param head_dim: 每个头的维度MHA 场景可设为 None :return: KV Cache 显存大小单位 GB if num_kv_heads is not None and head_dim is not None: # GQA / MQA 场景 kv_elem_per_token 2 * layers * num_kv_heads * head_dim else: # MHA 场景简化KV 大小约等于 hidden_dim 的双倍乘以层数 kv_elem_per_token 2 * layers * hidden_dim total_elem kv_elem_per_token * seq_len return total_elem * precision_bytes / GB def estimate_inference_flops(params_b, prompt_tokens, gen_tokens): 估算推理总 FLOPs :param params_b: 模型参数量单位亿 :param prompt_tokens: prompt 长度 :param gen_tokens: 生成 token 数 :return: 总 FLOPs total_params params_b * 1e8 # 推理单 token 计算量约 2N return 2 * total_params * (prompt_tokens gen_tokens) def estimate_time_with_gpu(flops, gpu_tflops, mfu0.2): 按 GPU 理论算力估算计算耗时 :param flops: 总 FLOPs :param gpu_tflops: GPU 理论 FP16/BF16 算力单位 TFLOPS :param mfu: 算力利用率默认 0.2实际可取 0.1~0.4 :return: 耗时秒 gpu_flops gpu_tflops * 1e12 return flops / (gpu_flops * mfu) def estimate_decode_bound_speed(weights_gb, bandwidth_gbs): 估算 Decode 阶段的理论单 token 耗时上限 :param weights_gb: 模型权重大小单位 GB :param bandwidth_gbs: GPU 显存带宽单位 GB/s :return: 单 token 耗时秒 # 每生成一个 token需要完整读取一次权重 return weights_gb / bandwidth_gbs def format_report(model_name, params_b, layers, hidden_dim, prompt_len, gen_len, precision_bytes, gpu_name, gpu_tflops, gpu_bandwidth_gbs, gpu_mem_gb, mfu0.2): 生成完整性能报告 weights estimate_weights_mem(params_b, precision_bytes) kv_cache estimate_kv_cache_mem(layers, hidden_dim, prompt_len gen_len, precision_bytes) activation_mem 1.0 # 简化激活值预留 framework_overhead 2.0 # CUDA context 等预留 total_mem weights kv_cache activation_mem framework_overhead flops estimate_inference_flops(params_b, prompt_len, gen_len) calc_time estimate_time_with_gpu(flops, gpu_tflops, mfu) decode_token_time estimate_decode_bound_speed(weights, gpu_bandwidth_gbs) decode_speed 1.0 / decode_token_time report { model: model_name, params_b: params_b, precision_bytes: precision_bytes, weights_gb: round(weights, 2), kv_cache_gb: round(kv_cache, 2), est_total_mem_gb: round(total_mem, 2), total_flops: f{flops:.2e}, est_prefill_decode_time_sec: round(calc_time, 2), decode_upper_bound_speed_tokens_per_sec: round(decode_speed, 1), decode_single_token_ms_lower_bound: round(decode_token_time * 1000, 2), gpu: gpu_name, gpu_mem_gb: gpu_mem_gb, } return report这个脚本的核心思路是先算权重显存再算 KV Cache最后分别用算力和带宽估算时间。mfu参数是最不确定的可以按实际情况调整。5.2 GPU 规格配置与主流程为了演示我给出常见的 GPU 规格表和主流程代码。这里的规格数据是公开的厂商标称值实际卡型不同批次可能有差异。# 文件路径gpu_profiles.py GPU_PROFILES { A100-80G: { tflops_fp16: 312, bandwidth_gbs: 2039, mem_gb: 80 }, H100-80G: { tflops_bf16: 989, bandwidth_gbs: 3350, mem_gb: 80 }, L40S: { tflops_fp16: 362, bandwidth_gbs: 864, mem_gb: 48 }, RTX-4090: { tflops_fp16: 165, bandwidth_gbs: 1008, mem_gb: 24 }, }# 文件路径main.py from llm_perf_model import format_report from gpu_profiles import GPU_PROFILES if __name__ __main__: # 示例一个 7B 模型32 层隐藏维度 4096 # 输入 prompt 1024 token生成 256 token model_cfg { model_name: example-7B, params_b: 7, layers: 32, hidden_dim: 4096, prompt_len: 1024, gen_len: 256, precision_bytes: 2, # FP16 / BF16 } for gpu_name, spec in GPU_PROFILES.items(): report format_report( model_namemodel_cfg[model_name], params_bmodel_cfg[params_b], layersmodel_cfg[layers], hidden_dimmodel_cfg[hidden_dim], prompt_lenmodel_cfg[prompt_len], gen_lenmodel_cfg[gen_len], precision_bytesmodel_cfg[precision_bytes], gpu_namegpu_name, gpu_tflopsspec[tflops_fp16], gpu_bandwidth_gbsspec[bandwidth_gbs], gpu_mem_gbspec[mem_gb], mfu0.2, ) print(json.dumps(report, indent2, ensure_asciiFalse))运行方式python main.py这个脚本输出的是一个抽象模型的估算结果不针对某个具体开源模型。真实使用时要替换成目标模型的层数、隐藏维度和 KV 注意力头配置。5.3 对已部署模型做简单压测估算脚本算完还需要和真实服务的压测结果对比。下面给一个简单的 HTTP 压测脚本用 Python 标准库发请求测量完整端到端延迟。# 文件路径bench_local_http.py 对本地 OpenAI 兼容推理服务做简单性能测试 注意压测前确认服务已经正常启动并且你有合法调用权限 import json import time import urllib.request BASE_URL http://127.0.0.1:8000/v1/chat/completions MODEL_NAME your-model-name PROMPT 用三句话解释什么是大语言模型。 def one_request(): payload { model: MODEL_NAME, messages: [{role: user, content: PROMPT}], max_tokens: 128, temperature: 0.0, } data json.dumps(payload).encode(utf-8) req urllib.request.Request( BASE_URL, datadata, headers{Content-Type: application/json}, methodPOST, ) start time.time() with urllib.request.urlopen(req, timeout120) as resp: result json.loads(resp.read().decode(utf-8)) elapsed time.time() - start output result[choices][0][message][content] return elapsed, len(output) if __name__ __main__: total_time 0.0 total_chars 0 rounds 5 for i in range(rounds): elapsed, chars one_request() total_time elapsed total_chars chars print(fround{i1}, elapsed{elapsed:.2f}s, output_chars{chars}) avg_time total_time / rounds avg_chars total_chars / rounds print(favg_elapsed{avg_time:.2f}s, avg_output_chars{avg_chars:.1f}) print(frough_speed{avg_chars / avg_time:.1f} chars/s)这段脚本用urllib而不依赖requests是为了在最小环境里也能跑。要注意的是它输出的字符数不是 token 数中文字符大约对应 1~2 个 token用来估算数量级是够的。6. 运行结果与效果验证以示例中的 7B 模型为例脚本输出的估算量大致会落在下面几个区间模型权重约 14GBFP16/BF16KV Cache在 2048 序列长度时根据层数和隐藏维度不同大概在 1GB 左右预估总显存约 17~20GB在 H100 上 Prefill Decode 的总计算耗时大约在 1 秒以内按 20% 利用率估算Decode 单 token 耗时下限在 H100 上大约 4ms 级别在 RTX 4090 上大约 14ms 级别如何判断估算是否合理把脚本输出和真实压测结果放在一起看三个指标显存用nvidia-smi观察服务启动后的显存占用和估算值偏差应该在 20% 以内。Decode 速度真实单并发生成速度和 Decode 下限做对比如果比理论下限还快说明数据有误如果慢 5 倍以上要检查是否没用批处理、自定义配置是否合理、模型是否被 CPU 启动占用了初始阶段。总延迟真实端到端延迟比 Prefill Decode 的下限高是正常的因为还有网络、调度、采样等开销但通常不应超过估算值的 3~5 倍。如果偏差特别大第一步不要急着改代码先看nvidia-smi的显存占用和 GPU 利用率曲线。在性能面板和 GPU 利用率曲线里你会看到 Prefill 阶段算力利用率很高Decode 阶段算力利用率很低。这属于正常现象因为 Decode 是访存瓶颈不能用GPU 利用率低直接推断GPU 坏掉了。7. 常见问题与排查思路问题现象可能原因排查方式解决方案启动时报 CUDA out of memory权重显存 KV Cache 框架开销超限nvidia-smi查看残留进程和实际占用清理残留进程降低 max_seq_len使用量化升级到更大显存卡生成速度远低于理论下限Decode 阶段带宽受限框架未开批处理对比单 token 耗时和理论下限启用 continuous batching增加并发检查是否误用 CPU 推理Prefill 阶段延迟极高MFU 估太低或输入序列过长在推理框架日志里查看 prompt 处理耗时增大 batch size升级算力更强的卡限制用户输入长度并发量一上来就 OOMKV Cache 随并发线性增长监控运行时显存变化调低 max_num_seqs限制最大序列长度启用 KV Cache 复用如 PagedAttention估算脚本显示可行实际跑不了模型是 GQA但估算按 MHA 计算或激活值预留不足核对模型 config.json 的层数、隐藏维度、KV 头数修正估算参数给激活值和框架开销多留 2~4GBFP16 推理结果数值异常部分层激活值溢出对比 FP32/BF16 的生成结果优先用 BF16量化场景先做校准集验证CPU 推理不在上面的讨论范围内。如果是纯 CPU 推理瓶颈通常来自 CPU 访存带宽和算子库优化公式里的 GPU 带宽不再适用建议换一个思路评估。8. 最佳实践与工程建议8.1 先把估算脚本纳入技术方案评审从第一性原理建模 LLM 性能的最大价值不是算出精确值而是在需求评审阶段就暴露风险。我们现在的做法是任何涉及 LLM 服务的需求先填一张性能估算表内容包括模型参数量、精度、预估并发、平均输入输出长度、目标延迟然后跑估算脚本。表里不达标就不进入开发。这样能避免模型先跑起来再说导致的返工。8.2 精度选择要结合模型特性BF16 是当前大模型推理和训练的主流精度因为它动态范围接近 FP32训练时不容易出现梯度溢出。FP16 在部分模型上会出现数值不稳定。INT8/INT4 量化能显著降低显存占用但量化后必须用真实业务 prompt 做质量验证不能只看 benchmark 分数。从估算角度看精度字节数直接进入权重显存和 KV Cache 的计算一分钱一分货。8.3 用批处理换吞吐量单并发场景下Decode 阶段受带宽限制延迟已经接近物理下限。要提升单卡吞吐量正确方向不是调模型而是上动态批处理。vLLM 的 PagedAttention 和 continuous batching 之所以被广泛使用就是因为它把 KV Cache 的内存利用率做上去了让多请求共享显存单位时间能生成的 token 总数显著提高。如果你在做一个日请求量很大的 LLM 应用不要只盯单请求延迟要测端到端吞吐量。8.4 注意序列长度对显存的影响是线性的KV Cache 随序列长度和并发数线性增长这在估算里很容易被忽略。一个 7B 模型权重 14GB 放得下但 100 个并发请求每个序列 8192 tokenKV Cache 可能就把显存吃光了。生产环境一定要给 max_seq_len 设上限并做显存监控告警。8.5 验证时先跑最少 50 个请求压测不要只跑 1 个请求也不要在服务刚启动时立刻测。服务启动后 CUDA context 和 KV Cache 预热需要一点时间。至少要跑 50 个请求取稳定阶段的延迟和吞吐量才适合和估算脚本做对比。9. 总结与后续学习方向这篇文章的核心观点是LLM 性能的绝大部分情况不需要跑 benchmark 才能判断靠参数量、精度、层数、隐藏维度和 GPU 带宽几个公开参数就能算出足够指导决策的数量级。通过一个几十行 Python 脚本你可以在部署前回答这个模型能不能在这张卡上跑单请求延迟大概多少并发上限在哪里三类问题。下一步如果你的工作已经进入 LLM 推理系统优化阶段建议继续深入几个方向一是 KV Cache 的内存管理机制比如 PagedAttention 这类算法的取舍二是量化推理的质量验证流程尤其是在工具调用和 Agent 场景下小数位变化都可能改变最终输出结构三是 GPU 性能分析工具的实际使用先看懂 Prefill 和 Decode 两个阶段在性能面板上的差异再谈优化。这套估算方法不是要替代实测而是让实测之前的所有决策都更稳一点。建议把这个脚本保存下来下次评估新模型时直接改参数跑一遍会比凭经验猜快得多。

相关新闻