共计 3616 个字符,预计需要花费 10 分钟才能阅读完成。
很多极客在刚接触本地大模型时,都习惯用 Ollama 或者 llama.cpp 快速跑个 Demo。单个用户在本地聊天确实丝滑,但只要你把接口暴露给内网团队做 Agent 调用、RAG 向量检索或是接入自动化工作流,并发一上来(哪怕只有三五个长上下文请求),Ollama 就会开始漫长的排队,甚至直接把显卡显存挤爆崩溃(CUDA Out of Memory)。
上个月我帮内网构建内部代码助理后端时,就彻底踩平了这个坑。玩具和生产环境的鸿沟,核心在于 KV Cache(键值缓存)的动态分配机制与连续批处理(Continuous Batching)能力 。这篇文章带你抛开笨重的全家桶,手把手用 vLLM + Ray 搭建一套支撑高并发、低延迟流式响应的大模型推理服务,并附带避坑指南与真实的压测对比数据。
一、为什么不用 Ollama?底层的技术硬伤
Ollama 的底层主要依赖 llama.cpp,虽然它通过 GGUF 量化让模型能在 CPU/ 消费级显卡上跑得飞快,但其并发机制相对原生:
- 内存碎片化严重 :静态分配上下文显存,遇到长短不一的并发请求时,大量的显存闲置却无法被其他请求利用。
- 排队阻塞机制 :缺乏细粒度的 Token 级 Continuous Batching,一个长 Prompt 会直接卡死后续所有轻量级请求的响应。
相比之下,加州大学伯克利分校开源的 vLLM 引入了操作系统虚拟内存思想的 PagedAttention 算法。它将 KV Cache 切分成固定大小的 Block 存储在不连续的显存空间中,几乎彻底消除了显存碎片,让同一个显卡上的并发吞吐量(Throughput)直接提升 2 到 4 倍。
二、硬件拓扑与基础环境编排
本次实战环境基于一块配备双卡 RTX 4090 (24GB * 2) 的工作站节点,操作系统采用 Ubuntu 22.04 LTS。目标模型为 Qwen/Qwen2.5-14B-Instruct-AWQ 或 Qwen/Qwen2.5-32B-Instruct-GPTQ-Int4。
首先准备好干净的 Python 虚拟环境与 CUDA 12.1+:
# 创建独立虚拟环境
conda create -n vllm-prod python=3.10 -y
conda activate vllm-prod
# 安装最新版 vLLM (支持 AWQ/GPTQ 硬件加速内核)
pip install --upgrade pip
pip install vllm ray
pip install fastapi uvicorn httpx locust
三、生产级部署:防 OOM 核心启动参数调优
vLLM 官方的一键启动命令看似简单,但生产部署如果不对显存配比做精确切分,很容易在启动初期就被预分配显存撑爆。下面是我在多轮压测后稳定使用的启动脚本:
#!/usr/bin/env bash
# start_vllm.sh
MODEL_PATH="/data/models/Qwen2.5-14B-Instruct-AWQ"
HOST="0.0.0.0"
PORT=8000
python -m vllm.entrypoints.openai.api_server \
--model ${MODEL_PATH} \
--tensor-parallel-size 2 \
--dtype auto \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--max-num-seqs 64 \
--swap-space 8 \
--disable-log-requests \
--trust-remote-code \
--host ${HOST} \
--port ${PORT}
核心参数避坑剖析:
--tensor-parallel-size 2:启用两张卡做张量并行(Tensor Parallelism),Ray 会在后台自动切分模型权重。注意:4090 没有 NVLink,走 PCIe 互联时,模型层切分会稍有通信开销,但显存合并后能轻松承载大批量请求。--gpu-memory-utilization 0.90:显存利用率上限阈值。留出 10% 的显存缓冲极其关键!如果设置成 0.98,一旦有异常庞大的前向计算中间变量,显卡直接报 CUDA OOM 退出。--max-num-seqs 64:单卡最大并发序列数。限制并发队列峰值,防止在极端流量倾泻时引发连锁雪崩。--swap-space 8:分配 8GB 主机内存作为二级缓存。当显存耗尽时,超出的 KV Cache 会被临时移交至 CPU 内存,而非粗暴抛出 500 异常。
四、高可用流式代理封装(FastAPI 异步非阻塞)
虽然 vLLM 自身提供了兼容 OpenAI 标准的接口,但在生产网关中,我们通常需要在前置节点做 API Key 认证、请求速率限制(Token Bucket)以及审计日志记录。切记不能在转发流式响应(Server-Sent Events)时使用同步循环,否则会彻底阻塞事件循环。
这里给出一个经过生产验证的极简高性能代理层代码:
# proxy_server.py
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
app = FastAPI()
VLLM_BACKEND_URL = "http://127.0.0.1:8000/v1/chat/completions"
# 维护长连接池,避免频繁建立 TCP/TLS 握手
client = httpx.AsyncClient(timeout=httpx.Timeout(connect=5.0, read=60.0, write=5.0, pool=10.0),
limits=httpx.Limits(max_keepalive_connections=100, max_connections=200)
)
@app.post("/v1/chat/completions")
async def chat_proxy(request: Request):
payload = await request.json()
is_stream = payload.get("stream", False)
req = client.build_request("POST", VLLM_BACKEND_URL, json=payload)
response = await client.send(req, stream=is_stream)
if is_stream:
async def event_generator():
try:
async for chunk in response.aiter_raw():
yield chunk
finally:
await response.aclose()
return StreamingResponse(event_generator(), media_type="text/event-stream")
else:
content = await response.aread()
return StreamingResponse(iter([content]), status_code=response.status_code, media_type=response.headers.get("content-type"))
五、性能实测数据:Ollama vs vLLM
我们采用 Locust 模拟 50 个并发客户端,每个客户端不断发起包含 2000 Tokens 上下文并在限定输出 300 Tokens 的问答场景。实测指标如下:
| 指标 / 引擎 | Ollama (llama.cpp) | vLLM (PagedAttention) |
|---|---|---|
| 首字延迟 (TTFT, P95) | 8.42 秒 (严重排队) | 0.83 秒 |
| 系统总吞吐量 | 42 tokens/s | 285 tokens/s |
| 请求失败率 (OOM/ 超时) | 18.5% | 0.0% |
数据结果一目了然:在单一请求时,两者生成速度差异并不大;但在多用户并发压入长上下文时,vLLM 的分块批处理让 GPU 的计算单元(Tensor Cores)几乎处于饱和计算状态,而 Ollama 大量时间都耗费在了请求排队和粗暴的内存复制转移上。
写在最后与避坑心得
将大模型服务从“能跑”推向“稳定可用”,需要极客思维下的工程打磨。几个关键实操经验建议大家记在小本子上:
- 别迷信 FP16 原生精度 :在生产推理场景中,AWQ 或 GPTQ 量化版模型对显存带宽要求的降低,能够带来远超精度微小损耗的吞吐红利。
- 警惕 Ray 的 IPC 共享内存 :使用多卡张量并行时,Linux 系统的
/dev/shm必须调大(如 Docker 部署时务必设置--shm-size="16gb"),否则 Ray 进程间通信会直接 SIGBUS 崩溃。 - 网络超时的合理设置 :流式请求中,如果用户客户端中途掐断连接,代理层必须主动感知并中断底层推理(通过 Cancel Token),不要让被抛弃的请求继续浪费宝贵的 GPU 算力。
希望这套经过内网高并发检验的方案,能为你后续部署 Agent 基础设施扫平道路。