vLLM + LiteLLM 搭建生产级大模型流式网关:KV Cache 优化、并发压测与降级实战

1次阅读
没有评论

共计 3890 个字符,预计需要花费 10 分钟才能阅读完成。

很多同学在本地或内网跑模型时,第一反应往往是装个 Ollama 加上 Open-WebUI。跑单个问答确实爽快,但在真实业务场景或者团队内网中,只要并发稍微上来(比如有 3-5 个人同时调用接口,或者接入自动化 Agent 连续发 prompt),Ollama 的排队延迟就会飙升,甚至直接卡死爆显存。

Ollama 的底层封装虽然傻瓜,但在动态批处理(Continuous Batching)和显存精细化控制上几乎没有给工程师留操作空间。上个月我把部门内部的知识库后端从 Ollama 迁移到了 vLLM + LiteLLM 架构,不仅单卡(RTX 4090 24G)跑 DeepSeek-R1-Distill-Qwen-14B 的并发吞吐提升了将近 4 倍,还顺手搞定了多模型混合路由与云端自动降级。今天把这套经过压测验证的生产级配置与踩坑细节完整记录下来。

一、架构设计:为什么是 vLLM + LiteLLM?

本地 AI 服务的痛点通常集中在两端:底层的显存利用率 与上层的协议管理。

  • 推理后端(vLLM):核心优势是 PagedAttention 算法,把注意力机制的 KV Cache 切块分页,几乎彻底消除了内存碎片,配合连续批处理,让 GPU 显存利用率压榨到 95% 以上。
  • 网关层(LiteLLM):提供完全兼容 OpenAI 标准格式的轻量网关,自带负载均衡、模型 Fallback(降级)、速率限制与 Token 级消耗统计,可以直接无缝对接现有的各类 Agent、Dify 或 NextChat。

二、底层部署:vLLM 的容器化与显存边界配置

不要在宿主机直接跑 Python 源码部署,CUDA 版本依赖容易把人搞崩溃。推荐使用官方 Docker 镜像配合 Compose 管理。这里以搭载 AWQ 4-bit 量化版本的模型为例,既保住了显存,又保证了推理精度与速度。

# docker-compose.vllm.yml
services:
  vllm-deepseek:
    image: vllm/vllm-openai:v0.7.2
    container_name: vllm-deepseek
    runtime: nvidia
    environment:
      - CUDA_VISIBLE_DEVICES=0
      - HUGGING_FACE_HUB_TOKEN=your_hf_token # 如需拉取受限模型
    ports:
      - "8000:8000"
    volumes:
      - /data/huggingface_cache:/root/.cache/huggingface
      - /data/models:/models
    ipc: host
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    command: >
      --model /models/DeepSeek-R1-Distill-Qwen-14B-AWQ
      --served-model-name deepseek-r1-14b
      --quantization awq
      --trust-remote-code
      --dtype half
      --max-model-len 8192
      --gpu-memory-utilization 0.90
      --swap-space 4
      --enforce-eager
    restart: unless-stopped

关键参数避坑说明:

  • --gpu-memory-utilization 0.90:我在测试时踩过坑,千万不要直接拉到 0.98。vLLM 启动时会预先根据这个比例分配 KV Cache。留出 10% 的缓冲显存,可以避免 PyTorch 偶发的短时显存峰值直接导致 OOM 挂掉。
  • --max-model-len 8192:上下文窗口尽量根据实际业务卡死。如果设成模型默认的 32k 或 64k,预留给单连接的 KV Cache 尺寸过大,能并发处理的 Request 数量会断崖式下跌。
  • --enforce-eager:对于中小型模型,开启 eager 模式可以跳过冗长的 CUDA Graph 编译时间,在单卡冷启动时能省下 2-3 分钟,同时也能节省一部分 Graph 占用的固定显存。

三、中间件:配置 LiteLLM 统一网关与高可用容灾

启动了单点推理引擎后,我们在上层架设 LiteLLM 作为流量入口。这套配置实现了:优先调用本地 14B 模型;如果本地并发队列打满报错,或者请求超出长度限制,自动无缝降级走云端 DeepSeek-V3 API。

# litellm_config.yaml
model_list:
  # 本地主推理引擎
  - model_name: local-deepseek
    litellm_params:
      model: openai/deepseek-r1-14b
      api_base: http://vllm-deepseek:8000/v1
      api_key: "none"
      rpm: 120
      timeout: 60

  # 云端备用容灾路由
  - model_name: deepseek-cloud
    litellm_params:
      model: openai/deepseek-chat
      api_base: https://api.deepseek.com/v1
      api_key: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"

router_settings:
  routing_strategy: "latency-based-routing"
  redis_host: "redis"
  redis_port: 6379
  timeout: 45
  num_retries: 2
  fallbacks:
    - local-deepseek: ["deepseek-cloud"]

general_settings:
  master_key: "sk-litellm-master-key-xyz"

启动 LiteLLM 代理只需要一条简单的 Docker 命令:

docker run -d \
  --name litellm-proxy \
  --network host \
  -v $(pwd)/litellm_config.yaml:/app/config.yaml \
  ghcr.io/berriai/litellm:main-latest \
  --config /app/config.yaml --port 4000

四、关键踩坑实录与排障细节

1. 反向代理层吞掉 SSE 流式输出(打字机假死)

部署完成后,许多人发现前端请求接口时,文字不是像打字机一样一个个吐出来,而是卡死 10 几秒后一次性全部喷出来。这是由于网关上层通常有一层 Nginx 做域名解析和 SSL 卸载,Nginx 默认的 buffer 缓存把 SSE(Server-Sent Events)数据包拦截了。

必须在对应的 Nginx location 中加入以下强制不缓存策略:

location /v1/chat/completions {
    proxy_pass http://127.0.0.1:4000;
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    chunked_transfer_encoding off;
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 600s;
}

2. 客户端主动中断导致的孤儿计算

在前端页面中,用户经常在模型输出到一半时点击“停止生成”。如果你的代理层没有正确向后端广播 disconnect 信号,vLLM 依然会在显卡中将后续几千个 Token 算完,浪费大量算力。在 Python 业务层封装流式代理时,务必使用异步生成器并监听断开事件:

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import httpx

app = FastAPI()

@app.post("/proxy/stream")
async def proxy_stream(request: Request):
    client = httpx.AsyncClient(timeout=60.0)
    body = await request.json()
    
    req = client.build_request("POST", "http://localhost:4000/v1/chat/completions", json=body)
    res = await client.send(req, stream=True)

    async def event_generator():
        try:
            async for chunk in res.aiter_raw():
                # 检测客户端是否已经挂断
                if await request.is_disconnected():
                    await res.aclose()
                    break
                yield chunk
        finally:
            await client.aclose()

    return StreamingResponse(event_generator(), media_type="text/event-stream")

五、实测压测数据对比

我在同一台测试机上(AMD EPYC 7302 + 64G RAM + RTX 4090 24G),对 Ollama 默认部署 与 vLLM+LiteLLM 进行了 Locust 脚本并发压测,模型均为 Qwen-14B 对应量化版本,Prompt 统一为 512 Tokens,生成长度为 256 Tokens:

指标 / 引擎方案 Ollama (Default) vLLM + LiteLLM 架构 提升幅度
首字延迟 (TTFT) 并发 =1 280 ms 210 ms 快 25%
首字延迟 (TTFT) 并发 =10 2,450 ms (严重排队) 540 ms 正文完
 0
评论(没有评论)
Copyright©2013-2025 Nas的天空 nassky.top 版权所有
 Theme by Puock