告别Ollama高并发崩溃:vLLM + Ray自建流式LLM代理服务实战(含压测与防爆显存配置)

3次阅读
没有评论

共计 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 大量时间都耗费在了请求排队和粗暴的内存复制转移上。

写在最后与避坑心得

将大模型服务从“能跑”推向“稳定可用”,需要极客思维下的工程打磨。几个关键实操经验建议大家记在小本子上:

  1. 别迷信 FP16 原生精度 :在生产推理场景中,AWQ 或 GPTQ 量化版模型对显存带宽要求的降低,能够带来远超精度微小损耗的吞吐红利。
  2. 警惕 Ray 的 IPC 共享内存 :使用多卡张量并行时,Linux 系统的 /dev/shm 必须调大(如 Docker 部署时务必设置 --shm-size="16gb"),否则 Ray 进程间通信会直接 SIGBUS 崩溃。
  3. 网络超时的合理设置 :流式请求中,如果用户客户端中途掐断连接,代理层必须主动感知并中断底层推理(通过 Cancel Token),不要让被抛弃的请求继续浪费宝贵的 GPU 算力。

希望这套经过内网高并发检验的方案,能为你后续部署 Agent 基础设施扫平道路。

正文完
 0
评论(没有评论)