公司动态
LLM推理速度优化全解析:从原理到可复现评测实战
在 Show HN 上看到 Frontier.fast 这个项目时标题写得很直白Help push the frontier of LLM speed forward。翻译过来就是“一起把 LLM 的推理速度前沿往前推”。这类项目和传统论文、商业白皮书最大的区别在于它不满足于“某个框架宣称自己快了多少”而是把一个更深层的问题摆到开发者面前——LLM 的推理速度到底应该怎么度量、怎么优化、怎么复现对比。如果你最近在做 LLM 应用开发或者正在搭 RAG、Agent 这类依赖多轮调用和长上下文的产品你应该能明显感觉到模型能力再强如果生成速度跟不上用户体验和成本账单都会很难看。本文不打算去猜 Frontier.fast 项目内部用了什么具体实现而是围绕这个主题把 LLM 推理速度的底层原理、优化技术、实战评测方法完整拆解一遍。读完你可以自己搭建一套可复现的速度评测环境也能理解为什么有的优化方案在某些场景下有效、在另一些场景下反而拖慢速度。1. LLM 推理速度为什么值得专门研究1.1 速度直接决定应用形态LLM 推理速度和普通的 Web 接口延迟不一样它不是一个可以靠加机器“无脑解决”的数字。做过生成式应用的人都知道一次对话背后是几十甚至几百个 token 的逐个生成。如果每秒只能生成几个 token用户在输入框前等几十秒才看到完整的回复这种体验对大多数产品是不可接受的。更重要的是新一代 LLM 应用已经从“单轮问答”演进到 Agent、RAG、多工具编排等复杂形态。一个 Agent 任务可能要调用 3 到 5 次模型推理还要在长上下文里反复读取中间结果。这种情况下单次推理的延迟会被放大好几倍。底层推理引擎的吞吐能力决定了系统是能支撑 10 个并发用户还是 100 个并发用户也决定了 GPU 资源是能够复用还是频繁空转。1.2 Frontier.fast 这类项目在解决什么问题从项目标题定位来看Frontier.fast 代表了一类“面向 LLM 速度前沿”的社区项目。这类项目通常有一个共同特征把性能优化从“某家公司内部的 benchmark 报告”变成“社区可以共同参与、对比、贡献改进的公共基准”。这样做是有实际价值的。因为 LLM 推理性能是一个极度依赖环境的东西同样的模型在 A100 和 4090 上的表现不同同一张显卡用 FP16 和 INT4 量化后的吞吐量也不同同一个框架不同版本之间的优化差异可能超过 20%。如果没有统一的评测口径社区里就会出现大量“我的模型比你快”但实际上是环境变量不同的无效争论。所以与其把 Frontier.fast 理解为某个具体工具不如把它理解为一种技术共识LLM 速度优化需要工程化、可复现、可对比。这也是本文后续所有实战内容的出发点。1.3 掌握评测能力是优化能力的前提很多开发者一上来就研究各种“加速黑科技”结果模型换了好几个代码改了一大堆却连“这次改动到底快了多少”都说不清楚。问题就出在缺少一套稳定的评测工具。评测能力是整个 LLM 推理优化体系中最容易被忽视却又最关键的一块。有了评测脚本你才能回答几个基础问题当前模型在目标硬件上的真实吞吐是多少切换推理引擎后性能提升了多少量化、换精度、调并发参数之后性能曲线是怎么变化的生产环境扩容的依据是什么本文后面给的实战脚本解决的就是这些问题。2. 先搞清楚 LLM 推理为什么慢2.1 Prefill 与 Decode两个完全不同的阶段在量化优化手段之前必须先理解 LLM 生成过程的两个阶段因为两者的瓶颈完全不同。第一阶段叫 Prefill也就是预填充阶段。模型拿到用户输入的 prompt需要并行计算出所有输入 token 对应的 KV 缓存和第一个输出 token。这个阶段的特点是计算密集整个 prompt 的所有 token 是一起处理的GPU 的算力利用率通常比较高。第二阶段叫 Decode也就是解码阶段。模型逐个生成后续 token每个 token 的生成都依赖前面所有的历史 token。这个阶段的特点是从计算密集变成访存密集——每一步虽然只生成一个 token但需要把整个模型的权重和越来越长的 KV 缓存都从显存里读一遍。理解了这两个阶段你就明白为什么 LLM 推理不能简单用“每秒生成多少 token”概括。首字延迟主要由 Prefill 决定后续 token 的稳定速度主要由 Decode 决定。优化方案也往往只对其中某个阶段有效。2.2 内存带宽被忽略的真正瓶颈很多人以为推理慢是因为 GPU 算力不够其实 Decode 阶段的主要瓶颈是内存带宽。这个结论对做推理优化非常重要。可以这样理解GPU 的算力像一条很宽但流量有限的高速公路而显存带宽像路口的闸机。Decode 阶段每个 token 都要把模型的所有参数在读一遍这意味着权重多大、每读一次的时间就有下限。模型参数尺寸是固定的显存带宽也是硬件决定的所以单请求的 Decode 速度在底层逻辑上就有一个天花板。这也是为什么小模型在消费级显卡上反而可能生成速度更快——参数量小每次读取权重的时间更短。也是为什么工程上会有“一个 GPU 上同时跑多个请求反而总吞吐更高”的现象多个请求可以共享权重读取把内存带宽充分利用起来。2.3 KV Cache 与长上下文压力除了权重Decode 阶段还要反复读取 KV Cache也就是历史 token 对应的键值缓存。上下文越长KV Cache 越大占用的显存越多读取耗时也越长。KV Cache 的存在让“长上下文”变成一种昂贵的资源。比如一个支持 32K 上下文的模型如果实际输入非常长KV Cache 可能占用十几个 GB 的显存。生产环境里常见的 OOM 报错很多时候不是模型权重太大而是并发请求的 KV Cache 总和超过了显存上限。2.4 精度选择FP16 / BF16 / FP32 到底怎么选精度选择是 LLM 推理优化里最基础也最值得认真理解的一环。我们经常听到 FP16、BF16、FP32 这些名词它们是浮点数的不同表示格式核心区别在于“范围”和“精度”的取舍。精度类型位宽指数位尾数位特点典型场景FP3232 位8 位23 位数值范围大精度高显存占用最大、速度最慢训练初始权重、CPU 兜底FP1616 位5 位10 位表示范围窄大数值容易溢出混合精度训练、部分推理场景BF1616 位8 位7 位范围接近 FP32尾数精度较低大模型训练和推理的主流选择FP16 和 BF16 虽然都占 16 位但区别很明显。FP16 的指数位只有 5 位导致它的数值范围窄训练时容易出现梯度溢出BF16 的指数位有 8 位和 FP32 一样宽所以范围很大不容易溢出代价是尾数从 23 位被压缩到 7 位精度降低。对 LLM 推理来说BF16 通常是比 FP16 更稳妥的选择尤其是激活值和权重数值范围变化较大的场景。更进一步的优化是 INT8 甚至 INT4 量化把每个参数压缩到 8 位或 4 位显存占用更低读取速度更快但精度损失需要针对性评估。关于精度问题网上有大量针对 FP16、BF16、FP32 的对比例子这里不再展开但有一条原则始终成立精度选择本质上是速度、显存、效果三者之间的权衡没有绝对的“最优”只有“适合当前场景”。3. 环境准备搭建一套可复现的 LLM 推理评测环境3.1 硬件与依赖说明LLM 推理评测对硬件的要求取决于你要测的模型规模。如果想快速跑通全流程建议先从 1B 到 3B 量级的小模型开始这类模型在消费级显卡上就能运行调试成本低迭代速度快。如果你手上只有 CPU 环境也可以运行评测脚本但得到的绝对速度数值没有太多参考意义更重要的是掌握流程和方法。软件层面的依赖如下Python 3.10 或更高版本PyTorch建议根据你的 CUDA 版本安装对应版本Hugging Face TransformersvLLM用于高吞吐推理对比OpenAI Python SDK用于调用 vLLM 启动的兼容 API 服务一个可以访问的模型权重本文以 Qwen 系列的开源 instruct 模型为例。需要提醒的是这些库的版本更新非常快。本文不会写死具体版本号因为实际安装时应该以你本机 CUDA 环境和模型要求为准。遇到版本兼容问题优先查看对应框架的官方安装文档。3.2 创建 Python 环境推荐先用 conda 或 venv 创建独立环境避免不同项目的依赖互相污染。下面以 conda 为例conda create -n llm-speed python3.10 -y conda activate llm-speed pip install torch transformers vllm openai安装 vLLM 时有一个常见注意点vLLM 对 PyTorch 和 CUDA 版本有明确要求有些情况下它会自动帮你调整依赖所以建议先单独安装 vLLM让它自己拉取配套的 PyTorch如果先手动装了一个 vLLM 不兼容的 PyTorch 版本反而会报版本冲突。用 CPU 环境跑评测时可以跳过 vLLM 安装只保留 transformers 和 torch。3.3 项目结构规划为了演示一个完整的评测流程我们把项目组织成下面的结构llm-speed-bench/ ├── benchmark_transformers.py # transformers 单请求基准 ├── benchmark_vllm.py # vLLM 离线批量基准 ├── requirements.txt # 依赖清单 └── scripts/ └── stress_test.py # 在线服务并发压测这样划分的考虑是先有一个“基线实现”再用 vLLM 这类服务化引擎做对比最后做并发压测三个脚本对应三种不同维度的性能评估。4. 核心优化技术拆解4.1 连续批处理与 PagedAttention传统推理系统用的是静态批处理把一批请求打包好全部生成完再一起释放资源。这个方式的问题在于不同请求的长度不一样短请求早就生成完了却要等长请求一起结束GPU 资源利用率很低。连续批处理Continuous Batching改变了这个逻辑。它允许新的请求在任意时刻进入当前批也允许已经完成的请求立刻退出调度粒度从“一个完整的批”细化到“一个生成步骤”。这样 GPU 上的空闲计算资源可以随时被新请求填满整体吞吐量会明显提升。PagedAttention 是 vLLM 的核心创新思路和操作系统的虚拟内存分页很像。传统 KV Cache 需要分配一段连续显存而且要先预留最大长度碎片化严重PagedAttention 把 KV Cache 拆成固定大小的块按需分配、按页管理显存利用率大幅提高。理解这一点你就明白为什么 vLLM 在高并发、长上下文场景下会比 transformers 原生推理有数量级的吞吐优势。4.2 投机解码用“小模型试写大模型批改”投机解码Speculative Decoding是另一种思路目标是降低 Decode 阶段的每 token 延迟。核心理念是先用一个又快又小的小模型草稿生成 k 个候选 token再用大模型一次性验证这 k 个 token。如果都对了一次前向就多了 k 个 token如果错了再从错误位置重新生成。这个方案之所以有效是因为 Decode 阶段是访存密集而不是计算密集大模型每步的算力其实有富余。用小模型快速试写再用大模型并行验证可以用算力换时间。不过投机解码对草稿模型的质量要求较高如果小模型写的候选经常被驳回反而会浪费算力。实际项目中还需要权衡多出来的小模型显存开销。4.3 推理引擎选型现代 LLM 推理不会傻傻直接用原生 transformers 跑生产服务而是会选一个推理引擎。不同引擎定位差异很大这里整理了一个选型参考。引擎定位优势注意点vLLM高吞吐推理服务PagedAttention、连续批处理、OpenAI 兼容 API对 NVIDIA GPU 支持最好安装较重TensorRT-LLMNVIDIA 官方深度优化算子融合、低延迟、可定制化强引擎构建时间长学习成本高llama.cpp本地与边缘推理跨平台、量化生态好、CPU 也能跑高并发能力相对弱transformers研究、原型验证生态全、灵活、容易调试服务化能力弱性能不是重点在实际项目中如果目标是做一个高并发 API 服务vLLM 是目前社区认可度和上手成本最均衡的选择如果追求极致延迟并且有专门的 GPU 资源做优化TensorRT-LLM 值得研究如果模型要部署在用户本机或边缘设备llama.cpp 更合适。评测和选型要结合起来不要凭感觉选而要用同一套 prompt、同样的模型、同样的精度去压测对比。5. 完整实战编写一套 LLM 推理速度评测脚本5.1 评测指标设计评测一个 LLM 推理系统核心指标有四类TTFTTime To First Token从发起请求到收到第一个 token 的时间反映用户感知的首字延迟。TPOTTime Per Output Token平均生成一个输出 token 的耗时也就是推断出字速度。生成吞吐量Throughput单位时间内生成的 token 总数单位通常是 tokens/s。并发能力在多请求并发场景下系统能否保持稳定的吞吐和延迟。下面三个脚本分别覆盖单请求、离线批量、在线并发三种评测场景。5.2 基于 transformers 的基线评测先写第一个脚本用 transformers 直接加载模型测单请求的生成速度。这个脚本的价值是提供一个“最朴素”的基线方便和优化后的引擎做对比。# 文件路径benchmark_transformers.py import time import torch from transformers import AutoModelForCausalLM, AutoTokenizer MODEL_NAME Qwen/Qwen2.5-1.5B-Instruct DEVICE cuda if torch.cuda.is_available() else cpu def main(): tokenizer AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( MODEL_NAME, torch_dtypetorch.bfloat16 if torch.cuda.is_available() else torch.float32, device_mapauto, trust_remote_codeTrue, ) model.eval() prompt 请用 200 字以内介绍大语言模型推理加速的常用方法。 inputs tokenizer(prompt, return_tensorspt).to(DEVICE) # 预热加载权重、初始化 CUDA kernel避免把冷启动时间算进结果 with torch.no_grad(): _ model.generate(**inputs, max_new_tokens16) if torch.cuda.is_available(): torch.cuda.synchronize() start time.perf_counter() with torch.no_grad(): outputs model.generate( **inputs, max_new_tokens128, do_sampleFalse, use_cacheTrue, ) if torch.cuda.is_available(): torch.cuda.synchronize() elapsed time.perf_counter() - start new_tokens outputs.shape[1] - inputs[input_ids].shape[1] speed new_tokens / elapsed print(f模型: {MODEL_NAME}) print(f设备: {DEVICE}) print(f生成 token 数: {new_tokens}) print(f总耗时: {elapsed:.3f} s) print(f生成速度: {speed:.2f} tokens/s) print(输出内容:) print(tokenizer.decode(outputs[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue)) if __name__ __main__: main()脚本里有几个细节值得注意。一是加了预热步骤因为模型第一次推理会触发 CUDA kernel 初始化和权重加载这些时间不反映真实生成能力。二是调用了torch.cuda.synchronize()确保 GPU 上的操作真正执行完再计时。三是固定do_sampleFalse避免采样随机性影响多次测试的可比性。运行命令python benchmark_transformers.py预期会看到模型加载日志、生成耗时和速度输出。具体的 tokens/s 数值取决于你的 GPU这里不给出固定指标你只需要记住这个数值作为基线。建议多跑几遍取平均数避免波动。5.3 基于 vLLM 的离线批量评测第二个脚本改用 vLLM测多个 prompt 同时生成的吞吐。vLLM 会自动处理批处理调度你只需要提交一个 prompt 列表。# 文件路径benchmark_vllm.py from vllm import LLM, SamplingParams MODEL_NAME Qwen/Qwen2.5-1.5B-Instruct def main(): llm LLM( modelMODEL_NAME, dtypebfloat16, max_model_len8192, ) prompts [ 请用 200 字以内介绍大语言模型推理加速的常用方法。, 请分别解释 Prefill 阶段和 Decode 阶段的区别。, 如何选择 LLM 推理引擎请给出你的建议。, 请写一段招聘 AI 工程师的职位描述200 字以内。, ] sampling_params SamplingParams( max_tokens256, temperature0.0, ) outputs llm.generate(prompts, sampling_params) for i, output in enumerate(outputs): generated_text .join([o.text for o in output.outputs]) print(f请求 {i 1}:) print(f prompt: {output.prompt[:40]}...) print(f 输出: {generated_text[:80]}...) print(- * 50) if __name__ __main__: main()运行python benchmark_vllm.pyvLLM 首次加载模型时会构建相关缓存所以第一次运行会比后续慢。如果只想测速度可以多次运行脚本读取稳定后的输出。你也会注意到vLLM 在打印日志时会展示每批请求的调度和吞吐统计这些信息本身就是很好的性能参考。5.4 在线服务并发压测前面的脚本测的都是离线推理。生产环境中我们更关心服务在高并发下的表现。先用 vLLM 启动一个兼容 OpenAI API 的服务python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-1.5B-Instruct \ --dtype bfloat16 \ --port 8000服务启动后再运行下面的并发压测脚本。脚本用线程池模拟多个用户同时发请求统计总吞吐和平均耗时。# 文件路径scripts/stress_test.py import argparse import time from concurrent.futures import ThreadPoolExecutor from openai import OpenAI client OpenAI( base_urlhttp://localhost:8000/v1, api_keyEMPTY, # 本地服务不需要真实 key ) PROMPT 请写一段 200 字左右的文字介绍大语言模型推理加速。 def send_one_request(_): t0 time.perf_counter() response client.chat.completions.create( modelQwen/Qwen2.5-1.5B-Instruct, messages[{role: user, content: PROMPT}], max_tokens256, temperature0.0, streamFalse, ) elapsed time.perf_counter() - t0 output_tokens response.usage.completion_tokens return output_tokens, elapsed def main(concurrency: int, total: int): wall_start time.perf_counter() results [] with ThreadPoolExecutor(max_workersconcurrency) as pool: futures [pool.submit(send_one_request, i) for i in range(total)] for f in futures: results.append(f.result()) wall_time time.perf_counter() - wall_start total_tokens sum(r[0] for r in results) total_latency sum(r[1] for r in results) print(f请求总数: {total}) print(f并发数: {concurrency}) print(f总耗时: {wall_time:.3f} s) print(f总生成 tokens: {total_tokens}) print(f系统吞吐: {total_tokens / wall_time:.2f} tokens/s) print(f平均单请求耗时时: {total_latency / total:.3f} s) if __name__ __main__: parser argparse.ArgumentParser(descriptionvLLM 在线服务并发压测) parser.add_argument(--concurrency, typeint, default8) parser.add_argument(--total, typeint, default64) args parser.parse_args() main(args.concurrency, args.total)运行python scripts/stress_test.py --concurrency 8 --total 64建议分别用并发 1、4、8、16 跑几轮观察吞吐和耗时的变化。通常你会看到随着并发数上升系统总吞吐先上升然后趋于平稳甚至下降平稳点就是当前配置下的最佳工作负载区间。5.5 结果如何分析实战完成后你会得到三组数据transformers 单请求速度、vLLM 离线吞吐、vLLM 在线并发吞吐。分析时不要只盯着绝对值而要关注几个关系vLLM 相比 transformers 基线提升了多少倍如果提升不明显先确认是不是显存、模型版本或精度设置不一致。并发数从 1 升到 16 时总吞吐是线性增长还是提前饱和提前饱和通常说明 GPU 资源已经被占满或者服务端存在线程瓶颈。平均单请求耗时是否出现明显劣化如果并发上去后延迟暴涨说明调度或者显存分配遇到了瓶颈。建议把每次测试的环境信息记录下来包括 GPU 型号、框架版本、精度、并发数、输入输出长度这样后续任何一次代码改动都能通过对比历史记录来判断是否真正有效。6. 常见问题与排查思路评测和优化过程中下面几类问题属于高频问题整理成表格方便对照排查。问题现象常见原因解决思路显存不足OOM模型过大、并发过高、上下文过长导致 KV Cache 溢出减小 batch 或 max_token切换 INT8/INT4 量化或者使用更大的 GPU首次请求特别慢模型权重加载、CUDA kernel 初始化服务启动后先做一次预热请求把初始化开销和真实延迟分开多次评测速度波动大未做 warmup、GPU 频率变化、后台进程干扰固定 seed多次运行取中位数评测期间关闭其他 GPU 任务vLLM 与 transformers 输出不一致采样参数、精度、KV 缓存实现有差异统一温度、top_p、精度设置再对比并发升高后吞吐反而下降请求过长或显存碎片化调度器频繁等待调低单请求 max_tokens检查是否显存碎片升级 vLLM 版本CPU 上推理速度极慢transformers 默认用 FP32 在 CPU 跑内存带宽有限换用 llama.cpp 等 CPU 友好推理引擎或使用量化模型模型加载失败权限不足、模型不存在、网络受限确认模型 ID 正确使用 Hugging Face 登录授权或提前下载到本地目录其中显存问题是最常见的生产事故来源。任何修改模型、并发数、上下文长度、精度的操作都应该先在测试环境压一遍确认显存水位正常再上生产。涉及模型文件替换或推理参数调整时别忘了保留旧配置以便快速回滚。7. 最佳实践与工程建议7.1 评测口径要统一速度评测最大的坑就是口径不统一。同样一个“速度”有人统计的是包括网络传输在内的端到端耗时有人统计的是纯模型生成耗时有人算的是首字延迟有人算的是完整生成耗时。一旦口径不一致任何对比都是无效的。在团队里我建议把评测脚本和评测 prompt 固定下来作为仓库里的标准工具。任何关于“框架 A 比框架 B 快”的结论都必须用同一套脚本、同一段 prompt、同样的 token 长度跑出来。最理想的做法是像 Frontier.fast 这类项目倡导的把基准和结果做成公开可复现的而不是各自报一个数字。7.2 不要只看每秒生成 token 数每秒生成 token 数是衡量 LLM 推理性能最直观的指标但不是唯一的指标。对于交互型应用TTFT 往往更重要用户最无法忍受的是“半天不出第一个字”。对于批量离线任务总吞吐更重要。对于多租户服务还要关注延迟的稳定性也就是 P95 甚至 P99 延迟。优化是系统工程模型尺寸、精度、量化、批处理策略、KV Cache 管理、硬件选型都会相互影响。改一个参数可能让平均延迟降低但让长尾延迟上升。因此做优化时建议同时记录多个指标不要只盯一个数字。7.3 安全、权限与生产变更意识在生产环境进行推理引擎升级、模型替换或者显存相关参数调整时要遵守最小权限原则和灰度发布流程。先在小流量验证观察显存、延迟、错误率等指标稳定后再扩大范围。任何涉及模型权重、推理服务的变更都应该有回滚方案并且定期备份关键配置。如果使用的是开源模型注意检查模型的 License 和使用限制不要把内部评测数据随意上传到外部公共评测平台上尤其是包含业务敏感信息的 prompt。本地评测优先选择内网模型仓库或者本地权重目录。7.4 从评测走到容量规划评测脚本最终要服务于容量规划。当你知道了单卡在目标模型下的吞吐上限就能估算出线上业务需要多少张 GPU、是否需要做推理服务多副本、是否需要引入请求排队和限流。一个实用的方法是上线前先做一次线上流量模型模拟统计平均请求长度、峰值并发、Token 消耗量然后对照压测数据确定部署规格预留 30% 到 50% 的余量避免突发流量打爆显存。8. 总结与学习路线本文从 Frontier.fast 这类推动 LLM 速度前沿的项目出发围绕 LLM 推理速度这个主题做了四件事解释 Prefill 和 Decode 两个阶段的瓶颈讲清楚 FP16、BF16、FP32 等精度的取舍拆解连续批处理、PagedAttention、投机解码等核心优化技术并给出了三套可以直接运行的评测脚本。整体来看你会发现 LLM 推理速度优化并不神秘它更像一个可测量、可复现、可迭代的工程问题。如果你想继续深入建议按照下面的路线往下走先跑通本文的评测脚本用自己的机器和模型建立基线对比 transformers 和 vLLM 的差异理解推理引擎到底优化了什么尝试在 vLLM 上调整并发、max_model_len、量化参数建立“参数与性能”的直觉再去看 TensorRT-LLM 或推理芯片相关的底层优化弥补算子层面和内存调度层面的知识最后结合自己的业务形态把吞吐、延迟、成本三者统一进容量规划。这个方向的知识更新非常快框架几乎每隔几个月就有新版本、新优化所以不建议死记硬背某个版本的参数而要把方法论掌握扎实先明确评测指标再动手改参数最后用数据说话。我自己的习惯是每台新 GPU 机器到手上先跑一遍固定 prompt 的评测脚本把结果存档后续无论改模型还是换框架都拿同一套脚本复测对比。这比看任何宣传数据都实在。如果你也在做类似的 LLM 速度工程希望这篇文章能给你一套能直接落地的调试工具。收藏起来下次推理引擎调优的时候直接对照实践即可。