Triton Inference Server 架构与前后处理方案梳理
Triton Inference Server 架构与前后处理方案梳理
文章目录
- Triton Inference Server 架构与前后处理方案梳理
- 0 引言
- 1 client方案--自己在client端增加前处理和后处理
- 1.1 client的README总结
- 1.2 client模块解析
- 1.2.1 整体目录结构
- 1.2.2 client/src/c++/library 文件夹
- 1.2.3 client/src/c++/examples 文件夹
- 1.2.5 client/src/c++/analyzer 文件夹
- 1.3 结论--怎么在client端做图片前处理和模型后处理
- 2 ensemble model--一个模型的输出馈送到另一个模型的场景
- 2.1 使用集成模型在 NVIDIA Triton 推理服务器上为 ML 模型管道提供服务 - NVIDIA 技术博客
- 2.1.1 自定义 Python 后端存根
- 2.1.2 自定义执行环境
- 2.1.3 使用 Python 后端存根和自定义执行环境
- 2.1.4 用于预处理和后处理的 model.py 文件的结构
- 2.1.4 在 GPU 上执行 NVIDIA Triton 中的整个管道
- 2.2 [model_analyzer/docs/ensemble_quick_start.md at main · triton-inference-server/model_analyzer](https://github.com/triton-inference-server/model_analyzer/blob/main/docs/ensemble_quick_start.md)
- 2.3 [Bobo-y/triton_ensemble_model_demo: triton server ensemble model demo](https://github.com/Bobo-y/triton_ensemble_model_demo)
- 2.4 [https://github.com/triton-inference-server/server/blob/main/docs/user_guide/architecture.md#ensemble-models](https://github.com/triton-inference-server/model_analyzer/blob/main/docs/ensemble_quick_start.md)
- 2.5 结论--怎么用ensemble models做图片前处理和模型后处理
- 3 Business Logic Scripting (BLS)--管道包含循环、条件或其他自定义逻辑
- 3.1 https://github.com/triton-inference-server/server/blob/main/docs/user_guide/bls.md
- 3.2 https://github.com/triton-inference-server/backend/tree/main/examples/backends/bls
- 3.3 [Business Logic Scripting (BLS)](https://github.com/triton-inference-server/python_backend#business-logic-scripting)
- 3.4 结论--怎么用BLS做图片前处理和模型后处理
- 4 结论--前处理/后处理的几种方案对比
- 4.1 client方案的结论
- 4.2 ensemble models方案的结论
- 4.3 BLS方案的结论
- 4.4 最终结论--待讨论
- 5 参考文献
0 引言
本文原本计划是针对前处理和后处理方案进行整理,但在梳理过程中,有意识的对 Triton Client、Ensemble Model、Business Logic Scripting 等模块 细节做了尽可能多的了解,希望通过这次调研,能了解更多的 Triton 相关知识。
1 client方案–自己在client端增加前处理和后处理
参考文献主要是 https://github.com/triton-inference-server/client 的readme和代码。
1.1 client的README总结
首先把readme整体看一遍,这是最笨但也是最快整体了解client的方法,而且readme一般都会包含所有的比较有价值的点
-
client里面包含了很多库和使用demo;
-
支持使用系统共享内存或者cuda 共享内存给服务端发送输入数据以及从服务端接收输出数据;
-
构建方式可以下载release包,也可以用docker镜像,也可以使用cmake从源码构建;
-
在docker容器中使用cuda共享内存的时候,需要增加–pid host;
-
triton server 支持 ensemble 模型,也就是支持把多个后端模块串成 pipeline,比如第一个用 DALI 后端做前处理,第二个用算法模型做推理,client 直接发原始图像就能拿到推理结果。
通过readme可以看到其实是支持在server端做前处理和后处理的,接下来的章节还是先继续分析下client模块的东西,然后后面会分析下这个ensemble以及DALI的具体内容,然后再最终决定前后处理是在client还是在server去做。
1.2 client模块解析
1.2.1 整体目录结构
先列一下目录结构
root@login:/volume/triton_client_code_2.37.0/client/src# tree -L 2
.
|-- c++
| |-- CMakeLists.txt
| |-- examples
| |-- library
| |-- perf_analyzer
| `-- tests
|-- grpc_generated
| |-- go
| |-- java
| `-- javascript
|-- java
| |-- CMakeLists.txt
| |-- README.md
| |-- pom.xml
| |-- src
| `-- target
|-- java-api-bindings
| `-- scripts
`-- python|-- CMakeLists.txt|-- examples`-- library17 directories, 5 files
然后继续列一下C++里面的结构
root@login:/volume/triton_client_code_2.37.0/client/src# cd c++
root@login:/volume/triton_client_code_2.37.0/client/src/c++# tree -L 2
.
|-- CMakeLists.txt
|-- examples
| |-- CMakeLists.txt
| |-- ensemble_image_client.cc
| |-- image_client.cc
| |-- resnet50.cc
| |-- resnet50.cc.bak
| |-- reuse_infer_objects_client.cc
| |-- simple_grpc_async_infer_client.cc
| |-- simple_grpc_cudashm_client.cc
| |-- simple_grpc_custom_args_client.cc
| |-- simple_grpc_custom_repeat.cc
| |-- simple_grpc_health_metadata.cc
| |-- simple_grpc_infer_client.cc
| |-- simple_grpc_keepalive_client.cc
| |-- simple_grpc_model_control.cc
| |-- simple_grpc_sequence_stream_infer_client.cc
| |-- simple_grpc_sequence_sync_infer_client.cc
| |-- simple_grpc_shm_client.cc
| |-- simple_grpc_string_infer_client.cc
| |-- simple_http_async_infer_client.cc
| |-- simple_http_cudashm_client.cc
| |-- simple_http_health_metadata.cc
| |-- simple_http_infer_client.cc
| |-- simple_http_model_control.cc
| |-- simple_http_sequence_sync_infer_client.cc
| |-- simple_http_shm_client.cc
| |-- simple_http_string_infer_client.cc
| |-- yolov5s.cc
| |-- yolov7-tiny.cc
| `-- yolov7-tiny.cc.bak
|-- library
| |-- CMakeLists.txt
| |-- build
| |-- cencode.c
| |-- cencode.h
| |-- cmake
| |-- common.cc
| |-- common.h
| |-- grpc_client.cc
| |-- grpc_client.h
| |-- http_client.cc
| |-- http_client.h
| |-- ipc.h
| |-- json_utils.cc
| |-- json_utils.h
| |-- libgrpcclient.ldscript
| |-- libhttpclient.ldscript
| |-- shm_utils.cc
| `-- shm_utils.h
|-- perf_analyzer
| |-- CMakeLists.txt
| |-- README.md
| |-- base_queue_ctx_id_tracker.h
| |-- client_backend
| |-- command_line_parser.cc
| |-- command_line_parser.h
| |-- concurrency_ctx_id_tracker.h
| |-- concurrency_manager.cc
| |-- concurrency_manager.h
| |-- concurrency_worker.cc
| |-- concurrency_worker.h
| |-- constants.h
| |-- ctx_id_tracker_factory.h
| |-- custom_load_manager.cc
| |-- custom_load_manager.h
| |-- data_loader.cc
| |-- data_loader.h
| |-- docs
| |-- doctest.h
| |-- fifo_ctx_id_tracker.h
| |-- ictx_id_tracker.h
| |-- idle_timer.h
| |-- iinfer_data_manager.h
| |-- infer_context.cc
| |-- infer_context.h
| |-- infer_data.h
| |-- infer_data_manager.cc
| |-- infer_data_manager.h
| |-- infer_data_manager_base.cc
| |-- infer_data_manager_base.h
| |-- infer_data_manager_factory.h
| |-- infer_data_manager_shm.cc
| |-- infer_data_manager_shm.h
| |-- inference_profiler.cc
| |-- inference_profiler.h
| |-- ischeduler.h
| |-- iworker.h
| |-- load_manager.cc
| |-- load_manager.h
| |-- load_worker.cc
| |-- load_worker.h
| |-- main.cc
| |-- metrics.h
| |-- metrics_manager.cc
| |-- metrics_manager.h
| |-- mock_concurrency_worker.h
| |-- mock_data_loader.h
| |-- mock_infer_context.h
| |-- mock_infer_data_manager.h
| |-- mock_inference_profiler.h
| |-- mock_model_parser.h
| |-- mock_request_rate_worker.h
| |-- mock_sequence_manager.h
| |-- model_parser.cc
| |-- model_parser.h
| |-- mpi_utils.cc
| |-- mpi_utils.h
| |-- perf_analyzer.cc
| |-- perf_analyzer.h
| |-- perf_analyzer_exception.h
| |-- perf_analyzer_unit_tests.cc
| |-- perf_utils.cc
| |-- perf_utils.h
| |-- rand_ctx_id_tracker.h
| |-- rate_schedule.h
| |-- report_writer.cc
| |-- report_writer.h
| |-- request_rate_manager.cc
| |-- request_rate_manager.h
| |-- request_rate_worker.cc
| |-- request_rate_worker.h
| |-- sequence_manager.cc
| |-- sequence_manager.h
| |-- sequence_status.h
| |-- test_command_line_parser.cc
| |-- test_concurrency_manager.cc
| |-- test_ctx_id_tracker.cc
| |-- test_custom_load_manager.cc
| |-- test_idle_timer.cc
| |-- test_infer_context.cc
| |-- test_inference_profiler.cc
| |-- test_load_manager.cc
| |-- test_load_manager_base.h
| |-- test_metrics_manager.cc
| |-- test_model_parser.cc
| |-- test_perf_utils.cc
| |-- test_report_writer.cc
| |-- test_request_rate_manager.cc
| |-- test_sequence_manager.cc
| `-- test_utils.h
`-- tests|-- CMakeLists.txt|-- cc_client_test.cc|-- client_timeout_test.cc`-- memory_leak_test.cc8 directories, 138 files
所以,本质上其实client是个SDK库,只不过它里面还给加了一些调用的demo,然后里面还有个性能测试的文件夹。
1.2.2 client/src/c++/library 文件夹
这里面其实就是库文件,里面就定义了各种结构体、各种类、各种函数接口。
文件名 | 主要内容 |
---|---|
client/src/c++/library/cencode.c | 这就是个做base64编码的,而且看着像是直接抄的http://sourceforge.net/projects/libb64 。 |
client/src/c++/library/common.cc | 这里面就是比如定义了发送推理请求时用到的类,还有一些描述结构体, 还有接收服务端返回数据时要用到的类和结构体,以及这些类里面的那些函数成员的定义 |
client/src/c++/library/grpc_client.cc | grpc所用到的几十个api |
client/src/c++/library/http_client.cc | http请求所用到的几十个api |
client/src/c++/library/json_utils.cc | 里面就一个函数ParseJson,内部实际是用的rapidjson |
client/src/c++/library/shm_utils.cc | 共享内存的相关接口,但是只是对shm_open mmap这种接口的简单封装, 并没有设计内存池以及共享内存管理相关的类,需要自己实现相应的内存池或者管理策略。 |
1.2.3 client/src/c++/examples 文件夹
我把client/src/c++/examples/下面的这些xxxxx.cc文件全都过了一遍,大体上了解了每个cc文件做的东西。
文件名 | 主要内容 |
---|---|
client/src/c++/examples/image_client.cc client/src/c++/examples/resnet50.cc client/src/c++/examples/yolov5s.cc | 这就是给server发送图片请求的例子,其中resnet50.cc和yolov5s.cc是模仿image_client.cc写的, 图片的前处理和模型的后处理都是在这个cc文件中实现的。 |
client/src/c++/examples/ensemble_image_client.cc | 这个例子就是直接将jpg图片读取成char形式,不解码,然后发给server端,由server端去做图片的解码、cvcolour、resize并做算法推理,这就是ensemble,就是将server的两个后端串联起来,一个后端做前处理,一个后端做推理。 |
client/src/c++/examples/reuse_infer_objects_client.cc | 这个是关于重复利用同一个input对象发送三次推理请求的, 里面也有关于共享内存的使用方法。 |
client/src/c++/examples/simple_grpc_async_infer_client.cc | 这里面就是异步请求的例子,然后里面就是用了回调函数以及条件变量 互斥锁,其他的也没什么东西。 |
client/src/c++/examples/simple_grpc_cudashm_client.cc | 利用 CUDA设备显存作为共享内存作为进程间通信的共享内存,然后客户端和服务端直接共享内存通信。 |
client/src/c++/examples/simple_grpc_custom_args_client.cc | 这个看着就是个最简单的怎么设置grpc参数的demo。 |
client/src/c++/examples/simple_grpc_custom_repeat.cc | 这个就是模拟一次发送一个stream或者多帧数据的情况,然后也是用回调函数以及条件变量 互斥锁,等待所有帧数据都返回后被notify。 |
client/src/c++/examples/simple_grpc_health_metadata.cc | 这个是检查服务器是否健康以及获取服务器元数据的,通俗点说就是比如用接口查看服务是否存在、模型是否准备好、获取模型配置等一些接口。 |
client/src/c++/examples/simple_grpc_infer_client.cc | 最普通的推理,没用共享内存,没用异步,没repeat。 |
client/src/c++/examples/simple_grpc_keepalive_client.cc | 这个例子通过 keepalive_options 参数确保 client 和 server 之间保持长连接(KeepAlive),具体机制就是:在空闲时间也发送 ping 请求帧,告诉 server“客户端还活着”,从而防止连接被关闭或意外断开。 |
client/src/c++/examples/simple_grpc_model_control.cc | 就只是演示了加载模型、卸载模型、检查模型是否ready等几个接口。 |
client/src/c++/examples/simple_grpc_sequence_stream_infer_client.cc | 序列推理,也就是说服务端的模型是个状态模型(在这个例子中这个模型是累加模型,也就是每次客户端发送的请求数据都跟之前的历史数据做累加和),能够保存这个模型在前后的一些状态,而客户端的这个所谓的sequence_id就是指哪个序列或者说不同用户,比如用户A 用户B都有各自的序列请求,那么服务端需要根据不同的ID去维护不同用户的状态信息。 这里的sequence_id不能理解成是帧数据的ID,而是要理解成不同用户的ID,比如用户A发送了20帧数据,用户B发送了20个帧数据,那么服务端针对用户A B内部比如说用map或vector维护两个数据结构,一个保存用户A的这20帧数据的计算结果,另一个保存用户B的这20帧数据的计算结果。 |
client/src/c++/examples/simple_grpc_sequence_sync_infer_client.cc | 跟client/src/c++/examples/simple_grpc_sequence_stream_infer_client.cc类似,只不过这个是同步阻塞调用方式,而client/src/c++/examples/simple_grpc_sequence_stream_infer_client.cc是用了回调函数+条件变量+互斥锁的异步调用。 |
client/src/c++/examples/simple_grpc_shm_client.cc | 这个就是演示了client和server之间的共享内存通信,实现输入输出和 Triton Server 的 零拷贝。 |
client/src/c++/examples/simple_grpc_string_infer_client.cc | 发送字符串进行推理的示例 |
simple_http_async_infer_client.cc simple_http_cudashm_client.cc simple_http_health_metadata.cc simple_http_infer_client.cc simple_http_model_control.cc simple_http_sequence_sync_infer_client.cc simple_http_shm_client.cc simple_http_string_infer_client.cc | 这个看文件名字感觉跟前面分析的grpc开头的文件是一样的,只不过这里面使用的是http,就不再挨着看了。 |
1.2.5 client/src/c++/analyzer 文件夹
PerfAnalyzer
其实就是一个性能评测工具,能够分析 Triton 推理服务端的吞吐量、延迟、服务器端队列/执行时间等各种性能指标。
这个文件夹从名字就能看出来是性能分析的,然后我看了下里面的那一堆文件,他和前面的example文件夹不一样,examples文件夹里面是每个.cc文件最终都会给编译成一个可执行程序,因为每个.cc文件里面都有main函数,但是在analyzer文件夹里面,只有main.cc里面是有main函数的,其他的那些文件其实相当于是analyzer的库函数。
而main.cc里面的main函数就几行
int
main(int argc, char* argv[])
{try {triton::perfanalyzer::CLParser clp;pa::PAParamsPtr params = clp.Parse(argc, argv);PerfAnalyzer analyzer(params);analyzer.Run();}catch (pa::PerfAnalyzerException& e) {return e.GetError();}return 0;
}
然后就是通过这里的params可以进行各种功能分析,然后这些参数怎么给,详细介绍在client/src/c++/perf_analyzer/docs/cli.md里面。
也可以运行root@login:/volume/triton_client_code_2.37.0/client/install/bin# ./perf_analyzer
查看。
1.3 结论–怎么在client端做图片前处理和模型后处理
在 resnet50.cc
和 yolov5s.cc
示例中,图片的前处理和模型的后处理都是在这个cc文件中实现的,imread–cvtcolor-resize,然后把前处理之后的数据发送给服务端,然后服务端返回结果之后再做nms或softmax.
-
针对前处理和后处理慢的问题
-
目前的demo中前处理和后处理都是直接在cpu上去做的,时间比较长。
-
如果client也是部署在带DCU的服务器上,那么这里的图片前处理可以换成用海光硬件接口去加速,然后后处理可以用HIP/CUDA编写kernel去做,
-
如果client是部署在一些嵌入式前端设备上(IPC相机、边缘设备),只有服务端是部署在带DCU的服务器上面,那么该方法不可行。
-
-
针对网络发送耗时的问题
- 在client做前处理和后处理,处理后的图像通常是浮点格式(如 FP32/FP16 Tensor),数据量远大于原始 JPEG/PNG 图像;
- 在 HTTP / gRPC 协议下传输体积大的张量,会明显增加带宽负担,进而拉高整体推理延迟;
- 针对发送数据量大的问题,可以采用共享内存的方式去做,但是目前demo中只是提供了像shm_open mmap这种简单函数,如果用共享内存,那么需要开发内存池相关代码作为管理员的角色。
2 ensemble model–一个模型的输出馈送到另一个模型的场景
主要参考文献是
使用集成模型在 NVIDIA Triton 推理服务器上为 ML 模型管道提供服务 - NVIDIA 技术博客
model_analyzer/docs/ensemble_quick_start.md at main · triton-inference-server/model_analyzer
Bobo-y/triton_ensemble_model_demo: triton server ensemble model demo
https://github.com/triton-inference-server/server/blob/main/docs/user_guide/architecture.md#ensemble-models
core/src/ensemble_scheduler at main · triton-inference-server/core
2.1 使用集成模型在 NVIDIA Triton 推理服务器上为 ML 模型管道提供服务 - NVIDIA 技术博客
2.1.1 自定义 Python 后端存根
Python 后端使用存根进程将model.py
文件连接到 NVIDIA Triton C++核心。 Python 后端可以使用安装在当前[ZGK8环境(虚拟环境或Conda环境)或全局 Python 环境中的库。
请注意,这假设用于编译后端存根的 Python 版本与用于安装依赖项的版本相同。写入时使用的 NVIDIA Triton 容器中的默认 Python 版本为 3.8 。如果需要运行预处理和后处理的 Python 版本与 NVIDIA Triton 容器中的版本不匹配,则需要编译 custom Python backend stub 。
要创建自定义 Python 后端存根,请在 NVIDIA conda
容器内安装conda
、cmake
、rapidjson
和 Triton 。接下来,创建一个 Conda 虚拟环境(请参见 documentation ),并使用以下命令激活它:
conda create -n custom_env python=<python-version>
conda init bash
bash
conda activate custom_env
将<python-version>
替换为感兴趣的版本,例如 3.9 。然后克隆 Python 后端回购,并使用以下代码编译 Python 后端存根:
git clone https://github.com/triton-inference-server/python_backend -b r<xx.yy>
cd python_backend
mkdir build && cd build
cmake -DTRITON_ENABLE_GPU=ON -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install ..
make triton-python-backend-stub
请注意,<xx.yy>
必须替换为 NVIDIA Triton 容器版本。运行上面的命令将创建名为triton-python-backend-stub
的存根文件。这个 Python 后端存根现在可以用来加载安装有匹配版本[ZGK8的库。
2.1.2 自定义执行环境
如果您想为不同的 Python 型号使用不同的 Python 环境,您需要创建一个 custom execution environment 。要为 Python 模型创建自定义执行环境,第一步是在已经激活的 Conda 环境中安装任何必要的依赖项。然后运行conda-pack
将我们的自定义执行环境打包为 tar 文件。这将创建一个名为custom_env.tar.gz
的文件。
在撰写本文时, NVIDIA Triton 仅支持conda-pack
用于捕获执行环境。请注意,当在 NVIDIA Triton Docker 容器中工作时,容器中包含的包也会在conda-pack
创建的执行环境中捕获。
2.1.3 使用 Python 后端存根和自定义执行环境
在创建 Python 后端存根和自定义执行环境后,将两个生成的文件从容器复制到模型存储库所在的本地系统。在本地系统中,将存根文件复制到每个需要使用存根的 Python 模型(即预处理和后处理模型)的模型目录中。对于这些模型,目录结构如下:
model_repository├── postprocess│ ├── 1│ │ └── model.py│ ├── config.pbtxt│ └── triton_python_backend_stub└── preprocess├── 1│ └── model.py├── config.pbtxt└── triton_python_backend_stub
对于预处理和后处理模型,还需要在配置文件中提供自定义执行环境的 tar 文件的路径。例如,预处理模型的配置文件将包括以下代码:
name: "preprocess"
backend: "python"...
parameters: {key: "EXECUTION_ENV_PATH",value: {string_value: "path/to/custom_env.tar.gz"}
}
要使此步骤生效,请将custom_env.tar.gz
存储在 NVIDIA Triton 推理服务器容器可以访问的路径中。
2.1.4 用于预处理和后处理的 model.py 文件的结构
每个 Python 模型都需要遵循 documentation 中描述的特定结构。在model.py
文件中定义以下三个功能:
1. **initialize**
(可选,在加载模型时运行):用于在推理之前加载必要的组件,以减少每个客户端请求的开销。特别是对于预处理,加载 cuDF tokenizer ,它将用于标记基于 BERT 的模型的摘录。还加载用于随机森林特征生成的停止词列表,作为对基于树的模型的输入的一部分。
2 .execute
(必需,请求推理时运行):接收推理请求,修改输入,并返回推理响应。由于preprocess
是推理的入口点,如果要对 GPU 执行推理,则需要将输入移动到[Z1K1’。
通过创建cudf.Series
的实例将摘录输入张量移动到 GPU ,然后利用initialize
中加载的 cuDF 标记器在[Z1K1’上标记整批摘录。
类似地,使用字符串操作生成基于树的特征,然后使用在 GPU 上操作的CuPY
数组对特征进行归一化。要在 GPU 上输出张量,请使用toDlpack
和from_dlpack
(参见 documentation )将张量封装到推理响应中。
最后,为了保持张量在 GPU 上,并避免在集合中的步骤之间复制到 CPU ,请将以下内容添加到每个模型的配置文件中:
parameters: {key: "FORCE_CPU_ONLY_INPUT_TENSORS"value: {string_value:"no"}
}
postprocess
的输入分数已经在 GPU 上了,所以只需将分数与CuPY数组再次组合,并输出最终的管道分数。对于在 CPU 上进行后处理的情况,在预处理步骤中将ML模型的输出移动到[Z1K2’。
3. **finalize**
(可选,在卸载模型时运行):使您能够在从 NVIDIA Triton 服务器卸载模型之前完成任何必要的清理。
2.1.4 在 GPU 上执行 NVIDIA Triton 中的整个管道
要让 NVIDIA Triton 运行执行管道,需要创建一个名为ensemble_all
的 ensemble model 。该模型与任何其他模型具有相同的模型目录结构,只是它不存储任何模型,并且只由一个配置文件组成。集成模型的目录如下所示:
├── ensemble_all│ ├── 1│ └── config.pbtxt
在配置文件中,首先使用以下脚本指定集成模型名称和后端:
name: "ensemble_all"
backend: "ensemble"
这里我在代码中找了下,其实没有个ensemble的backend,应该是core/src/ensemble_scheduler at main · triton-inference-server/core这块代码支持这个ensemble配置的。
接下来,定义集合的端点,即集合模型的输入和输出:
接下来,定义集合的端点,即集合模型的输入和输出:
input [{name: "excerpt"data_type: TYPE_STRINGdims: [ -1 ]},{name: "BERT_WEIGHT"data_type: TYPE_FP32dims: [ -1 ]}
]
output {name: "SCORE"data_type: TYPE_FP32dims: [ 1 ]}
管道的输入是可变长度的,因此使用 -1 作为尺寸参数。输出是一个单浮点数。
要创建通过不同模型的管道和数据流,请包括ensemble_scheduling
部分。第一个模型被称为preprocess
,它将摘录作为输入,并输出 BERT 令牌标识符和注意力掩码以及树特征。调度的第一步显示在模型配置的以下部分中:
ensemble_scheduling {step [{model_name: "preprocess"model_version: 1input_map {key: "INPUT0"value: "excerpt"}output_map {key: "BERT_IDS"value: "bert_input_ids",}output_map {key: "BERT_AM"value: "bert_attention_masks",}output_map {key: "TREE_FEATS"value: "tree_feats",}},
step
部分中的每个元素都指定了要使用的模型,以及如何将模型的输入和输出映射到集合调度器识别的张量名称。然后使用这些张量名称来识别各个输入和输出。
例如,step
中的第一个元素指定应使用preprocess
模型的版本一,其输入"INPUT0"
的内容由"excerpt"
张量提供,其输出"BERT_IDS"
的内容将映射到"bert_input_ids"
张量以供以后使用。类似的推理适用于preprocess
的其他两个输出。
继续在配置文件中添加步骤以指定整个管道,将preprocess
的输出传递到bert-large
和cuml
的输入:
{model_name: "bert-large"model_version: 1input_map {key: "INPUT__0"value: "bert_input_ids"}input_map {key: "INPUT__1"value: "bert_attention_masks"}output_map {key: "OUTPUT__0"value: "bert_large_score"}},
最后,通过在配置文件中添加以下行,将这些分数中的每一个传递到后处理模型,以计算分数的平均值并提供单个输出分数,如下所示:
{model_name: "postprocess"model_version: 1input_map {key: "BERT_WEIGHT_INPUT"value: "BERT_WEIGHT"}input_map {key: "BERT_LARGE_SCORE"value: "bert_large_score"}input_map {key: "CUML_SCORE"value: "cuml_score"}output_map {key: "OUTPUT0"value: "SCORE"}}}
]
在集成模型的配置文件中调度整个管道的简单性证明了使用 NVIDIA Triton 进行端到端推理的灵活性。要添加另一个模型或添加另一数据处理步骤,请编辑集成模型的配置文件并更新相应的模型目录。
请注意,集成配置文件中定义的max_batch_size
必须小于或等于每个模型中定义的max_batch_size
。整个模型目录,包括集成模型,如下所示:
├── bert-large
│ ├── 1
│ │ └── model.pt
│ └── config.pbtxt
├── cuml
│ ├── 1
│ │ └── checkpoint.tl
│ └── config.pbtxt
├── ensemble_all
│ ├── 1
│ │ └── empty
│ └── config.pbtxt
├── postprocess
│ ├── 1
│ │ ├── model.py
│ └── config.pbtxt
└── preprocess├── 1│ ├── model.py└── config.pbtxt
要告诉 NVIDIA Triton 执行 GPU 上的所有模型,请在每个模型的配置文件中包括以下行(集成模型的配置文档中除外):
instance_group[{kind:KIND_GPU}]
在 CPU 上执行 NVIDIA Triton 中的整个管道若要让 NVIDIA Triton 在 CPU 上执行整个管道,请重复在 GPU 上运行管道的所有步骤。在每个配置文件中将instance_group[{kind:KIND_GPU}]
替换为以下行:
instance_group[{kind:KIND_CPU}]
2.2 model_analyzer/docs/ensemble_quick_start.md at main · triton-inference-server/model_analyzer
如何使用 NVIDIA Triton 的 Model Analyzer 工具来分析一个简单的 ensemble 模型性能。
这里先记一下,以后用到了就去这个网址找。
2.3 Bobo-y/triton_ensemble_model_demo: triton server ensemble model demo
https://github.com/Bobo-y/triton_ensemble_model_demo/tree/main/models
这就是个pipeline的例子,不过他里面的backend是用python写的,后面可以参考里面的config.pbtxt,具体的C++ backend自己写。
2.4 https://github.com/triton-inference-server/server/blob/main/docs/user_guide/architecture.md#ensemble-models
直接翻译一下readme,理解一下
Ensemble 模型
Ensemble 模型表示由一个或多个模型组成的处理流水线,并定义这些模型之间输入输出张量的连接关系。Ensemble 模型的主要用途是封装一个包含多个模型的处理流程,比如“数据预处理 → 推理 → 数据后处理”。使用 ensemble 模型可以避免在多个模型间传输中间张量的开销,并减少发送到 Triton 的请求次数。
Ensemble 模型必须使用 ensemble scheduler(调度器),不管其中的子模型使用了哪种调度方式。对于 ensemble scheduler 来说,ensemble 模型本身并不是一个“实际模型”,而是通过模型配置中的 ModelEnsembling::Step
条目定义了子模型之间的数据流关系。调度器会收集每个步骤的输出张量,并按照配置将其作为输入提供给其他步骤。尽管如此,从外部来看,ensemble 模型仍然被视为一个“单一模型”。
需要注意的是,ensemble 模型会继承其所包含子模型的特性,因此请求头中的元数据必须满足所有子模型的要求。例如,如果其中一个子模型是有状态模型(stateful model),那么对 ensemble 模型的推理请求就必须包含与之相关的状态信息,这些信息会由调度器传递给对应的有状态模型。
然后下面是一个例子
name: "ensemble_model"
platform: "ensemble"
max_batch_size: 1
input [{name: "IMAGE"data_type: TYPE_STRINGdims: [ 1 ]}
]
output [{name: "CLASSIFICATION"data_type: TYPE_FP32dims: [ 1000 ]},{name: "SEGMENTATION"data_type: TYPE_FP32dims: [ 3, 224, 224 ]}
]
ensemble_scheduling {step [{model_name: "image_preprocess_model"model_version: -1input_map {key: "RAW_IMAGE"value: "IMAGE"}output_map {key: "PREPROCESSED_OUTPUT"value: "preprocessed_image"}},{model_name: "classification_model"model_version: -1input_map {key: "FORMATTED_IMAGE"value: "preprocessed_image"}output_map {key: "CLASSIFICATION_OUTPUT"value: "CLASSIFICATION"}},{model_name: "segmentation_model"model_version: -1input_map {key: "FORMATTED_IMAGE"value: "preprocessed_image"}output_map {key: "SEGMENTATION_OUTPUT"value: "SEGMENTATION"}}]
}
2.5 结论–怎么用ensemble models做图片前处理和模型后处理
-
**什么是ensemble models:**首先通俗点说,集成模型本身并不是个模型,也没有模型文件,他是用一个config文件,然后config文件里面相当于把其他已有的模型和backend给衔接起来,一个模型的输出作为另一个模型的输入,然后类似一个管道。
-
怎么用ensemble models做图片前处理和模型后处理:需要我们编写一个preprocess的backend,编写一个postprocess的backend,然后用ensemble models的形式把前处理、推理、后处理给串起来。
-
不同模型需要实现多个backend:比如不同的检测模型,他们的后处理是不一样的,那么需要写不同的backend做后处理,还有比如分类模型和检测模型,他们的图片前处理肯定也不一样,也需要实现不同的前处理的backend。
-
**支持动态替换:**只需修改 ensemble 的配置即可更换模型或处理逻辑,无需修改客户端代码。
3 Business Logic Scripting (BLS)–管道包含循环、条件或其他自定义逻辑
我在扒拉GitHub的时候又发现了一个BLS backend, https://github.com/triton-inference-server/backend/tree/main/examples/backends/bls ,不知道这个是干什么的,然后把这个看了下,发现这也可以为一种方案,后来又发现了一个地方:Business Logic Scripting (BLS) 。
3.1 https://github.com/triton-inference-server/server/blob/main/docs/user_guide/bls.md
这里面是一些bls的介绍,但只是python 的,没有C++的。
3.2 https://github.com/triton-inference-server/backend/tree/main/examples/backends/bls
这是个bls backend的C++demo,但是这个demo不支持batch,也只是在cpu上,后期如果真正使用,那么需要我们自己扩展这个demo。
3.3 Business Logic Scripting (BLS)
看了下,这里面的内容跟https://github.com/triton-inference-server/server/blob/main/docs/user_guide/bls.md其实是一样的基本上。
3.4 结论–怎么用BLS做图片前处理和模型后处理
- 为什么要用BLS:因为ensemble只适合那种串行的pipeline,当我们在加载不同模型的时候如果需要一些条件判断、循环或者复杂逻辑,那么ensemble就不适合,所以就要用BLS。
- 什么是BLS:BLS 是在 Triton 自定义后端(如 Python Backend 或 C++ Backend)中,通过编写自定义逻辑,在模型的
execute()
(或ModelInstanceExecute
)函数中显式地发起对其他模型的推理请求,构建自己的业务流程。 - BLS实现方式1:写一个前处理backend,后处理backend, 推理backend(用现有的),这个bls的backend他的推理不做具体单个模型的推理,而是在bls的execute接口内部调用其他三个backend的接口做前处理、推理、后处理、复杂逻辑判断。
- BLS实现方式2:不写前处理和后处理的backend,直接在bls的execute函数内部完完成前处理,然后调用推理接口,然后做后处理,只不过这样耦合性太高了,可复用度比较差。
- 其实bls其实也可以理解成类似ensemble的config.pbtxt的角色,只不过相当于triton内部已经支持了ensemble,所以我们只写一个config.pbtxt那么就可以用ensemble就行了,而bls我们是自己实现,里面是我们自己想要的复杂逻辑。
4 结论–前处理/后处理的几种方案对比
先把前面的几种方案的结论复制到这里来。
4.1 client方案的结论
在 resnet50.cc
和 yolov5s.cc
示例中,图片的前处理和模型的后处理都是在这个cc文件中实现的,imread–cvtcolor-resize,然后把前处理之后的数据发送给服务端,然后服务端返回结果之后再做nms或softmax.
-
针对前处理和后处理慢的问题
-
目前的demo中前处理和后处理都是直接在cpu上去做的,时间比较长。
-
如果client也是部署在带DCU的服务器上,那么这里的图片前处理可以换成用海光硬件接口去加速,然后后处理可以用HIP/CUDA编写kernel去做,
-
如果client是部署在一些嵌入式前端设备上(IPC相机、边缘设备),只有服务端是部署在带DCU的服务器上面,那么该方法不可行。
-
-
针对网络发送耗时的问题
- 在client做前处理和后处理,处理后的图像通常是浮点格式(如 FP32/FP16 Tensor),数据量远大于原始 JPEG/PNG 图像;
- 在 HTTP / gRPC 协议下传输体积大的张量,会明显增加带宽负担,进而拉高整体推理延迟;
- 针对发送数据量大的问题,可以采用共享内存的方式去做,但是目前demo中只是提供了像shm_open mmap这种简单函数,如果用共享内存,那么需要开发内存池相关代码作为管理员的角色。
4.2 ensemble models方案的结论
-
**什么是ensemble models:**首先通俗点说,集成模型本身并不是个模型,也没有模型文件,他是用一个config文件,然后config文件里面相当于把其他已有的模型和backend给衔接起来,一个模型的输出作为另一个模型的输入,然后类似一个管道。
-
怎么用ensemble models做图片前处理和模型后处理:需要我们编写一个preprocess的backend,编写一个postprocess的backend,然后用ensemble models的形式把前处理、推理、后处理给串起来。
-
不同模型需要实现多个backend:比如不同的检测模型,他们的后处理是不一样的,那么需要写不同的backend做后处理,还有比如分类模型和检测模型,他们的图片前处理肯定也不一样,也需要实现不同的前处理的backend。
-
**支持动态替换:**只需修改 ensemble 的配置即可更换模型或处理逻辑,无需修改客户端代码。
4.3 BLS方案的结论
- 为什么要用BLS:因为ensemble只适合那种串行的pipeline,当我们在加载不同模型的时候如果需要一些条件判断、循环或者复杂逻辑,那么ensemble就不适合,所以就要用BLS。
- 什么是BLS:BLS 是在 Triton 自定义后端(如 Python Backend 或 C++ Backend)中,通过编写自定义逻辑,在模型的
execute()
(或ModelInstanceExecute
)函数中显式地发起对其他模型的推理请求,构建自己的业务流程。 - BLS实现方式1:写一个前处理backend,后处理backend, 推理backend(用现有的),这个bls的backend他的推理不做具体单个模型的推理,而是在bls的execute接口内部调用其他三个backend的接口做前处理、推理、后处理、复杂逻辑判断。
- BLS实现方式2:不写前处理和后处理的backend,直接在bls的execute函数内部完完成前处理,然后调用推理接口,然后做后处理,只不过这样耦合性太高了,可复用度比较差。
- 其实bls其实也可以理解成类似ensemble的config.pbtxt的角色,只不过相当于triton内部已经支持了ensemble,所以我们只写一个config.pbtxt那么就可以用ensemble就行了,而bls我们是自己实现,里面是我们自己想要的复杂逻辑。
4.4 最终结论–待讨论
初步pipeline的方案
首选 Ensemble 方式实现推理流水线
- 先开发 Preprocess Backend 和 Postprocess Backend。
- 通过 Triton 的 Ensemble Model 配置文件,将前处理、推理、后处理串联成完整流程。
BLS(Backend Lifecycle Scheduler)方式实现
- 同时实现基于 BLS 的多模型推理逻辑,支持更复杂的模型调用流程,满足客户多模型推理需求。
Client 端推理方案实现
- 实现 Client 端前处理与后处理,支持数据预处理和结果后处理。
- 优化数据传输,开发共享内存与内存池方案,实现客户端与服务端零拷贝数据传输,提高效率。
后期扩展一:服务端视频解码 Backend
- 开发视频解码 Backend,支持服务端接收 RTSP 地址做视频解码,完成视频解码、前处理、推理、后处理的完整 Pipeline。
后期扩展二:服务端跟踪 Backend
- 利用 ByteTrack 算法开发跟踪 Backend,实现检测结果的多目标跟踪,满足客户更复杂的视频分析需求。
其它扩展:充分熟悉server代码
- 通过前面的开发和调试工作,充分熟悉triton的代码,后期可以对triton进行各种魔改和扩展,以适配更多的应用场景。
5 参考文献
https://github.com/triton-inference-server/client
使用集成模型在 NVIDIA Triton 推理服务器上为 ML 模型管道提供服务 - NVIDIA 技术博客
model_analyzer/docs/ensemble_quick_start.md at main · triton-inference-server/model_analyzer
Bobo-y/triton_ensemble_model_demo: triton server ensemble model demo
https://github.com/triton-inference-server/server/blob/main/docs/user_guide/architecture.md#ensemble-models
core/src/ensemble_scheduler at main · triton-inference-server/core
https://github.com/triton-inference-server/server/blob/main/docs/user_guide/bls.md
https://github.com/triton-inference-server/backend/tree/main/examples/backends/bls
Business Logic Scripting (BLS)