共计 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 就上线了,结果并发稍高就会遭遇两个致命瓶颈:
- KV Cache 静态显存浪费与显存碎片:传统实现必须预先为
max_model_len静态分配大块连续显存,哪怕用户只发了两个字,几百兆显存就占死了。并发一上来,显存没耗尽就被内存碎片拖垮导致 OOM。 - 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% 安全水位,没有发生过一次崩溃。
总结与避坑心得
很多极客在本地折腾大模型时,容易把过多精力放在模型量化参数的细枝末节上,而忽略了 吞吐架构 的重要性。对于个人开发者或团队内私有部署而言:
- 单机推理别死磕原生推断库:并发场景直接上 vLLM,显存管理完全是两个维度的体验。
- 用网关解耦业务和后端:引入 LiteLLM 这样的网关层,后续如果要将部分复杂 Query fallback 到公有云 Claude 3.5 或 GPT-4o,在配置文件里加几行权重策略就能无缝切换,业务代码完全零感知。
- 注意整个链路的流式穿透:从推理引擎、网关反代到前端 CDN/Nginx,全链路都必须禁用缓冲,否则流式体验会荡然无存。
用成熟的工程架构压榨硬件极限,这才是属于极客的极致性价比玩法。