1. 多模型并发部署的真实挑战与整体思路
把 DeepSeek-R1 和 Qwen3 这类大模型同时塞进一套推理服务里,还要扛住生产环境的并发量,这件事远比单模型跑个 demo 复杂得多。我最近刚完成一个多模型混部的推理集群改造,从最初的单卡单模型,到后来要在有限的 GPU 资源上同时服务两个不同架构、不同参数规模的模型,中间踩的坑足够写一本小册子。这篇文章就把整个部署思路、关键参数、排查过程完整拆开讲一遍,适合已经跑通过单模型推理、准备上多模型并发的同学参考。
先说清楚这套方案要解决的核心问题:在有限的 GPU 资源下,让 DeepSeek-R1 和 Qwen3 两个模型共享推理服务,同时保证并发请求下的吞吐和延迟可控。听起来简单,但实际落地时会遇到显存分配、KV Cache 抢占、调度策略冲突、模型加载顺序等一系列问题。我用的推理框架是 vLLM,原因是它对 PagedAttention 和连续批处理(continuous batching)的支持比较成熟,多模型场景下也有相对清晰的资源隔离机制。
整体思路可以概括为三个层次:资源层做显存预算和切分,框架层做多实例隔离,调度层做请求路由和优先级控制。这三层任何一层没处理好,都会导致线上出现 OOM、请求超时或者吞吐骤降。下面我会逐层拆解,把每个决策背后的逻辑讲透。
提示:多模型并发不是简单地把两个模型都加载进来就完事,核心矛盾在于显存是零和的,一个模型多吃一点,另一个就少一点,而两者的 KV Cache 需求是动态变化的。
2. 资源规划与显存预算的核心逻辑
2.1 为什么显存预算必须提前算清楚
很多人部署多模型时的第一反应是"先把模型加载起来看看能不能跑",这个思路在单模型时勉强可行,多模型时几乎必然翻车。原因是 vLLM 在启动时会根据gpu_memory_utilization参数预分配显存,如果两个实例都按默认的 0.9 去抢,第二个实例启动时就会直接失败。更隐蔽的问题是,即使两个实例都启动成功了,运行时的 KV Cache 增长也可能把显存吃满,触发抢占甚至 OOM。
我的做法是先算静态占用,再留动态余量。静态占用包括模型权重、激活值峰值、框架自身开销;动态余量主要是 KV Cache。以一张 80GB 显存的卡为例,DeepSeek-R1 的某个量化版本权重约占 40GB,Qwen3 的某个版本约占 20GB,两者加起来 60GB,剩下 20GB 要分给两个模型的 KV Cache 和运行时开销。这个账必须提前算,不能靠试。
2.2 显存切分的具体计算方法
KV Cache 的显存占用可以用一个简化公式估算:
KV Cache 显存 ≈ 2 × 层数 × 注意力头数 × 头维度 × 序列长度 × 并发数 × 数据类型字节数以 Qwen3 某个配置为例,假设 32 层、32 个注意力头、头维度 128、序列长度 4096、并发 16、FP16(2 字节):
2 × 32 × 32 × 128 × 4096 × 16 × 2 ≈ 34GB这个数字相当可观,所以并发数不能拍脑袋定。实际部署时我会先用小并发压测,观察 KV Cache 的实际增长曲线,再反推安全并发上限。DeepSeek-R1 因为层数和头数不同,需要单独算一遍。
| 模型 | 权重占用 | KV Cache(并发16) | 运行时开销 | 建议 gpu_memory_utilization |
|---|---|---|---|---|
| DeepSeek-R1 | 约 40GB | 约 20GB | 约 3GB | 0.75 |
| Qwen3 | 约 20GB | 约 12GB | 约 2GB | 0.45 |
注意:上表的 utilization 是相对单卡总显存的比例,两个实例的 utilization 之和不能超过 1,且要留出至少 5% 的系统余量,否则容易出现碎片化导致的分配失败。
2.3 多实例隔离还是单实例多模型
vLLM 支持两种多模型部署方式:一种是启动多个独立的 API Server 实例,每个实例加载一个模型;另一种是在同一个实例内通过 LoRA 等方式挂载多个模型。对于 DeepSeek-R1 和 Qwen3 这种架构差异大、权重都很大的情况,我强烈建议用多实例隔离。原因是单实例多模型在调度时会共享同一套调度器,一个模型的请求积压会拖累另一个模型的响应,而且显存管理也更复杂。
多实例隔离的代价是需要额外的端口管理和请求路由层,但换来的是故障隔离和资源可控。我当时的架构是每个模型一个独立进程,前面挂一个轻量级路由网关,根据请求里的模型标识转发到对应实例。这个网关可以用 Nginx 或者自己写一个简单的反向代理,关键是路由规则要清晰,避免请求打错实例。
3. vLLM 关键参数配置与实操要点
3.1 启动参数里最容易踩坑的几个
vLLM 的启动参数很多,但多模型场景下真正影响稳定性的就那么几个。我把它们分成三类:显存控制类、调度控制类、性能调优类。
显存控制类里,gpu_memory_utilization是重中之重,前面已经讲过。还有一个swap_space参数,它控制 CPU 内存作为显存溢出缓冲的大小,默认是 4GB。多模型场景下我建议适当调大,比如 8GB,因为当某个模型的 KV Cache 突然增长时,swap 能起到缓冲作用,避免直接 OOM。但要注意 swap 会带来延迟,不能当成常规手段。
调度控制类里,max_num_seqs控制单个批次的最大序列数,max_num_batched_tokens控制单批次的最大 token 数。这两个参数直接决定吞吐和延迟的平衡。我实测下来,DeepSeek-R1 这种大模型适合把max_num_seqs设小一点(比如 8-16),保证单请求延迟;Qwen3 可以设大一点(比如 32-64),提升吞吐。
# DeepSeek-R1 实例启动示例 python -m vllm.entrypoints.openai.api_server \ --model /path/to/deepseek-r1 \ --gpu-memory-utilization 0.75 \ --max-num-seqs 16 \ --max-num-batched-tokens 8192 \ --swap-space 8 \ --port 8001 # Qwen3 实例启动示例 python -m vllm.entrypoints.openai.api_server \ --model /path/to/qwen3 \ --gpu-memory-utilization 0.45 \ --max-num-seqs 48 \ --max-num-batched-tokens 16384 \ --swap-space 8 \ --port 80023.2 张量并行与流水线并行的选择
如果单卡放不下一个模型,就需要用到张量并行(TP)或流水线并行(PP)。DeepSeek-R1 这种大模型通常需要 TP。但多模型场景下,TP 的通信开销会和其他模型的推理争抢资源,所以 TP 的度数要谨慎选。我的经验是,能用单卡就别用 TP,必须用 TP 时尽量让 TP 组独占物理链路,避免跨模型通信干扰。
如果两个模型都要 TP,且 GPU 数量有限,可以考虑让它们分时复用同一组 GPU,但这会引入复杂的调度逻辑,生产环境不太推荐。更稳妥的做法是给每个模型分配固定的 GPU 集合,通过CUDA_VISIBLE_DEVICES隔离。
3.3 模型加载顺序与预热
多实例启动时,加载顺序会影响启动成功率和首次请求延迟。我踩过的坑是:两个实例同时启动,同时抢显存,结果都失败。后来改成串行启动,先启动大模型,等它完全就绪后再启动小模型,成功率大幅提升。
预热也很关键。vLLM 首次推理会有编译和缓存初始化的开销,如果不预热,第一个真实请求的延迟会高得离谱。我的做法是启动后立即发几个不同长度的预热请求,覆盖短序列和长序列,让 KV Cache 和 CUDA Graph 都初始化到位。
实操心得:预热请求的序列长度要覆盖实际业务的 P50 和 P99,否则长请求第一次进来还是会触发重新编译,延迟抖动明显。
4. 并发调度与请求路由的实战细节
4.1 请求路由层的设计
多实例隔离后,请求怎么分发就成了关键。最简单的做法是按模型名路由,请求里带model字段,网关根据字段转发。但生产环境往往更复杂,比如同一个模型可能有多个副本,需要做负载均衡;不同优先级的请求需要走不同的队列。
我的路由层设计了三层规则:第一层按模型名分流,第二层按请求优先级分流,第三层做副本间的轮询。优先级这块,我把在线交互请求设为高优先级,批处理任务设为低优先级,高优先级请求可以抢占低优先级的调度资源。vLLM 本身支持请求优先级,但需要在启动时开启相关配置。
4.2 连续批处理下的并发控制
vLLM 的连续批处理是它的核心优势,但在多模型场景下,如果两个模型的请求量差异很大,会出现资源倾斜。比如 Qwen3 的请求量是 DeepSeek-R1 的十倍,那么 Qwen3 实例的 GPU 利用率会很高,而 DeepSeek-R1 实例可能大部分时间空闲。这时候可以考虑动态调整两个实例的max_num_seqs,或者用更激进的批处理策略。
但动态调整有风险,参数改大了可能导致显存溢出。我的做法是设置一个安全区间,在这个区间内根据实时负载微调,而不是无限制地调。具体来说,我会监控每个实例的 KV Cache 使用率和请求排队长度,当排队长度超过阈值且显存有余量时,适当提高max_num_seqs。
4.3 超时与重试机制
多模型并发时,超时和重试是必须处理的。一个模型的请求超时不应该影响另一个模型,所以超时时间要按模型分别设置。DeepSeek-R1 因为模型大,推理慢,超时时间要设长一点,比如 60 秒;Qwen3 可以设短一点,比如 30 秒。
重试也要小心。如果重试策略不当,一个慢请求反复重试会加剧资源争抢。我的做法是只对特定错误码重试,且限制重试次数,同时对重试请求做降级处理,比如降低优先级或者缩短最大生成长度。
| 问题场景 | 现象 | 排查方向 | 解决方案 |
|---|---|---|---|
| 启动失败 | 第二个实例 OOM | 检查 utilization 之和 | 降低各自 utilization,串行启动 |
| 请求超时 | 某模型响应慢 | 查看排队长度和 KV Cache | 调整 max_num_seqs,增加副本 |
| 吞吐骤降 | 两模型互相拖累 | 检查 GPU 利用率和通信 | 隔离 GPU,调整批处理参数 |
| 显存碎片 | 运行一段时间后 OOM | 查看显存分配日志 | 开启 swap,定期重启实例 |
5. 常见问题排查与避坑经验实录
5.1 显存相关问题的排查思路
显存问题是最常见的,表现也最多样。轻则启动失败,重则运行中 OOM。排查时我一般按这个顺序:先看启动日志里的显存分配情况,确认两个实例的 utilization 之和是否合理;再看运行时的显存增长曲线,判断是 KV Cache 增长过快还是内存泄漏。
有一个隐蔽的坑是显存碎片化。即使总显存够用,碎片化也会导致大块分配失败。vLLM 的 PagedAttention 本身就是为了缓解碎片化,但在多实例场景下,两个实例的分配模式不同,碎片化会更严重。我的应对方法是定期重启实例,或者用PYTORCH_CUDA_ALLOC_CONF调整分配策略。
5.2 性能不达预期的调优路径
如果并发上不去或者延迟偏高,调优要按层次来。先看 GPU 利用率,如果利用率低,说明批处理没吃满,可以调大max_num_batched_tokens;如果利用率高但延迟也高,说明计算是瓶颈,需要考虑加卡或者换更高效的量化方案。
还有一个容易被忽略的点是CPU 和网络瓶颈。多实例场景下,请求路由、序列化、网络传输的开销会叠加,如果 CPU 核数不够,会成为隐形瓶颈。我当时的做法是给路由层单独分配 CPU 资源,避免和推理进程争抢。
5.3 多模型版本管理的坑
生产环境不可能只部署一次,模型版本更新是常态。多模型场景下,版本管理更复杂,因为两个模型的更新节奏可能不同。我的经验是给每个模型实例打上版本标签,路由层根据标签做灰度。新版本先接少量流量,观察稳定后再全量。
更新时要注意,不能直接 kill 旧实例再启动新实例,那样会有服务中断。正确的做法是启动新实例,等它就绪后,路由层把流量切过去,再优雅关闭旧实例。这个过程要保证显存够用,所以更新前要确认新实例的显存需求。
注意:优雅关闭时,vLLM 需要时间处理完队列里的请求,如果直接 kill,正在处理的请求会失败。建议设置一个合理的 drain 时间,比如 30 秒。
6. 生产环境稳定性保障的几个关键动作
6.1 监控指标的设计
多模型并发部署,监控必须做到模型级别。我重点监控这几个指标:每个实例的请求排队长度、KV Cache 使用率、GPU 利用率、首 token 延迟、每 token 延迟。这些指标能提前暴露大部分问题。
排队长度持续增长说明资源不足,KV Cache 使用率接近上限说明显存紧张,首 token 延迟突增说明调度或编译出了问题。把这些指标做成看板,设置合理的告警阈值,能在用户感知之前发现问题。
6.2 压测与容量规划
上线前必须压测,而且要分模型压、混合压。分模型压是为了摸清每个模型的极限,混合压是为了看两个模型互相干扰的程度。我当时的压测方案是用不同并发梯度逐步加压,记录每个梯度的延迟和吞吐,找到性能拐点。
容量规划要留余量。我的经验是按峰值流量的 1.5 倍规划资源,因为多模型场景下的资源争抢比单模型更难预测。如果资源实在紧张,可以考虑对低优先级请求做限流或降级。
6.3 故障演练与应急预案
生产环境要有应急预案。我做过几次故障演练,模拟一个模型实例挂掉的情况,验证路由层能否自动摘除故障实例,以及剩余实例能否承接流量。演练中发现的问题比想象中多,比如路由层的健康检查间隔太长,导致故障实例被摘除前已经积累了大量失败请求。
应急预案要包括:实例挂掉后的流量切换、显存不足时的降级策略、模型更新失败时的回滚方案。这些都要提前写好脚本,出事时直接执行,而不是临时想办法。
7. 一些个人体会与后续优化方向
这套多模型并发方案跑了一段时间,整体稳定性还不错,但也不是没有遗憾。最大的体会是显存永远是稀缺资源,任何优化都要围绕显存做文章。量化、KV Cache 压缩、请求调度优化,本质上都是在省显存。后续我打算尝试更激进的量化方案,以及基于请求特征的动态批处理策略,进一步提升资源利用率。
另一个体会是多模型部署的复杂度主要来自隔离和调度,而不是模型本身。把两个模型分别跑起来不难,难的是让它们和平共处。所以如果你的场景对延迟不敏感,其实可以考虑分时复用,白天跑在线模型,晚上跑批处理模型,这样资源利用率反而更高。
最后分享一个小技巧:vLLM 的日志级别可以调,排查问题时把日志开到 DEBUG,能看到很多调度和显存分配的细节。但生产环境记得调回去,DEBUG 日志量很大,会影响性能。这个细节看起来小,但排查问题时能省不少时间。