单卡扛住百人并发:vLLM + LiteLLM 搭建生产级私有大模型代理实战

3次阅读
没有评论

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

很多朋友在内网跑开源大模型时,第一反应往往是装个 Ollama 加上 Open-WebUI。对于单人尝鲜写写代码确实足够爽快,但只要把接口开放给团队的二三十号人做业务调用,或者接入自动化脚本跑批量任务,服务分分钟教你做人:请求排队卡死、显存毫无预警直接 OOM、流式响应断流甚至偶发丢包。

Ollama 的底层设计初衷是单机轻量交互,并非高吞吐 API 服务。前段时间我给一个内部业务线搭建 AI 代理层,需求很明确:单张 24G 显存(RTX 4090 / A10)跑 Qwen2.5-14B-Instruct,必须稳定扛住 50~100 人的并发请求;还要有自动熔断兜底(本地爆了自动降级到 DeepSeek 或 Claude 线上 API),同时支持 OpenAI 标准接口鉴权与计费。

经过一整套压测和踩坑,最终跑通了 vLLM(推理引擎)+ LiteLLM(网关代理)+ Docker 的工业级轻量化组合。今天把这套经过压测验证的生产级配置与踩坑经验完整公开,拿走就能直接抄作业。

一、架构设计与核心痛点拆解

在动手写配置文件前,先弄清这套架构为什么能在单卡上把吞吐量拉满:

  • 推理层(vLLM):核心武器是 PagedAttention 和 Continuous Batching。传统推理框架预先为每个请求锁死整个 Context Window 的显存,而 vLLM 像操作系统管理虚拟内存一样切分页管理 KV Cache。这意味着单卡可以在显存不崩的前提下,同时处理数十个动态长短不一的请求。
  • 网关层(LiteLLM Proxy):负责统一入口。它向上暴露标准的 /v1/chat/completions 接口,向下负责多模型轮询负载均衡、API Key 配额鉴权、故障自动重试(Fallback)。当本地推理集群遇到超长上下文爆显存或超时时,无缝切换到云端 API,上游业务完全无感知。

二、vLLM 服务端核心参数配置与显存精算

启动 vLLM 如果全用默认参数,直接跑 14B 模型非常容易暴毙。以下是我在单张 24G 显存显卡上榨干性能的 docker-compose.yml 推理服务片段:

version: "3.8"

services:
  vllm-engine:
    image: vllm/vllm-openai:latest
    container_name: vllm-qwen-14b
    runtime: nvidia
    restart: unless-stopped
    environment:
      - HUGGING_FACE_HUB_TOKEN=your_token_here
      - CUDA_VISIBLE_DEVICES=0
    volumes:
      - /data/models/huggingface:/root/.cache/huggingface
      - /data/models/local:/models
    ports:
      - "8000:8000"
    ipc: host
    command:
      - --model=/models/Qwen2.5-14B-Instruct-AWQ
      - --served-model-name=qwen-14b
      - --trust-remote-code
      - --quantization=awq
      - --max-model-len=8192
      - --gpu-memory-utilization=0.92
      - --max-num-seqs=64
      - --enforce-eager
      - --disable-log-requests

踩坑与调优关键参数解析:

  1. --quantization=awq:强烈建议跑 4-bit AWQ 权重。对于 Qwen2.5-14B,FP16 纯模型静态显存就要占 28G(单卡直接死掉);采用 AWQ 量化后模型仅占约 9.5G 显存,剩下的 12G+ 显存可以完全划给 KV Cache。实测 AWQ 的困惑度(PPL)损耗基本可以忽略不计。
  2. --gpu-memory-utilization=0.92:默认是 0.9。设置成 0.92 是为了榨干最后 1~2G 显存放 KV Cache,但 绝对不要拉到 0.98 以上,因为 PyTorch 在运行时申请临时动态张量若撞墙,会直接引发 CUDA OOM 崩溃。
  3. --max-num-seqs=64:允许引擎内最大连续批处理的并发数。如果不加限制,一旦瞬间涌入 100 个请求,每批请求分到的 KV Cache 被打碎,会导致首字响应时间(TTFT)呈指数级飙升。限制为 64,超出的请求会在队列中优雅等待,保证在跑请求的高吐字速度。
  4. --ipc: host:高并发下共享内存不足会导致 PyTorch DDP 或进程间通信莫名挂起,这一行必加。

三、LiteLLM 网关层:做统一路由与故障自愈

有了 vLLM 后,绝不能把 8000 端口直接暴露给调用方。我们要用 LiteLLM 接管路由,实现“本地优先,爆量降级”策略。

新建 litellm-config.yaml 配置文件:

model_list:
  # 本地主推理引擎
  - model_name: internal-chat
    litellm_params:
      model: openai/qwen-14b
      api_base: http://vllm-engine:8000/v1
      api_key: "EMPTY"
      tpm: 150000
      rpm: 300

  # 云端降级兜底引擎
  - model_name: internal-chat-fallback
    litellm_params:
      model: deepseek/deepseek-chat
      api_key: os.environ/DEEPSEEK_API_KEY
      api_base: https://api.deepseek.com/v1

router_settings:
  # 路由策略与故障转移
  routing_strategy: usage-based-routing
  fallbacks:
    - internal-chat: ["internal-chat-fallback"]
  timeout: 45
  num_retries: 2
  allowed_fails: 2
  cooldown_time: 30

general_settings:
  master_key: sk-MasterSecretPass123456
  database_url: "postgresql://litellm:password@postgres:5432/litellm_db"

这套配置实现了关键的生产级防护:当本地 vLLM 发生 500 错误、推理超时(45 秒)或队列被打满拒绝连接时,LiteLLM 会在 2 秒内静默把请求转发到云端 DeepSeek,对前端业务来说,只是响应多耗费了微秒级的时间,没有任何请求会被硬生生掐断。

四、深水区排障:Nginx SSE 流式传输假死大坑

系统跑起来后,测试非流式接口一切完美。但当我们用 Web 端做 Streaming(流式吐字)交互时,诡异的现象出现了:点击发送后,前端界面一直在转圈,等了十几秒,突然“唰”的一下把整段回答全喷出来了,完全没有打字机的效果。

我在排查日志时发现,vLLM 内部首字早在 200ms 内就已经生成,问题出在作为外网反向代理的 Nginx 身上。Nginx 默认开启了 Proxy Buffering,它认为流式输出是未完结的响应分块,会硬生生将字符攒进缓冲区,直到填满 4KB 或响应结束才一次性下发。

必须在 Nginx 代理配置中加入以下指令,彻底关闭流式缓冲:

server {
    listen 443 ssl http2;
    server_name api.nassky.top;

    location /v1/ {
        proxy_pass http://127.0.0.1:4000; # 转发到 LiteLLM
        proxy_http_version 1.1;

        # 核心防卡死参数:彻底关闭代理缓冲
        proxy_buffering off;
        proxy_cache off;
        
        # 保持长连接与 SSE 协议头正确穿透
        proxy_set_header Connection '';
        chunked_transfer_encoding on;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 大模型生成时间较长,超时时间至少拉到 180 秒
        proxy_read_timeout 180s;
        proxy_send_timeout 180s;
    }
}

重新 nginx -s reload 后,前端流式打字机效果立刻变得如丝般顺滑,首字延迟(TTFT)直接稳定在 300ms 以内。

五、压力实测对比(Locust 50 并发)

为了检验这套架构的真实战斗力,我用 Locust 模拟了 50 个用户持续发送 500~1000 tokens 提示词的问答请求,对比原生 Ollama 与 vLLM+LiteLLM 的实测表现:

压测指标 原生 Ollama (14B-Q4) vLLM (14B-AWQ) + LiteLLM 提升幅度
吞吐量 (Tokens/sec) 48.2 tokens/s 412.6 tokens/s 约 8.5 倍
首字延迟 (TTFT P95) 14.8 秒 1.1 秒 降幅超 90%
请求失败率 (50 并发持续 5 分钟) 28.4% (超时与排队失败) 0.0% (部分触发降级) 零中断保证
显存占用稳定性 随并发飙升,偶发 OOM 重启 平稳锁定在 22.1G,KV Cache 弹性回收 极其稳定

总结与避坑心得

跑本地大模型,工具链选型直接决定了是做“玩具”还是做“生产力工具”:

  1. 模型量化别盲目追 GGUF:如果在服务端纯 Linux + NVIDIA 环境下跑高并发,优先考虑 AWQ 或 GPTQ 格式,它们在 CUDA 核心上的计算密度和 vLLM 兼容性远优于 llama.cpp 生态的 GGUF。
  2. 一定要加网关熔断:本地 GPU 算力永远是有物理上限的。LiteLLM 可以在最前端帮我们挡住爆量流量,即使内部某天显卡驱动崩了,外部业务系统也不会收到一堆 502 报错,运维有充足的时间
正文完
 0
评论(没有评论)