推理延迟与吞吐:影响因素与优化思路
延迟不是“模型越大越慢”这么简单;输入长度、引擎、并发与缓存都影响显著。 注:本文为“1 月份重点篇”,给出调优要点与可复用的压测脚本。
🎯 文章目标
- 建立“TTFB/整体延迟/吞吐/TPS/TPM”的观测框架
- 理解影响因素:上下文长度、输出长度、模型/量化、引擎、并发与批处理
- 提供可复用的压测与调参清单
📚 背景/前置
- Attention 复杂度:长上下文会显著拖慢 prefill 阶段
- 引擎差异:vLLM(PagedAttention/高吞吐)、TGI、LMDeploy、Ollama 等
- 缓存:KV Cache/Prefix Caching、响应缓存(对完全相同请求)
🔧 核心内容
1) 影响因素速查
- Prompt 长度:prefill 时间 ∝ token 数;控制模板与冗余
- 输出长度:高温度/长输出 → 变慢且不稳定
- 模型体量与量化:7B/13B/32B;AWQ/GPTQ/INT4 牺牲一点效果换吞吐
- 并发与批处理:合理 batch 能提高吞吐,但注意 head-of-line blocking
- 引擎参数:max-num-batched-tokens、tensor-parallel、kv-cache-size
2) 调优实践清单
- 模板瘦身:减少 System 长文、用要点式上下文
- 上下文压缩:抽取关键片段 + 滑动窗口;召回后再重写
- 启用 Prefix Cache/KV Cache 复用:同一前缀请求显著降延迟
- Batching 窗口:按“token 数”而非“请求数”限流
- 观察 tokens/s 与 TTFB:分开看 prefill 与 decode
3) 引擎示例:vLLM 启动参数
bash
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-7B-Instruct \
--tensor-parallel-size 1 \
--max-model-len 4096 \
--gpu-memory-utilization 0.9 \
--max-num-batched-tokens 8192 \
--enforce-eager
4) 客户端并发与观测(Node.js)
javascript
import OpenAI from 'openai'
const client = new OpenAI({ baseURL: process.env.OPENAI_API_BASE, apiKey: process.env.OPENAI_API_KEY })
async function one(prompt){
const t0 = performance.now()
const resp = await client.chat.completions.create({ model: process.env.CHAT_MODEL, messages:[{role:'user',content:prompt}] })
const t1 = performance.now()
return { ms: t1 - t0, out: resp.choices[0].message.content }
}
const prompts = Array.from({length:20},(_,i)=>`第 \${i+1} 个请求,解释 RAG 在搜索中的作用`)
const results = await Promise.all(prompts.map(one))
console.table({ avg_ms: results.reduce((a,b)=>a+b.ms,0)/results.length })
5) 压测(Python,测 TTFB/吞吐)
python
# pip install httpx
import asyncio, time, httpx, json, os
URL = os.getenv('OPENAI_API_BASE', 'http://localhost:8000/v1') + '/chat/completions'
MODEL = os.getenv('CHAT_MODEL','qwen2.5-7b-instruct')
async def req(client, i):
t0 = time.time()
r = await client.post(URL, json={
'model': MODEL,
'messages': [{'role':'user','content': f'用 30 字解释 RAG 第 {i} 次'}],
'stream': True
})
ttfb = None
async for line in r.aiter_lines():
if line.startswith('data: '):
ttfb = ttfb or time.time()-t0
if line.strip() == 'data: [DONE]': break
return ttfb, time.time()-t0
async def main(n=32):
async with httpx.AsyncClient(timeout=60.0) as client:
ts = [asyncio.create_task(req(client,i)) for i in range(n)]
done = await asyncio.gather(*ts)
tt = [d[0] for d in done]
et = [d[1] for d in done]
print({ 'avg_ttfb': sum(tt)/len(tt), 'avg_ms': sum(et)/len(et) })
asyncio.run(main())
📊 对比/取舍(速查)
- 吞吐优先:更短上下文 + Batch + 量化 + vLLM
- 延迟优先:减上下文/温度、禁用过大 batch、开启前缀缓存
- 一致性优先:固定版本/模板、保守温度
🧪 踩坑与经验
- 头阻塞:大输入占满 batch 导致其他请求被拖慢;按 token 限流
- 长上下文幻觉:输入越长,越需加规则与引用上下文
- 观测缺失:不记录 TTFB/TPM 导致无法定位问题
📎 参考与延伸
- vLLM/TGI/LMDeploy 文档与性能对比
- KV Cache/Prefix Cache/Speculative Decoding
- 观测指标与压测工具(k6、wrk、locust)
💭 总结
- 先观测再优化:分解 prefill 与 decode;按“token 数”来调度
- 结合缓存/Batch/量化/模板瘦身,平衡延迟与吞吐