
文章目录1 - 引言2 - 一次请求包含两个性质不同的计算阶段3 - 先统一指标再讨论快慢4 - 显存先装权重剩余空间才属于 KV Cache5 - 每种优化手段解决的问题不同6 - 用一份日志计算 TTFT、TPOT 和系统吞吐7 - 一套可复用的排查顺序1 - 引言同一个大模型在个人电脑上可能每秒只生成几个 Token在服务器上可以支撑大量并发同一套服务在空载时响应很快用户一多却开始长时间排队。把这些差异全部归因于“GPU 不够快”或“模型太大”通常找不到真正的瓶颈。大语言模型推理是一条完整链路。请求要经过网络、排队和批处理模型先读取全部输入并建立 KV Cache再逐个生成输出 Token。模型权重占用显存KV Cache 随上下文和并发增长多张 GPU 之间还要同步中间结果。用户感受到的延迟是这些环节共同作用的结果。理解这条链路的意义不是要求每个人都去设计芯片而是学会把“推理很慢”拆成可以测量的问题首字慢在哪里后续生成为什么慢并发增加后吞吐是否仍然上升显存究竟被权重还是缓存占满。2 - 一次请求包含两个性质不同的计算阶段请求到达服务后通常先进入调度队列。服务端会根据显存、批次 Token 上限和调度策略把一个或多个请求组合起来。模型真正执行时可以把过程分成 Prefill 和 Decode 两个阶段。Prefill会并行处理全部输入 Token计算每一层注意力需要的 Key 和 Value并把它们保存到 KV Cache。输入越长需要处理的 Token 越多首个输出通常就越晚出现。Prefill 能形成较大的矩阵运算通常更容易利用 GPU 计算单元。Decode每次只生成一个新 Token。新 Token 会查询之前已经保存的 KV Cache并经过模型各层得到下一个 Token 的概率。这个过程具有自回归依赖前一个 Token 没生成后一个 Token 就不能开始。在较小批次下Decode 经常需要反复读取大量权重和缓存因而更容易受到显存带宽限制。“Prefill 偏计算密集、Decode 偏内存带宽密集”是常见近似不是对所有模型和批次都成立的铁律。批次、量化方式、模型结构和硬件都会改变算术强度。但这个近似足以解释一个常见现象增加计算峰值可能明显改善长 Prompt 的首字时间却不一定同比提高单用户的逐 Token 生成速度。FlashAttention 论文进一步说明注意力性能不仅取决于浮点运算量还取决于 GPU 高带宽内存与片上 SRAM 之间的数据读写。它通过分块计算减少内存访问在保持精确注意力结果的情况下改善运行时间和内存占用。这也是为什么只比较芯片的理论算力并不足以预测真实推理速度。3 - 先统一指标再讨论快慢推理系统最常用的指标包括 TTFT、TPOT、端到端延迟和系统吞吐。不同工具对边界的定义可能略有差异比较结果前必须确认口径。NVIDIA 的推理基准指标文档给出了这些指标的一套明确说明。指标含义主要受什么影响用户感受TTFT从提交请求到收到首个有效 Token排队、网络、输入长度、Prefill等多久开始看到回答TPOT / ITL首个 Token 之后相邻输出 Token 的平均间隔Decode、显存带宽、KV Cache、并行通信回答生成是否流畅E2E Latency从请求开始到完整回答结束TTFT、输出长度、TPOT整个任务花多久TPS系统单位时间输出的 Token 总数批处理、并发、硬件利用率服务总体处理能力RPS系统每秒完成的请求数请求长度分布、TPS、排队策略服务能承受多少请求若输出 Token 数为N并且N 1常见的 TPOT 计算方式是TPOT (end_time - first_token_time) / (N - 1)系统 TPS 则可以用测试窗口内输出 Token 总数除以从第一个请求开始到最后一个请求结束的时间。必须注意TPS 高不代表单个用户体验一定好。提高并发和批次可以增加系统总吞吐却可能让每个请求排队更久导致 TTFT 和 TPOT 上升。工程上要寻找满足延迟目标时的最高吞吐而不是单独追求一个最大数字。4 - 显存先装权重剩余空间才属于 KV Cache部署模型前首先要做容量预算。最粗略的权重显存可以这样估算权重容量 ≈ 参数量 × 每个参数的字节数一个 70B 参数模型以 BF16 或 FP16 保存理论权重约为70 × 10^9 × 2字节也就是约 140 GB。采用 8 位或 4 位量化后理论权重可以下降到约 70 GB 或 35 GB但实际运行还要考虑量化元数据、临时缓冲区、框架开销和显存碎片不能把理论值直接当成部署上限。KV Cache 的大小取决于模型层数、KV 头数量、每个头的维度、上下文 Token 数和数据精度。对常见 Transformer可以用下面的近似式KV Cache 字节数 ≈ 2 × 层数 × KV头数 × 头维度 × Token数 × 每元素字节数最前面的2分别代表 Key 和 Value。采用 GQA 或 MQA 的模型其 KV 头数可能显著少于查询头数因此缓存需求也会下降。下面的 Python 函数可以快速估算单个请求和多并发下的 KV Cache 理论容量defkv_cache_gib(layers:int,kv_heads:int,head_dim:int,tokens:int,bytes_per_element:int2,concurrency:int1,)-float:bytes_per_request(2*layers*kv_heads*head_dim*tokens*bytes_per_element)total_bytesbytes_per_request*concurrencyreturntotal_bytes/(1024**3)singlekv_cache_gib(layers32,kv_heads8,head_dim128,tokens8192,)concurrentkv_cache_gib(layers32,kv_heads8,head_dim128,tokens8192,concurrency16,)print(fsingle request:{single:.2f}GiB)print(f16 concurrent requests:{concurrent:.2f}GiB)这组参数下每个 Token 的 KV 数据约为 128 KiB8,192 Token 的单请求缓存约为 1 GiB16 个同长度并发请求约需 16 GiB。实际服务还会受到缓存块大小、序列长度差异、前缀共享和实现开销影响但这个数量级已经能说明为什么长上下文与高并发很容易吃掉权重之外的显存。PagedAttention 论文借鉴操作系统分页思想把每个请求的 KV Cache 划分为块减少连续内存预留造成的碎片并支持缓存块共享。它解决的主要是缓存管理效率不会让 KV 数据本身凭空消失。若上下文和并发都持续增长容量预算仍然不可省略。5 - 每种优化手段解决的问题不同推理优化不能从“最流行的技术”开始而应从已经测到的瓶颈开始。技术主要解决什么可能的代价或边界量化降低权重容量和内存带宽压力可能影响精度部分硬件未必有高效内核FlashAttention减少注意力计算的内存读写需要模型、精度与硬件内核支持PagedAttention降低 KV Cache 碎片支持动态分配仍受总显存和上下文长度限制连续批处理请求结束后立即补入新请求提高利用率批次过大可能损害交互延迟前缀缓存复用重复系统提示或公共前缀的 KV前缀必须一致缓存需要淘汰策略推测解码用较小模型提议多个 Token由大模型验证接受率低时收益有限还增加系统复杂度张量并行将单个模型权重拆到多张 GPU每层都可能发生通信互联性能很重要Prefill/Decode 分离分别调度两种性质不同的负载KV 传输、路由和容量规划更复杂连续批处理会在每一步 Decode 后重新组织批次已完成请求离开新请求立即加入不必等待整个静态批次结束。Hugging Face 的连续批处理文档也把调度策略、最大批次 Token、分页缓存和前缀缓存列为相互关联的配置。提高批次上限通常有利于吞吐但如果大量长 Prompt 与短交互请求混在一起用户可见延迟可能变差。张量并行则解决单张 GPU 放不下模型的问题。权重被切分后各 GPU 分别计算局部结果再通过高速互联同步。GPU 数量增加并不保证线性加速如果通信时间抵消了计算收益更多设备反而可能提高成本却没有改善延迟。6 - 用一份日志计算 TTFT、TPOT 和系统吞吐优化之前至少要保存请求开始时间、首个 Token 时间、结束时间、输入 Token 数和输出 Token 数。假设基准工具导出了下面的 CSVrequest_id,start_ts,first_token_ts,end_ts,input_tokens,output_tokens r1,0.00,0.42,3.10,1024,80 r2,0.15,0.71,4.36,4096,96 r3,0.40,0.88,2.92,512,64下面的标准库脚本可以计算每个请求的指标以及测试窗口的 p50、p95 和总 TPSfrom__future__importannotationsimportcsvimportmathfrompathlibimportPathdefpercentile(values:list[float],q:float)-float:ifnotvalues:raiseValueError(values must not be empty)orderedsorted(values)position(len(ordered)-1)*q lowermath.floor(position)uppermath.ceil(position)iflowerupper:returnordered[lower]weightposition-lowerreturnordered[lower]*(1-weight)ordered[upper]*weight rows:list[dict[str,float|int|str]][]withPath(requests.csv).open(encodingutf-8,newline)asfile:forrawincsv.DictReader(file):output_tokensint(raw[output_tokens])startfloat(raw[start_ts])firstfloat(raw[first_token_ts])endfloat(raw[end_ts])ifnotstartfirstend:raiseValueError(finvalid timestamps for{raw[request_id]})ifoutput_tokens1:raiseValueError(fno output token for{raw[request_id]})ttftfirst-start e2eend-start tpot0.0ifoutput_tokens1else(end-first)/(output_tokens-1)rows.append({id:raw[request_id],start:start,end:end,output_tokens:output_tokens,ttft:ttft,tpot:tpot,e2e:e2e,})ttfts[float(row[ttft])forrowinrows]tpots[float(row[tpot])forrowinrows]windowmax(float(row[end])forrowinrows)-min(float(row[start])forrowinrows)total_output_tokenssum(int(row[output_tokens])forrowinrows)system_tpstotal_output_tokens/windowprint(frequests:{len(rows)})print(fTTFT p50/p95:{percentile(ttfts,0.50):.3f}s /{percentile(ttfts,0.95):.3f}s)print(fTPOT p50/p95:{percentile(tpots,0.50):.4f}s /{percentile(tpots,0.95):.4f}s)print(fsystem TPS:{system_tps:.2f})真实压测时样本必须足够多并区分预热阶段。还要按输入长度、输出长度和并发分桶否则一个“平均 TTFT”可能只是请求分布变化的结果。对比两个系统时模型、精度、采样参数、上下文长度、并发和完成条件都应保持一致。7 - 一套可复用的排查顺序第一步先定义服务目标。例如交互场景可能更关注 TTFT 和 p95 TPOT离线摘要则更关注单位时间吞吐和成本。没有目标任何优化都可能只是把压力从一个指标转移到另一个指标。第二步固定工作负载。选择几组有代表性的输入和输出长度记录模型版本、精度、GPU 型号、服务框架和配置。先测单并发基线再逐级增加并发直到吞吐停止增长或延迟超出目标。第三步做容量预算。确认权重、KV Cache 和运行时缓冲区都能放入显存并预留安全余量。若模型本身放不下先解决量化、并行或硬件容量若只有长上下文和高并发失败再重点检查 KV Cache。第四步根据指标定位。TTFT 随输入长度明显上升要检查 Prefill 和排队TPOT 在上下文增长后恶化要检查带宽和 KV Cache总吞吐在高并发下降要检查调度、批处理和过载多 GPU 比单 GPU 更慢则要检查通信比例和并行策略。第五步一次只改变一个主要变量。启用量化、调整批次、打开前缀缓存和更换并行方式如果同时发生即使结果变好也无法知道哪项改变有效。每轮都应保存配置、指标和失败样本。模型能力决定了系统能回答什么服务工程决定了这种能力以多快、多稳定、多高成本到达用户。把推理看成一条可测量的链路后“换一张更快的卡”不再是默认答案。很多问题可能出在调度、缓存、上下文设计或通信也可能只是指标口径没有统一。性能优化的起点不是某个框架名称而是一份可重复的工作负载和一组定义清楚的指标。先找到瓶颈再选择技术最后用相同测试验证变化通常比堆叠所有流行优化更可靠。感谢各位大佬支持互三啦