共计 3698 个字符,预计需要花费 10 分钟才能阅读完成。
很多极客在个人工作站或自建 NAS 上折腾大模型,起手式往往是 ollama run qwen2.5。如果只是终端里自己单线程聊天,Ollama 确实开箱即用;但只要你尝试把本地模型挂载到真实的生产环境——比如接入公司的沉浸式翻译、Cursor、几个高频并发的自动化 Agent 脚本,或者丢给小团队十几个人共享,Ollama 的并发吞吐就会迅速雪崩,显存利用率低下、排队队列打满、首字延迟(TTFT)飙到几秒开外。
上周我彻底掀翻了工作站(Ubuntu 22.04 LTS + 单卡 RTX 4090 24G)原有的部署方案,切换到了 vLLM(推理引擎)+ LiteLLM Proxy(API 网关) 的企业级架构。实测下来,在 20+ 并发流式调用下,整体吞吐提升了将近 3 倍,而且把鉴权、限流、日志监控全部统一了。本文把这套架构的完整落地流程、核心参数取舍以及我踩过的三个深坑毫无保留地分享出来。
一、架构设计:为什么这套组合拳是目前的解法最优?
我们先梳理清楚数据流向。纯裸跑 vLLM 虽然吞吐炸裂,但它本质只是个单机推理引擎,缺乏完善的访问控制(API Key 分配)、速率限制(Rate Limit)以及消费额度统计。而 LiteLLM Proxy 是一个极其轻量的 API 网关,不仅完美兼容 OpenAI 接口规范,还能做健康检查、负载均衡与熔断降级。
[客户端 /Agent/Cursor/Open WebUI]
│ (HTTP / SSE 流式请求)
▼
┌─────────────────────┐
│ LiteLLM Proxy (8000)│ ── 负责:API Key 鉴权、限流、统一日志
└─────────────────────┘
│ (内网反代转发)
▼
┌─────────────────────┐
│ vLLM Engine (8001) │ ── 负责:PagedAttention、连续批处理、GPU 显存调度
└─────────────────────┘
│
▼
[NVIDIA RTX 4090 24GB] ── 运行:Qwen2.5-14B-Instruct-AWQ
二、Docker Compose 一键编排落地方案
直接通过纯容器化部署,不污染宿主机系统环境。创建目录 /opt/llm-gateway,并在其中创建 docker-compose.yml 和 litellm_config.yaml。
1. docker-compose.yml 配置
这里选用 vllm/vllm-openai:v0.6.3.post1 镜像,模型挂载本地预先下载好的权重目录,避免每次容器重启重复拉取模型。
version: '3.8'
services:
vllm-engine:
image: vllm/vllm-openai:v0.6.3.post1
container_name: vllm-engine
runtime: nvidia
restart: always
environment:
- CUDA_VISIBLE_DEVICES=0
- HF_HUB_ENABLE_HF_TRANSFER=0
volumes:
- /data/models/Qwen2.5-14B-Instruct-AWQ:/models/Qwen2.5-14B-Instruct-AWQ
ports:
- "8001:8000"
ipc: host
command: >
--model /models/Qwen2.5-14B-Instruct-AWQ
--served-model-name qwen-14b
--quantization awq
--dtype half
--gpu-memory-utilization 0.90
--max-model-len 8192
--max-num-seqs 64
--block-size 16
--enforce-eager
litellm-proxy:
image: ghcr.io/berriai/litellm:main-v1.52.0
container_name: litellm-proxy
restart: always
ports:
- "8000:4000"
volumes:
- ./litellm_config.yaml:/app/config.yaml
environment:
- LITELLM_MASTER_KEY=sk-MasterKeyForAdmin9988
command: ["--config", "/app/config.yaml", "--port", "4000"]
depends_on:
- vllm-engine
2. LiteLLM Proxy 规则配置 (litellm_config.yaml)
配置内网路由规则,将上游模型映射为标准接口:
model_list:
- model_name: gpt-4o-mini # 伪装成公有云常用名字,方便客户端无缝切换
litellm_params:
model: openai/qwen-14b
api_base: http://vllm-engine:8000/v1
api_key: "EMPTY"
rpm: 120 # 每分钟请求限制
tpm: 100000 # 每分钟 Token 限制
litellm_settings:
drop_params: true # 丢弃下游传递的不可识别非标准参数,防止 vLLM 报 400
telemetry: false
general_settings:
master_key: sk-MasterKeyForAdmin9988
store_model_in_db: false
三、核心显存参数调优:如何避免 OOM 并跑满带宽?
上面 vLLM 的启动命令看似平平无奇,但每一个参数都是用显存 OOM 炸出来的教训:
--gpu-memory-utilization 0.90:给 vLLM 预留 90% 的物理显存(24GB 的 90% 约为 21.6GB)。千万不要直接拉到 0.98,因为 Linux 内核的 CUDA Context 自身需要占用几百 MB,加上模型在动态分配 PyTorch Cache 时留一点 Buffer,否则高并发突发时必定硬件级崩溃。--max-model-len 8192:默认模型如果是 32K 或更长,vLLM 会提前按照最大长度预分配 KV Cache。在 24G 显存限制下,如果盲目开 32K,你的并发槽位(KV Cache 剩余块)会所剩无几。锁死在 8K 上下文,是平衡代码阅读、长文摘要与并发吞吐的甜点位。--enforce-eager: 关键避坑点!vLLM 默认会尝试使用 CUDA Graph 优化推理,这在小 Batch 下能提速 5%~10%。但是在单卡装填 14B 模型且并发度较高时,CUDA Graph 在预热捕获阶段会瞬间吃掉接近 2GB 显存,直接抛出CUDA out of memory。启用 eager 模式虽然损失极微小的极端加速,但极度稳定,显存开销非常线性。
四、实战踩坑与高并发排障实录
1. 踩坑一:Nginx 反代截断 SSE 流式传输,打字机变成“憋大招”
部署完成后,我将网关通过主机的 Nginx 转发给外部访问。测试终端 API 正常,但一旦网页端使用流式输出(Streaming),前端界面就完全没有打字机效果,而是卡住几秒,然后“啪”的一下把几百字全部吐出来。
原因排查: Nginx 默认启用了响应缓冲区(Proxy Buffering)。vLLM 吐出来的 data: {...}\n\n 块被 Nginx 截留在缓冲区中,直到写满 4KB 或响应结束才一次性发给客户端。
解决办法: 在对应的 Nginx 站点配置中彻底关闭代理缓存并关掉 Chunked 限制:
location /v1/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 核心:必须禁用代理缓冲以支持 SSE
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
chunked_transfer_encoding on;
}
2. 踩坑二:IPC 命名空间共享导致共享内存不足
如果后续尝试做多卡推理(--tensor-parallel-size 2),Docker 默认分配的 /dev/shm(共享内存)只有 64MB,vLLM 内部依赖 PyTorch Distributed 和 NCCL 通信,在多进程拉起通信的一瞬间容器会直接静默退出(Exit 137)。在 compose 里面必须显式加上 ipc: host,让容器直接使用宿主机的共享内存。
五、实测性能压测数据:vLLM vs Ollama
模型统一选用 Qwen2.5-14B-Instruct-AWQ,Prompt 长度统一设为 512 tokens,生成长度 256 tokens,并发逐步提升,使用 locust 模拟流式请求,实测核心指标对比:
| 并发请求数 | Ollama (首字延迟 TTFT) | vLLM (首字延迟 TTFT) | Ollama 整体吞吐 | vLLM 整体吞吐 |
|---|---|---|---|---|
| 1 并发 (单请求) | 180 ms | 165 ms | 58 tokens/s | 62 tokens/s |
| 8 并发 | 1420 ms | 310 ms | 115 tokens/s | 248 tokens/s |
| 正文完
评论(没有评论)
|