从什么时候开始,“把 LLM 跑起来”不再是一件值得炫耀的事情,而“把 LLM 跑得快”变成了默认要求?
过去一年里,我见过太多类似的场景:两个团队用同一个开源模型做同一个业务,A 团队把推理服务端延迟从 300ms 压到了 80ms,吞吐量翻了两三倍;B 团队还在为显存 OOM 反复调参。你说 A 团队掌握了什么黑科技吗?并没有。他们只是把量化、KV Cache、批处理调度、投机解码这些“已知技术”组合到了正确的位置上。
但另一个极端同样普遍。有人把能开的优化参数全开了,量化拉到 4bit,投机解码、前缀缓存、各种编译优化全部一股脑上,结果模型回答质量肉眼可见地变差,加速带来的收益反而被质量损失抵消。这种“优化过度”的案例,在工程群里每天都在发生。
这就是本文想解决的问题:LLM 推理的高效边界到底在哪里?
这里的边界不是指某一个量化档位或某一组配置参数,而是一条曲线——你每做一项优化,都要付出相应的成本,可能是显存、是延迟,也可能是模型质量。高效推理不是把所有优化手段堆上去,而是找到延迟、吞吐、质量、成本这几者之间的平衡点。
本文不会只堆概念。我会从推理引擎选型、核心优化技术、性能评测方法和工程落地经验四个角度展开,让读过文章的人至少能回答三个问题:我的推理瓶颈到底在哪一类?我应该优先做哪种优化?优化到什么程度就应该停下来?
1. 为什么“高效边界”是当下最有价值的工程问题
先看一个很典型的现状:模型能力已经不是主要瓶颈。
从开源社区的活跃度就能看出,普通业务团队很少从零训练大模型。大家做的是基于 Llama、Qwen、DeepSeek 等开源模型做微调、做部署、做服务化。当所有人的模型能力差不多时,比拼的自然就是谁能把同样的模型服务得更快、更稳、更便宜。
但 LLM 推理不是普通的 Web 服务。它的计算模式很特殊:每个请求的输入长度可能不同,生成过程是逐 token 进行的,不同请求的长度分布会剧烈波动。这就导致一个很尴尬的结果——你很难像优化传统微服务那样,靠简单的扩容和缓存解决问题。
LLM 推理服务时,每个 token 的解码过程本质上是一个访存密集型操作。这里有个经常被误解的概念:GPU 的算力(FLOPs)很高,但解码阶段因为需要频繁读取权重矩阵和 KV Cache,大部分时间其实卡在显存带宽上,而不是计算单元上。用一个通俗的类比:GPU 像一家高端餐厅,厨师技术很好(算力),但是传菜通道太窄(显存带宽),出餐速度上不去。
理解了这一点,你就能明白为什么单纯堆 GPU 不总是有效,为什么连续批处理能提升吞吐,为什么 KV Cache 优化如此关键,为什么量化能直接加速——因为它们都在解决同一个问题:减少访存压力,让有限的带宽服务更多 token。
这就是 LLM 推理“高效边界”问题的根源。
从工程视角看,这个边界至少包含四层:
- 模型层:模型参数量、上下文长度、量化感知训练等。
- 推理系统层:批处理策略、KV Cache 管理、调度器设计、投机解码。
- 硬件适配层:算子优化、编译优化、显存布局、通信优化。
- 业务层:请求是否可缓存、并发模型、超时设置、降级策略。
很多团队把优化重点放在推理系统层,这没有错。但如果模型上下文特别长、量化档位不合适、硬件选型不匹配,系统层的优化也会事倍功半。所以,真正的“高效边界”是一个全局概念,单点优化无法触达最优解。
这篇文章的读者应该是对 LLM 服务化有一定基础、但还没建立系统优化方法的工程师。如果你已经能部署一个 LLM 服务,但对“怎么让它更快”缺乏判断依据,那这篇文章就是为你准备的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础概念:理解 LLM 推理的延迟构成
2.1 Prefill 与 Decode
LLM 生成一个回答包含两个阶段。
第一阶段是 Prefill(预填充):把用户的输入 prompt 一次性送入模型,并行计算,生成第一个输出 token。这个阶段是计算密集型的,因为需要处理整段输入。
第二阶段是 Decode(解码):基于已生成的 token,逐个生成下一个 token。这个阶段是访存密集型的,每个 step 只生成一个 token,却要读取全部模型权重。
两个阶段的计算特征完全不同,优化手段也因此不同。比如 Prefill 阶段可以通过并行计算提升吞吐,Decode 阶段更适合通过连续批处理、KV Cache 复用、量化等手段降低访存压力。
2.2 关键性能指标:TTFT、TPOT、ITL
评测 LLM 推理性能,不能只看“总延迟”,必须区分这几个指标:
- TTFT(Time To First Token):从请求发出到返回第一个 token 的时间。用户打字后看到“对话框开始输出”快不快,取决于它。
- TPOT(Time Per Output Token):生成每个输出 token 的平均时间,一般折算成 tokens/s。这个指标直接决定回答整体流畅度。
- ITL(Inter-Token Latency):相邻两个 token 返回的时间间隔。保持稳定时,用户感受是“逐字输出”;如果波动大,就是“卡顿输出”。
这三个指标对应不同的用户体验问题。TTFT 太高,用户觉得“没反应”;TPOT 太高,用户觉得“打字太慢”;ITL 不稳定,用户觉得“一会快一会慢,像网络卡”。
2.3 吞吐量与成本
生产环境中,除了延迟,还必须看吞吐量:单位时间内能完成多少请求、每秒生成多少 token。
延迟和吞吐往往是矛盾的:追求最低延迟,就要降低批处理大小,GPU 利用率就上不去;追求最高吞吐,就要加大批大小,单个请求的延迟又会被拉长。这正是“高效边界”最核心的权衡点。
3. 主流推理引擎对比与选型判断
目前常用的推理引擎各有权重,不能简单说哪个最好。
以实际生产使用为例:
- vLLM:最流行,基于 PagedAttention 和连续批处理,社区活跃,支持模型多。适合大多数互联网业务的在线推理服务。
- SGLang:擅长前缀缓存和复杂 prompt 结构的场景,RadixAttention 能复用公共前缀的 KV Cache。适合多轮对话、few-shot 长 prompt、Agent 类应用。
- TensorRT-LLM:NVIDIA 官方生态,算子优化到位,适合对延迟极致敏感、硬件环境固定(N 卡)的生产场景,但构建和调试成本偏高。
- llama.cpp:CPU 和消费级 GPU 友好,量化支持好,适合本地部署、边缘设备、开发调试。
- MLC-LLM:跨硬件编译优化,适合需要部署到不同厂商硬件的场景。
- Hugging Face TGI:与 HF 生态结合好,部署简单,但高性能场景不如前两者灵活。
选型建议:
- 如果团队刚从 Transformers 直接部署迁移,首选 vLLM,因为资料多、坑少、上手快。
- 如果是长上下文、复杂 Agent 任务,先对比 SGLang 的前缀缓存收益。
- 如果对延迟有严格 SLA 要求,且硬件确定是 NVIDIA GPU,TensorRT-LLM 值得投入。
- 如果只是本地体验和原型验证,llama.cpp 最方便。
不要一开始就在多个引擎之间反复横跳。正确做法是选一个主引擎跑通基线,再根据瓶颈决定是否引入第二个引擎。生产系统最怕的不是性能不够,而是依赖混乱。
4. 核心优化技术拆解
4.1 连续批处理(Continuous Batching)与 PagedAttention
连续批处理解决的是“GPU 等请求”的浪费问题。
传统批处理中,一批请求必须等最慢的那个完成后,整个批次才能结束。而 LLM 请求长度差异巨大,一个短问题可能在 100ms 内结束,一个长回答可能要好几秒。把这两类请求放在同一个批次里,短请求会被长请求拖死。
连续批处理的做法是:每个 token 解码完成后,如果某个请求生成了结束符,立刻把它移出批次,换一个新请求进来。这样 GPU 始终在处理活跃请求,不会因为个别慢请求而空转。
vLLM 的 PagedAttention 解决的是另一个问题:KV Cache 的显存碎片化。LLM 的 KV Cache 会随序列长度动态增长,传统做法是预先分配最大长度,浪费大量显存。PagedAttention 像操作系统管理内存一样,把 KV Cache 分割成固定大小的块,按需分配,显存利用率显著提升。
这两个机制叠加起来,效果非常明显。这也是 vLLM 相比直接用 Transformers 做推理吞吐大幅提升的最主要原因。
4.2 Prefill 与 Decode 分离
因为 Prefill 是计算密集型,Decode 是访存密集型,两者混在一起会相互干扰。
有些团队开始采用“分离部署”策略:用多个 GPU 实例专门处理 Prefill,多个实例专门处理 Decode,中间通过队列衔接。这样可以根据两个阶段的不同特征分别做资源分配和并行优化。
但这属于较高阶的架构设计。一般业务在前期不需要做 Prefill/Decode 分离,用 vLLM 单集群就能解决大部分问题。当请求量达到规模、显存利用率遇到瓶颈时,再考虑这个架构。
4.3 量化:最直接的加速手段,也最容易失控
量化把模型权重从 FP16 压缩到 INT8、INT4,降低显存占用,同时减少访存数据量,所以能直接加速 Decode。
这里最容易踩的坑是:量化不是无损的。
FP16 到 INT8 一般影响较小,INT4 在某些模型、某些任务上质量下降很明显。尤其代码生成、数学推理、考试类任务,对精度非常敏感,低比特量化可能导致输出质量崩塌。
生产环境建议:
- 先做 FP16 基线,记录质量指标。
- 再尝试 INT8,对比质量和性能。
- 如果 INT8 不满足性能要求,再试 INT4,但必须做质量回归测试。
- 量化后模型必须重新做评测,不能只看显存节省数量。
如果模型本身很小,量化收益有限,反而增加部署复杂度。小模型优先考虑提升批处理和调度效率,而不是盲目量化。
4.4 投机解码(Speculative Decoding)
投机解码的思路是用一个小模型生成多个候选 token,再由大模型一次性验证。如果小模型猜得准,一次验证可以确认多个 token,等于把 Decode 从“逐 token 生成”变成“批量确认”。
这个技术在目标模型较大、小模型预测准确率较高时收益明显。但如果目标模型很小,或者小模型和任务领域不匹配,投机失败率会很高,反而增加额外开销。
从材料看,投机解码在生产环境的收益分布很不均匀。有些场景吞吐提升明显,有些场景几乎无收益。建议先用测试集做离线对比,不要默认开启。
4.5 前缀缓存(Prefix Caching)
多轮对话、Agent 工具调用场景下,每个新请求都携带很长的系统提示词和历史上下文。这些公共前缀如果每次都要重复计算,非常浪费。
vLLM 的自动前缀缓存和 SGLang 的 RadixAttention 都能复用 KV Cache。效果在长 prompt、高并发、多轮对话场景下非常显著。系统提示词越长,好处越明显。
如果业务场景是短请求、随机 prompt,前缀缓存收益有限,不需要刻意设计 prompt 结构去迎合它。
4.6 模型结构与量化感知训练
还有一些优化发生在模型侧,比如多头潜在注意力(MLA)、混合专家模型等。这些技术改变了 KV Cache 或参数量的大小,从源头降低推理成本。
MLA 的思路是把注意力机制的 KV Cache 压缩到一个低秩空间,大幅减少显存占用,同时保留相近的模型能力。混合专家模型的思路是每次只激活部分专家,虽然总参数量大,但单次推理算力消耗更低。
这类模型层优化通常需要训练阶段配合,已经发布的开源模型无法事后修改。所以选型阶段就要考虑:如果业务对长上下文和低延迟要求很高,优先选择推理友好的模型架构。
5. 效率边界的建立方法:先测量,再优化
很多人一上来就调参数,这不对。建立高效边界的第一步是测量。没有基线数据的优化都是碰运气。
建议四步走:
- 选定业务代表性请求集:覆盖短问短答、长问长答、多轮对话、长文档处理等典型场景。
- 跑基线:用默认配置部署,记录 TTFT、TPOT、ITL、吞吐量、显存占用、GPU 利用率。
- 做单点优化实验:每次只改一个变量(比如只改量化、只改批处理大小),对比基线。
- 引入质量门禁:每次优化都必须结合模型输出质量评估,不能只看速度指标。
5.1 性能测试脚本示例
用一个简单的 Python 脚本统计吞吐和延迟:
python复制# 文件路径:benchmark/llm_benchmark.py
import time
import threading
from concurrent.futures import ThreadPoolExecutor
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="EMPTY"
)
PROMPTS = [
"解释一下什么是数据库索引,用通俗语言说明。",
"写一段 Python 代码,实现快速排序,并解释时间复杂度。",
"什么是大模型推理中的 KV Cache?它解决了什么问题?",
"给我三道初中数学应用题,并附上解题步骤。",
]
def send_one_request(prompt: str):
start = time.time()
response = client.chat.completions.create(
model="llama-3-8b-instruct",
messages=[{"role": "user", "content": prompt}],
max_tokens=1024,
temperature=0.7,
)
elapsed = time.time() - start
# 从响应中解析生成 token 数量。这里以 completion_tokens 为例。
usage = response.usage
output_tokens = usage.completion_tokens
first_token_time = None
for chunk in response:
# 实际流式场景可以在这里记录首个 token 时间,简化版略过
pass
return {
"prompt_tokens": usage.prompt_tokens,
"output_tokens": output_tokens,
"elapsed": elapsed,
"tokens_per_sec": output_tokens / elapsed if elapsed > 0 else 0,
}
def run_benchmark(concurrency: int = 4):
tasks = []
with ThreadPoolExecutor(max_workers=concurrency) as pool:
for i in range(20):
prompt = PROMPTS[i % len(PROMPTS)]
tasks.append(pool.submit(send_one_request, prompt))
results = []
for future in tasks:
results.append(future.result())
total_tokens = sum(r["output_tokens"] for r in results)
total_time = sum(r["elapsed"] for r in results)
avg_latency = total_time / len(results)
avg_tokens_per_sec = total_tokens / total_time
print(f"请求数: {len(results)}")
print(f"平均延迟: {avg_latency:.2f} s")
print(f"平均吞吐: {avg_tokens_per_sec:.2f} tokens/s")
if __name__ == "__main__":
run_benchmark()
这段代码用 OpenAI 兼容接口发请求,统计平均延迟和吞吐。脚本里的模型名、base_url 以实际部署为准。
运行方式:
bash复制python benchmark/llm_benchmark.py
6. 完整示例:从部署到定位推理边界
6.1 部署环境
建议环境:
- Linux 主机,NVIDIA GPU,显存至少 16GB(8B 模型量化后可用,13B 模型建议 24GB 以上)。
- Python 3.10+,CUDA 环境可用。
- 创建虚拟环境并安装 vLLM。
bash复制python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install vllm
版本以实际发布为准,本文重点是通用思路。
6.2 启动推理服务
先启动一个最基础的 FP16 服务:
bash复制python -m vllm.entrypoints.openai.api_server \
--model /path/to/your/model \
--served-model-name my-llm \
--gpu-memory-utilization 0.90 \
--max-model-len 8192
参数说明:
--model:模型路径或 Hugging Face 模型名。--served-model-name:对外暴露的模型名称。--gpu-memory-utilization:GPU 显存利用率上限。太高容易 OOM,太低浪费显存。--max-model-len:最大序列长度,影响 KV Cache 预分配。
服务启动后,通过 curl 验证:
bash复制curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "my-llm",
"messages": [{"role": "user", "content": "请用一句话介绍你自己"}],
"max_tokens": 128
}'
6.3 增加连续批处理并观察吞吐变化
vLLM 默认开启连续批处理,不需要额外配置。但需要在压测中观察,请求并发提高后,吞吐是否持续增长、延迟是否可接受。
用上一节的 benchmark 脚本,分别以并发 1、4、8、16 跑一遍,记录数据。典型的趋势是:并发增加,吞吐先涨后降;延迟持续上升。当吞吐开始下降或延迟超过业务 SLA 时,就是当前配置下的一条边界。
6.4 加入量化对比
以 AWQ 量化模型为例,用量化后的模型再跑一遍同样的 benchmark:
bash复制python -m vllm.entrypoints.openai.api_server \
--model /path/to/awq-quantized-model \
--served-model-name my-llm-awq \
--quantization awq \
--gpu-memory-utilization 0.90 \
--max-model-len 8192
对比量化前后的平均延迟、吞吐、显存占用,同时用固定测试集检查输出质量是否下降。
6.5 找出过度优化信号
过度优化的信号很明确:
- 延迟提升幅度小于 5%,但质量下降明显。
- 量化档位下降后,GPU 利用率没有相应上升,说明瓶颈不在显存带宽。
- 投机解码开启后,吞吐不升反降。
- 为了压缩延迟,把批大小降到极低,导致 GPU 利用率长期低于 20%,单位成本急剧上升。
此时应该回退到上一个“收益/成本比”最优的配置点。
7. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动即显存 OOM | max-model-len 设置过大,或 gpu-memory-utilization 过高 | 查看 CUDA 报错和模型配置 | 调低 max-model-len,或降低 gpu-memory-utilization |
| Token 输出速度慢 | Decode 阶段访存瓶颈,批处理大小不理想 | 用 nvidia-smi 查看 GPU 利用率和显存带宽占用 | 尝试连续批处理提升并发,或量化减权重大小 |
| TTFT 很高 | Prefill 阶段计算量大,前缀缓存未命中 | 查看首 token 延迟分布 | 长 prompt 场景开启前缀缓存,或拆分 Prefill/Decode |
| 量化后质量明显下降 | 量化粒度或方法不适合当前模型/任务 | 用固定评测集对比输出 | 更换量化方法,或回退到 INT8/FP16 |
| 投机解码无收益 | 小模型预测准确率低,模型太小 | 统计投机接受率 | 关闭投机解码,或换更贴近任务的小模型 |
| 并发升高后吞吐下降 | 批大小过大导致单请求延迟过长 | 观察吞吐曲线拐点 | 调整 max-num-seqs 参数,找到吞吐最佳点 |
| CPU 内存占用过高 | 模型权重加载到 CPU 内存,或调度器开销 | 查看进程内存和日志 | 优化权重加载方式,检查引擎参数 |
| 多卡部署时不均衡 | 请求长度差异大,负载调度不均衡 | 查看每卡显存和利用率 | 开启更细粒度的调度策略,或做请求级别路由 |
8. 最佳实践与工程建议
8.1 先定质量基线,再做性能优化
任何性能优化开始之前,先锁定一个评测集。不用很大,20 到 50 个代表性问题即可。评测方式可以是规则检查、人工评分、调用教师模型对比,关键在于稳定复现。
每次调整性能参数后,跑一遍评测集。如果质量分数掉出可接受范围,就算速度提升再多,也要回退。
这个动作看起来简单,但很多团队不做。最终结果就是“服务变快了,用户变少了”,因为输出质量下降导致用户离开。
8.2 用分层策略管理性能目标
不同业务场景对性能的要求差异很大。
- 在线对话:TTFT 低于 1s,TPOT 低于 60ms/token(约 16 tokens/s 以上)体验较好。
- 批量离线处理:不追求单请求延迟,追求整体吞吐和成本最低。
- Agent 多轮调用:前缀缓存命中率比单次延迟更重要。
建议在配置层面对不同场景做分组,而不是用一套参数服务所有流量。
8.3 为资源配置设置上限
生产环境一定要有资源配置上限。比如:推理集群最大并发数、单实例最大 batch、显存利用率阈值。超过阈值时必须拒绝新任务或触发排队,而不是无限叠加导致系统崩溃。
还要设置成本预警。推理集群的 GPU 利用率、每万 token 成本、单位请求成本,都应该有看板和告警。很多团队直到月底看到账单,才发现推理集群的成本已经失控。
8.4 灰度发布与回滚
推理引擎、量化档位、模型版本都属于线上变更,必须走灰度流程:
- 先在一台实例上切换新配置。
- 对比基线实例的性能指标和质量指标。
- 观察 30 分钟以上,确认稳定后再扩容。
- 一旦出现异常,立即回滚到旧版本。
回滚的前提是旧版本权重和推理引擎版本都保留在制品库中,不要因为更新了权重就删除旧文件。
8.5 监控体系要覆盖三层
- 系统层:GPU 利用率、显存、CPU、网络 IO。
- 服务层:TTFT、TPOT、ITL、每秒请求数、排队长度。
- 业务层:每个请求的输入 token 数、输出 token 数、超时率、用户体验评分。
三层监控缺一不可。系统层告诉你资源够不够,服务层告诉你响应快不快,业务层告诉你优化值不值。
8.6 安全与权限提醒
如果推理服务有用户鉴权、数据隔离、内容审核需求,必须在优化前确认:
- 推理服务不能绕过原有的统一鉴权体系。
- 日志不能记录敏感输入和输出。
- 模型输出的使用范围必须合规。
- 涉及生产环境变更,遵循最小权限原则,并由团队复核。
性能优化不能以牺牲安全控制为代价,这个原则不能松动。
9. 总结:如何找到你自己的高效边界
回到开头的问题:高效边界到底在哪里?
没有一个万能答案。不同的模型大小、硬件资源、业务场景、质量要求,会形成不同的边界曲线。但找到边界的方法是共通的——测量、单点实验、加入质量门禁、寻找收益拐点。
我最后想强调几件具体的事:
第一,先跑通一个最小可用的推理服务,拿到基线数据。没有基线之前的优化,基本是自我感动。
第二,每次只改一个变量。同时改量化、改投机解码、改批处理参数,出了问题你根本不知道是哪一个造成的。
第三,质量评估不能省。LLM 推理的最终产品是文字,速度优化换来的是质量下降,那不是优化,是事故。
第四,成本视角要前置。不要只看延迟多降低了几毫秒,也要看为了这几毫秒多花了多少 GPU 成本。有些业务的用户根本感知不到 50ms 到 30ms 的差别,但成本可能翻倍。
如果你现在正要开始做 LLM 推理优化,建议路线是:先选一个小而稳的推理引擎(vLLM 是不错起点),跑通一个基线服务,用带业务代表性的测试集记录性能和质量,然后按“批处理大小 → 量化 → 前缀缓存 → 投机解码 → 架构分离”的顺序,逐项实验,每项实验都记录数据,直到找到那条收益开始急速下降的曲线。
这个过程不性感,但它可靠。真正能稳定跑在生产环境的推理优化,从来不是某一个“神奇参数”的功劳,而是一套可量化、可复现、可回退的工程方法。
