共计 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
踩坑与调优关键参数解析:
--quantization=awq:强烈建议跑 4-bit AWQ 权重。对于 Qwen2.5-14B,FP16 纯模型静态显存就要占 28G(单卡直接死掉);采用 AWQ 量化后模型仅占约 9.5G 显存,剩下的 12G+ 显存可以完全划给 KV Cache。实测 AWQ 的困惑度(PPL)损耗基本可以忽略不计。--gpu-memory-utilization=0.92:默认是 0.9。设置成 0.92 是为了榨干最后 1~2G 显存放 KV Cache,但 绝对不要拉到 0.98 以上,因为 PyTorch 在运行时申请临时动态张量若撞墙,会直接引发 CUDA OOM 崩溃。--max-num-seqs=64:允许引擎内最大连续批处理的并发数。如果不加限制,一旦瞬间涌入 100 个请求,每批请求分到的 KV Cache 被打碎,会导致首字响应时间(TTFT)呈指数级飙升。限制为 64,超出的请求会在队列中优雅等待,保证在跑请求的高吐字速度。--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 弹性回收 | 极其稳定 |
总结与避坑心得
跑本地大模型,工具链选型直接决定了是做“玩具”还是做“生产力工具”:
- 模型量化别盲目追 GGUF:如果在服务端纯 Linux + NVIDIA 环境下跑高并发,优先考虑 AWQ 或 GPTQ 格式,它们在 CUDA 核心上的计算密度和 vLLM 兼容性远优于 llama.cpp 生态的 GGUF。
- 一定要加网关熔断:本地 GPU 算力永远是有物理上限的。LiteLLM 可以在最前端帮我们挡住爆量流量,即使内部某天显卡驱动崩了,外部业务系统也不会收到一堆 502 报错,运维有充足的时间