共计 3508 个字符,预计需要花费 9 分钟才能阅读完成。
很多朋友在玩本地大模型时,第一反应往往是装个 Ollama。如果是单人通过 WebUI 尝鲜,Ollama 确实开箱即用;但上个月我帮一个研发小团队把内网知识库接入大模型服务时,十几个人同时跑代码生成,Ollama 直接卡成 PPT,首字延迟(TTFT)飙到了 8 秒以上,显存调度更是频繁引发 OOM。
为了榨干机房那台闲置的双路 RTX 3090 24GB(总显存 48GB),我彻底推倒了原有方案,改用 vLLM + Ray + 持续批处理(Continuous Batching) 架构,成功吃下了 DeepSeek-R1-Distill-Qwen-32B。这套架构在 30 个人并发提问时,仍能将端到端吞吐稳在 140+ tokens/s。今天把这套从硬件拓扑排错、生产配置到流式代理避坑的完整过程梳理出来。
一、硬件拓扑与 NCCL 踩坑排障
双卡并行不是插上主板就能跑满的。消费级卡跑多卡张量并行(Tensor Parallelism),最大的短板在于缺乏 NVLink 高速互联,完全依赖主板 PCIe 通道通信。
我在插卡后执行 nvidia-smi topo -m,输出结果直接泼了一盆冷水:
GPU0 GPU1 CPU Affinity NUMA Affinity
GPU0 X SYS 0-15,32-47 0
GPU1 SYS X 16-31,48-63 1
两张卡跨了 CPU Socket 和 NUMA 节点(SYS 状态),这意味着张量并行通信时,数据要从 GPU0 走 PCIe 到 CPU0,再跨 QPI 总线到 CPU1,最后到达 GPU1,通信延迟高达几十微秒。在初始化 NCCL 时频繁报 NCCL WARN Connection reset by peer,服务直接断开。
解决方案:
- 硬件插槽重构: 将两张卡插在直连同一个 CPU 的 PCIe 4.0 x16 / x8 插槽上,确保拓扑降级为
NODE或PIX。 - 环境变量强行覆写: 消费级主板若无法启用 P2P(Peer-to-Peer),必须在容器启动时注入以下 NCCL 参数,屏蔽 P2P 报错并走共享内存中转:
export NCCL_P2P_DISABLE=1
export NCCL_IB_DISABLE=1
export NCCL_DEBUG=INFO
二、vLLM 生产级 Docker Compose 配置
对于 DeepSeek-R1-Distill-Qwen-32B,其 16 位浮点(BF16)权重文件裸占用就达到了约 64GB,48GB 显存无论如何塞不下。这里我们必须上 AWQ / GPTQ 4bit 权重,或者直接采用 FP8 动态量化,同时开启 FP8 KV Cache。
这是我反复调优后的 docker-compose.yml 生产级配置,消除了大量默认参数导致的显存浪费:
services:
vllm-deepseek:
image: vllm/vllm-openai:v0.7.2
container_name: vllm-deepseek-32b
runtime: nvidia
restart: always
environment:
- CUDA_VISIBLE_DEVICES=0,1
- NCCL_P2P_DISABLE=1
- VLLM_WORKER_MULTIPROC_METHOD=spawn
volumes:
- /data/huggingface/hub:/root/.cache/huggingface/hub
- /dev/shm:/dev/shm
ipc: host
ports:
- "8000:8000"
command: >
--model deepseek-ai/DeepSeek-R1-Distill-Qwen-32B
--tensor-parallel-size 2
--gpu-memory-utilization 0.92
--max-model-len 16384
--max-num-seqs 64
--kv-cache-dtype fp8
--enforce-eager
--trust-remote-code
--port 8000
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
核心参数实战解析:
--tensor-parallel-size 2:切分为两路并行计算,两张 24GB 显存平均分摊权重。--kv-cache-dtype fp8: 杀手级参数 。传统 FP16 的 KV 缓存长文本极耗显存,开启 FP8 之后,显存占用直接斩半,省出的显存能供更多并发请求排队。--enforce-eager:消费级显卡在多卡模式下跑 CUDA Graph 经常出现不稳定显存暴增,开启 Eager 模式能彻底消除 CUDA Graph 捕获失败引发的崩溃,代价仅仅是极轻微的调度开销。ipc: host与/dev/shm映射:避免 PyTorch 多进程在通信时被 Docker 默认 64MB 的共享内存憋死抛出Bus error。
三、流式响应(SSE)避坑:反向代理为什么老被截断?
服务跑起来后,很多开发者直接在前端配 Nginx 反向代理,却发现: 前端根本没有“打字机”流式效果,而是卡住十几秒,然后哗啦一下全部吐出来。
这是因为 Nginx 默认开启了 HTTP Buffer 机制,把 vLLM 吐出来的 Server-Sent Events (SSE) 数据块全部存在了自身的内存缓冲区里,直到填满 buffer 或者连接关闭才一次性发给浏览器。
修改你的 Nginx 配置,在反代 location 块中加入这几行核心指令:
location /v1/chat/completions {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
# 核心:彻底关闭缓冲,实现实时流式透传
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
chunked_transfer_encoding on;
# 深度推理模型需要留足长思考时间,防止超时掐断
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
如果用的是轻量级的 Caddy,只需在代理块加上 flush_interval -1 禁用缓冲:
reverse_proxy 127.0.0.1:8000 {
transport http {flush_interval -1}
}
四、实测压测数据:Ollama vs vLLM
为了验证这套折腾的价值,我写了一个并发压测脚本,模拟 20 个用户持续向部署在同一台双路 3090 机器上的 DeepSeek-R1-Distill-32B 发起并发请求(Input tokens: 512, Max Output tokens: 1024)。
| 运行引擎 | 首字延迟 (TTFT P95) | 总吞吐量 (Tokens/s) | 并发错误率 (20 并发) | 显存占用状态 |
|---|---|---|---|---|
| Ollama (Default) | 6.8 秒 | 38.2 | 25% (超时熔断) | 静态分配,空闲不释放 |
| vLLM (PagedAttention + FP8) | 0.48 秒 | 151.4 | 0% (无一失败) | 动态调度,显存利用率 92% |
核心差异就在于 PagedAttention。Ollama 对显存的管理类似于传统的固定内存段,并发一来显存碎片化极其严重,后面的请求必须完全串行等待;而 vLLM 把显存划分为虚拟物理块(Virtual Blocks),不管来多少并发,只要显存物理块没被吃满,就能通过 Continuous Batching 同步动态插队推理,首字延迟实现了断崖式下降。
总结与避坑心得
在消费级硬件上部署大模型生产集群,不要轻易迷信“一键脚本”。踩坑一圈下来,给打算折腾的极客几点忠告:
- 双卡别碰单通道主板: 购买前必须查清主板的拆分通道(Bifurcation),如果第二根插槽是走芯片组(PCH)且只有 PCIe 3.0 x4,卡间通信会直接卡死你的张量并行。
- 显存分配要留有余地:
gpu-memory-utilization绝对不要设为1.0。留出 8%~10% 的显存不仅是给系统和 CUDA Driver 冗余,更重要的是防止用户突然输入超长上下文时导致瞬时 CUDA OOM 崩毁整套推理进程。 - 善用蒸馏模型: 对于真实生产力场景,DeepSeek-R1 的 32B 蒸馏版结合长上下文推理,在双卡 3090 上达到了性能与资源消耗的最佳甜点位,完全不需要硬上满血 671B 盲目消耗云算力。
把算力死死攥在自己手里,并且用最极客的方式把它