尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

LLM性能建模:从第一性原理估算延迟、吞吐与显存 大语言模型的性能问题不能等到部署完成、压测脚本跑完才暴露。很多时候项目还停留在选型阶段就需要回答“7B 模型在 A100 上生成一个 token 大概要多久”“8 万 token 上下文会占多少显存”“训练 1 万亿 token 需要多少张卡”。这些问题可以凭经验猜测也可以回到模型参数、数据类型、序列长度和硬件规格这些最基础的数值用几条公式推算出延迟、吞吐和显存占用的合理区间。这就是从第一性原理出发的 LLM 性能建模不依赖完整集群不做繁琐压测先从最底层的数量级判断开始。本文按这条主线展开先建立估算模型解释 Prefill 和 Decode 为什么是两种不同瓶颈再以 7B 模型为例做一次完整计算随后扩展到训练场景说明 FLOPs、显存和通信怎么估算最后给出实测校准方法和工程中常见的估算误区。读完以后你可以对着任意一个模型配置和 GPU 规格在 10 分钟内给出第一版性能估算并知道该在哪个环节用真实测量修正它。1. 从第一性原理建模先想清楚要算哪些数1.1 四个核心问题时延、吞吐、显存、成本性能建模不是算一个“性能分数”而是要回答四个相互关联的问题。要回答的问题常用指标第一性原理输入第一个 token 要等多久TTFTTime To First TokenPrefill 阶段计算量、batch 大小、GPU 算力输出一个 token 要多久TPOTTime Per Output TokenDecode 阶段每 token 计算量与权重搬运字节数每秒能处理多少请求吞吐量batch 大小、Prefill/Decode token 比例、调度方式最长能支持多少上下文显存峰值权重字节数、KV Cache、激活值、框架预留这四个问题不是孤立的。显存不足会让 batch 变小batch 变小会让吞吐下降上下文变长会让 KV Cache 膨胀进而压缩 batch 空间。建模的价值在于把所有变量放在同一个公式体系里改动一个输入就能看到它对时延、吞吐和显存的连锁影响。1.2 为什么需要建模而不是直接压测压测当然必要但在很多场景下压测来得太晚或者成本太高。首先是选型阶段。模型还没部署机器还没租好团队需要先判断“7B 模型用一张卡能不能跑”“量化到 INT4 后够不够快”。这时没有环境可压测只能靠估算。其次是定位问题。压测只能告诉你“服务慢”建模能告诉你是慢在哪个环节。如果实测 decode 单 token 延迟接近权重搬运的耗时瓶颈在显存带宽如果远高于估算值问题可能在注意力计算、Python 开销、服务框架调度或并发排队上。再次是框架隔离。压测结果受 vLLM、TGI、SGLang 等框架的预分配、连续批处理、显存碎片影响很大。第一性原理模型算的是“模型本身的下限”先有下限才能判断框架有没有把性能发挥出来。注意建模不能替代压测。估算给的是数量级和上下界最终上线前还是要用真实请求做验证。合理做法是“建模给先验实测做校准再更新模型”。1.3 Prefill 与 Decode 的瓶颈完全不同理解 LLM 推理性能第一步是分清两个阶段。Prefill 阶段处理整个输入 prompt。它像一次大的矩阵乘法输入 token 越多计算量越大。这个阶段可以充分利用 GPU 的并行算力通常属于计算密集compute-bound表现为高 GPU 利用率、单次耗时随输入长度增长。Decode 阶段逐 token 生成。每生成一个 token都需要把模型所有权重读一遍。对大多数 GPU 来说读取 14GB 权重的耗时远大于在 14GB 权重上做矩阵乘法的耗时。因此 Decode 阶段通常属于访存密集memory-bound表现为 GPU 利用率不高延迟主要受显存带宽限制。这解释了为什么“一个 token 要多久”和“第一个 token 要多久”必须分开估算。把两者混在一起用同一套利用率去算结果会偏差一个数量级。2. 三个基础量FLOPs、内存字节数和算术强度2.1 计算量Transformer 前向推理的 2NT 近似对于 decoder-only 的 Transformer业界常用的经验公式是处理 T 个 token 的前向推理计算量约为 2NT。其中 N 是模型参数量T 是处理的 token 数。这个近似把多头注意力、MLP、LayerNorm 等所有计算全部折算进去。对 1B 以上的模型它的误差通常在可接受范围内但对非常小的模型和极长序列注意力分数计算 O(T²d) 这一项会变大需要单独补上。按这个公式一次 Prefill 输入 T 个 token计算量约为 2NT。Decode 阶段每生成一个 token计算量约为 2N。前者是整段输入一起算后者是每 token 单独算这是两者时延差距的计算层原因。2.2 内存量权重、KV Cache 与激活值显存占用主要来自四部分权重、KV Cache、激活值和框架开销。其中权重和 KV Cache 可以用公式直接估算。权重内存等于参数量乘以每参数字节数取决于数据类型。数据类型每参数字节数常见场景FP324训练主权重、优化器状态FP162推理权重、部分训练BF162训练主流、推理权重FP81新一代 GPU 上的推理与训练加速INT81推理量化INT40.5推理量化显存压力显著下降例如 7B 模型FP16 权重是 14GBINT4 量化后约 3.5GB。数据类型直接改变权重搬运量也就直接改变 Decode 阶段的理论时延这是后续估算的核心参数。KV Cache 的估算公式是KV Cache 字节数 2 × 层数 × KV 头数 × 每头维度 × 序列长度 × 每元素字节数对于多头注意力KV 头数 × 每头维度等于隐藏维度 d_model公式可以简化为每 token 约 2 × 层数 × d_model × 字节数。以 32 层、d_model 4096、FP16 为例每个 token 的 KV Cache 约 0.5MB。2048 token 的输入需要约 1GB32768 token 需要约 16GB。这还只是一个请求batch 为 8 时直接翻 8 倍。激活值在 Prefill 阶段占比很高具体取决于 batch、序列长度、层数、隐藏维度和是否使用激活重计算。它不像 KV Cache 那样能用简单公式一算到底通常用显存统计工具实测确认。2.3 算术强度用 Roofline 思路判断瓶颈算出 FLOPs 和字节数之后需要判断瓶颈落在算力还是带宽上。这时用算术强度Arithmetic Intensity每搬运 1 字节数据能完成多少次浮点运算。算术强度 FLOPs / 内存字节数Prefill 阶段处理 T 个 token计算量是 2NT权重和 KV Cache 的写入量约为 N × 字节数加 KV 字节数。当 T 较大时算术强度随 T 增长通常远高于 GPU 的“平衡点”属于计算密集。Decode 阶段每 token 计算量是 2N需要搬运的权重字节数是 N × 字节数。算术强度约等于 2 除以字节数。FP16 时约为 1远低于平衡点属于访存密集。这个判断很重要Decode 阶段无论 GPU 算力多高只要显存带宽不变延迟就基本固定在“权重字节数 / 有效带宽”附近。给 7B 模型换更快的 GPU如果带宽提升不大Decode 速度也不会明显提升。3. 手算一次 7B 模型的推理性能3.1 确定输入参数下面用一个典型配置做完整计算。所有数字都是公开规格和常见假设实际项目中需要替换成自己的硬件型号。参数取值说明模型参数量 N7e97B 级别数据类型FP16每参数 2 字节GPUA100 80GBF16/FP16 峰值算力约 312 TFLOPSHBM 带宽约 2 TB/s计算利用率50%大矩阵乘法达不到理论峰值带宽利用率80%顺序读权重时相对容易接近峰值Prefill 输入长度2048单请求这些假设代表“合理的乐观估计”。实际运行时计算利用率可能是 30% 到 60%带宽利用率可能是 50% 到 85%所以算出来的应该是区间而不是精确值。3.2 先算 Decode再算 Prefill先算 Decode因为它决定了大模型交互的“手感”。权重字节数7e9 × 2 14 GB计算时间2 × 7e9 14e9 FLOPs 14e9 / (312e12 × 0.5) ≈ 0.09 ms带宽时间14e9 / (2e12 × 0.8) ≈ 8.75 ms取两者最大值Decode 单 token 延迟约 8.75ms折算约 114 token/s。可见计算时间不到带宽时间的 1/100瓶颈完全在显存带宽。再算 Prefill。输入 2048 token 的总计算量2 × 7e9 × 2048 2.87e13 FLOPs按 156 TFLOPS 有效算力2.87e13 / 1.56e14 ≈ 0.18 s也就是说TTFT 大约在 0.2 秒量级不含网络传输和调度排队。如果把它平均到每个 token大约是 0.09ms看起来很快但用户感知的是整段输入处理完之后才开始输出所以 Prefill 300ms 和 Decode 10ms 必须分开看。KV Cache 显存2 × 32 × 4096 × 2 0.5 MB/token 2048 token → 约 1 GB 32768 token → 约 16 GB权重 14GB 加上 16GB KV Cache再加上激活值和 CUDA 上下文80GB 显存勉强够单请求跑 32K 上下文。这也是为什么长上下文场景必须考虑 KV Cache 量化或 GQA 结构。3.3 把估算过程写成可复用脚本手算只能验证一次实际项目中会把公式写成函数方便批量对比不同模型、不同 GPU、不同精度。def estimate_llm_inference( n_params: float, # 模型参数量例如 7e9 seq_prefill: int, # Prefill 阶段输入 token 数 n_layers: int, d_model: int, kv_bytes_per_elem: int 2, # KV Cache 每元素字节数FP16 为 2 weight_bytes_per_param: float 2.0, # 权重每参数字节数 gpu_fp16_flops: float 312e12, # GPU 峰值算力 gpu_bandwidth_bps: float 2e12, # 显存带宽 compute_util: float 0.5, # 计算利用率 bandwidth_util: float 0.8, # 带宽利用率 ): flops_prefill 2.0 * n_params * seq_prefill flops_per_token_decode 2.0 * n_params weight_bytes n_params * weight_bytes_per_param prefill_time_s flops_prefill / (gpu_fp16_flops * compute_util) decode_compute_s flops_per_token_decode / (gpu_fp16_flops * compute_util) decode_memory_s weight_bytes / (gpu_bandwidth_bps * bandwidth_util) decode_time_s max(decode_compute_s, decode_memory_s) kv_per_token 2.0 * n_layers * d_model * kv_bytes_per_elem kv_bytes kv_per_token * seq_prefill return { prefill_time_s: prefill_time_s, ttft_s: prefill_time_s, decode_ms_per_token: decode_time_s * 1000, decode_tokens_per_s: 1.0 / decode_time_s, kv_cache_gb: kv_bytes / 1e9, } result estimate_llm_inference( n_params7e9, seq_prefill2048, n_layers32, d_model4096, ) for k, v in result.items(): print(f{k}: {v:.3f} if isinstance(v, float) else f{k}: {v})运行后得到一组估算值prefill_time_s: 0.184 ttft_s: 0.184 decode_ms_per_token: 8.750 decode_tokens_per_s: 114.286 kv_cache_gb: 1.074脚本的核心逻辑就是“计算时间和带宽时间取最大值”。这个脚本没有考虑注意力计算的额外耗时、连续批处理对带宽的复用、以及多请求并发时的调度开销所以它给出的更适合作为下限参考。4. 从推理扩展到训练FLOPs、显存与通信4.1 训练总计算量约为 6NT推理前向传播约 2NT训练还要算反向传播。反向传播的计算量约为前向传播的两倍因此训练一个 epoch 的总计算量约为总的训练 FLOPs ≈ 6 × N × T这里的 T 是所有训练样本的 token 总数。以 7B 模型训练 1 万亿 token 为例6 × 7e9 × 1e12 4.2e22 FLOPs假设在 8 张 A100 上训练每张卡有效算力约 156 TFLOPS4.2e22 / (8 × 1.56e14) ≈ 3.37e7 秒 ≈ 390 天这个结果说明7B 模型在 8 张 A100 上训练 1 万亿 token需要一年以上。如果把 GPU 数量提升到 64 张约 49 天。这个数量级判断足以在项目立项阶段排除不合理的算力方案。4.2 训练显存模型状态、梯度和激活训练显存比推理复杂得多。除了权重还要保存梯度和优化器状态。混合精度 Adam 训练时每个参数大约需要项每参数字节数FP16 权重2FP16 梯度2FP32 主权重4Adam 一阶动量 m4Adam 二阶动量 v4合计167B 模型的模型状态约 112GB远超过单张 A100 的 80GB。这就是为什么训练 7B 模型必须做分布式并行或 ZeRO 分片而不像推理那样一张卡放权重就够了。激活值在训练时同样很大尤其在大 batch 和长序列场景。激活重计算activation checkpointing用额外一次前向传播换回显存是一种典型的“用算力换空间”取舍训练脚本里通常会开启。4.3 分布式训练中通信和分片策略的影响模型状态放不下时需要用数据并行加 ZeRO 分片。不同策略的显存节省和通信成本差异明显。方案每卡保存的模型状态每步通信量DDP数据并行每卡完整副本一次全量梯度 AllReduceZeRO-1优化器状态分片梯度 ReduceScatter 参数 AllGatherZeRO-2优化器状态 梯度分片同上通信略增ZeRO-3参数、梯度、优化器全部分片每层前向/反向额外 AllGather通信量最高DDP 的通信量约为权重字节数的两倍。7B 模型 FP16 梯度约 14GB一次 AllReduce 实际传输约 28GB。在 NVLink 带宽约 50GB/s 量级的集群上这是几十毫秒到几百毫秒量级的开销小模型加多卡时通信甚至可能超过计算时间。从第一性原理估算训练性能不能只看 GPU 算力必须把“模型状态放不放得下”和“通信要多久”一起算进去。如果模型状态超过单卡显存先决定分片策略再算每步耗时。5. 用实测校准估算而不是停留在纸面5.1 实测需要采集哪些指标建模的价值在于可修正。拿到估算值之后需要跑一组小规模测量把估算和实际对齐。最需要采集的指标是指标采集方式用途Prefill 耗时单次 forward 计时校准计算利用率Decode 单 token 耗时逐 token 生成计时校准带宽利用率显存峰值nvidia-smi 或 torch.cuda.max_memory_allocated校验 KV Cache 和激活估算GPU 利用率采样工具判断是否像预期那样 Prefill 高、Decode 低采样 GPU 利用率可以用命令行工具nvidia-smi --query-gpuutilization.gpu,memory.used --formatcsv -l 1想要看每个 kernel 的耗时分布可以在 CUDA 环境下使用 nsys 和 ncu。nsys profile看整体时间线ncu --set full看每个 kernel 的算力、带宽和利用率。这些工具输出的实际瓶颈往往能直接验证估算时假设的利用率是否有偏差。5.2 一个最小测量脚本在没有服务框架的情况下先用 Transformers 跑一个最小测量脚本把 Prefill 和 Decode 分开计时。import time import torch from transformers import AutoModelForCausalLM, AutoTokenizer device cuda model_name your-org/your-7b-model # 替换成实际模型 tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypetorch.float16 ).to(device).eval() prompt The capital of France is * 200 inputs tokenizer(prompt, return_tensorspt).to(device) # warmup先触发 CUDA kernel 和显存分配 with torch.no_grad(): model(**inputs, use_cacheTrue) torch.cuda.synchronize() # Prefill 计时 start time.perf_counter() with torch.no_grad(): out model(**inputs, use_cacheTrue) torch.cuda.synchronize() prefill_s time.perf_counter() - start # Decode 计时逐 token 生成 input_ids out.logits.argmax(dim-1)[:, -1:] start time.perf_counter() with torch.no_grad(): for _ in range(128): out model(input_ids, use_cacheTrue) input_ids out.logits.argmax(dim-1)[:, -1:] torch.cuda.synchronize() decode_s time.perf_counter() - start print(fprefill: {prefill_s:.3f}s for {inputs[input_ids].shape[1]} tokens) print(fdecode: {decode_s / 128 * 1000:.3f} ms/token)脚本的关键是 warmup 和torch.cuda.synchronize()。没有 warmup第一次 forward 会包含 CUDA context 初始化和显存分配测出的时间偏大没有 synchronize测到的是 GPU 异步执行之前的时间几乎总是偏小。5.3 估算值和实测值对不上时从哪里找原因实测结果与估算不一致很常见。校准的思路不是直接改公式而是反推“哪个假设错了”。估算明显快于实测优先检查以下方向计算利用率没有达到 50%小 batch 时大矩阵乘法无法喂饱 GPU。Decode 阶段不是单纯的权重搬运还要读取 KV Cache序列越长KV 读取开销越大。LayerNorm、RMSNorm、RoPE、注意力 softmax 这些非矩阵乘算子占用大量 launch 时间。Python 和 PyTorch 调度开销在短序列上占比很高。显存带宽实际利用率低于 80%。实测快于估算可能原因权重被量化成了更低精度实际搬运字节数小于估算。使用了 GQAKV Cache 显著缩小Decode 阶段带宽压力降低。batch 大于 1多个请求共享权重复用摊薄了权重搬运成本。校准的最终结果是把公式里的利用率参数修正成“这台机器、这个模型、这个 batch 下的实测值”。以后换模型规模、换 GPU 时再拿同一套校准后的参数去估算准确度会明显提升。6. 五个常见的性能估算误区6.1 误区对照表下面是工程实践里最容易出现的五类估算错误。每一条都可以对照自己的估算过程检查一遍。| 误区 | 典型现象 | 根因 | 正确处理 | | --- | --- | --- |
返回列表