深度学习笔记-MNN Session创建流程
接续上一次构建流程,在构建完成后,我们直接来看推理相关的 API ,根据 MNN 文档,目前一共有两套推理 API:
- Session:提供会话式的推理接口
- Module:提供表达式的推理接口
本文首先分析 Session 接口,主要聚焦在Interpreter类以上的逻辑部分。
创建一个推理会话
为了进行模型推理,用户需要按顺序创建Interpreter和Session,二者分别有各自的职责:
Interpreter:负责模型数据的管理Session:负责推理相关资源的管理
这样听起来有些模糊,首先要理清模型和推理资源的关系。
抛却 MNN 本身来谈,如果要进行一次推理,任何框架大概率都会进行如下步骤:
- 从文件中读取模型数据
- 将模型转化为计算图
- 进行计算
上文我们提到的“模型数据”即从文件中读取的模型数据,而“推理资源”则是转化的计算图(张量、算子等计算时数据)。
按照 MNN 文档的说法,就是:
Interpreter是模型数据的持有者;Session通过Interpreter创建,是推理数据的持有者。
我们可以查看tools/cpp/mobilenetTest.cpp了解其创建流程,为了方便阅读,笔者删去了config的初始化:
std::shared_ptr<Interpreter> net(Interpreter::createFromFile(argv[1]));
ScheduleConfig config;
Session* session = net->createSession(config);Interpreter 的创建细节
上面的代码通过createFromFile创建了解释器,实际上,所有的createFrom族函数都只做了两件事,其一是加载Content类,其二是调用createFromBufferInternal。
抛除文件加载部分,我们可以通过createFromBuffer来分析最简单的创建流程:
Interpreter* Interpreter::createFromBuffer(const void* buffer, size_t size);其首先初始化了一个Content,然后将指针和长度赋值给它:
auto net = new Content;
net->buffer.reset((int)size);
::memcpy(net->buffer.get(), buffer, size);最后调用createFromBufferInternal:
return createFromBufferInternal(net, true);createFromBufferInternal的签名如下:
Interpreter* Interpreter::createFromBufferInternal(Content* net, bool enforceAuth);虽然它有enforceAuth这个参数,但实际上没有任何作用,该函数会先检查模型有效性,再检查模型oplists的有效性。
检查模型有效性,并初始化模型:
// 检查模型有效性
auto valid = OpCommonUtils::checkNet(net->buffer.get(), net->buffer.size());
// 从 buffer 中还原网络
net->net = GetNet(net->buffer.get());检查oplists:
// 获取 oplists ,检查其有效性
if (nullptr == net->net->oplists()) {
MNN_ERROR("Model has no oplist\n");
delete net;
return nullptr;
}
// 检查其中每一个 Op 的有效性
int opSize = net->net->oplists()->size();
for (int i = 0; i < opSize; ++i) {
auto op = net->net->oplists()->GetAs<Op>(i);
if (nullptr == op || nullptr == op->outputIndexes()) {
MNN_ERROR("Invalid Model, the %d op is empty\n", i);
delete net;
return nullptr;
}
}网络和算子
我们在编译时通过脚本生成了schema,而Net和oplists相关的内容结构就是在这个时候被生成的,以Net为例,其本质是flatbuffers::Table实现的偏移量表,而获取oplists,实际上就是按照偏移量获取其某个位置的指针:
const flatbuffers::Vector<flatbuffers::Offset<Op>> *oplists() const {
return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<Op>> *>(10);
}Session 的创建细节
Session的创建依赖于createSession族函数,其中涉及到两个配置结构:
ScheduleConfig:Session的运行时信息(推理运行时类型/线程数量等)BackendConfig:后端需要注意的配置信息(精度/内存策略/供电策略)
所有的Session创建流程最终都会汇集到createMultiPathSession,其签名为:
Session* Interpreter::createMultiPathSession(const std::vector<ScheduleConfig>& configs, const RuntimeInfo& runtime);值得注意的是,Session可以将模型推导分为多个子路径,createMultiPathSession支持传入一个配置列表来创建多个推理路径,每个路径拥有不同的配置选项。
该接口的第二参数是运行时信息,其本质是一张包含了所有推理运行时的std::map:
typedef std::pair<std::map<MNNForwardType, std::shared_ptr<Runtime>>, std::shared_ptr<Runtime>> RuntimeInfo;Runtime是推理使用的运行时实例,而MNNForwardType是推理运行时类型,其典型值为:
typedef enum {
MNN_FORWARD_CPU = 0,
MNN_FORWARD_AUTO = 4,
MNN_FORWARD_CUDA = 2,
MNN_FORWARD_OPENCL = 3,
MNN_FORWARD_OPENGL = 6,
MNN_FORWARD_VULKAN = 7,
// ...
} MNNForwardType;RuntimeInfo的第一项是所有可用的运行时列表,第二项是固定的 CPU 运行时。
运行时创建
createSession会调用createMultiPathSession的变体,其接口如下:
Session* Interpreter::createMultiPathSession(const std::vector<ScheduleConfig>& configs);该函数会根据我们填写的配置自动创建RuntimeInfo,通过createRuntime:
RuntimeInfo Interpreter::createRuntime(const std::vector<ScheduleConfig>& configs);其实现可以总结为通过configs创建不同的运行时,并自动创建 CPU 作为备用运行时(如果之前没有创建),限于篇幅不做展开。
如果有相同类型但不同配置的运行时,后初始化的会覆盖先初始化的。
Session的创建
当创建好运行时之后,就可以继续创建Session了,createMultiPathSession的后续流程可以总结为以下部分:
- 配置运行时(
setRuntimeHint) - 创建
SessionInfo(张量和其他运行时数据的真正位置) - 设置会话缓存(加速部分后端初始化)
- 创建
Session - 写入会话缓存
我们主要看SessionInfo和Session的创建。
SessionInfo的创建使用了Schedule::schedule:
bool Schedule::schedule(ScheduleInfo& scheduleInfo, const Net* net, const std::vector<ScheduleConfig>& configs, const RuntimeInfo& runtimeInfo);该函数的流程非常复杂,这里暂且把它看作一个黑盒子,其大致实现就是将模型复原为张量,并保存在info中,如果创建失败则返回false。
创建好SessionInfo后,函数会初始化一个Session的unique_ptr,其由Interpreter托管,最终返回给用户的是unique_ptr的 Handle,也就是指针本身。
auto newSession =
std::unique_ptr<Session>(new Session(std::move(info), mNet->modes, std::move(rt)));
auto result = newSession.get();
// ...
mNet->sessions.emplace_back(std::move(newSession));
// ...
return result;笔者在这里省略了大部分内部实现,这些部分笔者可能会放在其他专题展开分析。
后记
笔者后续还会继续分析 Session API 的其他部分实现细节,尽请期待。
更新日志
4edc1-勘误:后端->运行时于a8e17-移除算子相关迷惑表述于5a0d6-移除算子相关迷惑表述于42582-fix some text于942bc-rename于09cd9-session create于
