单卡爆改私有模型网关:vLLM + LiteLLM 生产级并发与流式降本实战

6次阅读
没有评论

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

上个月业务部门突然推来一个客服 Agent 的改造需求,预期峰值并发从原来的十几个暴涨到近两百。起初大家习惯性地接入公有云商业接口,结果跑了三天账单就直接拉响警报,而且遇到网络抖动时流式打字卡顿严重。为了彻底兜底并砍掉 80% 的 API 开销,我决定在本地工作站和单台 GPU 服务器上搭一套兼具低延迟与高吞吐的私有 LLM 代理网关。

很多朋友本地跑大模型习惯直接上 Ollama,开发调试确实爽,但一旦扔到生产环境承受几十个并发的流式请求,Ollama 的吞吐和显存复用能力很快就会见顶。最终我敲定的技术组合是 vLLM(底层推理引擎)+ LiteLLM Proxy(网关鉴权与路由)。这篇文章记录我从显存压榨、网关搭建到真实高并发压测的全套落地细节。

一、为什么放弃 Ollama,转向 vLLM + LiteLLM?

在小规模自建服务时,选型通常在 TGI、vLLM 和 Ollama 之间纠结。我们做了一轮基准测试,用一张 RTX 4090(24GB 显存)跑 Qwen2.5-14B-Instruct-AWQ,输入 512 tokens,输出 256 tokens,并发逐步从 1 压到 64:

  • Ollama (llama.cpp):并发超过 16 后,首字延迟(TTFT)急剧飙升,并发 32 时直接发生排队阻塞,显存无法做到零碎片的动态按需分配。
  • vLLM:得益于核心的 PagedAttention 机制,K-V Cache 被切成类似操作系统的虚拟内存页,显存几乎做到零浪费。并发飙到 48 时,系统吞吐量(Tokens/s)依然呈现线性增长,整体吞吐比 Ollama 翻了近 3.8 倍。

但 vLLM 本身只负责纯粹的推理计算,不具备生产环境必备的 API Key 轮询管理、请求速率限制(Rate Limit)、降级熔断以及跨模型路由功能。这时在最前端套一层轻量级的 LiteLLM Proxy,就能无缝提供 100% 兼容 OpenAI 格式的标准接口,两者的结合可以说是目前中小团队性价比最高的方案。

二、底层推理核心:vLLM 极限显存压榨配置

在单张 24GB 显存的卡上跑 14B 参数模型,显存分配必须精打细算。很多新手启动 vLLM 直接一行 python3 -m vllm.entrypoints.openai.api_server 就跑起来了,结果一发压测就直接爆显存(CUDA Out of Memory)。

关键参数在于显存预留比例(gpu-memory-utilization)与最大上下文长度(max-model-len)的博弈。下面是我在线上验证过最稳定的生产级启动脚本:

#!/usr/bin/env bash
# run_vllm.sh
docker run -d \
  --name vllm-engine \
  --runtime nvidia \
  --gpus '"device=0"' \
  -p 8000:8000 \
  -v /root/.cache/huggingface:/root/.cache/huggingface \
  -v /data/models:/models \
  --ipc=host \
  vllm/vllm-openai:v0.6.3 \
  --model /models/Qwen2.5-14B-Instruct-AWQ \
  --served-model-name qwen-14b \
  --quantization awq \
  --dtype half \
  --gpu-memory-utilization 0.90 \
  --max-model-len 4096 \
  --max-num-seqs 64 \
  --enforce-eager \
  --trust-remote-code

这里踩过的几个深坑必须提醒:

  1. --ipc=host 必须加:如果不挂载主机的 IPC 共享内存,当并发增高、多进程数据交换变大时,PyTorch 会毫无预警地崩掉 DataLoader 或直接死锁挂起。
  2. --enforce-eager 的取舍:vLLM 默认开启 CUDA Graph 优化,对于固定输入长度能提升约 10% 的推理速度。但它在服务初次启动时需要预热捕获图,如果你的显存只剩下 1~2GB 的安全边界,CUDA Graph 捕获阶段会瞬间 OOM。在 24G 显存跑 14B AWQ 的紧绷状态下,加上 --enforce-eager 关闭 CUDA Graph 能保住近 1.5GB 的显存用于承载更多并发请求。
  3. max-num-seqs:单批次最大处理请求数,直接控制调度池深度。设为 64 可以有效限制突发流量将显存打穿。

三、生产级网关组网:Docker Compose 编排 LiteLLM

后端推理引擎跑稳后,我们需要用 LiteLLM 作为网关层做安全拦截、鉴权、负载切分。推荐使用 Docker Compose 快速编排,将 PostgreSQL(用于保存使用量数据)和 Redis(用于高并发流控)一并带起。

新建 docker-compose.yml:

version: '3.8'

services:
  litellm-proxy:
    image: ghcr.io/berriai/litellm:main-v1.49.0
    container_name: litellm-proxy
    ports:
      - "4000:4000"
    volumes:
      - ./litellm-config.yaml:/app/config.yaml
    environment:
      - DATABASE_URL=postgresql://litellm:password123@postgres:5432/litellm_db
      - REDIS_URL=redis://redis:6379/0
      - STORE_MODEL_IN_DB=True
    command: ["--config", "/app/config.yaml", "--port", "4000", "--num_workers", "4"]
    depends_on:
      - postgres
      - redis
    restart: always

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: password123
      POSTGRES_DB: litellm_db
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: always

  redis:
    image: redis:7-alpine
    restart: always

volumes:
  pgdata:

编写关键网关配置 litellm-config.yaml,实现多模型路由与自动回退(Fallback):

model_list:
  # 优先路由到本地 vLLM 实例
  - model_name: default-chat
    litellm_params:
      model: openai/qwen-14b
      api_base: http://host.docker.internal:8000/v1
      api_key: "EMPTY"
      tpm: 100000
      rpm: 1200

  # 当本地单卡算力打满或宕机时,静默熔断到远程云端 API
  - model_name: default-chat-fallback
    litellm_params:
      model: deepseek/deepseek-chat
      api_key: "sk-your-remote-api-key"

router_settings:
  fallbacks: [{"default-chat": ["default-chat-fallback"]}]
  routing_strategy: "latency-based-routing"
  redis_host: "redis"
  redis_port: 6379

general_settings:
  master_key: "sk-nassky-super-admin-key"

这套配置最狠的地方在于:业务端只需要统一请求网关的 default-chat 模型。平日里所有流量全部白嫖本地单卡算力,当并发洪峰把本地显卡打爆触发超显存异常或超时时,LiteLLM 会在毫秒级内自动将失败请求转派到 DeepSeek 或云端接口,业务层完全无感知,既省钱又兼顾了可用性。

四、攻克 SSE 流式传输在反向代理中的三大陷阱

在将这套系统接给前端前端页面时,最常遇到的问题就是:终端打字机效果没了,页面死等 5 秒,然后一大坨字瞬间吐出来。或者在压测时发现客户端明明断开了连接,vLLM 的 GPU 却依然在疯狂计算。

1. Nginx 缓冲截断导致打字机失效

如果你在 LiteLLM 前面还挂了一层 Nginx 作为域名入口,必须在 location 块中显式关闭缓冲与代理缓存,否则 Nginx 会把分块的 Server-Sent Events (SSE) 先塞满缓冲区再下发:

location /v1/chat/completions {
    proxy_pass http://127.0.0.1:4000;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    
    # 彻底关闭缓冲,保障 SSE 实时推送
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;
    
    # 避免复杂思考长推理时网关超时掐断连接
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
}

2. 客户端取消但 GPU 继续空转(幽灵计算)

许多前端用户在看到生成的内容不合心意时会连续点击“重新生成”,这会在瞬间发送多条并发请求。如果你的中间件没有传递连接断开事件,vLLM 会老老实实把上一个请求的几百个 token 全部算完,无端消耗计算资源。

在 FastAPI 或自行编写的转发代理中,一定要监听底层 ASGI 连接状态。在 vLLM 原生接口中,确保请求头携带标准断连信号,并在网关层开启客户端取消传播机制,确保调用端 AbortController 一旦触发,推理引擎立即回收对应的 K-V Cache 页。

五、实战压测表现与收益核算

架构跑通后,我使用自定义的 Python 异步脚本对其进行压力测试。在维持首字延迟(TTFT)在 800ms 以内的前提下:

  • 纯单卡 Qwen2.5-14B-AWQ:支撑稳定并发达到 42 QPS,显存占用稳定在 21.6GB 左右,没有出现任何 OOM 报错。
  • 容灾表现:人工切断 vLLM 服务,LiteLLM 在第 2 次重试后成功切换至降级云端模型,降级切换耗时仅 120ms。
  • 财务降本:上线运行两周,本地网关承载了总请求量的 91.4%,原本月均预估 4500 元的公有云 API 账单直接压缩到了不足 400 元(仅包含了偶尔打满溢出到云端的费用和服务器电费)。

总结与避坑心得

大模型落地的工程化不是简单地找个 WebUI 跑起来就完事。私有化网关的核心在于 分层解耦:让 vLLM 专注于极致的计算吞吐和动态显存管理,把安全、流控、鉴权和灾备彻底推给轻量级的 LiteLLM 代理层。

在自己动手部署前,务必牢记三点:显存宁可留出 10% 缓冲也别贪大、SSE 链路上所有的反代缓冲必须统统掐死、降级熔断策略是避免本地单卡被打崩的最后底牌。按照这套骨架搭建,无论是团队内部降本,还是给中小业务提供稳定靠谱的私有 LLM 基础设施,都能跑得极其扎实。

===EOF===

正文完
 0
评论(没有评论)