性能调优实战:从理论到实践
性能调优是 LLM 应用落地的关键环节。本文系统讲解从理论到实战的优化方法。
性能指标定义
核心指标
| 指标 | 描述 | 目标 |
|---|---|---|
| 吞吐量 (Throughput) | 每秒处理的 token 数或请求数 | 越高越好 |
| 延迟 (Latency) | 单请求响应时间(P50, P95, P99) | P99 < 2s(交互场景) |
| 内存占用 | GPU/CPU 显存使用 | 模型能装下 + 批处理余量 |
| 成本 | 每千 token 成本或每请求成本 | 在质量前提下尽可能低 |
| 准确率 | 任务完成质量(如 pass@k, F1) | 与基线持平或更高 |
一、模型层面优化
1. 量化(Quantization)
原理:降低权值精度,减少内存和计算。
方法对比
| 方法 | 精度 | 速度提升 | 精度损失 | 工具 |
|---|---|---|---|---|
| FP16 → BF16 | 16位 | 1.0× | <0.5% | 原生支持 |
| INT8 量化 | 8位 | 1.5-2× | ~1-2% | ONNX, TensorRT |
| GPTQ (4-bit) | 4位 | 2-3× | ~3-5% | AutoGPTQ |
| AWQ (4-bit) | 4位 | 2-3× | ~2-3% | AutoAWQ |
| NF4 (4-bit) | 4位浮点 | 2-3× | <1% | bitsandbytes |
实战:使用 GPTQ 量化 LLaMA 2 7B
# 安装 AutoGPTQ
pip install auto-gptq optimum
# 量化(需要少量校准数据,500-1000 样本)
python -m optimum.cli.export \
--model meta-llama/Llama-2-7b-chat-hf \
--task text-generation \
--weight-format gptq \
--bits 4 \
--group-size 128 \
--output ./llama-2-7b-gptq-4bit# 加载量化模型推理
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained(
"./llama-2-7b-gptq-4bit",
device_map="auto",
torch_dtype=torch.float16
)
tokenizer = AutoTokenizer.from_pretrained("./llama-2-7b-gptq-4bit")
inputs = tokenizer("Hello, how are you?", return_tensors="pt").to("cuda")
outputs = model.generate(**inputs, max_new_tokens=50)
print(tokenizer.decode(outputs[0]))Benchmark 对比:
| 配置 | 显存占用 | 生成速度 (tokens/s) | 延迟 P99 |
|---|---|---|---|
| FP16 | 14 GB | 45 | 1.2s |
| INT8 | 7 GB | 75 | 0.7s |
| GPTQ 4-bit | 4 GB | 95 | 0.5s |
选择建议:
- 追求精度:INT8
- 追求速度与压缩:GPTQ/AWQ 4-bit
- CPU 推理:GGUF(llama.cpp)
2. 模型架构优化
使用 vLLM 提升吞吐
vLLM 使用 PagedAttention 技术,显著提升吞吐:
# 启动 vLLM 服务器(OpenAI API 兼容)
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-2-7b-chat-hf \
--quantization gptq \
--dtype half \
--max-model-len 4096 \
--tensor-parallel-size 1测试对比(批量大小=32,序列长度=512):
| 框架 | 吞吐量 (req/s) | 内存使用 | 首令牌延迟 |
|---|---|---|---|
| HuggingFace Transformers | 12 | 8 GB | 120 ms |
| vLLM (PagedAttention) | 95 | 8 GB | 80 ms |
PagedAttention 原理:将 KV Cache 分块存储,消除内部碎片,内存利用率提升 3-4 倍。
TensorRT-LLM 极致优化
NVIDIA 专有优化,性能最佳:
# 构建 TensorRT engine
trtllm-build \
--model_dir ./llama-2-7b \
--output_dir ./trt_engine \
--max_batch_size 32 \
--max_input_len 2048 \
--max_output_len 512 \
--precision fp16启动服务:
trtllm-serve \
--model_dir ./trt_engine \
--port 8000性能:比 vLLM 再提升 20-30%。
二、推理优化策略
1. Batch 处理优化
动态批处理(Dynamic Batching)
将短时间内到达的请求合并成一个 batch:
from vllm import LLM, SamplingParams
llm = LLM(model="meta-llama/Llama-2-7b-chat-hf")
sampling_params = SamplingParams(temperature=0.7, max_tokens=256)
# 提交多个请求(vLLM 自动批处理)
outputs = llm.generate(
["你好,请问", "解释一下", "如何学习"], # 多个独立请求
sampling_params
)效果:吞吐提升 5-10 倍(GPU 利用率越高越明显)。
连续批处理(Continuous Batching)
vLLM 的核心:当一个请求完成时,立即加入新请求,保持 GPU 始终满载。
对比传统静态批处理:
| Batch 策略 | 平均吞吐 | 尾延迟(P99) |
|---|---|---|
| 静态 batch (size=16) | 45 req/s | 2.1s |
| 连续 batch (vLLM) | 95 req/s | 1.3s |
2. 采样策略优化
选择合适的采样方法
- Greedy(贪心):速度快,确定性,适合需要稳定输出的场景(如代码生成)
- Beam Search:更优但慢,适合翻译等追求最优解的任务
- Top-k / Top-p (nucleus):多样性好,适合对话生成
sampling_params = SamplingParams(
temperature=0.7, # 控制随机性,0=确定性
top_p=0.9, # nucleus sampling
top_k=50, # 限制候选词数量
max_tokens=256
)性能影响:Beam Search 比 Greedy 慢 2-5 倍,但质量略高。
3. KV Cache 优化
问题:自回归生成中,KV Cache 存储所有 past key-value,内存占用随序列长度线性增长。
优化:
- FlashAttention:减少内存访问,提升速度 2×,降低内存占用
- KV Cache 量化:将 FP16 KV Cache 转为 INT8,节省 50%
# vLLM 自动使用 FlashAttention 和 KV cache 优化
llm = LLM(model="...", enable_chunked_prefill=True) # 支持长序列三、应用层优化
1. 缓存(Caching)
Prompt 缓存
相同 prompt 的结果缓存(Redis, Memcached):
import redis, hashlib, json
r = redis.Redis(host='localhost', port=6379)
def get_completion(prompt, use_cache=True):
# 生成缓存 key
key = "llm:" + hashlib.md5(prompt.encode()).hexdigest()
if use_cache:
cached = r.get(key)
if cached:
return json.loads(cached)
# 调用 LLM
response = llm(prompt)
# 缓存(TTL 24 小时)
r.setex(key, 86400, json.dumps(response))
return response适用场景:常见问题、静态文档问答。 命中率目标:>30%
向量缓存
RAG 中,相同查询的检索结果缓存:
from functools import lru_cache
@lru_cache(maxsize=1000)
def retrieve(query: str):
return retriever.retrieve(query)2. 请求合并(Deduplication)
短时间内相同用户查询合并,返回相同结果:
from typing import Dict
from datetime import datetime, timedelta
class RequestDeduplicator:
def __init__(self, ttl_seconds=5):
self.cache: Dict[str, (str, datetime)] = {}
self.ttl = timedelta(seconds=ttl_seconds)
def get_or_set(self, key, func):
now = datetime.now()
if key in self.cache:
result, timestamp = self.cache[key]
if now - timestamp < self.ttl:
return result
result = func()
self.cache[key] = (result, now)
return result效果:突发流量下减少 30-50% 的 LLM 调用。
3. 异步与非阻塞
FastAPI + 异步:
from fastapi import FastAPI
from langchain_core.runnables import RunnablePassthrough
app = FastAPI()
# LCEL 链是异步可调用的
rag_chain_async = rag_chain.with_config(configurable={"async": True})
@app.post("/chat")
async def chat(request: ChatRequest):
response = await rag_chain_async.ainvoke({
"question": request.question,
"chat_history": request.history
})
return {"answer": response}并发能力:单个服务器实例可支持数百并发(相比同步版提升 5-10 倍)。
四、系统架构优化
1. 负载均衡与水平扩展
用户 → Nginx (负载均衡) → vLLM 实例集群 (3 台 A100) → 向量数据库Nginx 配置:
upstream llm_backend {
least_conn; # 最少连接
server 10.0.1.1:8000 max_fails=3 fail_timeout=30s;
server 10.0.1.2:8000 max_fails=3 fail_timeout=30s;
server 10.0.1.3:8000 max_fails=3 fail_timeout=30s;
}
server {
location /v1/ {
proxy_pass http://llm_backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_next_upstream error timeout http_500;
}
}会话粘性(Session Stickiness):如果需要保持上下文,使用 ip_hash 或一致性哈希。
2. 向量数据库性能
索引参数调优(Milvus HNSW)
from pymilvus import Collection, FieldSchema, CollectionSchema, DataType, connections
connections.connect(host="localhost", port=19530)
collection = Collection("docs")
# 重建索引(优化参数)
index_params = {
"metric_type": "COSINE",
"index_type": "HNSW",
"params": {
"M": 32, # 每层节点最大出度,越大准确但慢
"efConstruction": 400, # 构建时搜索深度,越大构建慢但质量高
"ef": 100 # 查询时搜索深度,越大准确但慢
}
}
collection.create_index("embedding", index_params)权衡:
- 高吞吐:增大
ef牺牲一点精度,换取速度(PUBG 等实时应用) - 高精度:
M=16-32,ef=200-400 - 小数据集:可以不用 HNSW,用 IVF_FLAT
预过滤与查询优化
Milvus 支持查询时过滤(metadata 过滤):
# 检索 + 过滤(只查技术类文档)
results = collection.search(
data=[query_embedding],
anns_field="embedding",
param={"metric_type": "COSINE", "params": {"ef": 100}},
limit=10,
expr="category == '技术' and source == 'internal'"
)效果:减少扫描向量数量,加速 30-50%。
3. 缓存层
在向量数据库前加 Redis 缓存:
查询 → Redis(缓存) → Miss → 向量数据库 → 结果缓存import redis, pickle
from sentence_transformers import SentenceTransformer
r = redis.Redis()
model = SentenceTransformer("all-MiniLM-L6-v2")
def cached_search(query, top_k=5):
cache_key = f"search:{hashlib.md5(query.encode()).hexdigest()}"
cached = r.get(cache_key)
if cached:
return pickle.loads(cached)
# 向量化并搜索
q_vec = model.encode(query).tolist()
results = collection.search([q_vec], limit=top_k)
# 缓存 1 小时
r.setex(cache_key, 3600, pickle.dumps(results))
return results五、监控与诊断
1. 性能监控
Prometheus 指标:
from prometheus_client import Counter, Histogram
llm_requests = Counter("llm_requests_total", "Total LLM requests")
llm_duration = Histogram("llm_duration_seconds", "LLM response time")
@app.post("/chat")
async def chat(request):
llm_requests.inc()
with llm_duration.time():
response = await rag_chain.ainvoke(request.question)
return responseGrafana 面板:
- 请求 QPS
- P50/P95/P99 延迟
- 错误率
- GPU 利用率
2. 瓶颈诊断
使用 PyTorch Profiler:
import torch
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("...").cuda()
with torch.profiler.profile(
activities=[torch.profiler.ProfilerActivity.CUDA],
schedule=torch.profiler.schedule(wait=1, warmup=1, active=3, repeat=1),
on_trace_ready=torch.profiler.tensorboard_trace_handler('./log'),
record_shapes=True,
profile_memory=True
) as prof:
for step in range(5):
inputs = tokenizer([f"step {step}"] * 32, return_tensors="pt").to("cuda")
outputs = model(**inputs)
prof.step()分析:
- 注意力层耗时? → 考虑 FlashAttention
- 内存带宽瓶颈? →KV Cache 量化
- GPU利用率低 (<30%)? → 增大 batch size 或动态批处理
六、调优实战案例
案例:客服问答系统
初始架构:
- GPT-3.5 Turbo API
- 单用户请求,等待完整生成
- 无缓存
指标:
- P99 延迟:4.2s
- 成本:$0.002 / request(1000 tokens)
- QPS:15
优化步骤:
量化 + vLLM 自托管:LLaMA 2 13B + GPTQ 4-bit
- 延迟:P99 1.8s(↓57%)
- 成本:$0.0003 / request(↓85%)
Redis 缓存:常见问题缓存
- QPS 提升:15 → 45(3×)
连续批处理:vLLM 天然支持
- 吞吐:45 → 120 req/s(2.7×)
向量缓存:RAG 检索结果缓存 5min
- 检索耗时:从 150ms → 5ms(99% 命中)
最终结果:
- P99 延迟:1.2s(↓71%)
- 成本:$0.00015 / request(↓92.5%)
- QPS:200+
七、常用工具与脚本
1. 性能测试脚本
import time, statistics
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="token")
def benchmark(num_requests=100, max_tokens=256):
latencies = []
for i in range(num_requests):
start = time.time()
resp = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Hello"}],
max_tokens=max_tokens
)
latencies.append(time.time() - start)
print(f"P50: {statistics.median(latencies):.3f}s")
print(f"P95: {sorted(latencies)[int(num_requests*0.95)]:.3f}s")
print(f"P99: {sorted(latencies)[int(num_requests*0.99)]:.3f}s")
print(f"Throughput: {num_requests/sum(latencies):.1f} req/s")
benchmark(100, 256)2. 自动调参工具(vLLM)
# vLLM Benchmark
python -m vllm.benchmarks.benchmark_throughput \
--model meta-llama/Llama-2-7b-chat-hf \
--input-len 512 \
--output-len 256 \
--batch-size 32总结
性能优化是系统工程:
优先级:
- 先优化瓶颈最大处(通常是模型推理,用量化、vLLM)
- 再优化流水线(缓存、动态批处理)
- 最后优化细节(并行、内核级优化)
黄金法则:
- 测量,不要猜:用 profiler 找瓶颈
- A/B 测试:每项优化都对比前后指标
- 质量不妥协:优化后必须验证 accuracy 是否达标
这是一个持续迭代的过程,没有银弹。
