3. 多模型服务架构设计
当服务需要同时承载多个模型时,真正决定性能上限与运维复杂度的,往往不是单个引擎的优化程度,而是模型与进程、显存、请求队列之间的组织方式。TensorRT 的 ICudaEngine 本身是线程安全的只读对象,但 IExecutionContext 不是线程安全的,且每个 context 会占用独立的显存用于激活值、workspace 与 CUDA stream。因此“多模型服务”本质上是在回答三个问题:引擎放在哪个进程、context 如何分配、请求如何路由到正确的执行单元。
四种基本组织模式
进程内多引擎:一个 Python/C++ 进程内加载多个 ICudaEngine,每个模型配一个或多个 IExecutionContext,共享同一个 CUDA context。优点是显存共享、无进程间通信(IPC)开销、路由就是一次字典查找;缺点是 GIL 与单进程崩溃域,任一模型 OOM 或段错误会拖垮全部模型。适合模型数量少(2~5 个)、总显存可控、延迟敏感的场景。
多进程:每个模型(或每组模型)跑在独立进程,各自持有 CUDA context 与显存池,通过 Unix Domain Socket、gRPC 或共享内存传递张量。隔离性好,单模型崩溃不影响其他模型,可独立重启与灰度;代价是显存无法跨进程复用(CUDA context 本身约 300~500 MB),且张量跨进程拷贝会引入 0.1~2 ms 量级开销。适合模型显存需求大、需要独立伸缩的场景。
多实例:同一模型在同一进程或同一 GPU 上复制多份执行单元(多 context 或多进程副本),用于提升吞吐。关键约束是显存:N 个 context 的激活显存近似线性增长,而计算资源是共享的。经验上,当单实例 GPU 利用率低于 60% 且显存有余量时,增加实例数收益明显;当 SM 占用已接近饱和,增加实例只会加剧排队。
模型路由:在入口层根据请求中的 model 字段、版本号或流量比例,把请求分发到对应执行单元。路由可以是静态映射(模型名→进程端口),也可以是动态的(按队列深度、按权重灰度)。路由层必须处理模型未加载、版本不匹配、超时降级三类异常。
路由层的关键设计
路由不是简单的 if-else。生产环境至少需要处理三类情况。第一,模型未加载:请求到达时目标引擎尚未完成反序列化,应返回 503 并携带 Retry-After,而不是阻塞等待。第二,版本不匹配:请求指定 model_version,但当前只加载了默认版本,应按策略决定是回退到默认版本还是拒绝。第三,超时降级:当某模型队列深度超过阈值,路由层可将其流量按比例切到备用模型或返回降级结果。
一个可用的路由配置示例(YAML 形式,字段为示例设计):
models:
- name: det_v2
version: "2.1"
mode: inproc # inproc | multiproc
engine: /engines/det_v2.plan
contexts: 2 # 多实例数
max_queue: 32
timeout_ms: 20
fallback: det_v1
- name: cls_v3
version: "3.0"
mode: multiproc
endpoint: unix:///tmp/cls_v3.sock
max_queue: 64
timeout_ms: 50
routing:
strategy: least_queue # least_queue | weighted | hash
unknown_model: reject
version_mismatch: fallback
其中 contexts: 2 表示该模型在进程内创建两个 IExecutionContext,路由层用 least_queue 策略把请求分给队列更短的 context。fallback: det_v1 表示 det_v2 超时或不可用时切到 v1。这些字段的取值需要结合实测:先用单 context 压测得到基线 QPS 与 P99,再逐步增加 contexts,观察 QPS 是否近似线性增长、P99 是否恶化。若 QPS 增长不足 30% 而 P99 上升超过 20%,说明已到 SM 瓶颈,应停止加实例。
选型决策的量化依据
把上述判断整理成可执行的决策表,比凭经验拍板更可靠。下表给出示例阈值,实际项目应替换为压测数据。
| 约束 | 观察指标 | 推荐模式 | 理由 |
|---|---|---|---|
| 模型数 ≤ 5,总显存 < 70% | 显存占用、P99 | 进程内多引擎 | 无 IPC,路由开销最低 |
| 单模型显存 > 8 GB | 单模型显存峰值 | 多进程独占 | 避免 context 显存叠加 OOM |
| GPU 利用率 < 60% | SM 占用、QPS | 增加实例数 | 计算资源未饱和,复制有效 |
| 需独立灰度/重启 | 发布频率、故障域 | 多进程 | 隔离崩溃与版本 |
| 热模型稳定 + 冷模型偶发 | 调用频次分布 | 混合模式 | 热模型常驻,冷模型按需 |
最后需要强调,四种模式并非互斥。真实服务常采用混合结构:把调用量最高、延迟最敏感的 1~2 个模型放在同一进程内共享 CUDA context,把显存需求大或更新频繁的模型拆成独立进程,再由统一路由层按 least_queue 分发。选型的核心不是追求某种“最优架构”,而是让每个模型的显存、延迟、隔离需求与承载它的执行单元匹配。