2. TensorRT核心机制回顾
在进入多模型推理服务工程之前,必须先把 TensorRT 的运行时对象模型理清楚。很多服务化问题的根源,并不是网络结构或算子本身,而是对“引擎、上下文、执行器、绑定”这几个对象的生命周期与共享关系理解不到位。本节从对象职责、内存绑定、动态形状三个角度回顾核心机制,并给出面向多模型服务的判断依据。
一、四个核心对象的职责边界
Engine(引擎)是序列化后的推理计划在内存中的只读表示,它由 Builder 从网络定义与优化配置生成,包含层融合结果、kernel 选择、精度策略(FP32/FP16/INT8)、工作空间需求以及可用的优化 profile。Engine 一旦构建完成就不可修改,可以序列化到磁盘,也可以被多个执行上下文共享。它的关键属性是“只读、可复用、构建昂贵”。
ExecutionContext(执行上下文)是 Engine 的一次运行实例,持有推理过程中需要的中间激活内存、kernel 状态和流绑定。同一个 Engine 可以创建多个 Context,它们共享权重与 kernel,但各自拥有独立的激活缓冲区。因此 Context 是并发推理的最小单位:单 Context 内部推理是串行的,多 Context 才能实现真正的并行执行。
Executor(执行器)是较新版本 TensorRT 引入的抽象,用于管理一组 Context 与一条 CUDA Stream 的绑定关系,简化了多流调度。在传统 API 中,开发者需要自己维护 stream 与 context 的对应;使用 Executor 后,可以把“一个模型 + 若干并发槽位”封装成一个可调度单元,这对多模型服务尤其友好。
Binding(绑定)描述的是输入输出张量在设备内存中的地址映射。每个 Binding 有一个索引、名称、维度、数据类型和内存指针。推理前必须把所有 Binding 的地址设置好,TensorRT 才能按计划读写。绑定不是数据本身,而是“张量名 → 设备地址”的契约。
二、内存绑定与推理调用链
一次完整的推理调用可以拆成四步:准备输入数据到主机内存、拷贝到设备内存、设置 Binding 地址、调用 enqueue 并同步。其中最容易出错的是第三步——Binding 地址必须与 Engine 期望的维度、数据类型、内存对齐一致。如果使用动态形状,还需要在设置地址之前调用 setInputShape() 指定本次推理的实际输入维度。
// 伪代码:单 Context 推理调用链
context->setInputShape("input_0", Dims4{1, 3, 224, 224});
context->setTensorAddress("input_0", d_input);
context->setTensorAddress("output_0", d_output);
context->enqueueV3(stream);
cudaStreamSynchronize(stream);
注意 setTensorAddress 是“设置契约”,不是拷贝数据。真正的数据搬运需要开发者自己用 cudaMemcpyAsync 完成。这意味着在多模型服务中,输入输出缓冲区应当按 Context 独立分配,避免并发推理时互相覆盖。
三、动态形状与 Optimization Profile
动态形状是 TensorRT 支持变长输入的核心机制。构建 Engine 时,需要为每个输入张量指定一个或多个 Optimization Profile,每个 Profile 包含 min、opt、max 三组维度。opt 维度是性能最优的参考点,TensorRT 会围绕它选择 kernel;min 和 max 定义了合法输入范围。
运行时,输入维度必须落在某个 Profile 的 [min, max] 区间内。如果实际输入超出所有 Profile 的范围,推理会失败,必须重新构建 Engine 或增加 Profile。对于多模型服务,建议把同一模型的常见输入尺寸归并到少量 Profile 中,而不是为每个尺寸单独建 Engine,否则显存和构建时间都会迅速膨胀。
四、面向多模型服务的关键判断
把上述机制映射到多模型服务,可以得到几条明确的工程判断:
- Engine 按模型共享:同一模型的所有请求复用同一个 Engine,避免重复构建。
- Context 按并发度分配:并发数决定 Context 数量,每个 Context 独立绑定输入输出缓冲区。
- 显存预算 = 权重 + Context 数 × 激活内存:激活内存随 Context 线性增长,是并发上限的主要约束。
- 动态形状用 Profile 收敛:把请求维度归并到有限 Profile,减少 Engine 数量与切换开销。
- Binding 地址按 Context 隔离:禁止多个 Context 共用同一块输入输出缓冲区。