AI推理单元
推理服务供了一套面向 MLU(Machine Learning Unit,机器学习单元)设备的类似服务器的推理接口(C++11标准),以及模型加载与管理,推理任务调度等功能,极大地简化了面向MLU平台高性能深度学习应用的开发和部署工作。
概述
推理服务在软件栈中的位置,如下图所示:
推理服务共包含以下3个模块的用户接口:
- Model: 模型加载与管理
- Processor: 可自定义的后端处理单元
- InferServer: 执行推理任务
基本概念
本文描述推理服务中所涉及的具体概念。
InferServer
其整体架构如下图所示
推理服务架构
InferServer 是推理服务暴露给用户的功能入口,用户通过此入口进行加载或卸载模型(Model)、创建推理节点(Session)、请求推理任务(Request)等操作。 推理任务划分为预处理,推理,后处理三个环节,分别交由不同的后端处理单元(Processor)完成。 每个推理服务实例维护一个线程池(Scheduler),以处理环节(Task)作为最小调度单元,调度执行在该推理服务实例中运行的所有推理任务。
InferServer 使用pimpl指针隔离接口与实现,内部保证每个设备上仅有一个pimpl实例, 同一设备号下创建的 InferServer 链接同一个pimpl指针,提供对应功能。
InferServer s_0(0);
InferServer s_1(1);
// another_s_0 和 s_0 共用同一个任务调度器和相同的推理资源
InferServer another_s_0(0);
使用 InferServer 异步接口进行推理的步骤如下所示:
- 加载离线模型 InferServer::LoadModel() 。
- 创建异步推理节点 InferServer::CreateSession() 。
- 准备输入数据。
- 提交推理请求 InferServer::Request() ,推理任务完成后将结果通过 Observer::Response 发送给用户。
- 完成所有推理任务后,释放推理节点 InferServer::DestroySession() 。
class MyObserver : public Observer {
void Response(Status status, PackagePtr output, any user_data) { ... }
};
bool PreprocFunction(ModelIO*, const InferData&, const ModelInfo&) { ... }
// prepare resources
InferServer server(0);
SessionDesc desc;
desc.name = "sample infer";
desc.model = InferServer::LoadModel(model_path, func_name);
// create processors
desc.preproc = PreprocessorHost::Create();
desc.postproc = Postprocessor::Create();
desc.preproc->SetParams("process_function", PreprocFunction);
Session_t session = server.CreateSession(desc, std::make_shared<MyObserver>());
// run inference
// create an input package with tag "stream_0", containing two piece of InferData
auto input = Package::Create(2, "stream_0");
input->data[0].Set<cv::Mat>(image_1);
input->data[1].Set<cv::Mat>(image_2);
server.Request(session, input, nullptr);
// result will be passed to MyObserver::Response after finishing process
// wait until the "stream_0" tagged inference task done
server.WaitTaskDone(session, "stream_0");
// release resources
server.DestroySession(session);
ModelInfo
ModelInfo 提供了易用的用户侧模型管理和信息读取接口,各种模型实现的基类。 由 InferServer::LoadModel() 加载模型,得到模型基类的智能指针。当所有实例生命周期结束后,模型自动卸载。 用户可随时获取模型的各种信息,包含输入输出个数、输入输出数据形状以及batch_size等。
Session
Session 是推理任务节点的抽象,接收用户的推理请求并完成响应。一个Session为一个处理节点,内部的后端处理单元的顺序结构是固定的,处理同一类请求。
使用 InferServer::CreateSession 创建异步Session,异步Session只能使用异步 Request 接口,处理完毕后通过 Observer::Response 发送响应给用户; 使用 InferServer::CreateSyncSession 创建同步Session,同步Session只能使用同步 RequestSync 接口,响应作为 RequestSync 的输出参数返回。
InferServer s(0);
SessionDesc desc;
/*
* set desc params
* ...
*/
Session_t async_session = s.CreateSession(desc, std::make_shared<MyObserver>());
Session_t sync_session = s.CreateSyncSession(desc);
s.Request(async_session, input, user_data);
s.RequestSync(sync_session, input, &status, output);
Session根据传入的参数准备 SessionDesc::engine_num 份推理资源,使每份推理资源可以独立执行任务,达成并行推理。
注解
模型键值拼接预处理和后处理单元类型名称(modelkey_preproc_postproc)作为Session的键值,键值相同的Session共用同一簇推理资源。
Processor
后端处理单元,负责处理推理任务中的一个环节,由多个 Processor 链接起来构成整个推理任务处理流程。
BaseObject 基类为 Processor 提供设置任意参数的功能。
MyProcessor p;
p.SetParams("some int param", 1,
"some string param", "some string");
int a = p.GetParam<int>("some int param");
const char* b = p.GetParam<const char*>("some string");
InferData
InferData 表示一份推理数据,基于C++11编译器实现的 any 类使任意类型的推理数据都可以在 InferData 中设置。
InferData data;
cv::Mat opencv_image;
// 填充数据到InferData
data.Set(opencv_image);
// 获取一份数据的复制
auto image = data.Get<cv::Mat>();
// 获取一份数据的引用
auto& image_ref = data.GetLref<cv::Mat>();
try {
// 类型不匹配,抛出异常bad_any_cast!
auto non_sense = data.Get<int>();
} catch (bad_any_cast&) {
std::cout << "Data stored in any cannot convert to given type!";
}
video::VideoFrame vframe;
// 重新设置data后image_ref非法化
data.Set(vframe);
// cv::imwrite("foo.jpg", image_ref); // may cause segment fault
auto frame = data.Get<video::VideoFrame>();
auto& frame_ref = data.GetLref<video::VideoFrame>();
Package
Package 是一组推理数据的集合,既是Request的输入,也是Response的输出。用户通过Package可以一次请求多份数据进行处理。
使能 CNIS_RECORD_PERF 编译选项时, 输出中 Package::perf 包含每一个处理环节的性能数据,未使能时为空表。
Observer
进行异步请求需要在创建Session时,设置Observer实例。Session完成推理任务后,以通知Observer的方式完成Response。
class MyObserver : public Observer {
void Response(Status status, PackagePtr output, any user_data) {
std::cout << "Get one response ";
}
};
InferServer s(0);
SessionDesc desc;
Session_t async_session = s.CreateSession(desc, std::make_shared<MyObserver>());
功能
本文详细介绍推理服务的功能。
模型加载与管理
推理服务提供模型加载和管理功能,并在全局保有唯一一份模型缓存。
使用 InferServer::LoadModel(const
std::string&
uri,
const
std::string&
func_name
=
"subnet0") 从本地路径加载模型, 若使能 CNIS_WITH_CURL 编译选项,则可以从远端下载模型, 并加载至模型存储路径(由 InferServer::SetModelDir 设置, 默认值为当前目录)。 当检测到模型存储路径中已存在同名模型,则跳过下载,直接加载本地模型(请注意不要使用相同的模型名,可能会导致使用错误的模型)。 使用 InferServer::LoadModel(void*
mem_ptr,
const
std::string&
func_name
=
"subnet0") 从内存中加载模型,适用于模型加密的场景, 由用户对存储的模型解密后交由推理服务进行加载。
将模型路径和模型函数名进行拼接作为键值,对模型进行区分(从内存加载的模型路径是内存地址字符串)。 若加载模型时发现缓存中已存在该模型,则直接返回缓存模型。 模型缓存存在上限,超出上限自动卸载未在使用的模型。上限默认值是10,可通过环境变量 CNIS_MODEL_CACHE_LIMIT 更改默认值。 支持运行时清除模型缓存,从缓存中清除不会直接卸载模型,仅在无其他模型的智能指针实例时才会卸载模型(确保模型已经没有在使用,避免功能性错误)。
ModelPtr local_model = InferServer::LoadModel("../../resnet.cambricon", "subnet0");
// use function name "subnet0" as default
ModelPtr local_model = InferServer::LoadModel("../../resnet.cambricon");
ModelPtr net_model = InferServer::LoadModel("http://some-web.com/resnet.cambricon");
void *model_mem, *decoded_model_mem;
size_t len = ReadFromFile(model_mem, ...);
DecodeModel(decoded_model_mem, model_mem, len, ...);
ModelPtr mem_model = InferServer::LoadModel(decoded_model_mem);
推理任务调度
推理服务对所有请求的推理任务进行调度,以在保证通用性的前提下尽量达到最优性能。 推理服务使用三种共同作用的调度方式:批处理、优先级和并行处理。
批处理(Batch)
推理服务提供两种批处理模式,Dynamic模式( BatchStrategy::DYNAMIC )和Static模式( BatchStrategy::STATIC ), 在创建Session时通过 SessionDesc::strategy 指定。
- Dynamic模式会跨Request拼凑批数据,尽可能使用性能最优的批大小进行处理,达到较为理想的吞吐。 但由于跨Request拼凑数据会存在等待数据集齐的时间,单次Request的时延较高。达到设置的timeout时间后,即使未集齐批也会进行处理,以避免时延过高。
- Static模式以用户每次输入的Request数据为一批,不跨Request拼凑数据,可以达到较为理想的单次Request时延。 但相较于Dynamic模式更难达到性能较优的批大小,总吞吐量较Dynamic模式略低。
注解
目前底层尚未支持带状态的离线模型,待后端支持后,会增量支持Sequence模式的批处理策略。
优先级(Priority)
每个设备上的所有推理任务共用同一个调度器。 用户可以在创建Session时通过 SessionDesc::priority 设置优先级,高优先级的Session中的任务将会优先被处理。 优先级限制为0~9的整数,数值越大优先级越高,低于0的按0处理,高于9的按9处理。
并行处理(Parallel)
为达到最大性能,通常需要在一个设备上并行执行多组推理任务。 若某个Session代表的一类推理任务负载较重,可以通过设置 SessionDesc::engine_num 增大该类推理任务的并行度, 使该Session共占用 engine_num * model_core_number 个计算核,和对应份推理资源(内存,模型指令等)。 超出上限后,继续增加engine_num,可能出现由于资源竞争导致的总吞吐下降的情况。
后端处理单元(Processor)
推理服务内置三种后端处理单元。
预处理
预处理单元完成输入数据预处理的功能,推理服务内置了通用的预处理单元PreprocessorHost(在CPU侧完成运算)。 用户提供单份数据预处理的方法 bool(ModelIO*,
const
InferData&,
const
ModelInfo&) , 通过 BaseObject::SetParams("process_function",
func_ptr) 设置给 PreprocessorHost , 内置预处理单元内部实现并发对批数据进行任务处理,完成预处理后转换数据摆放 (从用户设置的 SessionDesc::host_input_layout 转换至模型接受的layout),拷贝入MLU,转给推理单元处理。 由于预处理函数由用户设置,预处理函数的输入是可保有任意类型数据的 InferData , 输出是固定类型的 ModelIO,故支持输入任意类型数据做推理。
注解
由用户提供的数据预处理方法仅处理单份数据,假如一个预处理过程(执行一次Process方法)的数据包中存在多份数据,多份数据将会被拆分,每份数据分别调用预处理方法并发执行预处理任务。
InferServer s(0);
SessionDesc desc;
desc.preproc = std::make_shared<PreprocessorHost>();
desc.preproc->SetParams("process_function", some_func);
/*
* set desc params
* ...
*/
Session_t sync_session = s.CreateSyncSession(desc);
预处理参数表 |
|||
参数名称 |
默认值 |
范围 |
描述 |
parallel |
2 |
[1, 8] |
处理并行度 |
process_function |
nullptr |
N/A |
用户定义的预处理方法 |
推理
推理单元完成推理任务(暂不支持用户设置推理单元,所有Session默认使用内置的推理单元)。 每个推理单元实例从模型获取包含指令数据的Context,多份Context由 cnrtForkRuntimeContext 生成,以避免指令的多份拷贝。 推理单元接受固定类型的输入,输出固定类型的推理结果( ModelIO ),输入输出数据均在MLU内存上。 推理完成后,将推理结果数据转至后处理单元解析。
后处理
后处理单元完成推理结果的拷贝和解析,推理服务内置了通用的后处理单元Postprocessor(在CPU侧完成运算)。 后处理单元首先将推理结果从MLU设备拷贝回CPU,并转换数据摆放格式(从模型输出的layout转换至用户设置的 SessionDesc::host_output_layout )。 用户提供单份数据后处理的方法 bool(InferData*,
const
ModelIO&,
const
ModelInfo&) , 通过 BaseObject::SetParams("process_function",
func_ptr) 设置给 Postprocessor , 内置后处理单元内部实现并发,调用用户设置的方法对模型输出的批数据进行解析任务。
注解
由用户提供的数据后处理方法仅处理单份数据,假如一个后处理过程(执行一次Process方法)的数据包中存在多份数据,多份数据将会被拆分,每份数据分别调用后处理方法并发执行后处理任务。
InferServer s(0);
SessionDesc desc;
desc.postproc = std::make_shared<Postprocessor>();
desc.postproc->SetParams("process_function", some_func);
/*
* set desc params
* ...
*/
Session_t sync_session = s.CreateSyncSession(desc);
若用户未设置后处理方法,则仅执行拷贝和转换layout操作,后处理单元输出CPU上的 ModelIO 数据。 此种情况下,用户无需在 SessionDesc 中设置postproc,保持默认值nullptr将自动创建一个仅执行拷贝操作的Postprocessor。
后处理参数表 |
|||
参数名称 |
默认值 |
范围 |
描述 |
parallel |
2 |
[1, 8] |
处理并行度 |
process_function |
nullptr |
N/A |
用户定义的后处理方法 |
扩展接口(contrib)
推理服务提供对图像推理任务的特化接口,简化图像推理过程, 其中包括 VideoFrame 数据类型,针对该数据类型的MLU预处理单元 PreprocessorMLU, OpenCV特化数据类型 OpencvFrame ,针对该数据类型的CPU预处理仿函数 DefaultOpencvPreproc , 请求推理任务简化接口的 VideoInferServer ,继承自 InferServer 。
// 一级推理接口
bool Request(Session_t session, const VideoFrame& vframe,
const std::string& tag, any user_data, int timeout = -1) noexcept;
bool RequestSync(Session_t session, const VideoFrame& vframe, const std::string& tag,
Status* status, PackagePtr output, int timeout = -1) noexcept;
// 二级分析接口
bool Request(Session_t session, const VideoFrame& vframe, const std::vector<BoundingBox>& objs,
const std::string& tag, any user_data, int timeout = -1) noexcept;
bool RequestSync(Session_t session, const VideoFrame& vframe,
const std::vector<BoundingBox>& objs, const std::string& tag,
Status* status, PackagePtr output, int timeout = -1) noexcept;
InferServer s(0);
SessionDesc desc;
desc.preproc = std::make_shared<PreprocessorHost>();
// 使用OpenCV实现的预处理函数
desc.preproc->SetParams("process_function", video::DefaultOpencvPreproc::GetFunction());
...
Session_t sync_session = s.CreateSyncSession(desc);
InferServer s(0);
SessionDesc desc;
// 使用MLU预处理单元
desc.preproc = std::make_shared<video::PreprocessorMLU>();
desc.preproc->SetParams("src_format", video::PixelFmt::NV21,
"dst_format", video::PixelFmt::RGBA,
"preprocess_type", video::PreprocessType::RESIZE_CONVERT);
...
Session_t sync_session = s.CreateSyncSession(desc);
``PreprocessorMLU`` 单元内置了BANG语言实现的 ``ResizeConvert`` 算子,和硬件scaler(仅MLU220支持),提供MLU上的预处理功能,支持YUV转至四通道BGR家族,同时图像缩放至指定大小。
扩展预处理参数表
参数 core_number 和 keep_aspect_ratio 仅对 RESIZE_CONVERT 生效。