共计 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
根据我们实操排错经历,整理出以下几个关键检查项:
- 显卡 P2P 状态检查:部分消费级主板的 PCIe 通道分配不均(如 x16 + x4),这会导致双卡张量并行时 NCCL 效率大跌。部署前务必运行
nvidia-smi topo -m确认两张卡之间的链接状态,尽量保证处于同一个 NUMA 节点或直通通道。 - 最大上下文与显存预留的博弈 :如果业务只需要 4K 上下文, 切勿盲目把
--max-model-len设成 32K。vLLM 会根据该参数为单请求的最大可能占用预留结构空间,设置过高会直接挤压 KV Cache 的并发容纳数量。 - 客户端长连接复用:业务调用方(如 LangChain、LlamaIndex 或自行封装的 SDK)务必启用连接池(
HTTP keep-alive)。每一次新请求都进行 TLS 握手会使得高并发下的整体耗时无端增加 80~120ms。
总结
对于大部分追求算力自主权的中小研发团队,两张消费级显卡搭配量化模型,配合 vLLM 与针对性的网关调优,足以支撑起几十个并发的内部 Agent 业务流。避开反代缓冲与显存超限配置这几个深坑,本地私有大模型不仅成本可控,在延迟和稳定性上也完全具备生产