基于 4 台 8 卡 RTX 4090 服务器的 vLLM 推理集群

目录

(打开文档后,右键目录选择"更新域"可刷新页码)

一、项目概述 2

二、集群架构 3

三、部署过程 4

3.1 模型分发与基础环境 4

3.2 TP=4 服务编排 5

3.3 Nginx 负载均衡(初版) 6

四、性能优化(CUDA Graph + MTP 投机解码) 7

五、KV-Cache 亲和路由与前缀缓存改造 9

5.1 问题背景 9

5.2 方案选型与实施 9

5.3 根因发现:前缀缓存被静默关闭 10

5.4 验证结果 11

5.5 运维要点 11

六、验证与测试 12

七、自动化运维体系 13

八、问题记录与解决方案 14

九、遗留事项与后续建议 15

一、项目概述

本报告记录了 Qwen3.8-27B-FP8 大语言模型在 32 张 NVIDIA GeForce RTX 4090(24GB)GPU 集群上的完整部署过程,涵盖架构设计、模型分发、服务编排、负载均衡、性能优化、KV-Cache 亲和路由与自动化运维体系建设。

集群由 4 台 GPU 服务器组成(内网地址 10.255.254.X / X / X / X),每台 8 张 4090。最终形态为 8 个 TP=4(张量并行 4 卡)vLLM 推理实例,通过 OpenResty 统一对外提供 OpenAI 兼容 API 服务,公网入口为 XXXX:XXXX。

经过 CUDA Graph 与 MTP=3 投机解码优化后,单请求(512 tokens)端到端耗时从 37-83 秒降至 4-9 秒,提速约 5-8 倍;再经会话粘性路由与前缀缓存改造,多轮会话长上下文的 prefill 提速 2.4-6 倍。每实例保持约 150 万 tokens 的 KV cache 容量,支持 262144 tokens 超长上下文约 6 倍并发。

项目

内容

模型

Qwen3.8-27B-FP8(FP8 量化,28.75 GiB,66 个 safetensors 分片)

最大上下文

262144 tokens(256K)

推理框架

vLLM 0.20.2

集群规模

4 台服务器 × 8 张 RTX 4090 = 32 GPU

实例规模

8 个推理实例(每台 2 个,TP=4)

对外入口

XXXXXXXX(OpenResty 会话粘性路由 + 一致性哈希)

API 兼容

OpenAI API(/v1/chat/completions、/v1/models 等),API Key 鉴权

二、集群架构

集群采用"OpenResty 统一入口 + 8 个独立 vLLM 实例"的对称架构。每个实例绑定 4 张 GPU(TP=4),实例间完全独立、互不干扰,单实例故障不影响整体服务。

服务器

内网 IP

实例端口

GPU 分配

备注

节点 1(兼网关)

10.255.254.X

8000 / 8001

GPU 0-3 / GPU 4-7

同时承担负载均衡入口(OpenResty)

节点 2

10.255.254.X

8000 / 8001

GPU 0-3 / GPU 4-7

节点 3

10.255.254.X

8000 / 8001

GPU 0-3 / GPU 4-7

GPU 6 显存为 23GB(其余 24GB)

节点 4

10.255.254.X

8000 / 8001

GPU 0-3 / GPU 4-7

请求链路:客户端 → 公网入口 XXXXXXXX → OpenResty(Lua 提取会话指纹 → hash consistent 一致性哈希)→ 固定到 8 个 vLLM 实例之一 → 对应 4 张 GPU 完成推理。同一会话的多轮请求始终落在同一实例,直接命中该实例的 KV cache。

三、部署过程

3.1 模型分发与基础环境

模型文件(约 30GB)通过内网 rsync 从节点 1(44)分发至其他三台服务器,存放路径统一为 /data/models/Qwen3.8-27B-FP8,并采用 md5sum 逐文件校验一致性:

rsync -avP User@10.255.254.X:/data/models/Qwen3.8-27B-FP8/ /data/models/Qwen3.8-27B-FP8/

md5sum tokenizer_config.json tokenizer.json   # 四台机器必须一致

部署环境为 Docker + docker-compose 1.29.2,镜像 ubuntu-python310:22.04,vLLM 0.20.2 以 venv 方式只读挂载进容器,模型目录只读挂载,/dev/shm 共享内存挂载,shm_size 设置为 32GB(TP=4 进程间通信需要)。

3.2 TP=4 服务编排

集群经历了从 TP=2(每台 4 实例,共 16 实例)到 TP=4(每台 2 实例,共 8 实例)的架构演进。TP=4 方案的决策依据:

1)节点 3(70)的 GPU 6 显存为 23GB(23028 MiB),小于其余 31 张卡的 24GB,TP=2 时该卡因 KV cache 不足(需要 4.09 GiB、仅有 3.89 GiB)无法承载 262144 全长上下文;TP=4 将模型权重摊到 4 张卡(每卡约 7GB),绕开了这一硬件短板。

2)TP=4 单实例 KV cache 容量从约 62 万 tokens 提升至约 150-160 万 tokens,262K 全长上下文并发能力从 2.36x 提升至 5.7-6.2x。

每个实例的核心启动参数如下(全集群统一,含前缀缓存显式开关):

/opt/venv/vllm/bin/python3 -m vllm.entrypoints.cli.main serve /data/models/Qwen3.8-27B-FP8

--served-model-name Qwen3.8-27B-FP8 --trust-remote-code

--tensor-parallel-size 4 --gpu-memory-utilization 0.9

--max-model-len 262144 --max-num-seqs 32 --kv-cache-dtype fp8

--disable-custom-all-reduce --distributed-executor-backend mp

--reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder

--speculative-config '{"method":"mtp","num_speculative_tokens":3}'   # MTP=3 投机解码

--block-size 16 --enable-prefix-caching   # 显式开启前缀缓存(见第五章)

--port <8000|8001> --host 0.0.0.0 --api-key <统一密钥>

关键环境变量:NCCL_P2P_DISABLE=1(4090 驱动层不支持 P2P)、NCCL_CUMEM_ENABLE=0。注意 4090 为 sm_89 架构,SymmMemCommunicator 等需要 sm_90+ 的特性自动跳过,日志中对应 Warning 为正常现象。

滚动迁移策略:为保持外网服务全程不中断,采用"逐台摘除 → 重建 → 验证 → 挂回"的滚动方式——先在网关中注释目标机器的后端,完成 TP=4 重建并验证健康后再挂回,其余机器继续承接流量。

3.3 Nginx 负载均衡(初版)

初版网关为 Nginx,部署在节点 1(44),监听 2196 端口,upstream 配置 8 个后端,采用 least_conn(最少连接)策略,并针对 LLM 推理的长连接与流式响应(SSE)做了专门配置:

upstream all_vllm { least_conn; keepalive 32; server 10.255.254.X44:8000; ... server 10.255.254.X:8001; }

proxy_read_timeout 3600s; proxy_buffering off; proxy_cache off;   # 流式输出必须关闭缓冲

proxy_pass_header Authorization; proxy_set_header Authorization $http_authorization;   # 透传 API Key

访问日志采用自定义 upstream_log 格式,记录 $upstream_addr 与 rt/uct/uht/urt 四段耗时,用于观察流量分布与后端响应性能。注:该 least_conn 方案无法满足 KV cache 亲和要求,已于第五章的改造中升级为 OpenResty 会话粘性路由,超时与流式等基础配置全部保留。

四、性能优化(CUDA Graph + MTP 投机解码)

部署完成后,通过对启动日志的系统分析,识别出两项关键性能优化点,并先在单实例(44:8001)上试验验证,再滚动推广至全集群。

优化 1:去除 --enforce-eager,启用 CUDA Graph

初始配置沿用了早期调试阶段的 --enforce-eager 保守参数,导致 torch.compile 与 CUDA Graph 被禁用,decode 阶段每次前向都承担完整的 CPU kernel 启动开销。去除后启用 CUDA Graph capture,显著压缩启动开销。代价是首次请求有约 50 秒的图预热(warmup),之后稳定在毫秒级调度。

优化 2:启用 MTP=3 投机解码

模型目录自带 mtp.safetensors(477MB 投机解码头),初始配置未启用。试验发现:在 eager 模式下 MTP 的收益被 draft 模型的 CPU 启动开销完全抵消(墙钟时间无差异);与 CUDA Graph 叠加后收益爆发。进一步对比 num_speculative_tokens=1 与 3,MTP=3 为甜点值。

配置

单请求耗时(512 tokens)

说明

eager,无 MTP(初始)

37 - 83 秒

基线

eager + MTP=1

约 39 秒

收益被 eager 开销抵消

CUDA Graph + MTP=1

6.3 - 9.3 秒

提速约 5 倍

CUDA Graph + MTP=3(最终)

4 - 5.4 秒

较 MTP=1 再快 30-40%

MTP=3 实测指标:平均接受长度(Mean acceptance length)2.17 - 2.38,即每步平均产出约 2.3 个 token;三个位置的接受率分别约为 61-69%、34-50%、18-25%。第 3 个 draft token 接受率已降至约 20%,因此 MTP=4 预期无收益,未再尝试。

优化代价:MTP draft 头与 CUDA Graph 图池合计占用约 8-10% 的 KV cache 空间(每实例 KV cache 从 162 万 tokens 降至约 149 万 tokens),262K 全长并发从 6.2x 降至 5.7x,在可接受范围内。

五、KV-Cache 亲和路由与前缀缓存改造

5.1 问题背景

least_conn 负载均衡会把同一会话的多轮请求分散到不同实例,而 vLLM 的前缀缓存(Prefix Cache,即 KV cache 复用)是每个实例各自独立的——第二轮请求落到另一台机器时,前几轮已算好的 KV cache 完全用不上,长历史会话的 prefill 成本每轮都全价重算。改造目标:把"同一个会话"固定路由到"同一个实例"(session affinity / sticky routing),让前缀缓存真正命中。

5.2 方案选型与实施

方案:由 Lua 在网关层读取请求体提取会话指纹(OpenAI 协议多轮对话为全量 messages 历史,首条消息在同一会话中恒定不变,天然是稳定的会话标识),哈希与故障转移仍由原生 upstream 的 hash ... consistent(ketama 一致性哈希)承担,保留 keepalive 与 max_fails 自动摘除能力。

实施中发现 Ubuntu 22.04(jammy)官方源不包含 libnginx-mod-http-lua 动态模块(20.04 有、22.04 移除),遂改用 OpenResty 官方 apt 源安装宿主机版 OpenResty 1.31(自带 LuaJIT 与 cjson),替换原 Nginx 接管 XXX 端口;原 nginx 服务停用但保留(systemctl disable),配置与包均未删除,可随时一键回滚。

路由键提取优先级:请求体 session_id / conversation_id 字段 → 首条消息内容指纹(role + content)→ 客户端 IP 兜底(/v1/models 等无 body 请求)。同 key 经一致性哈希固定到 8 个后端之一;某后端故障时仅其承载的会话被重新映射,其余会话不受影响。

upstream all_vllm { hash $session_key consistent; keepalive 32;

    server 10.255.254.X:8000 max_fails=2 fail_timeout=10s; ... 共 8 个后端 }

rewrite_by_lua_block { -- 读 body,提取 session_id 或 messages[1] 内容写入 ngx.var.session_key }

5.3 根因发现:前缀缓存被静默关闭

粘性路由上线后验证,命中率始终为 0.0%。排查启动日志发现根因:vLLM 在引擎参数校验阶段将 enable_prefix_caching 静默置为 False(启动日志无对应 WARNING),触发条件为 MTP 投机解码的保守默认行为。

通过单实例(X:8001)隔离实验逐项排除:先去除 --kv-cache-dtype fp8,开关仍为 False(排除 fp8 KV cache 嫌疑);再显式传入 --enable-prefix-caching,开关变为 True 且运行正常、无任何报错。结论:该自动关闭只是保守默认值,MTP=3 + fp8 KV cache + 前缀缓存三者可以共存,显式声明即可零代价全保留。随后按 顺序滚动推广至全集群 8 个实例,逐台验证开关状态并预热。

5.4 验证结果

验证项

方法

结果

会话亲和

同一会话连发两轮,查网关upstream_log

key= 相同,两轮落同一后端 ✅

会话分散

不同会话首发请求

落在不同后端(一致性哈希分散)✅

缓存命中(直连实例)

约 3600 token 长 prompt 同实例连发两次

4.02s → 0.67s,prefill 提速约 6 倍 ✅

缓存命中(端到端)

经网关 + 粘性路由,加盐全新长 prompt 连发

3.10s → 1.27s,提速约 2.4 倍 ✅

全集群推广

4 台 8 实例滚动重建

8/8 实例 enable_prefix_caching=True,健康检查全 200 ✅

指标口径说明:vLLM 日志中的 Prefix cache hit rate 为实例启动以来的累计值,会被历史冷请求稀释;该版本不回填响应中的 usage.prompt_tokens_details.cached_tokens 字段。判断单次请求是否命中,以相同长前缀二次请求的 rt 差异为准。

5.5 运维要点

1)实例重启后的首个推理请求有 40-50 秒懒编译开销(torch.compile 新长度区间 + CUDA Graph 捕获),重启流程必须内置预热请求,预热完成才视为就绪。

2)未来升级 vLLM 版本后的第一项检查:docker logs 确认 enable_prefix_caching=True,防止新版本再次将其静默关闭。

3)配置备份:各机器 ~/vllm-deploy/ 下 docker-compose.yml.bak.prefix2 为本次改造前快照;网关配置位于 XXX 的 /usr/local/openresty/nginx/conf/nginx.conf。

六、验证与测试

部署与优化完成后,执行了三层验证:

1)后端健康检查:8 个实例 /health 端点全部返回 200。

2)负载分布与会话亲和验证:连续请求网关入口,access log 确认不同会话经一致性哈希分散到 8 个后端、同一会话固定在同一后端。

3)端到端推理验证:公网入口发起 /v1/chat/completions 请求,返回结构完整,content 与 reasoning(思维链)字段均正常输出,finish_reason 正常;长前缀二次请求命中前缀缓存,prefill 明显加速。

指标

数值

单实例 KV cache 容量

约 149 万 - 162 万 tokens

262144 全长上下文并发

每实例 5.7x - 6.2x

单请求 decode 吞吐

42 - 51.7 tokens/s

单请求端到端耗时(512 tokens)

4 - 9 秒(优化前 37 - 83 秒)

前缀缓存命中提速(长前缀二次请求)

直连约 6 倍、端到端约 2.4 倍

8 路健康检查

全部 HTTP 200

公网入口推理

正常(reasoning 字段完整)

七、自动化运维体系

为保障集群长期稳定运行,建立了四层自动化保障:

场景

机制

恢复方式

vLLM 进程崩溃 / OOM

容器 restart: always

Docker 自动拉起,模型加载约 2-3 分钟恢复

机器重启 / 断电

docker.service 开机自启

开机后容器自动恢复,无需人工干预

误执行 docker-compose down

vllm-stack.service(systemd)

sudo systemctl start vllm-stack 一键拉起

OpenResty 网关异常(节点 1)

openresty.service 开机自启

系统自动恢复;极端情况可切回原 nginx(已保留)

常用运维命令:

docker logs -f vllm-deploy_vllm-8000_1          # 查看某实例实时日志

sudo systemctl restart vllm-stack              # 整台机器 vLLM 栈重启

docker rm -f vllm-deploy_vllm-8000_1 && docker-compose up -d vllm-8000   # 单实例重建

sudo tail -f /var/log/nginx/vllm_access.log    # 网关日志(含 key= 会话指纹与 upstream 分布)

注意一:docker-compose 1.29.2 存在 ContainerConfig 检查 bug,重建容器前必须先 docker rm -f 删除旧容器(包括带哈希前缀的残留容器),不能直接 up --force-recreate。

注意二:任何实例重建/重启后,先打一发预热请求(首个请求有 40-50 秒懒编译开销),预热成功再挂回网关承接流量。

八、问题记录与解决方案

问题

现象

根因

解决方案

节点 1 GPU 不可用

nvidia-smi 报 Driver/library version mismatch

NVIDIA 驱动热更新后版本不一致

重启服务器

节点 3 实例 8003 起不来

KV cache 需要 4.09 GiB、仅有 3.89 GiB

GPU 6 显存 23GB 小于其余 24GB

架构改为 TP=4,权重摊到 4 卡

节点 4 模型加载报错

Qwen3ReasoningParser 找不到 think token

模型下载中断,缺失约 7GB(含 tokenizer、3 个 layers 分片、6GB 的 outside.safetensors),残留 .incomplete 文件

从内网节点 1 rsync 补齐 + md5 校验

compose 重建失败

KeyError: 'ContainerConfig'

docker-compose 1.29.2 与新版 Docker API 不兼容

先 docker rm -f 删干净旧容器再 up

reasoning_effort=high 报 400

客户端传 high 被拒

vLLM qwen3 parser 仅支持 xhigh/medium/low

见"遗留事项"代理层方案

MTP 无效果

开启后墙钟时间无变化

eager 模式下 draft 开销抵消收益

与 CUDA Graph 叠加后提速 5 倍以上

jammy 装不了 Lua 模块

libnginx-mod-http-lua no installation candidate

Ubuntu 22.04 官方源移除了该模块包

改用 OpenResty 官方 apt 源装宿主机版

前缀缓存不生效

粘性路由后 hit rate 仍为 0.0%

vLLM 因 MTP 投机解码将 enable_prefix_caching 静默置 False

显式加 --enable-prefix-caching,三者共存

九、遗留事项与后续建议

1)reasoning_effort 兼容性:vLLM 的 qwen3 reasoning parser 只认 xhigh/medium/low,不支持 OpenAI 风格的 high。如客户端(如 WorkBuddy)会自动发送 reasoning_effort=high,可在网关所在机器部署一层 Python 代理(aiohttp),将 high 改写为 xhigh 后转发,脚本已备妥,OpenResty 配置无需回退。

2)FP8 GEMM 内核调优:vLLM 未内置 RTX 4090 的 W8A8 Block FP8 调优配置(日志中 "Config file not found" 提示),当前使用默认 Triton 内核参数,预计还有 5-15% 的 decode 提升空间,可通过运行 vLLM 自带 benchmark 脚本生成 4090 专属配置,需要时单独实施。

3)FP8 KV cache 精度:checkpoint 未带校准系数(q_scale=1.0),存在轻微精度损失风险。当前功能验证正常;如业务几乎不使用超长上下文,可考虑去掉 --kv-cache-dtype fp8 换精度(KV 容量减半,前缀缓存不受影响)。

4)vLLM 版本升级检查项:升级后第一时间确认 enable_prefix_caching 仍为 True(本次改造即因该开关被静默关闭而排查),并重新跑一遍前缀缓存命中验证。

5)配置备份:各机器 ~/vllm-deploy/ 下保留 docker-compose.yml.bak.tp2 / .bak.mtp3 / .bak.prefix2 等历史版本,可随时回滚到任一历史配置;网关侧原 nginx 配置与服务均已保留,可一键回切。

6)首次请求预热:实例重启后的首个请求约有 40-50 秒预热延迟(CUDA Graph 捕获与懒编译),属正常现象;重启流程已内置预热步骤,如需彻底消除可由监控脚本在健康检查通过后自动发预热请求。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐