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 字段、版本号或流量比例,把请求分发到对应执行单元。路由可以是静态映射(模型名→进程端口),也可以是动态的(按队列深度、按权重灰度)。路由层必须处理模型未加载、版本不匹配、超时降级三类异常。

多模型服务四种组织模式与路由关系 请求入口 / 路由层 model 字段 · 版本 · 权重 A 进程内多引擎 单进程 · 共享 CUDA context Engine A + ctx×2 Engine B + ctx×1 Engine C + ctx×1 低延迟 · 崩溃域共享 B 多进程隔离 每模型独立进程 + 显存池 Proc 1 · Engine A Proc 2 · Engine B Proc 3 · Engine C 强隔离 · IPC 拷贝开销 C 多实例扩容 同模型复制执行单元 Engine A · ctx #1 Engine A · ctx #2 Engine A · ctx #3 提吞吐 · 显存线性增长 D 混合 热模型进程内 冷模型独立进程 热 A/B 常驻 冷 C 按需加载 成本与延迟折中 选型依据:按约束条件逐项判断 显存预算 总显存 < 单卡 70% → 进程内多引擎 单模型 > 8 GB → 多进程独占 延迟目标 P99 < 10 ms → 避免跨进程拷贝 P99 > 50 ms → 多进程可接受 吞吐与利用率 GPU 利用率 < 60% → 增加实例数 SM 已饱和 → 加实例无收益 隔离与运维 需独立重启/灰度 → 多进程 模型频繁上下线 → 混合模式

路由层的关键设计

路由不是简单的 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增加实例数计算资源未饱和,复制有效
需独立灰度/重启发布频率、故障域多进程隔离崩溃与版本
热模型稳定 + 冷模型偶发调用频次分布混合模式热模型常驻,冷模型按需
判断实例数是否该继续增加,看两个数:QPS 的边际增量与 P99 的边际增量。前者趋近于零、后者明显上升时,就是该模型的实例数上限。这个上限由 SM 占用和显存带宽共同决定,不能只看显存剩余。
多进程模式下,张量通过共享内存传递时要注意生命周期:发送方写入后不能立即释放缓冲区,必须等接收方确认读取完成。否则会出现偶发的数据错乱,且难以复现。建议在共享内存头部放置序号与状态标志,接收方读完置位后再由发送方回收。

最后需要强调,四种模式并非互斥。真实服务常采用混合结构:把调用量最高、延迟最敏感的 1~2 个模型放在同一进程内共享 CUDA context,把显存需求大或更新频繁的模型拆成独立进程,再由统一路由层按 least_queue 分发。选型的核心不是追求某种“最优架构”,而是让每个模型的显存、延迟、隔离需求与承载它的执行单元匹配。