这几年我带过不少从算法岗转应用开发的同事一个普遍的错觉是模型精度搞定了产品就搞定了一半。结果真把模型接到服务里并发一上来响应要么超时要么直接内存溢出整个系统像个纸糊的灯笼。如果你也有类似的感受说明你正卡在会用模型和能交付模型之间的那条性能河上。作为LLM大模型系统学习指南系列的第36篇这篇不聊Transformer原理也不讲微调技巧专门聊一个被绝大多数学习路线图忽略的环节推理性能工程。它解决的问题很具体——为什么同一个模型别人能撑住高并发你的服务一压就垮延迟和吞吐到底怎么测、怎么调、怎么定位瓶颈内容面向已经掌握LLM基本概念、跑通过开源模型或调用过API、但准备让模型真正服务业务场景的学习者。这篇学习指南更像一份性能工程实操手册读完你能建立自己的性能基线知道下一次遇到模型很聪明但服务很慢时该从哪里下手。1. 为什么到了系列第36篇我把推理性能当成真正的分水岭1.1 从模型精度达标到服务可用之间隔着一条性能河我见过一个很典型的模拟项目X一位同学花了三周时间把某个7B量级的开源对话模型微调得非常好回答质量在测试集上表现令人满意。上线那天预期并发用户是30实际压测到20路并发时平均响应时间已经超过15秒其中还有大量请求直接报错。他第一反应是模型是不是出问题了但单条请求测试时模型明明一切正常。这个案例不是个例。模型推理和模型训练有一个本质区别训练时你关心的是收敛曲线跑慢一点无非多等几小时推理是线上服务用户不会等一个转圈超过十秒的聊天机器人。精度只是模型价值的一部分服务可用性才是模型能不能真正交付的分水岭。所谓服务可用拆开来看是三件事单位时间能处理多少请求吞吐、每个请求等多久才看到第一个字延迟、在硬件成本不变的前提下能不能撑住峰值流量效率。这三件事都和模型本身的智能水平无关纯粹是系统工程问题。很多学习路径恰恰在这里出现空白课程教你怎么训模型教程教你怎么调API却很少告诉你怎么把模型变成一台稳定高效的服务机器。1.2 性能优化不是玄学它是可拆解、可量化的工程学我对推理性能优化的第一点体会是它绝对不是一个调调参数碰运气的黑盒过程。相反它有一套完整的方法论核心就四个字——测量、定位。测量指的是用可控的手段压出系统的真实能力边界得到延迟、吞吐、显存占用这些硬指标。定位指的是当你发现某个指标异常时能判断瓶颈是出在模型本身、推理引擎的调度策略、还是硬件资源分配上。优化则是针对定位到的瓶颈做针对性改动可能是调整并发策略也可能是换一种量化方案。最后用同样的压测手段验证优化效果形成闭环。这套方法论之所以重要是因为LLM推理的性能瓶颈往往不是单点的。一个系统变慢可能是模型权重复载太慢、可能是KV Cache把显存挤爆了、可能是批处理策略浪费了算力甚至可能是压测脚本本身没写对。没有测量数据做锚点所有的优化都是在猜。所以这篇学习指南会从指标讲起再到压测方法最后才是优化手段每一步都建立在前面可量化的基础上。2. 延迟、吞吐、TTFT先搞清楚这些指标在说什么2.1 四个核心指标的定义与关系评估LLM推理性能业界最常用的是四个指标TTFT、TPOT、端到端延迟、吞吐。很多初学者一上来就看响应速度但这个词太模糊了必须拆开看。TTFTTime To First Token指从请求发出到返回第一个token的时间。它包含网络传输、排队等待、prefill阶段处理提示词的全部耗时。在流式输出场景里TTFT直接决定了用户感觉快不快。TPOTTime Per Output Token指生成阶段每产出一个token的平均耗时它决定了一段回答生成的流畅程度。端到端延迟就是整个请求从发出到全部token返回的总耗时对于非流式接口来说这就是用户感知的全部。吞吐则是指系统在单位时间内能生成的token总数通常用tokens/s衡量它代表了系统整体的处理能力。这几个指标之间是相互关联又不完全同步的。端到端延迟大约等于TTFT加上TPOT乘以输出token数但吞吐高不代表延迟低——服务器可以同时处理很多请求积累出很高的吞吐但每个请求因为排队而变慢。反过来某些量化方案能显著降低单请求延迟但牺牲了并发吞吐能力。实际操作中要根据业务场景决定优先优化哪个指标对话产品优先TTFT和TPOT离线批量任务优先吞吐。2.2 一个请求为什么会慢自回归解码的串行宿命和显存带宽瓶颈理解LLM推理的性能瓶颈首先要接受一个事实LLM天生是串行的。它生成回答的方式是自回归——每生成一个token都把它拼到已有的序列后面再重新送入模型计算一次才能得到下一个token。这个过程没法并行第二十一个token必须等第二十个token算完。这个串行特性带来一个严重后果生成阶段的计算效率极其低下。每生成一个token模型实际上要把全部参数从头到尾算一遍前向传播但真正新增的计算量只有一个token那么多。打个比方这就像每次只做一道菜却要把整个厨房的锅碗瓢盆全部清洗一遍再开工。更关键的是这个过程的瓶颈不在算力而在显存带宽。生成token时模型权重从显存搬运到计算单元的速度决定了生成速度的上限。主流GPU的算力远大于显存带宽所以哪怕算力利用率不高只要权重搬运得快生成速度也能接受。这也是为什么同样一个模型在显存带宽更高的硬件上跑得更快的根本原因。KV Cache的显存占用也必须重视。手头一个常见的7B量级开源模型假设32层、KV头数为8、每个头的维度是128、加载精度为FP16那么每个token的KV Cache占用大约是2K和V各一份乘以32层乘以8头乘以128维度乘以2字节算下来约128KB。看起来单个token不多但并发16个请求、上下文长度2048时KV Cache总量就是128KB乘以16乘以2048约4GB显存。很多优化方案的核心本质上都在想办法压住这笔开销。3. 高并发下的隐性瓶颈批处理策略与KV Cache管理3.1 传统静态批处理的木桶效应单请求性能搞清楚之后高并发场景下又会冒出新问题多个请求同时进来怎么调度才能让GPU高效运转最直觉的做法是静态批处理——攒够一批请求一起送给模型计算等整个批次全部生成完毕再统一返回。静态批处理的问题在于木桶效应。一个批次里A请求要生成20个token就结束了B请求需要生成200个token那么A请求必须空等着B请求跑完整个批次的GPU资源被最慢的请求拖住。一批请求处理完之前新进来的请求只能在缓冲区排队系统吞吐被切割成一个个气泡实际利用率远低于理论值。我实际压测过这样的系统静态批处理下GPU算力利用率能到60%就已经不错更多的算力都浪费在等待和排队上。请求输出长度越不均匀浪费越严重。这也是所有高性能推理框架都在努力解决的问题。3.2 连续批处理请求级调度的核心思想针对静态批处理的问题连续批处理给出了一个很朴素但有效的方案不再以批为单位而是以请求为单位动态调度。连续批处理的思路是GPU上同时存在多个请求但每个请求独立计算。某个请求生成结束时立刻释放它占据的显存和计算资源新的请求马上补进来不用等整个批次全部完成。这样GPU上的坑位始终被填满气泡被压到最低。打个比方静态批处理像是旅行团的固定大巴一人没逛完景点全车人等连续批处理则像随时发车的公交下车一个就能上来一个站台永远不空。这个策略对吞吐的提升非常明显。在模拟项目X的压测里同样硬件、同样模型从静态批处理迁到支持连续批处理的推理框架后吞吐直接翻了一倍多TTFT也有明显下降。如果你现在自己搭推理服务不建议自己实现调度逻辑直接用现成的推理框架它们底层默认就做了这层优化。3.3 KV Cache分页管理减少碎片提升容量利用率连续批处理解决的是资源能不能及时腾出来KV Cache管理解决的是腾出来的显存能不能被有效用上。前面算了账KV Cache是显存消耗大户但它的分配问题常被忽略。问题在于KV Cache的显存不是一次性分配好的而是随着生成过程不断增长。不同请求的生成长度不一样有些开头就分配了大块显存结果没写几个token就结束了留下无法再利用的细碎空洞。这个问题很像操作系统的内存碎片问题你手里显存总量看着够但真正能分配给新请求的连续空间不够了。主流的解决思路是分页管理也就是说把KV Cache切成固定大小的小页面按需分配不要求物理连续。这跟操作系统的虚拟内存分页是同一个道理逻辑上是一段连续的序列物理上落在分散的页框里用完随时回收。配合前面说的连续批处理显存利用率和灵活度都大幅提升。对做应用层的开发者来说你不需要自己实现这套机制但理解它有助于你正确设置最大并发数——那个参数背后其实就是在约束KV Cache的内存预算。4. 实战压测把性能问题从感觉变成数据4.1 一套简单可复现的压测脚本前面讲了这么多理论实际动手的时候第一步永远是压测。一套能复现的压测脚本是性能工程的基石我每次搭新服务都会先写好这套工具。下面这个Python脚本思路很通用并发发送一批请求分别统计TTFT、TPOT和端到端延迟。重点是记录分位数而非平均值因为平均值会被个别慢请求拉高掩盖大多数用户的真实体验。import time import concurrent.futures import numpy as np def single_request(url, prompt, max_tokens): # 这里根据你的服务实际协议改写请求逻辑 ttft_total 0 tokens [] start time.time() first_token_time None with requests.post(url, json{prompt: prompt, max_tokens: max_tokens}, streamTrue) as resp: for line in resp.iter_lines(): if line: token parse_token(line) # 按你的流式协议解析 if first_token_time is None: first_token_time time.time() - start tokens.append(token) end_time time.time() tpots [] if len(tokens) 1: delta (end_time - start - first_token_time) / (len(tokens) - 1) tpots.append(delta) return first_token_time, np.mean(tpots), end_time - start, len(tokens) def run_load(url, prompts, concurrency, max_tokens): ttfts, tpots, latencies, total_tokens [], [], [], 0 with concurrent.futures.ThreadPoolExecutor(max_workersconcurrency) as pool: futures [pool.submit(single_request, url, p, max_tokens) for p in prompts] for f in concurrent.futures.as_completed(futures): ttft, tpot, latency, n_tokens f.result() ttfts.append(ttft) tpots.append(tpot) latencies.append(latency) total_tokens n_tokens total_time time.time() - start_time print(TTFT p50/p95: %.2fs / %.2fs % (np.percentile(ttfts, 50), np.percentile(ttfts, 95))) print(E2E p50/p95: %.2fs / %.2fs % (np.percentile(latencies, 50), np.percentile(latencies, 95))) print(Throughput: %.2f tokens/s % (total_tokens / total_time))有几个细节必须注意。第一压测前要留预热时间让模型加载完、显存分配稳定否则前几个请求的数据会异常慢。第二请求的prompt长度和max_tokens要固定变量越多测出来的数据越难比较。第三用固定并发而不是固定QPS来压测因为QPS本身受系统性能影响会掩盖真实能力。4.2 从压测数据反向定位瓶颈拿到压测数据后下一步是根据指标特征定位瓶颈。我整理了实际排障中常见的几类现象和对应方向你压测时可以直接对照。现象可能原因排查手段常见解决方向TTFT高但TPOT正常prefill阶段太长或排队严重看并发数、请求输入长度分布缩短输入、限制并发、提高prefill并行度TPOT高端到端延迟高显存带宽吃满或量化不到位观察生成阶段GPU利用率和带宽换低精度推理、减少batch size吞吐上不去CPU空闲批处理策略低效检查服务线程数和推理引擎配置启用连续批处理、调大请求并发上限显存OOMKV Cache占满查看KV Cache监控指标降低最大上下文长度、开启KV Cache量化、减少并发数这里还想强调一个经常被忽视的点TTFT高并不一定是服务端慢可能只是请求排队时间长了。压测时如果并发开得很大TTFT的p95会迅速恶化但TPOT指标还正常——这种时候不要急着优化模型先看看并发配置是不是超过了系统的合理上限。性能优化的第一步永远是确认瓶颈在哪一层而不是对着模型硬调。5. 性能优化三板斧投机采样、量化、并行策略5.1 投机采样用小模型打草稿、大模型做审核换速度压测数据定位到模型生成本身就慢之后第一板斧是投机采样。它的思路很巧妙既然自回归生成是串行的而且每生成一个token都要搬一遍全部权重那我们能不能用小一点的模型先快速草拟一串候选token再让大模型一次性校验具体流程是一个小草稿模型快速自回归生成K个候选token大模型把这K个token一次性前向计算校验哪些能被接受。因为大模型验证K个token的计算量只比验证一个token多一点如果小草稿模型写得够好大模型一次计算就能跳过多个生成步骤实际生成速度接近小草稿模型而质量由大模型兜底。这套机制适合生成任务文本重复度较高的场景比如代码生成、固定格式输出草稿模型命中率高了提速明显。但对开放式创意写作草稿命中率低收益会缩水。实际使用时要拿自己的业务数据测一下草稿接受率能到60%以上就是划算的长期低于40%建议别开。5.2 量化降低精度、压低带宽需求第二板斧是量化。前面说过LLM生成阶段的瓶颈在显存带宽而量化可以直接降低模型权重的字节数等于减少每次搬运的数据量生成速度自然提升。常见的三档量化方案对比如下方案权重精度显存占用变化生成速度变化适用场景FP1616位基准基准追求极致精度的离线分析INT88位约减少50%提升约20%-40%大部分线上对话服务INT44位约减少70%提升约40%-70%显存紧张、对精度要求不极端量化对精度的影响没有很多人想象中那么可怕。现代量化方法会把异常权重单独处理7B模型在INT8下能力损失通常很小INT4下在一些逻辑推理任务上会略有下滑但普通对话和内容生成几乎感觉不到。如果你的服务显存是硬约束优先考虑KV Cache量化它不影响权重精度只压缩缓存部分的占用对显存紧张是立竿见影的。5.3 并行策略单卡、单机多卡、多机的边界第三板斧是并行策略但很多初学者在这里会误用。要搞清楚数据并行多个副本各处理不同请求提升的是吞吐不降低单请求延迟模型并行把大模型切到多张卡是为了放下装不下的大模型本身不会让单请求更快甚至因为卡间通信反而更慢。在实践中如果你的模型在单机上能装下优先单卡服务延迟最稳。模型太大单卡装不下再考虑模型并行这里又分为按层切分和按层内切分两种方式前者适合几十B到百B量级的模型后者适合更大规模的模型。多机分布式推理的成本很高通常是万级并发才需要考虑的事。对你来说最实际的选择原则是先量化再考虑投机采样最后才上并行。量化是零成本收益最大的操作并行则要付出额外的工程复杂度。学习指南阶段不用把分布式啃透能把单机方案的性能榨干已经超过大多数应用团队了。6. 在性能优化这条路上我踩过的几个坑6.1 默认参数陷阱被环境配置悄悄偷走的性能第一条坑来自压测环境本身。我曾经拿一个推理框架的默认配置做压测发现显存占用异常高TTFT也偏大排查了很久才意识到是默认的最大序列长度设得非常大导致KV Cache提前预分配了巨额显存。更隐蔽的坑是压测时长不够。短时间压测时显存和缓存还没触顶测出来的吞吐虚高只有压到持续几分钟让缓存、垃圾回收、并发排队全部进入稳定状态数据才接近真实水平。我现在做压测的标准流程是预热1分钟正式压测至少3分钟取第2分钟到第3分钟的数据做统计。别嫌麻烦这部分时间换来的数据可靠性是值得的。6.2 输入长度分布是被忽略的最大变量第二个坑和测试数据的构造有关。很多网上教程教你用固定长度的prompt压测但真实业务里用户输入长度可能从几十到几千不等。输入长度直接影响prefill阶段的计算量进而决定TTFT。我做过一个印象很深的对比用固定短输入压测系统TTFT的p95只有0.8秒换成混合长度输入后TTFT的p95直接飙到3秒以上。模型完全没变只是prompt分布变了。所以压测数据要想和线上对得上必须模拟真实的输入长度分布。最简单的方法是从线上日志里抽样一批真实prompt按用户真实使用比例混入压测脚本。6.3 从调参到系统习惯建立自己的性能基线档案最后一个建议可能有些反直觉性能优化真正拉开差距的不是某一次惊为天人的调参而是持续记录和追踪性能指标的纪律。我习惯给每个服务建一份性能档案内容包括服务版本、模型版本、并发数、输入长度分布、量化方案、压测得到的TTFT/TPOT/吞吐分位数、显存峰值。每次改动任何配置都在同一条件下重跑压测把新旧数据并排对比。这套习惯救了我很多次——有时候改动模型版本后感觉似乎更快了一对比数据才发现TPOT其实退步了10%只是感官骗人。这么做还有一个额外的好处你在写性能优化方案时不需要靠我觉得而是有一整条数据链支撑。这个习惯是学习指南里最值得长期保持的一个。回头再看从模型到可用服务之间的路确实很长但每一步都可以被测量、被拆解、被优化。先跑通压测脚本拿到第一份属于自己的性能基线剩下的所有优化手段都会在那个基线上清晰显现出来。别急着把网上所有技巧都堆上去先在真实业务流量的模拟下把基线数据做扎实后面每一步优化都看得见回报。