共计 4032 个字符,预计需要花费 11 分钟才能阅读完成。
很多朋友在本地服务器或公司自建机房折腾开源大语言模型(LLM)时,第一步往往是拉起一个 Ollama 或者直接跑 HuggingFace 的 Transformers 原生接口。用来自己单人玩玩确实省事,但只要接入自动化 Agent 工作流,或者前端稍微挂上十几二十个人同时发请求,服务延迟就会瞬间拉爆,甚至直接抛出 CUDA out of memory。
上周我在为小团队搭建一套统一的内部 LLM 推理网关,目标是用单张 RTX 4090(24GB)或者单张 A100(80GB)稳定支撑业务层千级高并发调用,同时做到端到端流式响应(SSE)延迟尽可能低。在对比了 TGI、llama.cpp 和 vLLM 后,最终基于 vLLM + 异步 FastAPI 代理层 搞定了一套低延迟、高吞吐的架构。今天把这套方案的底层原理、配置脚本以及我踩过的三个深坑毫无保留地整理出来。
一、为什么原生部署撑不住并发?
在动手改配置前,必须弄明白高并发下 GPU 到底卡在哪里。传统的推理框架有两大硬伤:
- KV Cache 静态分配导致显存碎片化:原生 PyTorch 机制需要提前为请求的最大输出长度分配连续显存。如果设了
max_tokens=4096,哪怕用户只输出了 20 个字,显存依然被霸占,导致批处理(Batching)根本塞不下多少并发请求。 - Naive 批处理与动态序列长度冲突:常规的批处理需要 Padding 到最长序列,无效计算极其严重。而 vLLM 的核心杀手锏就是 PagedAttention,它借鉴了操作系统的虚拟内存分页思想,将显存切成离散的 Block,彻底消除了显存内部碎片。
二、生产级 vLLM 启动参数实战微调
很多人直接运行 vllm serve Qwen/Qwen2.5-7B-Instruct 就完事了,这完全浪费了硬件性能。以下是我压测数天后总结出的黄金启动脚本,以 Qwen2.5-7B 运行在单张 24GB 显存设备为例:
#!/usr/bin/env bash
export CUDA_VISIBLE_DEVICES=0
export NCCL_IGNORE_DISABLED_P2P=1
# 启动高吞吐、低碎片化的 vLLM 实例
python3 -m vllm.entrypoints.openai.api_server \
--model /data/models/Qwen2.5-7B-Instruct \
--served-model-name qwen-7b-chat \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.92 \
--max-model-len 8192 \
--block-size 16 \
--swap-space 4 \
--max-num-batched-tokens 8192 \
--max-num-seqs 256 \
--enable-prefix-caching \
--disable-log-requests
核心参数避坑解析:
--gpu-memory-utilization 0.92:默认是 0.90。如果显卡只跑模型,可以拉到 0.92。千万别拉到 0.98 以上,因为 CUDA 运行时分配上下文、动态激活值需要极小余量,设得太满在长请求峰值时必崩 CUDA 核心。--block-size 16:PagedAttention 的分页粒度。在长文本推理中,设为 16 或 32 是吞吐最优解;小显卡建议 16,大显卡(如 A100)可以设为 32 提升内存传输效率。--enable-prefix-caching:强烈建议开启! 如果你在做 Agent、多轮对话或者 RAG 检索,系统 Prompt(System Prompt)和历史检索块基本相同。开启后,命中相同前缀的 KV Cache 直接重用,TTFT(首字延迟)直接从几百毫秒骤降到几毫秒。--disable-log-requests:高并发场景下,每一条请求的文本如果都打进 stdout,Python 解释器的控制台 I/O 会成为隐性瓶颈,甚至锁住事件循环。
三、构建高并发异步代理流转网关
直接把 vLLM 的 8000 端口暴漏给业务端并不明智,缺乏鉴权、动态限流、超时重试以及上游连接池复用。我们在业务前端套了一层轻量化的异步代理服务,采用 FastAPI + HTTPX 连接池,避免流式响应时客户端意外断开导致 GPU 空转算力。
# gateway.py
import httpx
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
app = FastAPI()
# 维持持久连接池,避免每次请求频繁进行 TCP/TLS 握手
client = httpx.AsyncClient(
base_url="http://127.0.0.1:8000",
timeout=httpx.Timeout(connect=5.0, read=60.0, write=5.0, pool=60.0),
limits=httpx.Limits(max_keepalive_connections=500, max_connections=1000)
)
@app.post("/v1/chat/completions")
async def chat_proxy(request: Request):
payload = await request.json()
# 强制加上针对后端的流式标识处理
is_stream = payload.get("stream", False)
headers = {
"Content-Type": "application/json",
"Authorization": request.headers.get("Authorization", "")
}
if is_stream:
async def stream_generator():
try:
async with client.stream("POST", "/v1/chat/completions", json=payload, headers=headers) as upstream_resp:
if upstream_resp.status_code != 200:
yield f"data: {{\"error\": \"Upstream status {upstream_resp.status_code}\"}}\n\n"
return
async for chunk in upstream_resp.aiter_raw():
# 业务层可在此处做敏感词拦截、Token 计数等
yield chunk
except httpx.RequestError as exc:
yield f"data: {{\"error\": \"Proxy error: {str(exc)}\"}}\n\n"
return StreamingResponse(stream_generator(),
media_type="text/event-stream"
)
else:
resp = await client.post("/v1/chat/completions", json=payload, headers=headers)
return resp.json()
@app.on_event("shutdown")
async def shutdown_event():
await client.aclose()
四、压测实测对比数据与避坑经验
我使用 Locust 对原生 HuggingFace 部署(FastAPI + ThreadPoolExecutor)与本次优化的 vLLM 架构进行了单卡 RTX 4090 上的并发压力对比。测试模型均为 Qwen2.5-7B-Instruct,输入 Prompt 约 500 tokens,要求模型生成约 150 tokens:
| 部署方案 | 并发连接数 (Concurrency) | 首字延迟 TTFT (p95) | 整体吞吐量 (Tokens/s) | 显存崩溃率 |
|---|---|---|---|---|
| HF Transformers + Pipeline | 16 | 2,450 ms | 42.8 | 并发达 24 时 OOM |
| vLLM(默认参数无调优) | 64 | 420 ms | 430.5 | 0% |
| vLLM + Prefix Cache + 调优网关 | 128 | 68 ms | 1,120.4 | 0% |
踩坑实录:三个生产中极易致命的配置点
- Linux 系统 File Descriptor 耗尽:高并发下如果不改
/etc/security/limits.conf,很容易报Too many open files。在启动 vLLM 和代理前,务必执行ulimit -n 65535,并在 systemd 服务文件中明确写入LimitNOFILE=65535。 - 网络中断未注销导致的“显卡僵尸算力”:HTTP 客户端(如 Web 页面刷新的用户)断开后,如果代理网关没有中断与上游 vLLM 的连接,vLLM 默认会把这 1000 个 Token 默默生成完。FastAPI 中必须使用原生异步流,只要下游连接中断,立即触发
upstream_resp.aclose()。 - 长上下文突发引发 swap 抖动 :如果你将
--swap-space设得太大(如默认 4GB 甚至 16GB),当显存撑不住时,vLLM 会把 KV Cache 换入主机的 CPU RAM。一旦发生频繁 Swap,吞吐会断崖式暴跌 90% 以上。 宁愿在网关层做 429 降级或排队,也尽量不要让显存频繁依赖 CPU Swap 救场。
五、选型与落地方案建议
对于个人开发者、独立极客或是微型团队,不要盲目去上一套厚重的 Kubernetes + KServe 体系。单纯靠 vLLM 的内置引擎 + 一层轻量异步代理,在一张消费级显卡(如 RTX 4090 / 3090)上跑 7B~14B 的 Q4 或 AWQ 量化版本,就已经能轻松应付内部几十个 Agent 和自动化爬虫流水线的实时调用。
如果未来要扩容多张显卡,只需要在 vLLM 参数中加入 --tensor-parallel-size 2(TP)或者利用 Ray 跑多实例分布式调度。先用这套精简的参数跑通性能基线,才是把算力压榨到极限的最佳姿势。