LLM推理加速实战:从精度选择到KV Cache优化的工程路线 LLM 推理速度已经不是“能跑就行”的问题了。聊天应用要求首 token 足够快离线批处理要求吞吐足够高本地部署还要控制显存占用这几件事经常互相打架。Frontier.fast 在 Show HN 上被介绍为一个目标很直接的项目把 LLM 的速度边界再往前推一步。这个目标听起来简单真正落地时涉及精度格式选择、推理引擎调度、批处理策略、KV Cache 管理、采样解码方式以及一整套性能测量手段。这篇文章不打算只讲某个框架的某个按钮而是把“让 LLM 跑得更快”拆成一条可复现的工程路线先理解瓶颈在哪再按精度、引擎、调度、验证的顺序逐步推进。读完你可以得到一套属于自己的优化清单至少能在本地环境里建立基线、对比改动、定位瓶颈并知道什么时候值得继续优化、什么时候该停止优化。1. 先看明白LLM 推理的速度瓶颈到底卡在哪做速度优化之前必须先回答一个基本问题用户抱怨“慢”到底慢在哪LLM 推理不是单一操作它由多个阶段组成不同阶段的性能特征完全不同。如果连慢在哪个阶段都没确认优化就会变成盲目调参。1.1 延迟、吞吐、首 token 时延要分开看在实际部署中“速度慢”是一个很模糊的说法。不同角色关心不同的指标前端同学更在意首 token 什么时候出现后端同学更在意高并发下吞吐够不够算法同学则关心整条请求的端到端耗时。把这些指标混在一起讨论往往得不出有效结论。指标定义典型场景常见优化目标端到端延迟从请求进入到完整响应返回的墙钟时间一次完整问答、RAG 查询降低总等待时间吞吐单位时间内处理的请求数或生成 token 数离线批量推理、数据生产提升每秒完成量TTFT从请求进入到第一个输出 token 出现的耗时流式对话、助手应用让用户尽快看到响应TPOT/ITL每个输出 token 之间的平均间隔长文生成、流式展示让生成过程更流畅一段话总结交互式应用优先优化 TTFT 和单 token 间隔批量生产任务优先优化吞吐RAG 或多轮对话还要把上下文长度对耗时的影响算进去。同一个模型优化目标不同改动方案也会完全不同。1.2 预填充和生成阶段的计算特征完全不同Transformer 模型一次推理过程可以粗分成两个阶段。预填充阶段Prefill把用户输入的 prompt 一次性喂进模型所有 token 可以并行计算这时 GPU 的算力和显存带宽都会被大量利用计算特征更接近“算力密集型”。生成阶段Decode则不同。模型每一步只产生一个新 token而这个 token 又要依赖前面所有 token 的结果存在天然的顺序依赖无法像预填充那样大幅并行。每一步都要重新读取完整的权重参数和 KV Cache所以这一阶段通常受显存带宽限制而不是受算力限制。这就是为什么单纯比较“模型参数有多大、GPU 算力有多强”并不能预测推理速度。实际项目中经常出现的情况是算力很猛的卡在 decode 阶段并没有完全跑满因为带宽成了瓶颈。理解这一点后面看量化、批处理、投机解码这些方案时才能明白它们到底在解决哪部分问题。1.3 从一次请求的耗时分布理解完整链路一次标准的 LLM 推理请求大致经过下面这些环节请求输入 - 分词tokenize - embedding 向量化 - 多层 Transformer 前向计算 - 采样sampling选择下一个 token - 结束判断 - 反分词detokenize - 流式/一次性返回很多初学者只盯着“Transformer 前向计算”这一块实际上分词、采样、内存分配、显存碎片整理和排队等待都可能占用大量时间。特别是高并发场景请求可能在调度队列里已经等了几百毫秒模型本身反而没有慢。这里要特别注意一个容易忽略的点KV Cache。长上下文场景下每轮生成都要把历史 token 的 Key 和 Value 缓存住缓存越大每一步读取和写入的开销越大。这也是为什么很多推理引擎会专门优化 KV Cache 的分配和管理而不只是优化算子的计算速度。注意优化速度前先把一次请求的完整链路画出来标注每个环节大概消耗多少时间。没有这个分解后续所有优化都缺少参照系。2. 第一个提速杠杆FP16、BF16、FP32 精度选择精度问题是 LLM 推理提速里最基础也最容易被误解的一环。很多人知道 FP16 比 FP32 快但说不清为什么也有人听说过 BF16却不知道它和 FP16 的适用场景完全不同。这部分把精度和速度的关系讲透。2.1 为什么现代大模型不会默认用 FP32 做推理从数值正确性上说FP32 是很多开发者最早接触的浮点格式所以潜意识里会觉得它最“保险”。但在大模型场景FP32 有三个实际问题第一是显存占用翻倍。同样的模型权重FP32 比 FP16/BF16 多占一倍显存一个 7B 参数的模型用 FP32 存储权重就需要约 28GB很多单卡环境放不下。第二是显存带宽压力翻倍。decode 阶段本质是带宽密集型计算每生成一个 token 都要把权重从显存搬到计算单元。数据位宽越大搬运时间越长生成速度越慢。第三是计算单元支持问题。现代 GPU 的 Tensor Core 对 16 位浮点有专门加速路径对 FP32 的支持反而相对基础。所以 FP32 更多作为“计算过程中的累积精度”存在而不是作为推理时的主要存储格式。2.2 FP16 与 BF16看起来都是 16 位行为差别很大FP16 和 BF16 都占 16 bit都只有 2 字节但内部结构完全不同。FP16 的指数位有 5 位尾数位有 10 位能表示的数值范围比较窄容易出现上溢或下溢。训练大模型时如果梯度很小FP16 很容易直接变成 0所以早期 FP16 训练必须配合 loss scaling 这类技巧。BF16 的指数位有 8 位尾数位只有 7 位。它的数值范围和 FP32 几乎一样不会轻易上溢下溢但尾数精度低代表“小数部分不够精细”。格式指数位尾数位数值范围主要风险常用场景FP328 位23 位大且精确显存和带宽开销大训练累积、数值敏感计算FP165 位10 位容易上溢下溢小数值变 0早期训练、部分推理BF168 位7 位与 FP32 接近尾数精度不足大模型训练和推理的主流选择对大模型推理来说BF16 通常是比 FP16 更稳的起点。因为模型权重经过训练后数值范围往往分布在一个较宽的区间里BF16 能覆盖住这个范围不容易出现整体数值坍塌。而 FP16 对 weight 里某些极值更敏感一旦权重数值超出可表示范围输出质量会明显变差。2.3 再往下走INT8 和 INT4 量化如果 16 位仍然不够快、显存仍然不够用下一步就是量化。量化最核心的思路是把权重从浮点表示转成更小的整数表示比如 INT8 甚至 INT4从而减少显存占用和带宽压力。量化的收益非常直观INT8 比 FP16 的权重体积又缩小一半INT4 再缩小一半。一个 7B 模型用 BF16 存储约 14GBINT8 约 7GBINT4 约 3.5GB。这直接影响单卡能不能放下模型以及能不能把更多请求塞进同一张卡。但量化不是免费的。权重被压缩后信息必然有损失。实际项目里常见的问题是量化后生成结果看起来还行但在专业评测集或特定领域数据上分数下降明显。还有一个容易被忽略的风险部分低比特量化方案对激活值也做量化如果量化范围选择不当推理时可能出现个别 token 出现明显错误。量化等级权重体积示例7B 模型速度收益潜力精度风险BF16/FP16约 14GB基线低INT8约 7GB中中低INT4约 3.5GB高中高实际部署时不要把量化等级直接当作“越低级越好”。正确做法是准备一个评测集分别跑 FP16/BF16、INT8、INT4对比输出质量和速度再决定取舍。2.4 精度选择的落地建议如果项目刚起步推荐按这条路径走先用 BF16 跑通模型记录 TTFT、生成速度、显存占用作为基线。如果显存够用、速度达标就不需要量化。如果显存不够或并发量上不去优先考虑 INT8 量化。只有当显存极度紧张或批量吞吐要求极高时才尝试 INT4并且必须配套质量评测。所有精度切换都重新跑一遍评测不要只靠肉眼判断。注意FP16、FP32、BF16 的选择不是越高级越好而是在数值稳定性、显存占用和计算速度之间做取舍。改精度后输出质量出现异常首先应该回到精度对比而不是怀疑模型本身。3. 引擎和调度同样的模型为什么不同框架速度差很多很多人在本地用 PyTorch 把模型加载起来写一段 generate 代码发现速度不理想于是得出结论“这个模型太慢”。但真实情况很可能是原生推理代码没有做任何调度优化而生产级推理引擎做了大量你看不到的工作。3.1 从“能跑”到“跑得快”框架补了什么一个面向生产的推理引擎通常会在这些方面做优化算子融合把多个小算子合并成一个大算子减少内核启动次数和中间显存读写。内存池与显存复用避免每次请求都重新申请和释放显存减少碎片和分配开销。连续批处理不再等一个请求完全生成完才开始下一个请求而是动态地把正在生成的请求拼在一起由调度器决定每一步处理哪些请求。KV Cache 管理为长上下文场景优化缓存分配、复用和淘汰策略。量化算子为 INT8/INT4 权重提供高效的计算内核让量化真正转化为速度收益。这些优化对性能影响极大。同样是 Llama 这类开源模型用原生 PyTorch 跑和用专业推理引擎跑吞吐差距可能达到数倍但这种差距不代表模型本身变快了而是显存和带宽被利用得更充分了。3.2 主流开源推理引擎的优化思路速览引擎核心优化思路适合场景vLLM 系连续批处理 PagedAttention 式 KV Cache 管理高并发在线服务、多请求吞吐TensorRT-LLM 系强算子融合、图优化、平台深度适配NVIDIA 显卡上的生产部署llama.cpp 系CPU 友好、内存占用可控、量化支持完善本地单机、边缘设备、个人电脑Hugging Face 生态原生推理兼容性最好、上手快原型验证、研究调试这里不展开某个引擎的内部实现重点是要建立一个判断选引擎不是选“哪个最强”而是选“哪个更适合你的部署环境和请求模式”。如果跑在 NVIDIA 数据中心卡上图优化型引擎更容易发挥算力如果跑在 Mac 或 CPU 机器上 llama.cpp 这类内存友好型方案可能更实际。3.3 不要把硬件条件排除在速度问题之外推理速度不仅由模型和框架决定硬件资源的三个维度也在起作用显存容量决定单卡能否放下模型、能否同时处理多个长上下文请求。显存带宽决定 decode 阶段每生成一个 token 能多快读取权重和 KV Cache。算力决定 prefill 阶段处理大段 prompt 的速度。实际排查时可以先看资源监控GPU 利用率高但速度慢可能是算力密集型阶段GPU 利用率不高但速度慢很可能是带宽、内存碎片或调度等待问题。Mac 这类统一内存设备和 NVIDIA 独立显卡的瓶颈表现也不同前者更依赖内存带宽后者更依赖显存管理和算子优化。4. 一条可复现的加速路线从基线到生产前面讲的是概念这一章给出一个可以照着执行的顺序。核心原则只有一个每次只改一个变量改完都重新测量不做没有基线的优化。4.1 第一步永远是建立基线不要一上来就切换精度、换引擎、开量化。先写一个最小性能测试脚本把当前环境下的速度指标记录下来。下面是一段 Python 风格的性能测量示意用来说明思路实际项目要结合自己的模型接口和推理框架调整import time def measure_latency(generate_fn, prompt, max_new_tokens64, repeat5): # 预热先跑一次让显存分配、缓存等进入稳定状态 generate_fn(prompt, max_new_tokens16) ttft_list [] total_list [] for _ in range(repeat): start time.time() first_token_time None tokens 0 for item in generate_fn(prompt, max_new_tokensmax_new_tokens, streamTrue): if first_token_time is None: first_token_time time.time() tokens 1 cost time.time() - start ttft_list.append(first_token_time - start) total_list.append(cost) avg_ttft sum(ttft_list) / len(ttft_list) avg_total sum(total_list) / len(total_list) avg_tokens_per_sec (max_new_tokens 1) / avg_total return { avg_ttft: round(avg_ttft, 4), avg_total: round(avg_total, 4), avg_tokens_per_sec: round(avg_tokens_per_sec, 2), }这段脚本只是示意但它体现了三个关键点先预热、多次测量取平均、同时记录首 token 时延和整体生成速度。建立基线后把模型名、精度、输入长度、最大输出 token 数、batch 大小、硬件型号都记在备注里后续对比才有意义。4.2 第二步按优先级引入批处理与 KV Cache 优化基线建立后最常见的提速手段是解决“单条请求独占资源”的问题。连续批处理是第一个要理解的概念。传统批处理要把一批请求凑齐后才一起跑响应快的请求要等响应慢的请求完成。连续批处理则允许调度器在每一步动态插入新请求、移除已完成请求让 GPU 始终在处理有效 token提升整体吞吐。KV Cache 优化是第二个要理解的概念。多轮对话和长文本场景中重复计算已有前缀非常浪费。优化方向包括缓存历史轮次的 KV、复用公共前缀、使用 PagedAttention 式分页管理来减少显存碎片。这个阶段的检查点是相同并发量下吞吐是否提升显存峰值是否可控。如果吞吐提升但显存频繁溢出说明 batch 大小和缓存策略需要重新权衡。4.3 第三步用投机解码和采样策略压缩生成延迟如果延迟仍然是核心痛点可以尝试投机解码Speculative Decoding。投机解码的思路很巧妙用一个更小、更快的草稿模型一次性预测多个候选 token再用大模型并行验证这些候选。如果小模型猜得准大模型一次验证就能确认多个 token省去逐 token 生成的等待。它的收益高度依赖草稿模型的命中率。任务越难、上下文越复杂草稿模型越容易猜错收益就越小。实际项目中投机解码更适合对单请求延迟敏感的在线场景不适合所有负载。此外采样参数也会影响生成速度。max_new_tokens 上限、temperature、top_p 会改变采样分支行为但更关键的是前者决定最长生成时间。如果业务不需要超长输出在服务端限制最大生成长度是最简单也最容易被忽略的延迟优化。4.4 生产环境的额外要求学习环境里模型能跑、速度有提升就够了。生产环境还至少需要补齐这些能力日志与追踪记录每次请求的 TTFT、生成 token 数、总耗时和错误信息。监控与告警GPU 利用率、显存占用、排队长度、超时比例。并发与超时控制限制最大并发设置合理的超时时间防止雪崩。权限与安全推理接口需要鉴权流式输出需要做好连接管理。回滚能力模型、精度、引擎配置改动前保留旧版本便于快速回退。维度学习环境生产环境模型小模型、样例输入业务级模型、真实流量精度单次切换对比完整评测集 AB 对比并发单请求或少量并发压测验证稳定性和超时日志无或简单输出结构化日志、调用链追踪配置写在代码里外置配置、环境隔离回滚不需要必须保留版本快照5. 验证与排错如何证明速度提升了并且质量没坏优化做得再多最终都要回答两个问题速度到底快了多少质量有没有变差这两个问题都需要严谨的验证方法否则很容易被偶然波动误导。5.1 性能指标怎么测才可信性能测试最容易犯的错误是只测一次、只测成功路径、没做预热。推荐按下面这个清单做预热 1 到 2 次让缓存和显存分配进入稳定状态。使用固定输入文本和固定最大输出长度控制变量。同一配置至少跑 5 轮记录平均值和中位数。分别记录 TTFT、单 token 间隔、整体吞吐和显存峰值。对比不同配置时必须保证输入、输出长度、seed 和 batch 大小一致。高并发场景下记录 P50、P95、P99 延迟而不是只看平均值。一个常见误区是只报告“生成速度提升了 X%”却不说明输入长度和输出长度。实际上 prefill 和 decode 耗时对输入输出长度非常敏感不写清楚条件的数据没有复现价值。5.2 常见问题排查表问题现象常见原因检查方式处理建议显存 OOMbatch 过大、KV Cache 上限过高、模型精度占用超预期查看启动日志和显存监控降低 batch、限制最大上下文、切换 INT8首 token 极慢prompt 过长、prefill 计算量大、排队等待久分别测空 prompt 和长 prompt 的 TTFT压缩 prompt、使用缓存前缀、提高并发上限生成速度慢decode 受带宽限制、KV Cache 读取频繁观察 GPU 利用率和带宽指标开启连续批处理、量化权重、投机解码并发升高后延迟暴涨调度队列过长、显存碎片化、无超时控制查看排队长度和请求超时数限制最大并发、优化调度、增加实例量化后输出明显变差INT4/INT8 精度损失过大对比相同 prompt 下 BF16 与量化输出改用更高精度、使用量化感知微调推理结果出现乱码 token精度格式选择不当或采样参数不合理复现并检查异常 token 位置切换 BF16、检查 temperature 和 top_p5.3 精度下降怎么定位如果切换精度后输出质量下降不要立刻放弃新精度。先用固定 prompt 集合做对比找出质量明显变差的样本分析是整体变差还是个别长 token 出错。整体变差通常是量化范围选择问题或量化算法不匹配个别 token 乱码可能是权重中极端值被截断。还有一个值得注意的点模型推理是累积过程早期一个 token 选错后续生成会沿着错误方向持续放大。所以质量对比不能只看最终回答还要看生成过程中的 token 分布是否变化。条件允许时可以用相同 seed 和相同采样参数强制模型走相同路径减少随机性干扰。6. 在 Frontier.fast 这类速度开源项目中真正做出贡献Frontier.fast 这类项目的价值不只是给出一个更快的结果更重要的是它把“推动 LLM 速度边界”变成了一件可以协作、可以量化、可以复现的工程任务。参与这类项目的正确姿势不是盲目提交代码而是先学会建立可复现的对比环境。6.1 最小复现环境怎么搭参与前先回答四个问题用什么模型建议先选一个社区常用、权重容易获取的开源模型。用什么硬件记录 GPU 型号、显存大小、驱动版本。用什么推理框架锁定框架和关键参数。用什么指标固定输入、输出长度定义 TTFT 和吞吐计算方式。这四个问题确定后写一个 README 说明复现步骤。一个只能在自己机器上跑通、别人无法复现的优化对开源项目几乎没有价值。6.2 提交优化结论前要准备的材料向项目提交速度优化经验或代码前建议准备以下材料完整环境信息硬件、驱动、依赖版本、模型文件来源。数据条件输入 prompt、上下文长度、最大输出 token 数、采样参数。基线数据优化前测出的 TTFT、吞吐、显存占用。对比数据每次改动后的新数据并注明改动点。日志和报错崩溃时的完整报错信息不要只贴结论。质量验证如果改动涉及精度附上质量评测结果。这些材料的作用是让维护者能在最短时间内判断“这个改动是否值得合入”也方便后续其他人在不同硬件上复现对比。6.3 新手最适合切入的方向如果你刚接触 LLM 推理加速最值得长期投入的方向不是直接改内核算子是做评测和回归。第一性能基准脚本维护。把测量方法代码化、自动化让每次改动都有统一的口径这是项目非常需要的基建工作。第二多硬件适配报告。同一个引擎在不同 GPU、不同内存环境下的表现差异很大整理这些实测数据能帮整个社区少走弯路。第三精度与质量回归。帮助维护量化方案的质量评测集记录不同精度下模型输出变化这类资料比单纯宣传速度提升更可信。第四真实场景案例。把某个垂直场景的请求模式、上下文特征和速度瓶颈整理成文档能直接指导下一轮优化方向。注意参与开源速度项目时最忌讳只报喜不报忧。速度提升的数据要报显存波动、质量下降、失败案例也要报。对工程社区来说失败边界往往比成功数据更有价值。回到本文最初的问题Frontier.fast 想推动的“LLM 速度边界”本质上是把精度、引擎、显存管理、批处理、解码策略、评测方法这些环节的工程能力整体往前推一步。对普通开发者来说不需要一开始就挑战最底层实现先把基线、测量、精度对比和问题排查这套基本功练扎实就已经是在参与这条边界推进了。真正决定你能不能把模型调快的不是你用的工具多新而是你对瓶颈环节有多清楚。