把微调好的大模型部署成线上服务,是很多 LLM 应用团队都会经历的一步。模型能跑通只是起点,模型跑得又快、又稳、又省显存才是真正的工程难题。我在这类项目里反复踩过几个坑:单条请求延迟不高但并发一上来吞吐骤降;显存明明还有剩余却不断触发 OOM;量化之后速度上去了,回答质量却明显缩水。网上资料大多只讲某一个优化点,很少有人把“延迟、吞吐、显存、精度”放在同一张图上讨论。这篇文章想梳理一套完整的 LLM 推理优化思路,从原理到可运行的代码,帮你在自己的真实场景里找到那个性价比最高的平衡点。
本文适合两类读者:一类是刚接触大模型部署,想搞清楚为什么推理这么慢的新手;另一类是已经能跑通服务,想系统提升性能、解决生产环境问题的开发同学。读完你会掌握 LLM 推理的核心性能瓶颈、常用优化手段、一版可复现的基线测量代码,以及一套生产环境常用的排错清单。
1. 背景与核心概念
1.1 什么是 LLM 推理
LLM 推理,全称是 Large Language Model Inference,指的是用已经训练好的大语言模型,根据用户输入(Prompt)生成输出文本的过程。训练阶段已经过去,模型参数不再更新,推理阶段要做的只有一件事:把输入变成输出。
从用户视角看,推理就是“发一段文字出去,等模型回一段文字”。从系统视角看,推理是把一次请求拆成无数次的矩阵运算和前向传播,并且每次都把输入和已生成的中间状态一起喂给模型。这个过程需要显存、算力和带宽的紧密配合,任何一个环节跟不上,用户体验都会直接受影响。
LLM 推理和传统深度学习推理有明显区别。传统分类模型一次前向传播就得到结果,耗时是固定的;LLM 是自回归模型,每生成一个 token(可以理解为一个字或一个词块)都要做一次完整的前向传播。生成 100 个 token 就意味着 100 次前向传播,这也是 LLM 推理“慢”的结构性原因。
1.2 Prefill 与 Decode 两个阶段
大模型生成过程可以分成两个阶段:Prefill(预填充)和 Decode(解码)。
Prefill 阶段处理完整的输入文本。系统把整个 Prompt 一次性喂给模型,并行计算出所有输入 token 的中间表示,并生成第一个输出 token。这个阶段的特征是“并行度高、计算密集”,因为输入序列可以被同时处理。
Decode 阶段则逐 token 生成。每生成一个 token,模型都要读取全部权重参数以及之前所有 token 的 KV Cache(键值缓存),再做一次前向传播。这个阶段是串行的,而且绝大多数情况下受限于显存带宽,而不是算力。
理解这两个阶段的区别非常重要。优化 Prefill 要提升计算效率和并行能力,优化 Decode 要重点解决访存瓶颈和缓存管理。很多统一的“推理加速”方案,本质上都是在针对这两个阶段的不同特性做 trade-off。
1.3 “高效边界”到底指什么
Efficient Frontier 本来是金融投资里的概念,指在给定风险水平下能获得的最高收益组合,是一条帕累托最优曲线:在这条曲线上,你无法在不损害某一目标的前提下改善另一个目标。
LLM 推理也存在类似的边界。我们通常会同时关注几个指标:延迟(快不快)、吞吐(并发高不高)、显存占用(省不省)、模型精度(准不准)。这几个指标之间经常互相牵制:
- 把 batch size 调大,吞吐上升,但单个请求的延迟也会上升;
- 把模型量化成 INT4,显存占用下降、速度变快,但精度可能受损;
- 用更大的模型,质量更好,但成本和延迟同步增加。
“高效边界”不是某一个固定配置,而是一组配置曲线。工程上的目标不是把所有指标同时做到极致(那不可能),而是找到适合你业务场景的那个最优工作点。比如流式对话场景更看重低首 token 延迟,离线批处理场景更看重高吞吐,这两类场景对应的最优配置完全不同。
1.4 关键性能指标
为了后面讨论方便,先统一几个术语。
TTFT(Time To First Token)指从请求发出到收到第一个输出 token 的耗时,主要受 Prefill 阶段影响,适合衡量流式交互体验。
TPOT(Time Per Output Token)指生成阶段每个输出 token 的平均耗时,主要受 Decode 阶段影响,决定整段回复的流畅度。
Throughput 指单位时间内模型处理完成的 token 总数,通常用 tokens/s 表示,是吞吐类场景的核心指标。
显存占用则要区分模型权重、KV Cache、激活值和 CUDA 上下文几部分,后面会单独展开。
很多刚接触的同学会把“生成 100 个 token 耗时多少”当作唯一指标,但实际生产环境更看重 TTFT、TPOT、吞吐三者的组合。加上“边界”视角之后,你应该关注的是:在当前硬件约束下,我的服务运行在哪条曲线上,能不能通过调整配置移动到更优的位置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与版本说明
2.1 硬件环境
LLM 推理对显存要求很高,这是由模型参数量和中间状态共同决定的。一个 70 亿参数(7B)的模型,用 FP16 存储权重大约需要 14GB 显存;加上 KV Cache、激活值和推理框架的额外开销,实际部署至少建议 24GB 以上的显卡。如果你使用 130 亿或 700 亿参数的模型,则要考虑多卡并行或者更强的量化方案。
不同显卡对推理优化的支持不同。较新的 NVIDIA 显卡通常支持 bf16 和更好的 Tensor Core 性能,这会影响 FP16 / BF16 推理的效率。量化方案(如 INT8、INT4)在不同架构上的加速效果也不一样,具体数值需要以实际压测为准。
2.2 软件栈
本文示例以 Python 生态为主,需要的核心组件如下:
- Python 3.9 或更高版本;
- PyTorch,用于模型加载和计算;
- Hugging Face Transformers,用于加载预训练模型;
- Accelerate,用于设备映射和多卡加载;
- vLLM(可选),用于高性能服务化推理;
- bitsandbytes(可选),用于 4bit 量化加载。
这里特别说明一下版本问题。LLM 推理相关组件迭代非常快,不同版本之间的 API 差异不小。本文给出的代码以常见写法为例,重点展示配置思路。你在实际项目里遇到 API 报错时,优先查看对应版本的官方文档,不要盲目套用。
2.3 示例项目结构
为了便于理解,下面所有示例可以放在同一个项目目录里。这里是一个推荐的结构:
code复制llm-inference-lab/
├── baseline_infer.py # 基线推理脚本
├── quantized_infer.py # 量化推理脚本
├── vllm_serve.py # vLLM 推理脚本
├── requirements.txt # 依赖清单
└── README.md # 说明文档
依赖清单 requirements.txt 可以按需写入:
txt复制torch
transformers
accelerate
bitsandbytes
vllm
生产环境千万不要直接装最新版了事。建议在虚拟环境或容器里固定版本号,并在部署前跑一遍完整冒烟测试。
3. 推理性能的核心瓶颈与优化原理
3.1 访存密集型:Decode 阶段的算术强度
要理解 LLM 推理为什么慢,先要理解“算术强度”这个概念。算术强度 = 计算量 / 访存量,衡量一次运算中有多少计算可以掩盖内存读取的时间。
Prefill 阶段输入序列长,矩阵乘法的计算量远大于参数读取量,更接近计算密集型。Decode 阶段完全不同:每步只生成一个 token,但模型仍然需要把全部权重从显存读一遍。假设一个 7B 模型权重是 14GB,即使 GPU 算力再强,每生成一个 token 也至少要搬运 14GB 数据。GPU 的显存带宽通常在每秒几 TB 级别,所以 14GB 权重的读取时间远远超过计算时间。
这就是为什么 Decode 阶段的速度往往由“显存带宽”而不是“算力”决定。换句话说,在大模型推理里,很多时候瓶颈不是 GPU 计算得快不快,而是数据读取得快不快。
认识到这一点,很多优化方向就很清晰了:
- 减少每次读取的数据量,比如量化;
- 让更多请求共享一次权重读取,比如增大 batch;
- 减少不必要的中间结果搬运,比如算子融合;
- 提前把需要的计算隐藏到访存过程中,比如流水线调度。
3.2 KV Cache 与显存占用
KV Cache 是自回归推理中最关键的临时数据结构。在生成第 N 个 token 时,模型需要对前面 N-1 个 token 计算注意力。如果没有缓存,每一轮都要重新计算前面所有 token 的 Key 和 Value 向量,计算量随序列长度二次增长,完全不可接受。
KV Cache 的作用就是把这些中间向量缓存下来,每一轮只计算当前 token 的 Key 和 Value,然后追加到缓存里。它极大减少了重复计算,但也带来了新的问题:显存消耗非常大。
KV Cache 的理论显存占用可以用如下公式估算:
code复制KV Cache 大小 ≈ 2 × 层数 × KV 头数 × 头维度 × 序列长度 × batch 大小 × 每个元素字节数
公式里的“2”对应 Key 和 Value 两份。以 70 亿参数的 LLaMA 架构为例,每层头维度和数量都比较大,序列长度一上去,KV Cache 会轻松占用数 GB 显存。
对在线服务来说,KV Cache 的管理直接影响并发能力。每个请求都会独占一定长度的 KV Cache,如果显存碎片化严重,缓存利用率就会很低。这也是 vLLM 的 PagedAttention 要解决的问题:把 KV Cache 切成小块按需分配,避免静态预留带来的浪费,后面会细讲。
3.3 Batch Size 与 Throughput 的关系
一个很反直觉的事实是:对 GPU 推理来说,单条请求的吞吐通常很低,多请求并行处理反而能大幅提升整体吞吐。
原因在于 Decode 阶段内存带宽是瓶颈。假如单条请求生成一个 token 需要读取 14GB 权重,耗时为 T;那么同时处理 8 条请求,虽然要读取的权重总量还是 14GB(权重是所有请求共享的),但 KV Cache 会变为 8 份,整体访存时间不是线性增长 8 倍,而是只增加 KV Cache 部分的开销。于是单位时间内能完成的 token 总数大幅上升,吞吐提升非常明显。
所以在线推理框架都支持“动态批处理”。每个请求的 Prefill 和 Decode 进度不同,传统静态 batch 只能等最慢的请求完成再整体释放;现代框架则允许新请求随时加入 batch,这就是 continuous batching / iteration-level scheduling。
不过 batch 大小也不是越大越好。显存有限,KV Cache 会随 batch 线性增长;batch 过大还会让单个请求的“排队时间”变长,延迟上升。最优 batch 需要通过压测找,而不是拍脑袋定。
3.4 量化:精度与速度的权衡
量化是降低模型推理成本最重要的手段之一,核心思路是用更低精度的数值(如 INT8、INT4)替代 FP16 权重,从而减少显存占用和访存量。
常见的量化路径有几类:
- 后训练量化(PTQ):用一小部分校准数据确定量化参数,不需要重新训练,速度快;
- 量化感知训练(QAT):训练时就模拟量化误差,精度更高但成本大;
- 运行时动态量化:加载模型后立即转换,简单但效果略差。
具体到工具上,Hugging Face Transformers + bitsandbytes 可以快速把模型以 4bit 加载,适合快速验证。GPTQ 和 AWQ 则是更成熟的权重量化方案,量化后的模型可以直接被 vLLM 等推理引擎加载。
量化必须在“显存省多少、速度快多少、精度掉多少”之间做平衡。一般经验是:7B 模型用 INT8 量化精度损失很小,INT4 量化在多数任务上也可以接受,但某些逻辑推理和专业领域任务可能出现明显的质量下降。最关键的一点是,量化之后必须用你自己的真实业务数据做评测,不能只看速度指标。
3.5 投机解码(Speculative Decoding)
投机解码的思路很巧妙:先用一个小模型(草稿模型)快速生成若干个候选 token,再用大模型一次并行验证这些 token。如果草稿模型和真实模型预测一致,这组 token 就被接受,大模型一次前向传播产出了多个有效 token;如果不一致,则丢弃错误 token,回到验证点重新生成。
这个方案能生效的前提是:小模型和大模型的预测结果有一定重合度。对某些代码、格式化输出等规律性较强的场景,投机解码的加速效果明显;但对自由创作类场景,命中率可能很低,收益不稳定。
实现层面,投机解码需要把小模型的草稿输出拼接成新序列,然后在大模型上执行一次“非自回归”的并行验证,这要求推理框架支持特殊的解码逻辑。并不是所有框架都原生支持,部署前要确认框架版本。
3.6 Continuous Batching 与 PagedAttention
最后介绍两个让 vLLM 这类框架性能大幅领先基础 Transformers 方案的关键设计。
Continuous Batching 解决的是“静态 batch 存在空转”的问题。传统做法是先攒够一批请求,全部处理完再接收下一批,导致请求排队等待。Continuous Batching 在每一轮迭代都检查当前 batch 里哪些请求已经完成,及时释放它们的显存和算力,并立刻插入新到达的请求。这样 GPU 始终在处理有效请求,利用率明显提高。
PagedAttention 解决的是“KV Cache 碎片化”的问题。它借鉴操作系统虚拟内存分页的思想,把 KV Cache 按固定大小的块划分,不需要在物理显存里连续存放。注意力计算时通过块表把逻辑位置映射到物理位置。这个设计显著缓解了显存碎片问题,也让共享前缀等场景变得更容易实现。
这两个机制结合起来,效果很直观:同样一块 GPU,vLLM 往往能做到比基础 Transformers 高数倍的吞吐,同时显存占用更稳定。这也说明“推理快不快”不只看显卡算力,框架的调度策略同样重要。
4. 从基线到优化:完整实测案例
这一节我们按“先测基线、再加量化、最后上服务化框架”的顺序走一遍。完整流程可以帮你建立自己的性能基准,也方便后续对比不同优化方案的真实收益。
4.1 创建项目依赖
先创建虚拟环境并安装依赖。这里以常见的推理环境为例,如果你用的是 Docker 镜像,可以跳过虚拟环境这一步。
bash复制python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install torch transformers accelerate
注意:PyTorch 的安装命令在不同 CUDA 版本下不同,不建议直接从公共源装 CPU 版。请根据你的显卡驱动和 CUDA 版本到 PyTorch 官网选择对应命令。
4.2 编写基线推理脚本
我们先写一个最朴素的推理脚本,目的是拿到一个“什么优化都没做”的性能基线。这里面有一点要留意:Transformers 的 generate 接口本身已经包含了一些默认优化(比如 KV 缓存),但它依然是逐请求串行执行的,吞吐能力有限。
python复制# 文件路径:llm-inference-lab/baseline_infer.py
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL_NAME = "meta-llama/Llama-2-7b-chat-hf"
PROMPT = "请用三句话介绍什么是大语言模型。"
MAX_NEW_TOKENS = 128
def main():
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
torch_dtype=torch.float16,
device_map="auto",
)
model.eval()
inputs = tokenizer(PROMPT, return_tensors="pt").to(model.device)
start = time.time()
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=MAX_NEW_TOKENS,
do_sample=False,
)
cost = time.time() - start
generated = outputs[0][inputs["input_ids"].shape[1]:]
text = tokenizer.decode(generated, skip_special_tokens=True)
print("输出内容:", text)
print(f"生成 {MAX_NEW_TOKENS} 个 token 耗时:{cost:.2f} 秒")
print(f"平均每秒生成 token 数:{MAX_NEW_TOKENS / cost:.2f} tokens/s")
if __name__ == "__main__":
main()
这里有几个细节值得说明:
torch_dtype=torch.float16把权重加载为半精度,显存占用比 FP32 少一半;device_map="auto"让 Accelerate 自动把权重分配到可用设备上;do_sample=False使用贪心解码,保证结果可复现,但采样模式下的耗时可能略有差异;torch.no_grad()禁用梯度计算,推理时一定要写。
运行命令:
bash复制python baseline_infer.py
记录下这次运行的速度。后续每一个优化方案,都可以和这个基线对比。基线数字可能偏低,但它的价值在于让你知道“起点在哪”。
4.3 添加量化优化
接下来用 bitsandbytes 把模型以 4bit 精度加载。这个方案改动很小,适合快速验证量化带来的显存和速度收益。注意 bitsandbytes 依赖系统 CUDA 环境,装有错误时需要检查 CUDA 和驱动版本。
python复制# 文件路径:llm-inference-lab/quantized_infer.py
import time
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
)
MODEL_NAME = "meta-llama/Llama-2-7b-chat-hf"
PROMPT = "请用三句话介绍什么是大语言模型。"
MAX_NEW_TOKENS = 128
def main():
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_quant_type="nf4",
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
quantization_config=bnb_config,
device_map="auto",
)
model.eval()
inputs = tokenizer(PROMPT, return_tensors="pt").to(model.device)
start = time.time()
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=MAX_NEW_TOKENS,
do_sample=False,
)
cost = time.time() - start
generated = outputs[0][inputs["input_ids"].shape[1]:]
text = tokenizer.decode(generated, skip_special_tokens=True)
print("输出内容:", text)
print(f"生成 {MAX_NEW_TOKENS} 个 token 耗时:{cost:.2f} 秒")
print(f"平均每秒生成 token 数:{MAX_NEW_TOKENS / cost:.2f} tokens/s")
if __name__ == "__main__":
main()
这里的关键参数是 load_in_4bit=True,它把权重压缩为 4bit。bnb_4bit_quant_type="nf4" 表示使用更稳定的 NF4 量化格式;bnb_4bit_compute_dtype 决定计算时用哪种精度,常见的做法是保持 FP16 计算,避免精度下降过多。
运行这个脚本,记录新的速度和显存占用。在很多场景下,显存占用会明显下降;生成速度是否提升,取决于显卡带宽和量化算子的实现。如果你的任务对精度敏感,一定要准备一组测试题,对比量化前后的输出质量。
4.4 使用 vLLM 进行服务化推理
如果你的目标是部署一个真正的在线服务,不建议直接用 Transformers 写 HTTP 接口。vLLM 集成了 PagedAttention、Continuous Batching、量化加载等能力,是目前性价比很高的选择。
先安装 vLLM:
bash复制pip install vllm
下面是 vLLM 的 Python 推理示例:
python复制# 文件路径:llm-inference-lab/vllm_serve.py
from vllm import LLM, SamplingParams
MODEL_NAME = "meta-llama/Llama-2-7b-chat-hf"
def main():
llm = LLM(
model=MODEL_NAME,
tensor_parallel_size=1,
dtype="float16",
max_model_len=4096,
gpu_memory_utilization=0.85,
)
prompts = [
"请用三句话介绍什么是大语言模型。",
"用 Python 写一个快速排序函数。",
"解释一下什么是 KV Cache。",
]
params = SamplingParams(
temperature=0.7,
max_tokens=256,
)
outputs = llm.generate(prompts, params)
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
print(f"Prompt: {prompt}")
print(f"生成结果: {generated_text}")
print("-" * 50)
if __name__ == "__main__":
main()
参数说明:
tensor_parallel_size=1:单卡时保持 1,多卡时可以设为显卡数量;dtype="float16":以半精度加载权重;max_model_len:允许的最大序列长度(Prompt + 生成结果),设置过大会增加显存预留,设置过小会截断长输入;gpu_memory_utilization=0.85:允许 vLLM 使用 85% 的显存,剩下的留给 CUDA 上下文和应用本身。
如果你不需要长时间保持 Python 进程,也可以直接启动 vLLM 自带的 OpenAI 兼容服务:
bash复制python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-2-7b-chat-hf \
--tensor-parallel-size 1 \
--max-model-len 4096 \
--gpu-memory-utilization 0.85
不同版本启动入口可能不同,请以官方文档为准。服务启动后,可以用 curl 验证:
bash复制curl http://localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Llama-2-7b-chat-hf",
"prompt": "请用一句话介绍大语言模型。",
"max_tokens": 64
}'
4.5 压测与结果对比
只看单条请求的速度没有意义,必须压测。最简单的方式是并发请求同一个服务,统计平均响应时间、TTFT、TPOT 和吞吐。
可以用 Python 的 concurrent.futures 写一个轻量压测脚本,或者使用 locust、hey、wrk 等工具直接压 HTTP 接口。下面是一个简易并发脚本思路:
python复制# 文件路径:llm-inference-lab/simple_load_test.py
import time
import concurrent.futures
from vllm import LLM, SamplingParams
MODEL_NAME = "meta-llama/Llama-2-7b-chat-hf"
CONCURRENCY = 8
PROMPTS = ["请用一句话介绍大语言模型。"] * CONCURRENCY
def run_one(llm, prompt):
params = SamplingParams(max_tokens=128, temperature=0.0)
start = time.time()
llm.generate([prompt], params)
return time.time() - start
def main():
llm = LLM(model=MODEL_NAME, dtype="float16", max_model_len=2048)
with concurrent.futures.ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
futures = [pool.submit(run_one, llm, p) for p in PROMPTS]
results = [f.result() for f in futures]
total_time = max(results)
total_tokens = CONCURRENCY * 128
print(f"并发数: {CONCURRENCY}")
print(f"总耗时: {total_time:.2f} 秒")
print(f"总生成 token 数: {total_tokens}")
print(f"吞吐: {total_tokens / total_time:.2f} tokens/s")
if __name__ == "__main__":
main()
注意生产压测一定要用真实业务数据和真实请求分布,模拟纯文本问答的压测结果参考价值有限。压测时记录几个关键数据并横向对比:
| 方案 | 显存占用 | 单请求耗时 | 吞吐 tokens/s | 输出质量 |
|---|---|---|---|---|
| Transformers 基线(FP16) | 较高 | 记录实际值 | 记录实际值 | 基准 |
| Transformers + 4bit | 明显下降 | 记录实际值 | 记录实际值 | 需要评测 |
| vLLM(FP16) | 稳定 | 记录实际值 | 通常更高 | 基准 |
| vLLM + 量化 | 最低 | 记录实际值 | 通常最高 | 需要评测 |
通过这张表,你能更清楚地看到自己的服务目前运行在“高效边界”的哪个位置,从而决定下一步优化方向。
5. 常见问题与排查思路
5.1 CUDA Out Of Memory(显存溢出)
显存溢出是 LLM 推理里最常遇到的错误之一。它的本质很简单:模型权重、KV Cache、激活值、CUDA 上下文的总占用超过了 GPU 显存上限。
解决思路从三个方向入手:降低单请求显存占用(量化、减小 max_new_tokens),提高显存利用率(上限 gpu_memory_utilization),或者换更大的显存/多卡。
排查方法可以按顺序来:
- 先用
nvidia-smi查看实际显存占用,确认是权重占了大头还是 KV Cache 占了大头; - 观察报错是在 Prefill 阶段还是 Decode 阶段出现;
- 如果是 Decode 阶段 OOM,优先考虑减小 batch size、max_model_len,或开启 KV Cache 的量化;
- 如果是加载权重时就 OOM,优先考虑量化或者使用多卡加载。
5.2 首 token 延迟过高
TTFT 高通常说明 Prefill 阶段太慢或者排队时间过长。Prefill 阶段需要处理完整的输入序列,输入越长,计算量越大。
处理思路:
- 检查输入要不要截断。很多线上请求实际不需要完整长文本,加一层长度控制能显著降低 TTFT;
- 检查框架是否启用了 FlashAttention。FlashAttention 能减少注意力计算中的显存读写,对长输入尤其有效;
- 检查并发场景下请求排队时间。如果 GPU 已经满载,新请求只能排队,这时候要么提升单卡吞吐,要么横向扩容。
需要注意的是,TTFT 高和 TPOT 高的原因往往不同。不要看到一个“响应慢”就直接调模型或换框架,先用指标定位阶段。
5.3 量化后输出质量下降
量化之后回答质量变差,是很正常的现象,只是幅度问题。关键是判断“能不能接受”。
排查和缓解方法:
- 用固定的测试集跑量化前后对比,最好包含你业务里最典型、最难的样本;
- 优先尝试 INT8 而不是 INT4,精度通常更接近 FP16;
- 检查计算精度设置。量化权重搭配 FP16 计算通常比搭配 INT8 计算更稳;
- 如果是逻辑推理类任务掉点明显,考虑对关键模块不做量化,或者用 QAT 重新训练。
永远不要只看速度就上线量化模型。质量评估、线上监控、灰度发布缺一不可。
5.4 并发升高时吞吐反而下降
单请求很流畅,并发一高整体吞吐反而掉,这种情况往往不是模型问题,而是框架调度或者资源争抢问题。
常见原因:
- 框架还在用静态 batch,请求排队导致 GPU 空闲;
- 多个请求的 KV Cache 碎片化严重,显存利用率降低;
- 使用了不合理的锁或线程模型,Python GIL 造成瓶颈;
- 显存不足以容纳大 batch,频繁出现局部 OOM 和重试。
遇到这种问题,可以先用 vLLM 这类带 Continuous Batching 的框架替代基础 Transformers;然后逐步调整并发数和 max-model-len,观察吞吐是否线性增长。吞吐不涨时,停下来看显存带宽是否已经打满。
5.5 常见问题速查表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 加载模型就 OOM | 权重精度高或显存不足 | 量化、多卡加载、换更大显存 |
| Decode 阶段 OOM | KV Cache 增长过快 | 减小 batch、限制 max_model_len、开启缓存量化 |
| TTFT 很高 | Prompt 过长或排队严重 | 截断输入、开启 FlashAttention、扩容 |
| 生成速度慢 | 未充分利用带宽/框架调度差 | 用 vLLM、增大 batch、量化 |
| 量化后回答变差 | 精度损失超过阈值 | 换 INT8、调整计算精度、评测后灰度 |
| 并发提高后吞吐下降 | 静态批处理/显存碎片 | 换 Continuous Batching 框架、限制并发 |
| 模型输出重复 | 采样参数不合理 | 调整温度、添加 repetition_penalty |
6. 最佳实践与工程建议
6.1 先测量,再优化
这是最重要的一条原则。不要上来就调参数、换框架。先跑一遍基线脚本,记录 TTFT、TPOT、吞吐、显存四个指标,再逐个尝试优化方案。没有基线,你无法判断一个改动是正向还是负向,也无法向团队证明方案收益。
建议把压测脚本和测试集纳入版本管理,每次改动都跑同一套评测。线上变更前至少要有速度指标和质量指标两份报告。
6.2 配置管理与灰度发布
推理服务的配置项很多:模型路径、权重精度、量化方式、最大序列长度、显存利用率、并发上限、采样参数。这些配置应该集中管理,不能散落在启动命令里。
推荐的方式:
- 用 YAML 或环境变量管理模型和部署参数;
- 配置项按“基础配置、资源限制、服务参数、采样参数”分组;
- 每次变更前保存旧配置,方便快速回滚;
- 高强度优化(量化、换框架、换模型)上线前先灰度,比如先切换 10% 流量观察。
6.3 监控与日志
线上推理服务至少要监控以下几类指标:
- 系统层:GPU 显存占用、GPU 利用率、显存带宽、CPU 占用;
- 服务层:QPS、TTFT、TPOT、吞吐、P99 延迟;
- 业务层:输入 token 数分布、输出 token 数分布、错误率、超时率。
日志要把请求ID、模型版本、量化配置、采样参数等关键上下文带上。否则线上出现诡异问题,很难定位是模型问题还是配置问题。
6.4 安全与合规边界
部署 LLM 服务要特别意识到内容安全和数据合规问题。实际项目中应该做到:模型输出的内容安全过滤、敏感信息脱敏、访问鉴权、限流、审计日志。对涉及用户隐私的输入输出,建议在服务链路中加入脱敏环节,避免敏感数据进入模型日志。
任何涉及线上系统的变更,都要遵循最小权限原则。生产环境的推理服务不要开放不受限的部署权限,模型文件、配置内容、日志数据都要有明确的访问控制。
6.5 成本意识:高效不等于一味追求低延迟
最后想说一点:不要为了“快”而无限堆配置。高效边界本质上是一个成本优化问题。
- 如果你的场景是离线批量处理,延迟要求不高,可以把 batch 拉大,用吞吐换成本;
- 如果并发很低,不需要盲目上服务化框架,简单脚本也能满足需求;
- 如果显存够用,不要为了省显存强行 INT4,精度损失可能带来更大的业务成本;
- 如果单卡已经吃满,优先优化框架调度,再考虑换卡和扩容。
做一个决策前,先算一笔账:速度提升多少?质量下降多少?硬件成本多大?部署复杂度增加多少?把这些放在一起,才能找到真正适合你业务的“高效边界”。
7. 总结与学习路线
这篇文章的核心思路只有一句话:LLM 推理优化不是孤立地调某一个参数,而是在延迟、吞吐、显存、精度之间寻找适合自己业务的平衡点。
你从本文应该带走几个关键认知:LLM 推理分为 Prefill 和 Decode 两个阶段,瓶颈各不相同;Decode 阶段是显存带宽主导,量化、批处理、连续调度都是为了更充分地利用带宽;KV Cache 是显存大户,也是框架优化的核心对象;vLLM 这类框架通过 PagedAttention 和 Continuous Batching 实现了远超基础推理脚本的吞吐表现。
动手路线可以参考下面几步:
- 第一步,用 Transformers 跑通一个最小推理脚本,记录基线和显存占用;
- 第二步,尝试 4bit 量化,观察显存和质量的权衡;
- 第三步,切换到 vLLM,用并发脚本压测吞吐,并记录 TTFT、TPOT;
- 第四步,针对自己的业务场景,找出合适的 batch、max_model_len、量化精度组合;
- 第五步,把压测和质量评估纳入每次变更的常规流程。
更进一步,你可以继续研究 FlashAttention 的具体实现、KV Cache 的量化与复用、投机解码在代码生成场景的效果、以及多卡场景下的张量并行策略。这些方向并不难,前提是你已经能把“测量”和“对比”刻在习惯里。
如果这篇文章对你有帮助,建议收藏备用。后面遇到推理性能问题的时候,先拿出压测记录,再按排查清单一项项过。理性分析永远比凭感觉调参更有效。
