自建生产级高并发 LLM 推理网关:vLLM + LiteLLM 架构调优与实战避坑

1次阅读
没有评论

共计 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 推理网关并不只是把模型跑起来那么简单,从实验环境到生产可用,核心差异就在于 ** 并发调度能力 ** 与 ** 显存精细控制 **:

  1. 别拿 Ollama 当集群网关 :它适合开发者本地开箱即用,但在多线程无状态调用的工程架构里,vLLM 或 TGI 才是唯一解。
  2. 慎重控制上下文长度 :永远根据下游场景设定 --max-model-len,不要让 128k 的理论上限挤占掉宝贵的批处理 Block 显存。
  3. 网络全链路必须支持流式 :从模型层、网关层(LiteLLM)到反向代理(Nginx),必须全面关闭 Buffer 并放开长连接超时,避免流式响应断流。
  4. 架构分层解耦 :让 vLLM 专心做好 Tensor 计算,让 LiteLLM 处理 Key 分配、限流、故障降级与用量统计,系统的鲁棒性会高出好几个台阶。
正文完
 0
评论(没有评论)