共计 3504 个字符,预计需要花费 9 分钟才能阅读完成。
上个月给团队搭建内部代码补全和多智能体协同后台,硬件给到了一台双路 RTX 4090(24GB x 2)的 Ubuntu 工作站。刚开始为了图省事,直接跑了 Ollama 挂载 Qwen2.5-32B-Instruct。原本以为 8 并发以内足够应付,结果并发一拉到 12,首 Token 延迟(TTFT)直接从 300ms 飙到 5 秒开外,甚至数次触发 CUDA OOM 导致进程被 systemd 无情 SIGKILL。
对于个人极客或微型团队来说,消费级显卡的显存带宽和容量极度宝贵。要想在有限显卡资源下吃满并发,必须抛弃黑盒封装过度的方案。本文记录我将本地推理栈全面重构成 vLLM + LiteLLM 统一网关 的实战调优过程,包含真实的踩坑参数、Nginx 流式代理缓冲陷阱以及压测对比数据。
一、架构选型痛点:Ollama 为什么扛不住高并发?
Ollama 底层基于 llama.cpp,在 Mac 本地单人单任务推理时体验极佳,但在多用户并发请求、长上下文(Context Length > 8k)场景下有几个难以规避的短板:
- 内存碎片与固定 KV 预分配: 并发多任务下,静态分配显存会导致严重的内部与外部碎片,导致显存利用率看起来很高,实际计算单元却在空转等待。
- 批处理(Continuous Batching)调度能力有限: 当不同请求的 Prompt 长度和生成长度差异巨大时,调度器无法高效动态交错生成,短请求会被长请求卡死。
- 缺乏工业级网关治理: 没有原生提供完善的多模型负载均衡、虚拟 API Key 配额管控以及细粒度的降级策略。
因此,工业级部署的标配是:底座采用基于 PagedAttention 的 vLLM 负责模型并发推理与显存精细化管理,上层接入轻量级的 LiteLLM Proxy 充当 API 网关,提供鉴权、负载均衡、重试与 OpenAI 标准协议兼容。
二、vLLM 底座部署与核心参数“黄金配比”
在双卡 4090 上跑 Qwen2.5-32B(推荐 AWQ 4-bit 量化版),显存总量为 48GB,模型权重占用约 19.5GB,剩余显存几乎全部需要划拨给 KV Cache。启动脚本绝不能用默认参数裸跑,必须做精细限制。
1. 核心启动脚本(systemd 守护进程或 Docker)
以下是我线上跑通的生产级启动参数组合(模型路径请替换为你本地存储路径):
python3 -m vllm.entrypoints.openai.api_server \
--model /data/models/Qwen2.5-32B-Instruct-AWQ \
--tensor-parallel-size 2 \
--dtype float16 \
--gpu-memory-utilization 0.92 \
--max-model-len 16384 \
--max-num-seqs 64 \
--kv-cache-dtype auto \
--swap-space 16 \
--trust-remote-code \
--port 8000 \
--host 0.0.0.0 \
--disable-log-requests
2. 关键参数排障与避坑解析
--tensor-parallel-size 2:双卡 4090 走张量并行。 踩坑提示: 消费级显卡没有 NVLink,走 PCIe 4.0 4x/8x 互联时 NCCL 通信可能成为瓶颈。如果运行报 P2P 错误,必须在环境变量中补上一句export NCCL_P2P_DISABLE=1,强制走共享内存或系统内存通信。--gpu-memory-utilization 0.92:默认 0.90。实测设置到 0.95 时,如果在高并发瞬间遭遇长文本突发,极易因 CUDA 内部工作缓存分配失败而 Crash;保留 8%(约 3.8GB)余量最为稳健。--max-model-len 16384:务必按实际业务截断。Qwen2.5 官方支持 32k/128k,但 KV Cache 显存消耗与上下文长度呈线性正比。如果开到 32k,留给并发槽位的数量会被大幅压缩,导致稍有并发就触发排队甚至请求降级。--max-num-seqs 64:限制调度器每批处理的最大并发请求数,避免单次迭代中调度过多序列造成 Attention 计算延迟剧增。--disable-log-requests:高并发时实时输出每个 Request 的文本日志会带来不可忽视的 I/O 阻塞,生产环境务必关掉。
三、网关层接入:LiteLLM 动态路由与反代缓冲陷阱
直接把 8000 端口暴露给客户端极不安全,且缺乏并发队列缓冲和请求限流。架构第二层是在前置部署 LiteLLM,并配合 Nginx 进行统一代理。
1. LiteLLM 生产配置 (config.yaml)
model_list:
- model_name: qwen-32b
litellm_params:
model: openai/Qwen2.5-32B-Instruct-AWQ
api_base: http://127.0.0.1:8000/v1
api_key: "none"
tpm: 120000
rpm: 120
general_settings:
master_key: "sk-nassky-prod-master-key-xyz"
store_model_in_db: false
litellm_settings:
num_retries: 3
request_timeout: 60
stream_chunk_timeout: 5.0
2. 踩坑最深处:Nginx 反向代理流式传输“卡顿吐字”
在调试 SSE(Server-Sent Events)流式输出时,开发人员反馈光标总是不动,然后隔几秒“哗啦”一下子蹦出一整段字,毫无流式体验。排查发现是 Nginx 默认启用了 Proxy Buffering。
在 Nginx 的 location 块中必须强制禁用缓冲并调整 Keep-Alive 超时:
location /v1/ {
proxy_pass http://127.0.0.1:4000; # 转发到 LiteLLM
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 核心配置:彻底关闭反向代理缓冲,保持 SSE 管道畅通
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
# 防止长提示词推理初始等待过长触发 504 网关超时
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
四、实测压测对比:Ollama vs vLLM 吞吐大比拼
使用 vllm/benchmarks/benchmark_serving.py 在同一台机器上对两套方案进行压力测试。模型均为 Qwen2.5-32B 4-bit 量化,输入 Prompt 平均长度约 1800 Token,输出限制 512 Token。
| 指标项 | Ollama 方案 | vLLM + LiteLLM 方案 | 改善幅度 |
|---|---|---|---|
| 并发请求数 (Concurrency) | 16 | 16 | – |
| 整体吞吐 (Tokens/s) | 112.4 | 438.7 | +290% |
| 首字延迟 P90 (TTFT) | 4,820 ms | 680 ms | -85.8% |
| 单 Token 生成延迟均值 (ITL) | 42.5 ms | 18.2 ms | -57.1% |
| 显存溢出发生率 (OOM) | 偶发 (约 8.3%) | 0% (无单次中断) | 稳定性彻底解决 |
实测数据差异极其明显。PagedAttention 对显存的精细化物理分页(Block-level 分配)让显存几乎没有碎片,Continuous Batching 则保证了新请求到达时不需要等整个 Batch 的请求全部生成完毕,而是随时插入 Attention 算子运算,吞吐直接拉满翻倍。
总结与避坑心得
折腾这一套推理集群,最大的收获就是: 不要迷信默认开箱即用的封装工具 。对于单人学习体验,Ollama 依然是开箱即用的首选;但只要涉及到局域网服务、团队共享、Agent 链式调用(大量并发短时任务),底层换上 vLLM 是唯一能够最大化榨干显卡计算潜力的路径。
回顾部署要点,建议按如下 checklist 进行自检:
- 双卡消费级显卡(无 NVLink)务必注意 P2P 通信配置,必要时配置环境变量切断 NCCL P2P 规避内存死锁。
- 严格根据场景控制
--max-model-len,把宝贵的显存让位给并发 KV Cache 块,而不是保留用不到的超长无用空间。 - 上游网关和 Nginx 代理必须关闭
proxy_buffering,否则你的流式传输永远处于“便秘”状态。 - 前置 LiteLLM 做 API Token 隔离与 Rate Limit,防止个别客户端脚本写死无限循环直接把单机后端压穿。