单卡RTX 4090跑满300并发:vLLM生产级流式网关与显存优化实录

1次阅读
没有评论

共计 4486 个字符,预计需要花费 12 分钟才能阅读完成。

上个月帮朋友的小团队改造一个内部知识库与智能客服后台,原本他们的架构是直接用 Ollama 挂载 14B 模型对外提供接口。日常一两个人提问很流畅,但只要业务端跑批量摘要或者并发上到 15 左右,显存直接被打满,TTFT(首字延迟)直接飙升到 6 秒以上,SSE(Server-Sent Events)流式连接成片断开。换上单张 24G 显存的 RTX 4090 后,我们通过重构底层推理引擎为 vLLM 并手写高并发流式代理层,最终在保持 P95 首字延迟低于 450ms 的前提下,单卡扛住了 300+ 的流式并发压测。今天把这套已经稳定运行一个月的实战架构、配置参数和踩坑细节完整复盘出来。

一、为什么 Ollama 不适合生产级高并发?

很多开发者在本地开发时习惯用 Ollama,因为它开箱即用,通过简单的 REST API 就能调用。但在并发推理场景下,Ollama 存在几个先天致命伤:

  • KV Cache 显存碎片化严重: 连续请求进来时,固定长度的显存预分配机制会导致大量未利用的显存空间被锁死,无法动态分页共享。
  • 缺少成熟的连续批处理(Continuous Batching): 当一个请求已经生成完毕时,系统无法以 Token 为粒度立即插播新的请求进入计算批次,算力空转严重。
  • 缺乏细粒度流式背压(Backpressure): 一旦下游客户端网络慢,长连接积压会拖死整个模型实例的输出队列。

而 vLLM 核心的 PagedAttention 机制类似虚拟内存的分页管理,几乎将 KV Cache 的显存浪费降低到了 4% 以下。配合 Continuous Batching,单卡吞吐量(Throughput)能比传统方案提升 5 到 10 倍。

二、显卡极限压榨:vLLM 核心参数精准调优

部署模型选用当前在中文和代码综合能力最均衡的 Qwen/Qwen2.5-7B-Instruct(兼顾延迟与并发)以及 Qwen2.5-14B-Instruct-GPTQ-Int4。以下是我们在生产服务器上跑了多轮基准测试后锁定的启动配置:

# 生产启动命令:针对 24GB 显存显卡的极致参数调配
python3 -m vllm.entrypoints.openai.api_server \
    --model /data/models/Qwen2.5-7B-Instruct \
    --served-model-name qwen2.5-7b \
    --trust-remote-code \
    --host 0.0.0.0 \
    --port 8000 \
    --tensor-parallel-size 1 \
    --gpu-memory-utilization 0.92 \
    --max-model-len 4096 \
    --max-num-seqs 256 \
    --max-num-batched-tokens 8192 \
    --kv-cache-dtype auto \
    --disable-log-requests

踩坑点与参数拆解解析:

  • --gpu-memory-utilization 0.92: 千万不要头铁设成 0.98 或 0.99!vLLM 会在启动时把剩余的显存全部分配给 KV Cache 显存池。一旦设过高,当出现大 batch 且包含较长 System Prompt 时,CUDA context 会因瞬时碎片分配失败直接抛出 OOM(Out Of Memory)错误并导致整个守护进程崩溃。0.90 ~ 0.92 是 RTX 3090/4090 的安全甜点位。
  • --max-num-seqs 256: 这个参数控制当前正在执行的最大序列数。对于 7B 浮点模型,设置在 200~256 能充分打满 4090 的 Tensor Core,再往上加虽然并发能抗住,但每个请求的解码速度(Decoding Speed)会出现体感断崖式下跌。
  • --max-num-batched-tokens 8192: 单个批次能处理的最大 Token 数。如果调得太小,Prefill(首字生成阶段)的并行优势就废了;太大会瞬时占用极高显存。8192 是 24G 显存下的黄金平衡点。
  • --disable-log-requests: 压测时一定要加。高并发下 Python 自带的终端 I/O 打印会严重阻塞事件循环,实测不关日志每秒处理能力直接腰斩。

三、生产级高并发流式代理网关开发

绝不能把 vLLM 的 8000 端口直接暴露给公网。生产环境需要一个前置反向代理层,用来处理 API Key 鉴权、速率限制、异常熔断以及最核心的 异步流式转发防阻塞。下面是我用 FastAPI + httpx 构建的高性能流式网关核心实现:

# gateway.py: 高并发异步流式网关核心切片
import httpx
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse

app = FastAPI()

# 维持一个长连接池,避免每次请求走三次握手
limits = httpx.Limits(max_keepalive_connections=500, max_connections=1000)
timeout = httpx.Timeout(connect=5.0, read=60.0, write=5.0, pool=10.0)
client = httpx.AsyncClient(base_url="http://127.0.0.1:8000", limits=limits, timeout=timeout)

@app.post("/v1/chat/completions")
async def chat_proxy(request: Request):
    headers = dict(request.headers)
    headers.pop("host", None)
    headers.pop("content-length", None)
    
    body = await request.body()
    
    # 向 vLLM 发送流式请求
    req = client.build_request(
        method="POST",
        url="/v1/chat/completions",
        headers=headers,
        content=body
    )
    
    try:
        response = await client.send(req, stream=True)
    except httpx.RequestError as exc:
        raise HTTPException(status_code=503, detail=f"LLM Engine Unreachable: {exc}")

    # 生成器:带背压保护的 Token 转发
    async def token_stream_generator():
        try:
            async for chunk in response.aiter_bytes():
                yield chunk
        finally:
            # 关键:当客户端主动断开(如前端关闭网页)时,必须关闭后端响应,触发 vLLM 释放序列
            await response.aclose()

    return StreamingResponse(token_stream_generator(),
        status_code=response.status_code,
        headers=dict(response.headers),
        media_type="text/event-stream"
    )

避坑细节: 注意 token_stream_generator 中的 finally: await response.aclose()。如果省略这行,当用户提问到一半关掉浏览器标签页时,vLLM 后端依然会固执地把剩下的 Token 生成完毕,无端浪费昂贵的 GPU 算力。显式关闭连接能让 vLLM 触发 AbortProcessing,瞬间释放显存。

四、自动化部署:Docker-Compose 架构编排

为了让整套服务具备容灾自愈能力,我们采用 Docker Compose 进行统一编排。这里直接调用 NVIDIA Container Toolkit 映射显卡:

# docker-compose.yml
version: '3.8'

services:
  vllm-engine:
    image: vllm/vllm-openai:v0.6.3
    container_name: vllm-qwen-7b
    runtime: nvidia
    restart: unless-stopped
    environment:
      - CUDA_VISIBLE_DEVICES=0
      - NCCL_IGNORE_DISABLED_P2P=1
    volumes:
      - /data/models/Qwen2.5-7B-Instruct:/model
    ports:
      - "8000:8000"
    command: >
      --model /model
      --served-model-name qwen2.5-7b
      --gpu-memory-utilization 0.92
      --max-model-len 4096
      --max-num-seqs 256
      --disable-log-requests
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

  llm-gateway:
    build:
      context: ./gateway
    container_name: llm-fastapi-gateway
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - VLLM_BACKEND_URL=http://vllm-engine:8000
    depends_on:
      - vllm-engine

五、性能对比与选型实测数据

我们使用 locust 对这套系统进行了高强度压测,模拟用户连续向模型发送包含 500 Token 上下文的问题,并要求输出约 300 Token 的回答。对比原有的 Ollama 方案与优化后的 vLLM 方案:

指标 Ollama (原方案) vLLM + 异步网关 (新方案) 提升幅度
最大稳定并发数 16 310 19.3x
首字延迟 (TTFT P95) 6,200 ms 418 ms 降低 93.2%
系统吞吐量 (Tokens/s) 85 tokens/s 1,420 tokens/s 16.7x
高并发显存 OOM 崩溃率 34% (频繁崩溃) 0% (自适应排队) 完全消除

总结与避坑心得

在消费级显卡部署生产级别 LLM 服务,算力不是唯一瓶颈,显存调度与流水线管理 才是拉开差距的核心。最后整理三条关键实操法则:

  1. 严禁生产裸奔 Ollama: Ollama 适合做开发测试和本地单人助理,一旦进入多人协同或业务 API 级调用,必须果断切换到 vLLM、SGLang 或 TensorRT-LLM。
  2. 留足系统显存冗余: 24G 显存不要分配超过 92% 给模型与 KV Cache,给 PyTorch 运行期以及 CUDA Graphs 预留至少 1.5G 缓冲区,是系统常驻 30 天不挂掉的铁律。
  3. 流式连接必须有主动取消机制: 前端无论用 fetch 还是 EventSource,在页面切换时务必调用 AbortController.abort(),网关捕获后立即中断后端计算,否则任何高性能方案都会被无意义的后台残留任务拖垮。
正文完
 0
评论(没有评论)