Skip to content

推理延迟与吞吐:影响因素与优化思路

延迟不是“模型越大越慢”这么简单;输入长度、引擎、并发与缓存都影响显著。 注:本文为“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/量化/模板瘦身,平衡延迟与吞吐