上周在RTX 4090上部署Qwen2.5-72B-Instruct做内部代码审查服务,初始性能惨不忍睹:首Token延迟3.2秒,生成速度只有5 tokens/s。一个稍长的PR diff要等将近2分钟才能审完,开发团队直接弃用了。
折腾了三天,最终把TTFT压到0.4秒,生成速度提到40 tokens/s。这篇文章记录整个优化过程,每一步的原理和效果都有数据对比。
性能瓶颈定位
优化之前先搞清楚慢在哪。用vLLM自带的metrics端点采集了一组数据:
指标 原始值 说明
TTFT 3.2s 首Token延迟
生成速度 5.2 tok/s 输出速度
KV Cache使用率 98% 显存几乎打满
GPU计算利用率 35% 核心大量空转
排队请求数 12 经常排队
GPU利用率才35%但KV Cache已经98%,说明瓶颈在显存而不是计算。72B模型的KV Cache是大头,每层128个头、每头128维,32层算下来单请求的KV Cache就要吃掉好几GB显存。
优化一:量化模型
第一步是把模型从FP16量化到GPTQ-Int4。这是最直接的显存优化:
# 用AutoGPTQ量化
python -m auto_gptq.quantize \
Qwen/Qwen2.5-72B-Instruct \
--bits 4 \
--group_size 128 \
--desc_act True \
--output_dir Qwen2.5-72B-Instruct-GPTQ-Int4
量化后显存占用从144GB降到42GB,一张A100-80G就能放下。但精度损失需要验证——在内部代码审查测试集上跑了评估:
任务 FP16准确率 Int4准确率 差异
Bug检测 91.2% 89.8% -1.4%
代码规范 87.5% 86.1% -1.4%
安全漏洞 83.3% 81.9% -1.4%
精度损失在2%以内,完全可接受。关键收益是显存降了67%,意味着可以开更大的并发。
优化二:连续批处理配置
vLLM的连续批处理是吞吐量的核心。调整了几个关键参数:
vllm serve Qwen2.5-72B-Instruct-GPTQ-Int4 \
--max-num-seqs 8 \
--max-num-batched-tokens 16384 \
--gpu-memory-utilization 0.92 \
--enable-prefix-caching \
--enable-chunked-prefill \
--max-model-len 8192
逐个解释:
max-num-seqs 8:最大并发请求数。从默认的256改成8,因为72B模型每个请求的KV Cache很大,256个请求根本放不下。max-num-batched-tokens 16384:单批次最大token数。配合并发数,确保GPU能充分利用。gpu-memory-utilization 0.92:GPU显存使用率上限。留8%给系统和临时缓冲。enable-prefix-caching:开启前缀缓存,相同system prompt的请求复用KV Cache。enable-chunked-prefill:分块预填充,长输入不会阻塞其他请求。max-model-len 8192:最大上下文长度。代码审查场景8K够用,开到32K会大幅增加KV Cache占用。
优化三:Prompt精简
原始system prompt有2000多个token,里面塞了一大堆通用规则。分析后发现真正有用的核心指令只有500多token,剩下都是冗余的示例和说明。
精简策略:
优化项 效果
删除冗余示例 -800 token
合并重复规则 -300 token
移除"你是一个AI助手"废话 -200 token
压缩输出格式说明 -400 token
System prompt从2000 token压到500 token。这不只是省token的问题——更短的prompt意味着更少的prefill计算,TTFT直接降了0.5秒。
优化四:投机采样
投机采样(Speculative Decoding)是用一个小模型先猜几个token,大模型再验证。猜对了就直接用,猜错了回退。实测能提升2-3倍的生成速度:
vllm serve Qwen2.5-72B-Instruct-GPTQ-Int4 \
--speculative-model Qwen/Qwen2.5-7B-Instruct \
--num-speculative-tokens 5 \
--speculative-max-model-len 4096
原理是7B模型推理速度快10倍,它先生成5个候选token,72B模型一次性验证。如果命中率高(实测60-70%),生成速度就接近7B模型了。
优化五:请求预处理
在请求到达vLLM之前加了一层预处理:
class RequestPreprocessor:
def __init__(self, max_diff_chars=5000):
self.max_diff_chars = max_diff_chars
def preprocess(self, request):
"""预处理请求,减少不必要的token消耗"""
diff = request["diff"]
# 1. 截断过长的diff
if len(diff) > self.max_diff_chars:
diff = self._smart_truncate(diff)
request["diff"] = diff
request["truncated"] = True
# 2. 移除无意义的变更(空行、格式化、import排序)
diff = self._remove_noise(diff)
request["diff"] = diff
# 3. 估算token数,超长请求用低优先级队列
est_tokens = len(diff) // 3
if est_tokens > 4000:
request["priority"] = "low"
return request
def _smart_truncate(self, diff):
"""智能截断,保留变更头和关键文件"""
lines = diff.split('\n')
# 保留文件头信息
headers = [l for l in lines if l.startswith('diff --git') or l.startswith('@@')]
# 保留变更内容
content = [l for l in lines if not l.startswith('diff --git') and not l.startswith('@@')]
# 截断内容但保留头部
truncated = '\n'.join(content[:self.max_diff_chars // 50])
return '\n'.join(headers) + '\n' + truncated
def _remove_noise(self, diff):
"""移除无意义的变更"""
lines = diff.split('\n')
filtered = []
for line in lines:
# 跳过纯空行变更
if line.startswith('+') and line[1:].strip() == '':
continue
# 跳过纯格式化变更(只有空格/tab变化)
if line.startswith('+') and line.startswith('-'):
old = line[1:].strip()
new = line[1:].strip()
if old.replace(' ', '') == new.replace(' ', ''):
continue
filtered.append(line)
return '\n'.join(filtered)
优化效果
每一步优化的效果对比:
优化项 TTFT 生成速度 并发能力
基线 3.2s 5.2 tok/s 1-2请求
Int4量化 2.8s 5.8 tok/s 4-6请求
连续批处理配置 2.5s 12.3 tok/s 8请求
Prompt精简 2.0s 14.1 tok/s 8请求
投机采样 1.8s 35.2 tok/s 8请求
请求预处理 0.4s 40.1 tok/s 8请求
最终效果:TTFT从3.2秒降到0.4秒,生成速度从5.2 tokens/s提升到40.1 tokens/s,提升近8倍。一个中等长度的PR diff(3000字符)从原来的110秒降到15秒。
监控告警
上线后配置了性能监控:
ALERT_RULES = {
"ttft_p95": {"threshold": 2.0, "severity": "warning"},
"ttft_p99": {"threshold": 5.0, "severity": "critical"},
"generation_speed": {"threshold": 20, "severity": "warning", "direction": "below"},
"kv_cache_usage": {"threshold": 0.95, "severity": "warning"},
"queue_depth": {"threshold": 16, "severity": "critical"},
}
写在最后
LLM性能优化是一个系统工程,不是改一个参数就能解决的。从量化、批处理配置、prompt精简、投机采样到请求预处理,每一步都有明确的效果。
关键心得:先定位瓶颈再动手优化。GPU利用率低就调批处理配置,显存不够就量化,TTFT高就精简prompt,生成速度慢就上投机采样。盲目改参数只会浪费时间。