单卡4090跑满吞吐:vLLM + LiteLLM 构建私有高并发流式LLM网关实战

2次阅读
没有评论

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

上个月组内业务线开始密集接入大模型能力,客服机器人、内部代码审查、知识库检索同时涌入。原本用 Ollama 或者原生 HuggingFace pipeline 跑的 DeepSeek/Qwen 实例直接把单张 RTX 4090 显卡爆到 CUDA OOM,偶发响应阻塞甚至直接卡死 SSE(Server-Sent Events)流式连接。

业务侧的诉求很明确:既要保证百毫秒级的首字延迟(TTFT),又要兼顾多并发请求下的吞吐稳定性。为了不向老板申请动辄十几万的多卡 A100/H800 集群,我花了几天时间彻底重构了内部的推理架构——底层换成 vLLM(基于 PagedAttention 与连续批处理),上层挂一层轻量级统一代理 LiteLLM Proxy 实现鉴权、速率限制、流式缓存与监控。实测在 24G 显存的消费级卡上,轻松扛住了 30+ 并发的持续流式调用。

一、为什么原生部署接不住并发?

许多团队把 HuggingFace 或 Ollama 镜像挂到 Docker 里,外面包一层 FastAPI 就上线了,结果并发稍高就会遭遇两个致命瓶颈:

  1. KV Cache 静态显存浪费与显存碎片:传统实现必须预先为 max_model_len 静态分配大块连续显存,哪怕用户只发了两个字,几百兆显存就占死了。并发一上来,显存没耗尽就被内存碎片拖垮导致 OOM。
  2. Naive Batching 的“短板效应”:传统动态批处理会因为 Batch 里某一个长回答请求拖慢整个 Batch 的返回速度,流式首字延迟飙升至 3~5 秒以上。

vLLM 的 PagedAttention 机制将 KV Cache 切片分散存放在非连续物理块中,配合 Iteration-level Continuous Batching(迭代级连续批处理),每生成一个 token 就允许新请求插入或者完成的请求退出,真正把 24G 显存压榨到了极限。

二、底层引擎部署:vLLM 极限显存参数调优

在 RTX 4090(24G VRAM)上运行 Qwen2.5-14B-Instruct-GPTQ-Int4 或 Qwen2.5-7B-Instruct(全精度 BF16)是性价比最高的方案。这里我们以 Qwen2.5-14B-Instruct-AWQ 为例,既保留了接近原版的推理精度,又为 KV Cache 留出了足额显存空间。

直接通过 Docker 启动 vLLM,关键在于几个显存分配参数的微调:

docker run --gpus all \
  -d --name vllm-qwen \
  --restart unless-stopped \
  -p 8000:8000 \
  --ipc=host \
  -v /data/models:/models \
  vllm/vllm-openai:latest \
  --model /models/Qwen2.5-14B-Instruct-AWQ \
  --served-model-name qwen-14b \
  --quantization awq \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.92 \
  --max-num-seqs 64 \
  --enforce-eager \
  --disable-log-requests

核心参数排坑说明:

  • --gpu-memory-utilization 0.92:默认是 0.90。对于 24GB 显存,0.92 可以留出约 1.9G 显存作为 CUDA 上下文 buffer,剩下的约 22.1G 中扣除模型权重的 8.5G,足足有 13G 以上完全划归 KV Cache,足以支撑数十个长上下文并发。
  • --enforce-eager:在 4090 等 Ada Lovelace 架构上,CUDA Graphs 预热如果上下文长度设到 8192,启动时会吃掉近 2GB 显存做 capture,极易在初始化阶段直接 OOM。开启 --enforce-eager 能省下这部分显存给并发槽位。
  • --max-num-seqs 64:限制单实例最大并发序列数,防止瞬间涌入海量请求造成显存击穿,平稳排队比服务 crash 更重要。

三、网关层搭建:LiteLLM 流式代理与负载均衡

后端 vLLM 纯粹负责算力,前端各业务系统(Node.js 后台、Python 数据清洗、外部 Webhook)需要一套标准的 OpenAI 兼容接口,并做统一的 Token 预算管理与分发。LiteLLM Proxy 是这一层最舒服的轻量级解决方案。

创建网关配置文件 config.yaml:

model_list:
  - model_name: gpt-4o-mini # 业务端可以统一调用此别名
    litellm_params:
      model: openai/qwen-14b
      api_base: http://192.168.1.100:8000/v1
      api_key: "EMPTY"
      rpm: 1200
      stream: true

router_settings:
  routing_strategy: "latency-based-routing"
  timeout: 60

general_settings:
  master_key: "sk-nassky-master-key-2026"
  store_model_in_db: false

使用 Docker Compose 将 LiteLLM 一键拉起:

version: '3.8'

services:
  litellm:
    image: ghcr.io/berriai/litellm:main-latest
    container_name: litellm-gateway
    restart: always
    ports:
      - "4000:4000"
    volumes:
      - ./config.yaml:/app/config.yaml
    command: ["--config", "/app/config.yaml", "--port", "4000", "--num_workers", "4"]

四、实操踩坑记录:SSE 流式传输“假死”与 Nginx 缓冲

整套架构跑通后,前端工程师马上找过来吐槽:“本地 Python 脚本测一切正常,为什么接入到前端页面,SSE 流式输出不是逐字吐出,而是卡了十几秒后 哗啦一下全量弹出来?”

我在网关链路中抓包排查后发现,问题出在 LiteLLM 前面挂的 Nginx 身上。Nginx 默认启用了代理缓冲(proxy_buffering on),当 vLLM 产生几十字节的 chunk 时,Nginx 会把数据塞在 buffer 里,等攒满一个 buffer 块(通常 4k~8k)或者连接关闭时才 flush 给客户端。

在 Nginx 对应 location 块中加入以下关键配置即可解决:

location /v1/ {
    proxy_pass http://127.0.0.1:4000/v1/;
    proxy_http_version 1.1;
    
    # 彻底关闭缓冲,保证流式即刻推送
    proxy_buffering off;
    proxy_cache off;
    
    # 保持长连接
    proxy_set_header Connection '';
    proxy_set_header Host $host;
    chunked_transfer_encoding on;

    # 超时时间放宽,避免长上下文思考超时
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
}

五、压力实测:Native Ollama vs vLLM 网关

为了验证效果,我使用 Locust 编写测试脚本模拟真实高并发调用场景。测试环境:输入 Prompt 约 350 Tokens,生成 Output 约 200 Tokens,固定 30 个并发客户端持续压测 5 分钟:

部署方案 并发并发数 首字延迟 (TTFT P95) 平均生成速率 (Tokens/s) 请求失败率 (Error Rate)
Ollama 原生 Docker 30 并发 8.42s 28.4 tok/s 16.7% (爆显存排队超时)
vLLM + LiteLLM 方案 30 并发 0.48s 142.6 tok/s 0.00% (稳定无故障)

测试结果对比非常直观:在相同的硬件条件下,vLLM 的整体系统吞吐量达到了原生部署方案的 5 倍,首字延迟直接从体感无法接受的 8 秒以上拉回到了半秒内,且在高压下显存占用始终被稳稳锁死在 92% 安全水位,没有发生过一次崩溃。

总结与避坑心得

很多极客在本地折腾大模型时,容易把过多精力放在模型量化参数的细枝末节上,而忽略了 吞吐架构 的重要性。对于个人开发者或团队内私有部署而言:

  1. 单机推理别死磕原生推断库:并发场景直接上 vLLM,显存管理完全是两个维度的体验。
  2. 用网关解耦业务和后端:引入 LiteLLM 这样的网关层,后续如果要将部分复杂 Query fallback 到公有云 Claude 3.5 或 GPT-4o,在配置文件里加几行权重策略就能无缝切换,业务代码完全零感知。
  3. 注意整个链路的流式穿透:从推理引擎、网关反代到前端 CDN/Nginx,全链路都必须禁用缓冲,否则流式体验会荡然无存。

用成熟的工程架构压榨硬件极限,这才是属于极客的极致性价比玩法。

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