游戏百科

vLLM推理加速指南:7个技巧让QPS提升30-60%

GPU 永远不够用,这大概是每个做推理服务的人都有的共识。相比无脑加卡,更实际的办法是把现有资源榨干。下面这些是我在实际

GPU 永远不够用,这大概是每个做推理服务的人都有的共识。相比无脑加卡,更实际的办法是把现有资源榨干。下面这些是我在实际项目里反复用到的几个调优手段,有代码、有数据、也有一些踩坑经验。

1、连续批处理的请求塑形

vLLM 的核心优势在于 continuous batching,把多个请求的 token 打包进同一个计算步骤。想要发挥这个特性,需要在请求端做些配合。

max_new_tokens 必须要控制,对于聊天场景 256 到 512 基本够用。那种动不动要生成几千 token 的请求会拖垮整个 batch 的效率。流式返回可以降低用户感知到的延迟,同时让 batch 持续运转。另外 prompt 长度如果能相对统一会更好,长短差异太大会影响 packing 效率。

客户端可以这样写,异步流式加并发控制:

import asyncio  from openai import AsyncOpenAI    client = AsyncOpenAI(base_url="http://localhost:8000/v1", api_key="NOT_NEEDED")    SEM = asyncio.Semaphore(128)  # tune: concurrent in-flight requests    async def ask(msg: str):      async with SEM:          stream = await client.chat.completions.create(              model="your-vllm-model",              messages=[{"role":"system","content":"You are concise."},                        {"role":"user","content":msg}],              temperature=0.7,              max_tokens=256,   # keep it bounded              stream=True          )          out = []          async for chunk in stream:              token = chunk.choices[0].delta.content or ""              out.append(token)          return "".join(out)    async def main():      qs = [f"Question {i}" for i in range(500)]      answers = await asyncio.gather(*[ask(q) for q in qs])      print(answers[0])    asyncio.run(main())

这种模式能让服务端持续收到短小可控的生成请求,continuous batching 的效果就出来了。

2、KV cache 复用的前缀设计

vLLM 的 paged attention 和 KV cache reuse 机制要求共享前缀在字节级别完全一致。所以system prompt一定要固定下来,连标点和空格都别改。动态内容往后放,用户数据、工具输出这些全部挪到消息末尾。

template 里千万别在用户问题前面插时间戳或者随机 ID,cache miss 率会飙升。

SYSTEM = (      "You are a helpful assistant. Use bullet points. "      "Cite numbers when you can.\n"  # stable, byte-for-byte  )    def build_messages(user_msg: str, hints: list[str]):      # Put variable hints AFTER the user message to maximize shared prefix.      return [          {"role": "system", "content": SYSTEM},          {"role": "user", "content": user_msg + "\n\n" + "\n".join(hints)},      ]

FAQ 类应用或者 RAG 场景下,这招的收益非常明显,QPS 能有肉眼可见的提升。

3、推测解码配小模型

GPU 预算紧张的时候,speculative decoding 值得一试。小模型先提议 token,大模型负责验证,整体步数能减少不少。

拿个 1B 到 8B 的小模型做 draft,配合主模型使用。在 temperature 0.3 到 0.9 这个区间、中等长度输出的场景效果最好。acceptance rate 健康的话,tokens/sec 会有实打实的增长。

# e.g., vLLM server args  --model main-model  --speculative-draft-model tiny-draft-model

需要盯着 acceptance rate、draft 模型的显存占用,以及那些特别有创意的生成场景(draft 在这种情况下帮助有限)。

4、量化来换 batch size

权重量化能在同样的显卡上跑更大的 batch。AWQ 和 GPTQ 这类方法在聊天质量上的损失相对可控,比简单粗暴地全面 4-bit 要好。但不是所有模型都适合激进量化,有些架构压过头会丢失语言风格或者逻辑连贯性。

量化完重新调 batch 上限,通常能塞进去更多并发序列,QPS 自然就上来了。经验上讲,显存卡住计算之前成为瓶颈的话,量化是最该是最先考虑的。

5、拓扑匹配的并行策略

多卡机器上,tensor parallel size 要和模型用的 GPU 数量对齐,进程要 pin 住。

单模型多卡就用 tensor parallelism,让每张卡分摊模型的一部分并且每步都参与计算。

一台机器跑多个模型的话,进程隔离更合适,内存带宽和上下文切换会严重拖累 QPS。CPU pinning 和 GPU affinity 别偷懒,让数据加载和网络线程尽量靠近设备。

运维上还有几个点要注意:数据中心的 GPU 关掉节能模式;显存利用率控制在 90% 到 95%,给突发流量留点余量;多个 vLLM 实例记得分配独立的端口和 GPU(比如 CUDA_VISIBLE_DEVICES=0,1 和 2,3 分开)。

6、准入控制保护批处理引擎

高 QPS 不光是吞吐,还要在高负载下保持稳定。在 vLLM 前面加一层简单的门控来过滤请求。

超过 max tokens 或者 timeout 预算的请求直接拒掉或者降级处理。每个 API key 用 leaky bucket 或者 token bucket 限流。队列最好带 backpressure,避免超时堆积导致雪崩。

from fastapi import FastAPI, HTTPException, Request  import asyncio, time    app = FastAPI()  SEM = asyncio.Semaphore(256)     # max in-flight  MAX_NEW_TOKENS = 384  REQ_TIMEOUT_S = 20    @app.middleware("http")  async def guard(request: Request, call_next):      start = time.time()      params = await request.json() if request.method == "POST" else {}      if params.get("max_tokens", MAX_NEW_TOKENS) > MAX_NEW_TOKENS:          raise HTTPException(400, "max_tokens too large")      try:          async with asyncio.timeout(REQ_TIMEOUT_S):              async with SEM:                  response = await call_next(request)                  return response      except TimeoutError:          raise HTTPException(503, "Busy, try lower max_tokens")      finally:          # you can log (queue_depth, wait_ms, in_flight) here          pass

这个代码看着很简单但是用起来的话是真管用,可以防止少数贪婪请求把所有人的延迟都拉垮。

7、热路径预热和指标监控

还有两个看起来无聊但实际很关键的点。

第一是预热。部署的时候先用合成请求把常用的 system prompt 和 template 跑几遍,让 KV cache pages 提前填充好。5 到 10 个请求就够,能让第一分钟的性能稳下来。

import asyncio  HOT_PROMPTS = [      "Summarize this email in 3 bullets:",      "Draft a polite reply:",      "Explain this code block step-by-step:"  ]    async def warm():      await asyncio.gather(*[ask(p) for p in HOT_PROMPTS for _ in range(3)])    # call warm() right after deploy; ignore outputs

第二是监控指标别弄错了,generated tokens/sec 是 QPS 的核心指标,scheduler queue length 告诉你什么时候该扩容或者甩负载。平均值有时候没什么太大的用处,所以p50/p95/p99 的 time-to-first-token 和 time-to-last-token 才反映真实体验,产品质量活在 p95。

整体流程

最后整理一个完整的流程:

请求先到 FastAPI 的门控层,限制 max_tokens 和并发数,短暂排队后批量打到 vLLM 进程(绑定了特定 GPU)。vLLM 用 paged attention 管理多个序列,持续批处理新 token。稳定前缀命中 KV cache,speculative decoding 减少步骤,量化权重控制显存。token 流式返回,队列深度超阈值就开始丢弃低优先级流量。

总结

在最后总结之前先给一个实测的数据

单张 80GB 的 GPU 跑 7B 到 8B 的聊天模型,从无限制无流式改成流式加 256 token 上限,用户感知响应速度能翻倍,可持续 QPS 提升 30% 到 60%,能提高这么多的主要原因就是因为 batch 健康了。

而且配置合理的 speculative setup 能再加 15% 到 35% 的 tokens/sec,但是这个具体要看模型。上面说的前缀复用如果做得好,FAQ 场景基本就是开挂,基本能提高1倍多。

vLLM 上跑高 QPS 不能从单点突破,而是需要多个优化叠加才能产生好的结果。从请求塑形开始,把能复用的缓存用上,再叠加 speculative decoding 和量化。后面用并行和准入控制保证规模化的稳定性,热路径预热,盯住真正能反映问题的指标。

https://avoid.overfit.cn/post/fe3bc408622e424695dbcc27f0b7f14f

作者:Syntal