共计 4469 个字符,预计需要花费 12 分钟才能阅读完成。
很多极客在本地折腾开源大模型时,第一选择往往是 Ollama。不可否认 Ollama 开箱即用确实舒服,但当你的业务场景从“单人终端对话”转向“多并发 Agent 调用”或者需要挂载多个复杂工具时,Ollama 的局限性就暴露无遗:单卡并发吞吐直接崩盘、缺少细粒度的 PagedAttention 显存池管理、多卡并行调度极其黑盒。上周我把工作室两张闲置的 RTX 4090 塞进一台 4U 工控机,决定换掉 Ollama,用生产级的高性能推理框架 vLLM + Ray 重构底座,把 Qwen2.5-32B-Instruct 压榨到极致。
一、为什么放弃 Ollama 转投 vLLM?
简单粗暴看核心指标:吞吐量与显存利用率。Ollama 底层基于 llama.cpp,虽然对 CPU+GPU 混合推理兼容极佳,但在纯 NVIDIA 算力环境下,它的 KV Cache 显存分配策略相对原始,容易出现显存碎片。当并发请求超过 4 个,首字延迟(TTFT)和每秒输出 Token 数(TPS)会出现断崖式下跌。
vLLM 的杀手锏是 PagedAttention,它借鉴了操作系统的虚拟内存分页机制,将 KV Cache 离散存储在非连续的显存块中,显存浪费率几乎从传统方案的 60%~80% 压到了 4% 以下。配合 Ray 驱动的 Tensor Parallelism(张量并行),可以把原本塞不进单卡 24G 显存的 32B FP16 模型,近乎无损地切片拆分到两张卡上运行。
二、硬件拓扑与底层环境准备
在开始敲命令前,务必检查你的多卡拓扑。消费级主板插双卡最容易踩的坑是 PCIe 通道降速。两张卡如果一张跑在 PCIe 4.0 x16,另一张缩水到 PCIe 3.0 x4(走 PCH 南桥),张量并行时卡间 All-Reduce 通信延迟会直接拖垮推理速度。
# 检查 PCIe 链路速度与带宽
sudo lspci -vvv -d 10de: | grep -E "LnkCap|LnkSta"
# 查看多卡拓扑(确认无 NVLink 时的 NUMA 节点分布)nvidia-smi topo -m
实测在没有物理 NVLink 桥接器的双 RTX 4090 环境下,走 PCIe 4.0 x8 + x8 互联,通信开销依然完全可控。接下来准备纯净的 Docker 运行环境,避免本地宿主机 CUDA 版本污染:
# 安装 NVIDIA Container Toolkit(若未安装)curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker
三、生产级 Docker Compose 编排与参数调优
为了让部署环境工程化、可复现,推荐使用 Docker Compose 固化启动参数。这里我们部署 Qwen/Qwen2.5-32B-Instruct-AWQ(4-bit 量化版显存占用极小且速度爆表)或者未量化的 Qwen2.5-14B-Instruct(双卡跑原汁原味的 FP16/BF16)。
创建 docker-compose.yml:
version: '3.8'
services:
vllm-ray-cluster:
image: vllm/vllm-openai:v0.6.3.post1
container_name: vllm-cluster
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- HUGGING_FACE_HUB_TOKEN=hf_your_token_here
- NCCL_DEBUG=WARN
# 避免多卡在消费级主板上 P2P 通信死锁
- NCCL_P2P_DISABLE=0
volumes:
- /data/huggingface/hub:/root/.cache/huggingface
- /dev/shm:/dev/shm # 共享内存必须映射,防止 Ray 进程间通信崩溃
ports:
- "8000:8000"
ipc: host
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
command: >
--model Qwen/Qwen2.5-32B-Instruct-AWQ
--tensor-parallel-size 2
--pipeline-parallel-size 1
--gpu-memory-utilization 0.90
--max-model-len 8192
--max-num-seqs 64
--enforce-eager
--trust-remote-code
--port 8000
restart: unless-stopped
核心参数排坑指南:
--tensor-parallel-size 2:将模型切分到两张显卡,Ray 会在后台自动管理卡间通信与权重分配。--gpu-memory-utilization 0.90:显存预分配比例。不要给 1.0,留出 10% 给 CUDA 运行时上下文和 Ray 的内部通信缓冲区,否则跑高上下文时必报 CUDA OOM。ipc: host与/dev/shm:极其关键。Ray 底层依赖 Plasma 共享内存存储张量,Docker 默认 64MB 的/dev/shm瞬间就会爆掉导致容器直接退出。--enforce-eager:对于消费级 Ada Lovelace 架构(RTX 40 系),开启 Eager 模式可跳过冗长且偶尔报错的 CUDA Graph 捕获阶段,虽然牺牲 2%~5% 的极限性能,但启动速度提升 10 倍且大幅降低显存抖动。
四、编写 Python 流式网关与鉴权层
vLLM 原生暴露了 OpenAI 兼容的 API 接口,但在实际对外提供服务(例如对接沉浸式翻译、NextChat 或内部知识库)时,我们需要一个轻量级的代理层来实现 API Key 轮询鉴权、流式 SSE 监控与请求重试。
下面是一个经过压力验证的 FastAPI 网关中间件示例:
import httpx
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
app = FastAPI()
VLLM_BACKEND_URL = "http://127.0.0.1:8000/v1/chat/completions"
VALID_API_KEYS = {"nassky-sk-prod-9988", "nassky-sk-dev-1122"}
@app.post("/v1/chat/completions")
async def chat_proxy(request: Request):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer"):
raise HTTPException(status_code=401, detail="Missing or invalid token")
token = auth_header.split(" ")[1]
if token not in VALID_API_KEYS:
raise HTTPException(status_code=403, detail="Forbidden: Access denied")
payload = await request.json()
# 强制注入针对长文本生成的保护配置
payload["stream"] = payload.get("stream", True)
client = httpx.AsyncClient(timeout=120.0)
async def stream_generator():
try:
req = client.build_request("POST", VLLM_BACKEND_URL, json=payload)
response = await client.send(req, stream=True)
async for chunk in response.aiter_raw():
yield chunk
finally:
await client.aclose()
return StreamingResponse(stream_generator(), media_type="text/event-stream")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080, workers=4)
五、实测压测数据与 Ollama 真实对比
在双 RTX 4090(24G x 2)主机上,针对 Qwen2.5-32B-Instruct-AWQ 进行基准压测。输入 Prompt 长度为 1024 Token,输出生成 512 Token:
| 并发请求数 | 方案架构 | 平均首字时间 (TTFT) | 系统吞吐量 (Tokens/s) | 显存稳定性 |
|---|---|---|---|---|
| 1(单并发) | Ollama (双卡拆分) | 210 ms | 68.2 | 平稳 |
| 1(单并发) | vLLM + Ray (TP=2) | 142 ms | 84.5 | 平稳 |
| 16(高并发) | Ollama (双卡拆分) | 2150 ms | 112.4 | 偶尔显存交换降速 |
| 16(高并发) | vLLM + Ray (TP=2) | 580 ms | 428.6 | 极度平稳 (PagedAttention) |
实测数据显示,在单并发场景下,两者差距并不显著;但在 16 并发下,vLLM 的吞吐直接冲上了 Ollama 的近 4 倍,且首字延迟控制在 600ms 以内,完全感受不到“排队打字”的卡顿。这就是 PagedAttention 和原生并行推理的威力。
总结与避坑心得
折腾多卡本地大模型,最忌讳“拿着跑单卡的思路硬套集群”。在实际部署中,牢记三点:第一,显存切勿吃满 ,留下 10% 是系统稳定的底线;第二, 永远盯紧共享内存 /dev/shm,Ray 的很多崩溃根本不是模型问题,纯粹是 IPC 管道被写爆了;第三,消费级显卡虽然没有 NVLink,但只要 PCIe 带宽给足,TP=2 带来的吞吐收益完全能够支撑起一个几十人的小团队并发使用。
===END===