Triton 架构#
下图展示了 Triton 推理服务器的高级架构。模型仓库是一个基于文件系统的仓库,用于存放 Triton 将用于推理的模型。推理请求通过 HTTP/REST 或 GRPC 或 C API 到达服务器,然后被路由到相应的每个模型的调度器。Triton 实现了多种调度和批处理算法,可以基于每个模型进行配置。每个模型的调度器可以选择性地对推理请求进行批处理,然后将请求传递给与模型类型相对应的 后端。后端使用批处理请求中提供的输入执行推理,以生成请求的输出。然后返回输出。
Triton 支持 后端 C API,允许使用新功能扩展 Triton,例如自定义预处理和后处理操作,甚至新的深度学习框架。
Triton 服务的模型可以通过专用的 模型管理 API 进行查询和控制,该 API 可通过 HTTP/REST 或 GRPC 协议或 C API 获得。
就绪性和活动性健康端点以及利用率、吞吐量和延迟指标简化了 Triton 集成到 Kubernetes 等部署框架中的过程。
并发模型执行#
Triton 架构允许在同一系统上并行执行多个模型和/或同一模型的多个实例。系统可以有零个、一个或多个 GPU。下图显示了一个包含两个模型的示例:model0 和 model1。假设 Triton 当前未处理任何请求,当两个请求同时到达时,每个模型一个请求,Triton 会立即将它们都调度到 GPU 上,GPU 的硬件调度器开始并行处理这两个计算。在系统 CPU 上执行的模型由 Triton 以类似方式处理,不同之处在于每个模型 CPU 线程执行的调度由系统的操作系统处理。
默认情况下,如果同一模型的多个请求同时到达,Triton 将通过在 GPU 上一次调度一个来序列化它们的执行,如下图所示。
Triton 提供了一个 模型配置选项,称为 instance-group,允许每个模型指定应允许该模型的多少个并行执行。每个启用的并行执行都称为一个实例。默认情况下,Triton 为系统中的每个可用 GPU 为每个模型提供一个实例。通过使用模型配置中的 instance_group 字段,可以更改模型的执行实例数。下图显示了当 model1 配置为允许三个实例时的模型执行。如图所示,前三个 model1 推理请求立即并行执行。第四个 model1 推理请求必须等到前三个执行中的一个完成后才能开始。
模型和调度器#
Triton 支持多种调度和批处理算法,可以为每个模型独立选择。本节介绍无状态、有状态和集成模型,以及 Triton 如何提供调度器来支持这些模型类型。对于给定的模型,调度器的选择和配置通过 模型的配置文件 完成。
无状态模型#
关于 Triton 的调度器,无状态模型在推理请求之间不维护状态。在无状态模型上执行的每次推理都独立于使用该模型的所有其他推理。
无状态模型的示例是 CNN,例如图像分类和对象检测。默认调度器或 动态批处理器 可以用作这些无状态模型的调度器。
RNN 和类似的具有内部存储器的模型可以是无状态的,只要它们维护的状态不跨越推理请求。例如,如果 RNN 在推理请求批次之间不携带内部状态,则迭代批次中所有元素的 RNN 被 Triton 视为无状态。 默认调度器 可以用于这些无状态模型。动态批处理器 不能使用,因为模型通常不期望批次代表多个推理请求。
有状态模型#
关于 Triton 的调度器,有状态模型在推理请求之间维护状态。模型期望多个推理请求一起形成一个推理序列,这些请求必须路由到相同的模型实例,以便模型维护的状态得到正确更新。此外,模型可能要求 Triton 提供控制信号,例如指示序列的开始和结束。
序列批处理器 必须用于这些有状态模型。如下所述,序列批处理器确保序列中的所有推理请求都路由到相同的模型实例,以便模型可以正确维护状态。序列批处理器还与模型通信,以指示序列何时开始、序列何时结束、序列何时有推理请求准备好执行以及序列的关联 ID。
当为有状态模型发出推理请求时,客户端应用程序必须为序列中的所有请求提供相同的关联 ID,并且还必须标记序列的开始和结束。关联 ID 允许 Triton 识别请求属于同一序列。
控制输入#
为了使有状态模型与序列批处理器正确运行,模型通常必须接受一个或多个控制输入张量,Triton 使用这些张量与模型通信。模型配置的 ModelSequenceBatching::Control 部分指示模型如何公开序列批处理器应用于这些控制的张量。所有控件都是可选的。下面是模型配置的一部分,显示了所有可用控制信号的示例配置。
sequence_batching {
control_input [
{
name: "START"
control [
{
kind: CONTROL_SEQUENCE_START
fp32_false_true: [ 0, 1 ]
}
]
},
{
name: "END"
control [
{
kind: CONTROL_SEQUENCE_END
fp32_false_true: [ 0, 1 ]
}
]
},
{
name: "READY"
control [
{
kind: CONTROL_SEQUENCE_READY
fp32_false_true: [ 0, 1 ]
}
]
},
{
name: "CORRID"
control [
{
kind: CONTROL_SEQUENCE_CORRID
data_type: TYPE_UINT64
}
]
}
]
}
开始:开始输入张量使用配置中的 CONTROL_SEQUENCE_START 指定。示例配置表明模型有一个名为 START 的输入张量,其数据类型为 32 位浮点型。序列批处理器将在模型上执行推理时定义此张量。START 张量必须是一维的,大小等于批次大小。张量中的每个元素指示相应批次槽中的序列是否正在开始。在示例配置中,fp32_false_true 表示序列开始由张量元素等于 1 指示,非开始由张量元素等于 0 指示。
结束:结束输入张量使用配置中的 CONTROL_SEQUENCE_END 指定。示例配置表明模型有一个名为 END 的输入张量,其数据类型为 32 位浮点型。序列批处理器将在模型上执行推理时定义此张量。END 张量必须是一维的,大小等于批次大小。张量中的每个元素指示相应批次槽中的序列是否正在结束。在示例配置中,fp32_false_true 表示序列结束由张量元素等于 1 指示,非结束由张量元素等于 0 指示。
就绪:就绪输入张量使用配置中的 CONTROL_SEQUENCE_READY 指定。示例配置表明模型有一个名为 READY 的输入张量,其数据类型为 32 位浮点型。序列批处理器将在模型上执行推理时定义此张量。READY 张量必须是一维的,大小等于批次大小。张量中的每个元素指示相应批次槽中的序列是否具有准备好进行推理的推理请求。在示例配置中,fp32_false_true 表示序列就绪由张量元素等于 1 指示,非就绪由张量元素等于 0 指示。
关联 ID:关联 ID 输入张量使用配置中的 CONTROL_SEQUENCE_CORRID 指定。示例配置表明模型有一个名为 CORRID 的输入张量,其数据类型为无符号 64 位整数型。序列批处理器将在模型上执行推理时定义此张量。CORRID 张量必须是一维的,大小等于批次大小。张量中的每个元素指示相应批次槽中序列的关联 ID。
隐式状态管理#
隐式状态管理允许有状态模型将其状态存储在 Triton 内部。使用隐式状态时,有状态模型无需将推理所需的状态存储在模型内部。
下面是模型配置的一部分,指示模型正在使用隐式状态。
sequence_batching {
state [
{
input_name: "INPUT_STATE"
output_name: "OUTPUT_STATE"
data_type: TYPE_INT32
dims: [ -1 ]
}
]
}
sequence_batching 设置中的 state 部分用于指示模型正在使用隐式状态。input_name 字段指定将包含输入状态的输入张量的名称。output_name 字段描述模型生成的包含输出状态的输出张量的名称。模型在序列中第 ith 个请求中提供的输出状态将用作第 i+1th 个请求中的输入状态。dims 字段指定状态张量的维度。当 dims 字段包含可变大小的维度时,输入状态和输出状态的形状不必匹配。
出于调试目的,客户端可以请求输出状态。为了允许客户端请求输出状态,模型配置的 output 部分 必须将输出状态列为模型输出之一。请注意,从客户端请求输出状态可能会增加请求延迟,因为需要传输额外的张量。
隐式状态管理需要后端支持。目前,只有 onnxruntime_backend、tensorrt_backend 和 pytorch_backend 支持隐式状态。
状态初始化#
默认情况下,序列中的起始请求包含输入状态的未初始化数据。模型可以使用请求中的 start 标志来检测新序列的开始,并通过在模型输出中提供初始状态来初始化模型状态。如果模型的 state 描述中的 dims 部分包含可变大小的维度,Triton 将对起始请求的每个可变大小的维度使用 1。对于序列中其他非起始请求,输入状态是序列中先前请求的输出状态。有关使用隐式状态的 ONNX 模型示例,您可以参考从 create_onnx_modelfile_wo_initial_state()
从这个生成脚本 生成的此 onnx 模型。这是一个简单的累加器模型,它使用隐式状态在 Triton 中存储序列中请求的部分和。对于状态初始化,如果请求正在开始,则模型将“OUTPUT_STATE”设置为等于“INPUT”张量。对于非起始请求,它将“OUTPUT_STATE”张量设置为“INPUT”和“INPUT_STATE”张量的总和。
除了上面讨论的默认状态初始化之外,Triton 还提供了两种其他机制来初始化状态。
从零初始化状态。#
下面是从零初始化状态的示例。
sequence_batching {
state [
{
input_name: "INPUT_STATE"
output_name: "OUTPUT_STATE"
data_type: TYPE_INT32
dims: [ -1 ]
initial_state: {
data_type: TYPE_INT32
dims: [ 1 ]
zero_data: true
name: "initial state"
}
}
]
}
请注意,在上面的示例中,状态描述中的可变维度转换为固定大小维度。
从文件初始化状态#
要从文件初始化状态,您需要在模型目录下创建一个名为“initial_state”的目录。包含初始状态的文件需要在此目录下的 data_file 字段中提供。存储在此文件中的数据将以行优先顺序用作初始状态。下面是从文件初始化状态的示例状态描述。
sequence_batching {
state [
{
input_name: "INPUT_STATE"
output_name: "OUTPUT_STATE"
data_type: TYPE_INT32
dims: [ -1 ]
initial_state: {
data_type: TYPE_INT32
dims: [ 1 ]
data_file: "initial_state_data"
name: "initial state"
}
}
]
}
调度策略#
序列批处理器在决定如何批处理路由到同一模型实例的序列时,可以采用两种调度策略:直接 和 最旧。
直接#
使用直接调度策略,序列批处理器不仅确保序列中的所有推理请求都路由到相同的模型实例,还确保每个序列都路由到模型实例内的专用批次槽。当模型为每个批次槽维护状态,并期望给定序列的所有推理请求都路由到相同的槽时,以便正确更新状态,则需要此策略。
作为使用直接调度策略的序列批处理器的示例,假设一个 TensorRT 有状态模型具有以下模型配置。
name: "direct_stateful_model"
platform: "tensorrt_plan"
max_batch_size: 2
sequence_batching {
max_sequence_idle_microseconds: 5000000
direct { }
control_input [
{
name: "START"
control [
{
kind: CONTROL_SEQUENCE_START
fp32_false_true: [ 0, 1 ]
}
]
},
{
name: "READY"
control [
{
kind: CONTROL_SEQUENCE_READY
fp32_false_true: [ 0, 1 ]
}
]
}
]
}
input [
{
name: "INPUT"
data_type: TYPE_FP32
dims: [ 100, 100 ]
}
]
output [
{
name: "OUTPUT"
data_type: TYPE_FP32
dims: [ 10 ]
}
]
instance_group [
{
count: 2
}
]
sequence_batching 部分指示模型应使用序列批处理器和直接调度策略。在此示例中,模型仅需要序列批处理器的开始和就绪控制输入,因此仅列出了这些控件。instance_group 指示应实例化模型的两个实例,max_batch_size 指示每个实例应执行批次大小为 2 的推理。下图显示了此配置指定的序列批处理器和推理资源的表示。
每个模型实例都为每个批次槽维护状态,并期望给定序列的所有推理请求都路由到相同的槽,以便正确更新状态。对于此示例,这意味着 Triton 可以同时为最多四个序列执行推理。
使用直接调度策略,序列批处理器
识别推理请求何时开始新序列,并为该序列分配批次槽。如果新序列没有可用的批次槽,Triton 会将推理请求放入积压队列中。
识别推理请求何时属于已分配批次槽的序列,并将请求路由到该槽。
识别推理请求何时属于积压队列中的序列,并将请求放入积压队列中。
识别序列中的最后一个推理请求何时完成。该序列占用的批次槽会立即重新分配给积压队列中的序列,如果积压队列中没有序列,则释放以供将来的序列使用。
下图显示了如何使用直接调度策略将多个序列调度到模型实例上。在左侧,该图显示了到达 Triton 的多个请求序列。每个序列可以由任意数量的推理请求组成,并且这些单独的推理请求可以相对于其他序列中的推理请求以任何顺序到达,只是右侧显示的执行顺序假设序列 0 的第一个推理请求在序列 1-5 中的任何推理请求之前到达,序列 1 的第一个推理请求在序列 2-5 中的任何推理请求之前到达,依此类推。
该图的右侧显示了推理请求序列如何随着时间推移调度到模型实例上。
下图显示了序列批处理器如何使用控制输入张量与模型通信。该图显示了分配给模型实例中两个批次槽的两个序列。每个序列的推理请求随时间到达。START 和 READY 行显示了模型每次执行时使用的输入张量值。随着时间的推移,会发生以下情况
序列在槽 0 中的第一个请求到达。假设模型实例尚未执行推理,则序列调度器立即调度模型实例执行,因为有推理请求可用。
这是序列中的第一个请求,因此 START 张量中的相应元素设置为 1。槽 1 中没有可用的请求,因此 READY 张量仅显示槽 0 准备就绪。
推理完成后,序列调度器看到任何批次槽中都没有可用的请求,因此模型实例处于空闲状态。
接下来,两个推理请求在时间上非常接近地到达,以便序列调度器看到它们在各自的批次槽中都可用。调度器立即调度模型实例执行批次大小为 2 的推理,并使用 START 和 READY 来显示两个槽都有可用的推理请求,但只有槽 1 是新序列的开始。
处理以类似方式继续进行其他推理请求。
最旧#
使用最旧调度策略,序列批处理器确保序列中的所有推理请求都路由到相同的模型实例,然后使用 动态批处理器 将来自不同序列的多个推理批处理到一个批次中,以便一起推理。使用此策略,模型通常必须使用 CONTROL_SEQUENCE_CORRID 控制,以便它知道批次中每个推理请求属于哪个序列。通常不需要 CONTROL_SEQUENCE_READY 控制,因为批次中的所有推理将始终准备好进行推理。
作为使用最旧调度策略的序列批处理器的示例,假设一个有状态模型具有以下模型配置
name: "oldest_stateful_model"
platform: "tensorflow_savedmodel"
max_batch_size: 2
sequence_batching {
max_sequence_idle_microseconds: 5000000
oldest
{
max_candidate_sequences: 4
}
control_input [
{
name: "START"
control [
{
kind: CONTROL_SEQUENCE_START
fp32_false_true: [ 0, 1 ]
}
]
},
{
name: "END"
control [
{
kind: CONTROL_SEQUENCE_END
fp32_false_true: [ 0, 1 ]
}
]
},
{
name: "CORRID"
control [
{
kind: CONTROL_SEQUENCE_CORRID
data_type: TYPE_UINT64
}
]
}
]
}
input [
{
name: "INPUT"
data_type: TYPE_FP32
dims: [ 100, 100 ]
}
]
output [
{
name: "OUTPUT"
data_type: TYPE_FP32
dims: [ 10 ]
}
]
sequence_batching 部分指示模型应使用序列批处理器和最旧调度策略。最旧策略配置为使序列批处理器维护最多 4 个活动候选序列,从中它更倾向于形成大小为 2 的动态批次。在此示例中,模型需要序列批处理器的开始、结束和关联 ID 控制输入。下图显示了此配置指定的序列批处理器和推理资源的表示。
使用最旧调度策略,序列批处理器
识别推理请求何时开始新序列,并尝试查找具有候选序列空间的模型实例。如果模型实例没有新候选序列的空间,Triton 会将推理请求放入积压队列中。
识别推理请求何时属于已成为某个模型实例中的候选序列的序列,并将请求路由到该模型实例。
识别推理请求何时属于积压队列中的序列,并将请求放入积压队列中。
识别序列中的最后一个推理请求何时完成。模型实例立即从积压队列中删除一个序列,并使其成为模型实例中的候选序列,或者记录模型实例可以处理未来的序列(如果积压队列中没有序列)。
下图显示了如何将多个序列调度到上述示例配置指定的模型实例上。在左侧,该图显示了到达 Triton 的四个请求序列。如图所示,每个序列由多个推理请求组成。该图的中心显示了推理请求序列如何随时间推移批处理到模型实例上,假设每个序列的推理请求以相同的速率到达,序列 A 恰好在 B 之前到达,B 恰好在 C 之前到达,依此类推。最旧策略从最旧的请求形成动态批次,但从不在一个批次中包含来自给定序列的多个请求(例如,序列 D 中的最后两个推理请求不会批处理在一起)。
集成模型#
集成模型表示一个或多个模型的管道以及这些模型之间输入和输出张量的连接。集成模型旨在用于封装涉及多个模型的过程,例如“数据预处理 -> 推理 -> 数据后处理”。为此目的使用集成模型可以避免传输中间张量的开销,并最大限度地减少必须发送到 Triton 的请求数量。
集成调度器必须用于集成模型,无论集成模型内的模型使用哪个调度器。关于集成调度器,集成模型不是实际模型。相反,它在模型配置中将集成模型内模型之间的数据流指定为 ModelEnsembling::Step 条目。调度器收集每个步骤中的输出张量,并根据规范将其作为输入张量提供给其他步骤。尽管如此,从外部视图来看,集成模型仍然被视为单个模型。
请注意,集成模型将继承所涉及模型的特性,因此请求标头中的元数据必须符合集成模型内的模型。例如,如果其中一个模型是有状态模型,则集成模型的推理请求应包含有状态模型中提到的信息,这些信息将由调度器提供给有状态模型。
作为示例,考虑一个用于图像分类和分割的集成模型,其模型配置如下
name: "ensemble_model"
platform: "ensemble"
max_batch_size: 1
input [
{
name: "IMAGE"
data_type: TYPE_STRING
dims: [ 1 ]
}
]
output [
{
name: "CLASSIFICATION"
data_type: TYPE_FP32
dims: [ 1000 ]
},
{
name: "SEGMENTATION"
data_type: TYPE_FP32
dims: [ 3, 224, 224 ]
}
]
ensemble_scheduling {
step [
{
model_name: "image_preprocess_model"
model_version: -1
input_map {
key: "RAW_IMAGE"
value: "IMAGE"
}
output_map {
key: "PREPROCESSED_OUTPUT"
value: "preprocessed_image"
}
},
{
model_name: "classification_model"
model_version: -1
input_map {
key: "FORMATTED_IMAGE"
value: "preprocessed_image"
}
output_map {
key: "CLASSIFICATION_OUTPUT"
value: "CLASSIFICATION"
}
},
{
model_name: "segmentation_model"
model_version: -1
input_map {
key: "FORMATTED_IMAGE"
value: "preprocessed_image"
}
output_map {
key: "SEGMENTATION_OUTPUT"
value: "SEGMENTATION"
}
}
]
}
ensemble_scheduling 部分指示将使用集成调度器,并且集成模型由三个不同的模型组成。step 部分中的每个元素都指定要使用的模型以及模型的输入和输出如何映射到调度器识别的张量名称。例如,step 中的第一个元素指定应使用 image_preprocess_model 的最新版本,其输入“RAW_IMAGE”的内容由“IMAGE”张量提供,其输出“PREPROCESSED_OUTPUT”的内容将映射到“preprocessed_image”张量以供以后使用。调度器识别的张量名称是集成输入、集成输出以及 input_map 和 output_map 中的所有值。
组成集成的模型也可以启用动态批处理。由于集成模型只是在组成模型之间路由数据,因此 Triton 可以将请求输入到集成模型中,而无需修改集成的配置来利用组成模型的动态批处理。
假设仅服务于集成模型、预处理模型、分类模型和分割模型,则客户端应用程序会将它们视为四个不同的模型,可以独立处理请求。但是,集成调度器会将集成模型视为如下所示。
当收到集成模型的推理请求时,集成调度器将
识别请求中的“IMAGE”张量已映射到预处理模型中的输入“RAW_IMAGE”。
检查集成模型中的模型,并向预处理模型发送内部请求,因为所需的所有输入张量都已就绪。
识别内部请求的完成,收集输出张量并将内容映射到“preprocessed_image”,这是集成模型内已知的唯一名称。
将新收集的张量映射到集成模型内模型的输入。在本例中,将映射“classification_model”和“segmentation_model”的输入并标记为就绪。
检查需要新收集的张量的模型,并向输入就绪的模型发送内部请求,在本例中为分类模型和分割模型。请注意,响应将以任意顺序排列,具体取决于各个模型的负载和计算时间。
重复步骤 3-5,直到不再应发送内部请求,然后使用映射到集成输出名称的张量响应推理请求。
与其他模型不同,集成模型在模型配置中不支持“instance_group”字段。原因是集成调度器本身主要是事件驱动的调度器,开销非常小,因此几乎永远不会成为管道的瓶颈。集成模型中的组成模型可以使用各自的 instance_group
设置单独向上或向下扩展。为了优化您的模型管道性能,您可以使用 模型分析器 来找到最佳模型配置。
在构建集成步骤时,注意 input_map
/ output_map
上的 key 和 value 之间的区别很有用
key:组成模型上的
input
/output
张量名称。value:集成模型上的张量名称,用作连接集成
input
/output
与组成模型上以及组成模型之间的标识符。
其他资源#
您可以在以下链接中找到其他端到端集成示例