vLLM高并发流式推理实战:Qwen2.5多卡部署、PagedAttention调优与Nginx SSE排坑

2次阅读
没有评论

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

上周为了帮内部自动化工作流摆脱按 Token 计费的云端 API,同时确保代码生成与结构化数据提取不外泄,我们在两台配置了双路 RTX 4090(24G × 2)的机架式工作站上把 Qwen2.5-32B-Instruct 给立了起来。原以为只是跑一个现成的推理容器,结果在并发拉升至 30+ 以及对接前端流式打字机效果时,接连踩了几个隐蔽的暗坑:显存碎片化 OOM、多卡 NCCL 偶发通信超时,以及 Nginx 默认缓冲策略硬生生把流式打字机憋成了“整段吐出”。

本文不扯抽象架构,直接记录这次实战部署中经过验证的生产配置、性能压测数据以及几个致命排障点。

一、算力拓扑与多卡并行启动

双卡 4090 运行 32B 模型,AWQ / GPTQ 4-bit 量化版本是性价比极高的选择(显存占用约 20GB,单卡即可塞下但上下文直接锁死);而如果你需要完整的 FP16/BF16 精度推理,32B 参数的静态权重就需要占用约 64GB 显存,两张 24G 显卡吃不下。因此,我们采用 GPTQ-Int4 或 AWQ 实现张量并行(Tensor Parallelism, TP=2),将模型权重切分在两张卡上,留出足够的显存跑动态 KV Cache。

首先确保宿主机安装了最新的 NVIDIA Container Toolkit。基础的 docker-compose.yml 编排如下:

version: "3.8"
services:
  vllm-engine:
    image: vllm/vllm-openai:v0.6.3.post1
    container_name: vllm-qwen25-32b
    runtime: nvidia
    restart: unless-stopped
    environment:
      - CUDA_VISIBLE_DEVICES=0,1
      - NCCL_P2P_DISABLE=0
      - NCCL_IB_DISABLE=1
    volumes:
      - /data/models/Qwen2.5-32B-Instruct-AWQ:/model
      - ~/.cache/huggingface:/root/.cache/huggingface
    ports:
      - "8000:8000"
    ipc: host
    command: >
      --model /model
      --served-model-name qwen-2.5-32b
      --tensor-parallel-size 2
      --gpu-memory-utilization 0.92
      --max-model-len 8192
      --max-num-seqs 64
      --enforce-eager
      --trust-remote-code
      --disable-log-requests

关键参数避坑说明:

  • ipc: host:极为重要。多卡进程间通信使用共享内存,Docker 默认分配的 64MB 会在 NCCL 通信建立的瞬间直接报 Bus error (core dumped)。
  • --enforce-eager:4090 这类消费级显卡在启用 PyTorch 默认的 CUDA Graph 预编译捕获时,偶尔会出现未定义显存分配错误。开启 eager 模式启动速度提升数倍,稳定性极高,仅牺牲不到 3% 的纯推理吞吐。
  • NCCL_IB_DISABLE=1:单机环境显卡走 PCIe 总线,强制关闭 InfiniBand 探测,避免 NCCL 启动时无意义的网络寻址卡顿。

二、KV Cache 极限调优:PagedAttention 显存分配策略

很多开发者在部署 vLLM 时直接使用默认参数,结果并发稍微一高,服务就会抛出 Engine core process died with exit code -9 (OOMKilled)。vLLM 的核心杀手锏是 PagedAttention,它将显存切分成逻辑 Block(类似操作系统的虚拟内存分页机制)。

我们可以通过推理日志计算显存余量:

# 查看模型加载后的显存分布
INFO 11-04 10:22:15 config.py:380] # GPU blocks: 11520, # CPU blocks: 2048
INFO 11-04 10:22:15 config.py:385] Maximum concurrency for 8192 tokens per request: 22.50x

显存计算公式非常直观:

总显存 (24GB × 2) = 模型静态权重 (约 20GB) + CUDA 上下文开销 (约 2GB) + KV Cache 池 (约 22GB)

如果业务场景主要是“短 Prompt、长生成”(例如 Agent 写代码、长篇总结),建议增加 --block-size 16(默认),这能减少小片段生成时的内部碎片;如果业务是“长文档 RAG 检索、短回答”,则可以把 --block-size 设为 32,能够轻微降低管理 Block 的 CPU 开销,提升吞吐率。

三、流式 SSE 穿透深坑:Nginx 反代配置

把 vLLM 封装成对外暴露的 OpenAI 兼容接口(/v1/chat/completions)时,前端普遍采用 stream: true 实现打字机效果。但在实测中,前端请求后界面停滞了十几秒,最后“啪”地一下把整段结果吐了出来。这种流式体验是完全无法被业务接受的。

罪魁祸首就是前置的 Nginx 反向代理默认开启了 响应缓冲(Proxy Buffering)。Nginx 试图把后端上游返回的 chunk 存满自己的缓冲区(通常是 4k/8k)后再一口气发给客户端。

生产环境可用的 nginx.conf 关键块如下:

upstream vllm_backend {
    server 127.0.0.1:8000;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name api.nassky.top;

    # SSL 基础配置省略...

    location /v1/ {
        proxy_pass http://vllm_backend;
        proxy_http_version 1.1;

        # 核心:必须清理掉默认的 Connection 头,建立长连接
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 核心:关闭代理缓冲,实现真正的 SSE 实时推流
        proxy_buffering off;
        proxy_cache off;
        chunked_transfer_encoding on;

        # 超时时间放宽,防止长文本推理中间断连
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;

        # 禁用上游压缩,避免 gzip 阻断分块传输
        proxy_set_header Accept-Encoding "";
    }
}

配置完成后,执行 nginx -s reload。接着用 curl 进行流式探测:

curl -N -X POST https://api.nassky.top/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"qwen-2.5-32b","messages": [{"role":"user","content":" 写一个基于 Python 的异步网络扫描器 "}],"stream": true
  }'

参数 -N 代表 --no-buffer。此时在终端应当能够肉眼可见单个 token 的高频输出,首字延迟(TTFT)大幅下降。

四、实测吞吐与压测对比

为了探寻双 4090 运行 Qwen2.5-32B-AWQ 的实际性能承载上限,我们使用 vLLM 内置的基准测试工具 benchmark_serving.py,模拟 50 个并发客户端,平均输入 512 tokens,输出 256 tokens。

指标参数 原生 Transformers (Pipeline) Ollama (默认 llama.cpp) vLLM (PagedAttention + TP=2)
并发 1 吞吐 (TPS) 18.4 tokens/s 38.2 tokens/s 44.6 tokens/s
并发 32 整体吞吐 直接 OOM 崩溃 142.1 tokens/s 489.8 tokens/s
并发 32 平均 TTFT – 2140 ms 410 ms
显存碎片率 极高 (显存利用失控) 中等 极低 (< 4%)

测试数据显示,在单请求环境下,使用量化后底层优化良好的引擎差距不算悬殊。然而一旦将并发拉高,vLLM 的批处理调度(Continuous Batching)展现出压倒性的工程优势:当其他方案开始排队或频繁显存换页时,vLLM 可以高效填平 GPU 计算单元的时间空隙,吞吐暴涨 3 倍以上。

五、生产环境避坑 Checklist

根据我们实操排错经历,整理出以下几个关键检查项:

  1. 显卡 P2P 状态检查:部分消费级主板的 PCIe 通道分配不均(如 x16 + x4),这会导致双卡张量并行时 NCCL 效率大跌。部署前务必运行 nvidia-smi topo -m 确认两张卡之间的链接状态,尽量保证处于同一个 NUMA 节点或直通通道。
  2. 最大上下文与显存预留的博弈 :如果业务只需要 4K 上下文, 切勿盲目把 --max-model-len 设成 32K。vLLM 会根据该参数为单请求的最大可能占用预留结构空间,设置过高会直接挤压 KV Cache 的并发容纳数量。
  3. 客户端长连接复用:业务调用方(如 LangChain、LlamaIndex 或自行封装的 SDK)务必启用连接池(HTTP keep-alive)。每一次新请求都进行 TLS 握手会使得高并发下的整体耗时无端增加 80~120ms。

总结

对于大部分追求算力自主权的中小研发团队,两张消费级显卡搭配量化模型,配合 vLLM 与针对性的网关调优,足以支撑起几十个并发的内部 Agent 业务流。避开反代缓冲与显存超限配置这几个深坑,本地私有大模型不仅成本可控,在延迟和稳定性上也完全具备生产

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