共计 4110 个字符,预计需要花费 11 分钟才能阅读完成。
上个月我把团队内部的几个自动化 Agent 工作流从公有云 API 迁往私有化部署。刚开始为了省事,直接起了一套 Ollama 跑 Qwen2.5-14B-Instruct,结果并发一上来直接原地爆炸:多 Agent 并行生成时请求频繁排队超时,显存利用率忽高忽低,偶尔还会报 Context 长度溢出。Ollama 拿来当本地桌面玩具非常爽,但在多并发、需要严格控制显存与上下文复用的工作流场景下,它的调度能力远远不够看。
为了解决高吞吐推理与多模型动态分发的痛点,我花了一个周末重新搭建了一套由 vLLM(推理引擎)+ LiteLLM Proxy(网关与负载均衡)+ Redis(状态与缓存) 组成的推理网关。本文记录核心配置细节、性能调优参数,以及线上踩过的几个深坑。
一、推理底座:vLLM 生产级显存与并发调优
vLLM 凭借 PagedAttention 机制,有效解决了传统 HuggingFace Transformers 框架下 KV Cache 显存碎片化的问题。但如果直接用默认参数启动,你大概率会遇到显存吃撑(OOM)或者吞吐受限的情况。
这里以单卡 RTX 4090(24GB 显存)跑 Qwen2.5-7B/14B-Instruct-AWQ 为例,给出我在生产环境验证过的启动命令与关键参数:
# 生产级 vLLM 启动模板 (以 Docker 形式运行)
docker run --gpus all \
--shm-size 16g \
-p 8000:8000 \
-v /data/models:/root/.cache/huggingface \
--name vllm-qwen-engine \
vllm/vllm-openai:latest \
--model Qwen/Qwen2.5-14B-Instruct-AWQ \
--served-model-name qwen2.5-14b \
--quantization awq \
--max-model-len 8192 \
--gpu-memory-utilization 0.92 \
--max-num-seqs 64 \
--enable-prefix-caching \
--trust-remote-code
核心参数避坑指南:
- –gpu-memory-utilization 0.92:默认值是 0.9。对于 24GB 显存,设为 0.92 意味着预留约 2GB 显存给 CUDA Context 和临时张量计算。 千万不要设成 0.98 以上 ,否则一旦碰到长 prompt 并发输入,极容易触发 PyTorch 的底层 OOM 杀进程。
- –max-model-len 8192:如果不显式限制,vLLM 会尝试拉满模型的最大上下文(比如 Qwen2.5 原生支持 32k/128k)。长上下文会直接吞没剩余显存,导致能够分配给并发请求的 Block 数量暴跌,并发一高就进入“显存饥饿”等待状态。根据实际 Agent 业务设定上限才是最优解。
- –enable-prefix-caching: 这是多 Agent 场景下的神级开关! 在多轮对话或固定 System Prompt 的 Agent 任务中,开启后相同的 Prompt 前缀只会计算一次 KV Cache,后续请求直接复用显存,首字延迟(TTFT)能直接从秒级骤降到百毫秒内。
- –shm-size 16g:Pytorch 多进程与 CUDA IPC 严重依赖共享内存。如果使用 Docker 部署,不显式指定
--shm-size(默认只有 64MB),高并发批量推理时容器会直接崩溃抛出Bus error。
二、网关层:LiteLLM Proxy 实现统一协议与智能路由
虽然 vLLM 自带兼容 OpenAI 的 HTTP Server,但在真实业务架构中,我们通常需要挂载多个模型(例如一个低延时的小模型处理分类,一个大模型处理复杂推理),同时需要 Key 鉴权、速率限制、故障重试与 Token 统计。这时候由 LiteLLM Proxy 担任 API 网关是最轻量高效的方案。
通过 Docker Compose 编排 LiteLLM 与 Redis:
version: '3.8'
services:
litellm:
image: ghcr.io/berriai/litellm:main-latest
container_name: litellm-proxy
ports:
- "4000:4000"
volumes:
- ./config.yaml:/app/config.yaml
environment:
- DATABASE_URL=redis://redis:6379/0
- STORE_MODEL_IN_DB=False
command: ["--config", "/app/config.yaml", "--port", "4000", "--num_workers", "4"]
depends_on:
- redis
redis:
image: redis:7-alpine
container_name: litellm-redis
restart: always
command: redis-server --appendonly yes
配套的 config.yaml 路由配置示例:
model_list:
# 本地部署的 vLLM 主模型
- model_name: internal-qwen
litellm_params:
model: openai/qwen2.5-14b
api_base: http://host.docker.internal:8000/v1
api_key: "none"
rpm: 120
tpm: 60000
# 兜底 fallback:如果本地实例负载过高或挂掉,自动平滑切至公有云
- model_name: internal-qwen
litellm_params:
model: deepseek/deepseek-chat
api_key: os.environ/DEEPSEEK_API_KEY
litellm_settings:
drop_params: True
set_verbose: False
request_timeout: 60
router_settings:
routing_strategy: latency-based-routing
redis_host: redis
redis_port: 6379
这一层配置直接带来了企业级网关的能力:客户端无论调用本地 Qwen 还是外部云端模型,全都使用标准的 http://proxy-host:4000/v1/chat/completions 接口;当本地 GPU 显存爆满抛出 503 时,LiteLLM 毫秒级静默回退到备用模型,业务无感知。
三、网络层排障:Nginx SSE 流式响应断流与 Buffer 溢出
在上生产网关走外网域名时,几乎所有人都会加一层 Nginx 做反向代理和 SSL 卸载。但在调试流式输出(stream=True)时,我遇到了一个极其隐蔽的坑: 前端收到的打字机效果卡顿,甚至在长文本生成到一半时连接被强制切断。
根本原因在于 Nginx 默认开启了 HTTP 反向代理缓存机制(Proxy Buffering),它会尝试把后端 vLLM 吐出来的 Chunk 存入缓冲区,等凑满了一定字节数或者连接关闭才一次性发给客户端。同时,默认的超时时间对长推理模型极不友好。
必须在对应的 Nginx `location` 块中打上以下补丁:
location /v1/chat/completions {
proxy_pass http://127.0.0.1:4000;
# 核心:必须禁用代理缓存,保证 SSE 能够逐字推流
proxy_buffering off;
proxy_cache off;
# 维持长连接
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Transfer-Encoding chunked;
# 调大读写超时时间,防止长推理时被断链
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
四、真实并发压测对比
为了直观对比架构效果,我在单台配备 RTX 4090 (24GB) + AMD 7950X 的自建服务器上,使用 Locust 模拟 20 个高并发 Agent 客户端,发送包含 1.5k Token 预设 Prompt + 200 Token 新输入的任务,针对不同方案进行压测:
| 部署方案 | 首字延迟 (TTFT) | 整体生成吞吐 (Tokens/s) | 20 并发请求成功率 | 资源瓶颈表现 |
|---|---|---|---|---|
| Ollama (默认配置) | 3.82s | 48.5 | 65% (超时丢包) | 请求排队严重,频繁发生线程挂起 |
| vLLM (未开 Prefix Cache) | 1.21s | 185.3 | 100% | 显存吃满,KV Cache 分配紧绷 |
| vLLM (开启 Prefix Cache) | 0.24s | 342.1 | 100% | 复用命中率 82%,GPU 算力利用率拉满 |
从实测数据可以非常清晰地看出:在固定 System Prompt 的 Agent 场景下,Prefix Caching 直接将吞吐提升了接近一倍,首字延迟更是降低了一个数量级 。客户端再也不需要为了每次 ReAct 循环反复重复计算那几百个固定 Token 的注意力和权重。
总结与避坑心得
自建 LLM 推理网关并不只是把模型跑起来那么简单,从实验环境到生产可用,核心差异就在于 ** 并发调度能力 ** 与 ** 显存精细控制 **:
- 别拿 Ollama 当集群网关 :它适合开发者本地开箱即用,但在多线程无状态调用的工程架构里,vLLM 或 TGI 才是唯一解。
- 慎重控制上下文长度 :永远根据下游场景设定
--max-model-len,不要让 128k 的理论上限挤占掉宝贵的批处理 Block 显存。 - 网络全链路必须支持流式 :从模型层、网关层(LiteLLM)到反向代理(Nginx),必须全面关闭 Buffer 并放开长连接超时,避免流式响应断流。
- 架构分层解耦 :让 vLLM 专心做好 Tensor 计算,让 LiteLLM 处理 Key 分配、限流、故障降级与用量统计,系统的鲁棒性会高出好几个台阶。