前几个月我一直在折腾一件事把公司内部几条业务线的大模型推理全部统一到 vLLM 上既要跑 DeepSeek-R1 这种 671B 的大家伙也要跑 Qwen3 从 8B 到 32B 的一堆中尺寸模型还得让它们在一个入口下同时对外提供服务。中间踩的坑比想象中多得多从显存规划到路由调度从 KV Cache 配额到前缀缓存命中率几乎每个环节都有几个让人挠头的反直觉问题。这篇文章就是把这些实测经历整理成一份可复现的避坑指南重点讲清楚多模型并发部署时那些文档里不会明说的细节以及我最终稳定运行的配置方案。适合正在搭建或维护推理服务的同学参考不管你是已经踩过坑还是准备入坑应该都能找到一些有用的东西。1. 为什么要用 vLLM 统一承载多模型1.1 多模型并发的真实业务场景很多团队一开始没有多模型并发这个概念早期就是一个模型一个服务哪个业务要用什么模型自己跑自己的互相不打扰。但业务多了之后问题就出来了公司内部可能有十几个业务线每个人都在重复布推理服务GPU 资源利用率低得可怜有的卡上跑着 8B 小模型占不满旁边的业务却在排队等 32B 模型的资源。资源池化的需求随之而来。我们当时的处境是R1 模型负责复杂逻辑推理和代码生成Qwen3-32B 负责结构化数据抽取和文档理解Qwen3-8B 负责轻量级问答和意图识别。三条业务线的负载波动还各不相同白天轻量问答流量大晚上批处理任务反而吃重。如果每个模型单独维护一套部署脚本每套都配固定的显卡那资源永远是浪费的调度也没法做。所以摆在面前的第一问题就是需要一个统一的推理服务底座让不同模型能共享 GPU 资源池同时对外暴露一致的接口。1.2 为什么选 vLLM 而不是其他推理框架作为主力框架vLLM 的优势在于那一套成熟的 PagedAttention 和 Continuous Batching 机制。简单比喻一下传统推理框架处理请求像餐厅按桌点菜来一桌做一桌人多了就得排队桌子之间没法互相借用vLLM 把显存里的 KV Cache 切成小块分页管理像自助餐厅的共享餐盘谁的菜先做完盘子就给下一个用吞吐量在并发场景下明显提升。对比 TGI 和 SGLangTGI 在 Hugging Face 生态里集成度高但多模型并发和前缀复用方面支持没有 vLLM 那么顺SGLang 的 RadixAttention 前缀复用理论上更强但当时在我们环境里的稳定性和周边配套还是差了一点。我自己实测下来的感受是 vLLM 的社区活跃度最高出问题搜得到解决方案的概率大得多OpenAI 兼容接口也是最完整的这对后续做网关路由和统一接入非常关键。生产环境选型稳定和可维护性优先级永远高于纸面性能。1.3 整体架构多实例 网关路由最终我们采用的架构是多模型实例 网关路由每个模型独立跑一个 vLLM 实例实例之间通过CUDA_VISIBLE_DEVICES做 GPU 隔离上层用一个轻量网关统一接收请求根据请求体里的model字段路由到对应的 vLLM 服务。这套架构不是一开始就定下来的中间试过在单个 vLLM 进程里同时加载多个模型后面会发现隔离性和故障扩散的问题很难接受这会在后面章节详细展开。网关层我们最初用 Nginx 做透明反代后来发现路由逻辑越来越复杂干脆写了一个基于 FastAPI 的轻量路由服务核心逻辑只有几十行但规避了 Nginx 动态路由配置的很多坑。架构示意图我就不画了关键点就三层网关层负责路由、限流、鉴权实例层每个模型一个 vLLM 服务存储层统一管理模型权重和量化版本。2. 环境准备与显存规划2.1 硬件与驱动基线多模型并发部署的硬件预算要算的是峰值共存而不是单模型跑通。如果 DeepSeek-R1 需要 8 张卡Qwen3-32B 需要 2 张卡Qwen3-8B 需要 1 张卡同时要保证都能接受流量那么至少需要 11 张卡的容量而不是说你手里有一台 8 卡机器就能全跑起来。我们实际用的是多台 8 卡服务器组成推理集群通过网关统一对外这样横向扩容也更方便。驱动和 CUDA 版本是第一个容易翻车的地方。vLLM 对 CUDA 版本和 PyTorch 版本都有对应关系比如某些老版本需要 CUDA 11.8新版本又在 CUDA 12.x 下表现更好。我们在一台新机器上直接按官网默认命令装 vLLM结果跑模型的时候报 CUDA driver version is insufficient排查半天才发现是驱动版本太老又不敢随便升级驱动因为机器上还有其他服务在跑。后来统一规定部署 vLLM 的机器驱动版本至少 535 以上CUDA 版本 12.1 以上PyTorch 用 vLLM 官方镜像对应的版本彻底跟系统自带的 Python 环境解耦。2.2 显存估算权重、KV Cache、激活内存显存规划是整件事里最值得花时间的部分直接决定了你能并行多少个请求、能开多大的上下文。一个 vLLM 实例的总显存开销大致是三块模型权重 KV Cache 激活内存和碎片余量。模型权重这块比较直观。Qwen3-8B 用 BF16 加载需要约 16GBQwen3-32B 用 BF16 加载需要约 64GBDeepSeek-R1 是 671B 的 MoE 模型FP8 量化后权重约 700GB单机 8 卡 80GB 都装不下所以必须上多机张量并行或者用更激进的量化。KV Cache 是动态部分它的大小跟 max-model-len 和并发请求数强相关我习惯用一个简化公式估算KV Cache 显存 ≈ 模型层数 × 注意力头数 × 头维度 × 序列长度 × 并发数 × 每个 token 的字节数实际部署时不用自己手算vLLM 启动日志里会打印 KV Cache 的显存分配情况但理解这个关系对调优很有帮助。注意权重占多少是固定的KV Cache 才是并发能力的核心变量。很多人只看显存还剩多少忽略 KV Cache 预留结果一压测就 OOM。2.3 量化方案选择多模型并发的显存压力下量化是绕不开的话题。FP8 和 INT8 是精度损失最小的INT4 的 AWQ/GPTQ 能省一半显存但 R1 这种对数学推理要求高的模型INT4 量化后我在实测中确实观察到了推理质量的波动尤其是一些需要多步推导的场景。所以 R1 我们最终选了 FP8Qwen3 系列则根据规格不同32B 用了 AWQ INT48B 直接用 BF16 也够。Qwen3 官方发布的时候同步出了 AWQ 量化权重这个强烈建议直接用官方量化版别自己拿 GPTQ 脚本现量化踩过坑的人会知道自量化版本在新版本 vLLM 里经常出现权重格式不兼容的问题。还有一点vLLM 启动时量化参数要显式指定--quantization awq或者--dtype float8不要只靠权重目录里的配置文件自动识别机器上模型多了之后自动识别偶尔会抽风显式指定最保险。2.4 安装与版本匹配vLLM 的版本迭代速度非常快新版本可能会改变默认引擎行为老版本可能不兼容新发布的模型架构。我们经历过最痛的一次是Qwen3 刚发布时想第一时间上线结果当时的 vLLM 版本不支持 Qwen3 的注意力机制报了一大堆 attention 相关的错最后只能先更新到指定版本才跑起来。这里我给出一个稳定的组合参考vLLM 0.8.x 或更新版本运行 Qwen3 系列基本没问题DeepSeek-R1 建议至少 0.6.x 以上且尽量保持版本一致。多模型集群最忌讳的就是每个实例 vLLM 版本不一致一旦出现行为差异排查问题会非常痛苦。我们当时的做法是写死一个版本用 Docker 镜像锁版本升级之前先在测试环境把全模型清单跑一遍再统一换版本。3. 多模型并发部署的架构设计3.1 单实例多模型 vs 多实例隔离多模型并发最核心的架构决策就是到底一个 vLLM 进程里加载多个模型还是每个模型一个进程。我一开始倾向单实例多模型理由是省显存模型间可以共享一部分内存页管理也简单。但实际测试下来问题很多。单实例多模型在 vLLM 里的支持方式不够成熟多个模型之间要争抢同一条调度链路上的资源一个模型出现长尾请求会拖累其他所有模型的响应。更关键的是隔离性问题一个模型崩溃或 OOM整个进程挂掉所有在这台机器上的模型全部不可用。这在生产环境是没法接受的。所以后来我们明确模型之间必须进程级隔离用CUDA_VISIBLE_DEVICES切分 GPU每个实例独享自己的显存和调度队列。资源利用率上是牺牲了一部分但换来的是故障域隔离和独立扩缩容。3.2 上下文长度与 KV Cache 配额给每个模型分配多少 KV Cache 空间是资源切分里最精细的活。Qwen3-8B 主要处理短文本问答业务场景下上下文很少超过 4K但我们一开始把--max-model-len设成了模型支持的上限 32K结果发现显存里大量空间被 KV Cache 预留着但根本用不满并发能力反而下去了。后来我把 max-model-len 按业务实际需求重新规划8B 设 8K32B 设 16KR1 设 32K为什么 R1 要设 32K因为 R1 擅长长链路推理思维链推理过程中 tokens 消耗很大太短会被截断导致逻辑不完整。但 R1 设 32K 也带来了问题它的 KV Cache 单序列占用比小模型大得多并发 8 个长序列就吃掉了几十 GB 显存这对并发上限是硬约束。这部分的取舍后面会给具体调参建议。3.3 路由与调度层实现网关路由的逻辑不复杂但要考虑几个协议细节。核心是根据请求里的model字段做匹配转发到对应 vLLM 实例。需要注意 vLLM 启动时--served-model-name可以自定义对外暴露的名字网关里的模型映射表和这里保持一致否则会出现客户端请求的模型名和服务端实际加载的模型对不上。我当时踩过一个坑vLLM 默认对外模型名是权重目录的名字比如Qwen/Qwen3-32B但网关里我们写的是qwen3-32b路由匹配不上请求全打到了一个默认的后端。后来统一了命名规范网关层负责外部请求的模型名到内部服务名的转换vLLM 启动时显式传--served-model-name两边完全对应。网关里还加了简单的限流和排队逻辑当目标实例并发满时先排队而不是直接打过去极大减少了 vLLM 侧的排队风暴。4. 核心参数配置与调优4.1 GPU 显存利用率怎么设才安全--gpu-memory-utilization是决定 KV Cache 大小的关键参数也是新手最容易乱调的。默认值 0.9 表示 90% 的显存可以参与分配剩下 10% 留给 CUDA context 和激活内存。我之前图省事设成 0.95压测时频繁出现 CUDA OOM后来才明白 5% 的余量对激活内存大的模型根本不够尤其在做张量并行和长序列推理时。我的经验是BF16 小模型可以设 0.85 到 0.9FP8 大模型设 0.82 到 0.85 比较安全。低于 0.7 没必要显存利用率太低服务吞吐会大打折扣。正确的做法是看 vLLM 启动日志里实际报告的 KV Cache 分配量如果 KV Cache 显存占大头且没有出现碎片再把利用率逐步往上提每次增加 0.02压测一轮看稳定性而不是一次就给满。4.2 max-model-len 的权衡与 KV Cache 的关系--max-model-len的重要性被我严重低估了很久。它的值决定了单个序列能处理的最大 token 长度同时直接影响 KV Cache 的预留粒度。vLLM 是按 block 分配 KV Cache 的每个 block 默认是 16 个 tokenmax-model-len 大了每个请求最多可能占用的 block 数就多在并发场景下 KV Cache 池的压力会指数级上升。举一个实际计算例子Qwen3-32B 有 64 层每层注意力头数 40头维度 128每个 KV 对用 FP16 存储每 token 的 KV Cache 显存大约是 2K 和 V× 64 × 40 × 128 × 2 字节 ≈ 1.3MB这个数字乘以 max-model-len 就是单个请求的最大 KV 占用。如果是 32K 上下文单个请求最多吃掉约 42MB 的 KV Cache峰值 32 个并发就是 1.3GB但这只是中间量实际 KV Cache 池还要按 block 预留碎片浪费再乘以一个系数。理解了这一点就明白为什么不能盲目把 max-model-len 拉高。4.3 并发与批处理相关参数--max-num-seqs控制一个实例最多同时处理多少请求序列默认值 256 对我来说偏高。在模型参数量大的时候同时处理太多序列会造成显存突发压力而且输出长度不一致的请求混在一起调度延迟会明显增大。我按实例规格做了调整8B 小模型设 12832B 设 64R1 设 32实测吞吐和延迟的平衡最舒服。还有个隐藏参数是--max-num-batched-tokens它决定一次 forward 最多处理多少个 token。这个值设小了对吞吐有瓶颈设大了会抬高单次推理延迟。一个参考逻辑max-num-batched-tokens max-num-seqs × 平均输出长度但不要超过模型在处理长序列时的显存承受范围。这些参数的组合关系在负载测试中花了一些时间才摸出最佳值每个模型的配置我都是单独测试后固化下来的。4.4 张量并行与模型并行怎么选--tensor-parallel-size在多卡部署时几乎是必设的。R1 因为参数量巨大我们用了 8 卡张量并行中间有一个调速点TP 数越多卡间通信开销越大不能单纯为了装下模型而无脑增加并行度。Qwen3-8B 这种小模型单卡就能跑没必要开 TP反而浪费通信带宽。Qwen3-32B 用 BF16 时需要 2 卡 TP如果用 AWQ INT4 量化后单卡 48GB 显存就能装下就可以不开 TP这样单实例吞吐更高。我的建议是能用单卡解决的问题绝对不要上多卡 TPTP 的通信开销在小模型上会抵消并行收益这在我们的性能测试里表现得很明显。4.5 前缀缓存与多轮对话性能--enable-prefix-caching在多轮对话和知识库场景下是性能放大器。它会把相同的前缀计算缓存复用比如对话历史中相同的 system prompt、相同的文档片段相当于把前面已经算好的 KV 直接拿来用。实测中长文档 QA 场景开启前缀缓存后 TTFT 可以降低 60% 以上。但前缀缓存有一个非常隐蔽的坑它依赖输入前缀完全一致任何微小的差异都会导致缓存 miss。我们遇到一次缓存命中率从 70% 暴跌到 5%排查了半天发现是聊天模板里时间戳字段导致的——每次请求都带当前时间前缀自然全变了。后来把时间戳移到不可缓存的位置命中率才恢复。另外多轮对话场景历史消息的拼接格式也要保持一致不同客户端可能产生不同的消息格式导致前缀不同无法复用。这个问题我会在排查章节再详细说。5. 生产环境常见问题排查实录5.1 显存明明够用却报 CUDA OOM这是我们第一次压测时最崩溃的问题nvidia-smi看显存还剩 20 多 GB但 vLLM 直接报 CUDA out of memory 然后进程退出。后来才搞明白vLLM 的显存管理是预分配的CUDA 上下文和激活内存可能临时吃掉大量显存而且显存碎片让连续内存分配失败。这有点像文件系统的磁盘碎片总空间够但找不到一块连续的大空间。解决方案分几层第一降低--gpu-memory-utilization给碎片和激活留更多余量第二打开--enforce-eager模式关闭 CUDA graph 后能大幅减少额外的显存占用代价是首 token 延迟略高但在显存紧张时这是救命选项第三确认多个 vLLM 实例之间没有共用同一张卡用nvidia-smi核对显存归属我们真遇到过CUDA_VISIBLE_DEVICES写错导致两个实例挤在一张卡上的情况。5.2 多实例并发导致 GPU 利用率失衡多个模型实例跑在同一台物理机上最怕出现资源打架。我们当时观察到一个怪现象R1 实例在跑批处理任务时把 GPU 计算单元占满同时 Qwen3-8B 实例的响应延迟飙升到正常值的 5 倍。但是看显存大家明明各用各的没有互相争抢。原因在于 GPU 的计算资源SM和显存是不同的维度虽然显存隔离了但计算单元是共享的。大模型的长序列推理会持续占满 SM 利用率小模型的请求只能排队等 SM 空闲。解决办法一方面是在 GPU 层面做资源分组比如用 NVIDIA MPS 限制每个实例的计算份额占比另一方面从调度上做时间片隔离在网关层针对不同模型的流量设置配额避免一个实例打满整台机器的算力。注意GPU 多实例部署的隔离手段是双层的显存隔离不够计算资源隔离同样要做。MPS 或 MIG 的配置要提前在测试环境验证。5.3 前缀缓存命中率低到让人怀疑人生前缀缓存命中率是影响延迟和吞吐的重要指标vLLM 的/metrics接口能直接看vllm:prefix_cache_hit_rate。我们排查过一个命中率异常的案例最后定位出的原因有这么多第一个是请求里带了时间戳和随机 ID 这类动态字段这在真实业务里太常见了代码里日志带上请求 ID 没问题但把这类动态信息塞进了 prompt 就会让前缀永远对不上。第二个是不同客户端构造聊天消息的格式不统一有人传messages有人已经把它拼成了字符串前缀自然不同。第三个是 system prompt 和 few-shot 示例的拼接顺序在不同的 prompt 模板版本里不一致。解决方案是把系统提示词、few-shot 示例、知识库文档这些静态内容放在前面且保持完全一致把动态内容放最后同时客户端统一消息格式。5.4 延迟抖动与长尾请求生产环境最直观的体验问题是 P99 延迟忽高忽低。监控发现一个大规律一旦有请求的上下文特别长或者输出 tokens 特别多它就会占用当前 batch 的 slot 拖慢同一批所有请求的完成时间表现为小请求的 TTFT 正常但 TPOT 明显变长尾巴一下子就翘起来了。vLLM 的动态 batching 虽然能不断有新的请求插进来但长请求还是会在一定程度阻塞后续批次。解决办法是在网关层对请求做更细的分流长上下文和高输出量的大模型请求走 R1 专用通道短小的问答走 Qwen3 通道互不混跑。另一个有效手段是限制单个请求的最大输出长度--max-tokens业务上合理控制输出上限避免个别请求无限制生成拖垮整个队列。5.5 模型加载慢与启动失败多模型部署时频繁重启是常态Qwen3-32B AWQ 量化版首次加载花了将近 10 分钟期间 KV Cache 还没有预热服务不可用时间太长。后来发现启动慢的瓶颈在权重的读取和格式转换。解决办法是提前用--load-format指定合适的权重加载格式比如直接用 safetensors 的 mmap 模式可以边加载边映射显著减少加载时间。另外用--max-parallel-load-workers增加并行加载 worker 数小模型加载时间能从几分钟压缩到几十秒。对于 R1 这种超大模型重启一次要很久生产上必须配合实现优雅退出和加载失败自动重试我会在下一章详细说。6. 生产落地必须补齐的周边能力6.1 健康检查与自动恢复vLLM 进程不能裸奔必须纳入进程守护。我们用的是 systemd 托管每个实例一个 service 单元配置了Restartalways进程崩溃后 3 秒内自动拉起。但光有守护还不够因为进程活着不代表服务健康比如 GPU 显存泄漏导致推理接口超时就需要业务健康检查来判断。健康检查我用的是两层第一层是 TCP 探活检查端口是否监听第二层调用/health接口确认推理引擎正常对于 OpenAI 兼容实现/v1/models接口也能用来做探活。当连续三次健康检查失败时systemd 配置Restart还不够我加了一个简单的 watchdog 脚本超过阈值直接杀掉进程触发重启这个脚本在显存泄漏场景下救过我们好几次。6.2 关键监控指标多模型并发的监控比单模型复杂因为要按实例维度分别看也要按模型维度汇总。我重点盯这几个指标vllm:num_requests_running和vllm:num_requests_waiting反映实例当前的负载和排队情况vllm:gpu_cache_usage_perc反映 KV Cache 的使用率长期 95% 以上就该考虑扩容了vllm:time_to_first_token_seconds和vllm:time_per_output_token_seconds是用户体验的核心指标。网关层还要记录每个模型的请求量、错误率和端到端延迟。我发现一个特别有用的信号是请求进入网关的时间戳和进入 vLLM 的时间戳差值如果这个差值持续增大说明是网关排队导致不是 vLLM 本身慢。这种分层监控的方法能快速定位延迟发生在哪一段。6.3 日志、限流与灰度多模型部署之后最怕改一个配置影响所有模型所以灰度很重要。我们的策略是网关层把请求按模型名和版本号路由新版本 vLLM 实例先只接 5% 的流量跑一段时间没问题再逐步放量。数据库里维护一份路由规则表调整灰度比例只需要改表不用改代码。限流方面除了简单的 QPS 限制还要注意按用户的并发数限流否则一两个大流量用户就能把 KV Cache 池占满导致其他用户全部排队。日志方面 vLLM 的默认日志比较碎片化我把每个实例的日志统一采集到日志中心并关联请求 ID这样跨实例排查问题时能串起完整链路。这一块投入产出比很高多模型并发环境没有统一日志排障基本靠猜。最后分享一个我实际测试了很久才总结出来的经验多模型并发部署真正的难点不是让单个模型跑得快而是让一群模型在有限的资源池里跑得稳。每个模型的参数配置不是孤立的你要在吞吐、延迟、显存、稳定性之间反复权衡。而且生产环境一定要有压测数据和监控指标做支撑没有数据的调参都是玄学。如果这轮踩坑经历能帮你少走几个弯路那我这篇文章就没白写。