我不会用 Triton 系列:如何实现一个 backend

如何实现一个 backend

这篇文章主要讲如何实现一个 Triton Backend,以 Pytorch Backend 为例子。

Backend API

我们需要实现两个类来存储状态以及七个 Backend API。

  • ModelState
  • ModelInstanceState
  • TRITONBACKEND_Initialize
  • TRITONBACKEND_Finalize
  • TRITONBACKEND_ModelInitialize
  • TRITONBACKEND_ModelFinalize
  • TRITONBACKEND_ModelInstanceInitialize
  • TRITONBACKEND_ModelInstanceFinalize
  • TRITONBACKEND_ModelInstanceExecute

ModelState 和 ModelInstanceState 这两个类可以绑定到 Triton 提供的指针上,你可以在 ModelState 和 ModelInstanceState 里面存储任何你想要的状态,然后将它绑定到 Triton 提供的指针上。这两个类并非必须的,它的作用相当于存储。在阅读了 Pytorch Backend 之后,会发现如果要写新的 Backend,七个 Backend API 并不需要做任何更改,只需要修改 ModelState 和 ModelInstanceState 即可。这两个类里面只需要做几个事情:模型配置文件检验、处理请求。

API 调用的时机和次数

简单概括就是:

  • 动态链接库加载的时候,执行 TRITONBACKEND_Initialize
  • 当属于一个 Backend 的模型都被删除了,且 Triton 开启了热更新,它会卸载动态链接库,执行 TRITONBACKEND_Finalize
  • 其他的方法看名字就好了,不然就说了很多废话。比如 “在模型初始化的时候,调用模型初始化”
  • 一个 Backend 对应多个 Model,Backend 只调用一次,Model 调用次数和仓库中模型数量一样多
  • 一个 Model 对应多个 ModelInstance,根据模型的配置文件,调用 “模型实例” 的初始化方法。

Pytorch Backend 例子

地址:https://github.com/triton-inference-server/pytorch_backend/blob/main/src/libtorch.cc

我们以 Pytorch Backend 为学习例子,看看应该如何实现。

ModelState

一个 ModelState 和一个 TRITONBACKEND_Model 相关联,这个类主要提供一些模型配置检查、参数校验、模型实例共用的属性和方法。比如,加载模型的方法是所有模型实例初始化的时候需要的。

ModelInstanceState

一个 ModelInstanceState 和一个 TRITONBACKEND_ModelInstance 相关联,多个 ModelInstanceState 共享一个 ModelState。这个类主要提供一些处理请求、前向传播执行的方法。

令人颇感困惑的是,Pytorch 将模型输入输出配置的检查放到了这个函数里面,而 Tensorflow 的 backend 实现中,将模型输入输出的检查放到 ModelState。从抽象的分层来看,我认为模型配置的检查应该放到实例化之前,这样就可以避免每次初始化 “模型实例” 的时候都检查一次。Pytorch 这么做的原因是,设计了一个 ModelInstanceState 相关的内部状态 input_index_map_,这个状态的初始化依赖于模型的配置。

TRITONBACKEND_Initialize

Pytorch Backend 里面没有什么需要特别处理的东西,就是模板代码就好了,打印 backend 名字和版本之类的。

TRITONBACKEND_Finalize

没有提供实现。卸载动态链接库,直接移除就好了,没有需要清理的东西。

TRITONBACKEND_ModelInitialize

调用 Create 方法创建一个 ModelState,使用 TRITONBACKEND_ModelSetState 将 ModelState 绑定到传进来的 TRITONBACKEND_Model 上面。

TRITONBACKEND_ModelFinalize

前面绑定的是一个指针,所以要在这里删除指针。

TRITONBACKEND_ModelInstanceInitialize

“模型实例” 初始化和 TRITONBACKEND_ModelInitialize 的逻辑基本一致。不过需要使用多几个 API,这个方法传进来只有模型实例,我们可以从实例拿到绑定的 Model,再从 Model 拿出 ModelState,然后调用 “模型实例” 的 Create 方法进行初始化,最后同样调用 API 绑定到 ModelInstance。

TRITONBACKEND_ModelInstanceFinalize

前面绑定的是一个指针,所以要在这里删除指针。

TRITONBACKEND_ModelInstanceExecute

这个 API 的输入是 “模型实例” 和 “请求”,这里从 “模型实例” 中取出 ModelInstanceState,然后调用处理请求的方法即可。

实现细节

模型配置文件检验

在 Pytorch 的实现中,将模型配置文件的检验放到了 “模型实例” 初始化的时候,因为它设计了一些 “模型实例” 相关的状态,并且需要使用到模型配置文件。于是它一边进行模型配置文件的检验,一边初始化 “模型实例” 相关的状态。

在 OneFlow 的实现中,计划将模型配置文件的校验放到模型初始化 TRITONBACKEND_ModelInitialize 里面,而不是 “模型实例” 初始化的时候。不过,这取决于后面的 OneFlow C++ API 是如何实现的。

那么 Pytorch Backend 是如何实现的呢?

整个检验过程是:进行模型配置文件的检验,之后设置 “模型实例” 相关的状态。一边分析输入输出的名字是否符合规则,一边将输入输出的名字映射到 id。这么设计的 主要原因 是 forward 接口需要用户按照一定顺序将 tensor 输入,并没有提供一个 map 结构的输入。写成这样的 次要原因 是提高效率,一次 parse 就好。

关键数据结构

triton::common::TritonJson::Value  // 其实就是 JSONObject

关键函数调用:

GetBooleanSequenceControlProperties  // 获取 sequence control 属性
GetTypedSequenceControlProperties    // 同上
model_state_->ModelConfig().MemberAsArray("input", &ios)  // 获取一个 JSON 的成员并作为 JSONArray
ios.ArraySize()                      // JSONArray 数组大小
ios.IndexAsObject(i, &io)            // 获取 JSONArray 中的一个元素
io.MemberAsString("name", &io_name)  // 获取 JSONArray 中的一个元素并作为 String

TRITON_ENABLE_GPU

如果 Triton 开启了 GPU,那么需要做一些特别的处理。比如在同步 CudaStream。

处理请求

TRITONBACKEND_ModelInstanceExecute 拿到的是一个二维指针,即一个请求的数组,这一批请求需要一起做处理。在进行前向传播之前,我们需要将输出收集起来。Triton 提供了一些工具类帮助我们去做收集,估计下面这个收集的方法是一个异步的方法,这样可以提高性能,不过需要我们显式使用同步操作。下面的 input_buffer 是一个指针,可能指向 Host,也可能指向 Device,不管这个指针指向哪里,后面使用 libtorch 的方法,创建一个 tensor,这样我们就获取了输入了。

需要注意的是:分配内存需要调用 Triton 的方法,然后用 torch 创建 tensor。不管 Tensor 所属的内存是 CPU 还是 GPU 的,都是由 Triton 来管理。

collector->ProcessTensor(
    input_name, input_buffer, batchn_byte_size, memory_type,
    memory_type_id);

关键数据结构

BackendMemory  // 内存的抽象
BackendMemory::AllocationType  // 分配的类型: CPU 或者 GPU

关键 API 调用

TRITONBACKEND_RequestInputByIndex(requests[i], 0 /* index */, &input);  // 获取绑定在 request 上的输入
TRITONBACKEND_InputProperties(input, nullptr, nullptr, &shape, nullptr, nullptr, nullptr);  // 获取输入的属性
TRITONBACKEND_RequestInputCount(requests[0], &input_count)  // 获取输入的个数
const int64_t batchn_byte_size = GetByteSize(input_datatype, batchn_shape);  // 输入字节大小

响应请求

在前处传播完了之后,就可以获取到输出的 Tensor 了,我们只需要从 Tensor 中取出数据指针就可以了,然后调用 Triton 提供的工具,帮我们将数据拷贝到指定的 repsonse 上面。

responder.ProcessTensor(
    name, output_dtype, batchn_shape, output_buffer,
    (device_.type() == torch::kCPU) ? TRITONSERVER_MEMORY_CPU
                                    : TRITONSERVER_MEMORY_GPU,
    (device_.type() == torch::kCPU) ? 0 : device_.index());

关键 API 调用

auto err = TRITONBACKEND_ResponseNew(&response, requests[i]);  // 根据 request 创建 response

报告统计数据

时间戳宏

为了方便获取时间戳,Triton 提供了一个宏函数,方便调用。

#define SET_TIMESTAMP(TS_NS)                                         \
  {                                                                  \
    TS_NS = std::chrono::duration_cast<std::chrono::nanoseconds>(    \
                std::chrono::steady_clock::now().time_since_epoch()) \
                .count();                                            \
  }

需要哪些时间戳

uint64_t exec_start_ns = 0;     // 开始执行,从请求中取出数据开始
uint64_t compute_start_ns = 0;  // 推理开始
uint64_t compute_end_ns = 0;    // 推理结束
uint64_t exec_end_ns = 0;       // 结束执行,在报告统计数据之前。

统计数据

TRITONBACKEND_ModelInstanceReportStatistics       // 一条请求
TRITONBACKEND_ModelInstanceReportBatchStatistics  // 一个 Batch

总结

简单梳理了一下,其实就几个事情:

  • 模型配置文件检测,验证输入输出的写法是否正确
  • 请求和响应,从请求中取出输入,将输出写到响应
  • 内存管理的方法,Triton 管理内存,Pytorch 则接收或输出一个指针
  • 统计数据,获取几个时间戳,调用 Triton API 来设置

深度学习框架在上面的过程中,只负责了一小部分,从 Triton 拿到指针,变成一个框架可以处理的 Tensor,然后进行推理,获取输出,最后将输出变成一个指针,返回给 Triton。于是,Triton 拿到指针之后,写到 response 里面。

posted @ 2021-11-01 21:06  楷哥  阅读(3219)  评论(1编辑  收藏  举报