1. 表达式 API 简介

表达式 API 是把 AI 推理与通用计算结合在一起的 API 设计,它提供如下功能

  • 模型推理
  • 数值计算
  • 搭建模型

相关头文件如下:

  • MNN/expr/Executor.hpp
  • MNN/expr/ExecutorScope.hpp
  • MNN/expr/Module.hpp
  • MNN/expr/Expr.hpp
  • MNN/expr/ExprCreator.hpp

    2. 数据结构

    2.1 类定义

    2.1.1 常用类

  • Variable

    • 对外视为张量,对内视为计算图
    • 实际使用时一般用 VARP :std::shared_ptr
  • Module

    • 进行变量计算的函数
    • 实现 onForward 函数
      1. virtual std::vector<Express::VARP> Module::onForward(const std::vector<Express::VARP>& inputs) = 0;

      2.1.2 相关类

  • Expr :每个Variable 引用 Expr 的一个输出,用户不需要直接使用该数据结构

  • RuntimeManager :内部包含一个 Runtime ,分配 Module 、Expr 所需的计算资源,Runtime 对应 CPU / OpenCL / Vulkan / Metal 各类实现后端
  • Executor:包含若干个 RuntimeManager ,提供内存管理接口,每个 Executor 必须在单线程环境下运行。默认提供全局 Executor ,需要并发执行时,可自行创建。
  • ExecutorScope : 用于在子线程中绑定 Executor ,多线程并发必需
  • Tensor :MNN 基础数据结构,Interpreter-Session 系列 API 基础数据结构

    2.2 关系图

image.png

3. 表达式基础数据类型 —— VARP

表达式是一个延迟计算引擎,它提供如下功能:
(1)构造计算图
(2)加载、保存、修改计算图
(3)基于计算图进行计算(借助 MNN 推理引擎实现)

API 设计上使用”响应式编程”,修改输入的值之后,在对应的输出节点取值即可,没有显示的计算调用。
image.png

3.1 变量类型

用户操作的数据类型为 VARP,可按Tensor去读取它的值,按保存时的方式不同,分成三类

3.1.1 Input

由 MNN::Express::_Input 创建,或者加载模型而得,在保存时仅存储维度信息(shape),可以写入值

3.1.2 Const / Trainable

由 MNN::Express::_Const / _Trainable 创建,或者加载模型而得,在保存时存储数值,不能写入,只能读取

3.1.3 Function

非输入或者常量,一切由计算而得的变量,不能写入,在保存时存储与之相关的计算图
Function 变量可通过 fix_as_const / fix_as_input 调用转换为相应类型,转换时将值计算出来,并去除前置节点依赖。

3.2 读 / 写

3.2.1 获取变量信息

用 getInfo 可以获取 Variable::Info ,该数据结构描述变量基础信息

  • dim : 维度数组
  • order : 该变量维度排列
    • 类型:NHWC / NCHW / NC4HW4
      • NHWC 对应 Tensor::Tensorflow
      • NCHW 对应 Tensor::Caffe
      • NC4HW4 对应 Tensor::CaffC4
    • Convolution / Deconvolution / Image 等基于图像的计算,需要输入 NC4HW4 的类型
  • type : 数值类型
    • code : 浮点/整数/非负整数
    • bits : 位数
  • size: Tensor 的数组大小,一般为 dim 连乘积
    1. struct Info {
    2. Dimensionformat order = NHWC;
    3. std::vector<int> dim;
    4. halide_type_t type;
    5. int size;
    6. void syncSize();
    7. };
    8. const Info* getInfo();

    3.2.2 读写数据

    由 _Input 方法建出来的 VARP 可以写入数据,先获取写入数据的指针,然后填写数据
    示例如下: ```cpp

VARP input = _Input({1, 3, 224, 224}, NHWC, halide_type_of()); float inputPtr = input->writeMap(); ::memset(inputPtr, 0, input->getInfo()->size sizeof(float));

  1. 任意变量在数据可用后,均可读取数据,示例如下:
  2. ```cpp
  3. #include <MNN/expr/ExprCreator.hpp>
  4. using namespace MNN::Express;
  5. void demo() {
  6. VARP x = _Input({1, 3, 224, 224}, NHWC, halide_type_of<float>());
  7. int inputSize = x->getInfo()->size;
  8. float* inputPtr = x->writeMap<float>();
  9. for (int i=0; i<inputSize; ++i) {
  10. inputPtr[i] = (float)i / 1000.0f;
  11. }
  12. const float* inputPtr2 = x->readMap<float>();
  13. // Do something with inputPtr2
  14. VARP y = x * x + x;
  15. int outputSize = y->getInfo()->size;
  16. const float* outputPtr = y->readMap<float>();
  17. for (int i=0; i<outputSize; ++i) {
  18. printf("%d: %f\n", i, outputPtr[i]);
  19. }
  20. }
  • 若变量的值不可用(未被计算出来),readMap 将返回 nullptr ,如下情况 ```cpp

    include

    using namespace MNN::Express;

void demo() { VARP x = _Input({1, 3, 224, 224}, NHWC, halide_type_of()); VARP y = x x + x; const float outputPtr = y->readMap(); //由于 x 未写入数据,y 无法计算,outputPtr 此时为空 }

  1. <a name="2ad3c213"></a>
  2. ### 3.2.3 加载 / 存储
  3. 无论是哪种类型变量,均可保存为 mnn 模型,或从 mnn 模型加载,相关接口如下:
  4. ```cpp
  5. // 加载
  6. static std::vector<VARP> load(const char* fileName);
  7. static std::map<std::string, VARP> loadMap(const char* fileName);
  8. static std::vector<VARP> load(const uint8_t* buffer, size_t length);
  9. static std::map<std::string, VARP> loadMap(const uint8_t* buffer, size_t length);
  10. // 存储
  11. static void save(const std::vector<VARP>& vars, const char* fileName);

示例

  1. #include <MNN/expr/ExprCreator.hpp>
  2. using namespace MNN::Express;
  3. // 加载 model.mnn ,保存 prob 的计算部分
  4. void splitDemp() {
  5. auto varMap = Variable::loadMap("model.mnn");
  6. std::vector<VARP> vars = std::vector<VARP> {varMap["prob"]};
  7. Variable::save(vars, "part.mnn");
  8. }
  9. // 保存变量数据
  10. void saveOutput(float* data0, size_t n0, float* data1, size_t n1) {
  11. VARP input0 = _Const(data0, NHWC, {n0});
  12. VARP input1 = _Const(data1, NHWC, {n1});
  13. Variable::save({input0, input1}, "result.mnn");
  14. }
  15. // 加载输入输出分别为 input 和 output 的 model.mnn ,输入数据到 input ,计算 output
  16. void loadAndCompute() {
  17. auto varMap = Variable::loadMap("model.mnn");
  18. float* inputPtr = varMap["input"]->writeMap<float>();
  19. size_t inputSize = varMap["input"]->getInfo()->size;
  20. for (int i=0; i<inputSize; ++i) {
  21. inputPtr[i] = (float)i/(float)1000;
  22. }
  23. auto outputPtr = varMap["output"]->readMap<float>();
  24. auto outputSize = varMap["output"]->getInfo()->size;
  25. for (int i=0; i<outputSize; ++i) {
  26. printf("%f, ", outputPtr[i]);
  27. }
  28. }

3.3 计算

3.3.1 计算 / 建图

  • 对 VARP 进行计算操作,等效于创建计算图
  • MNN/expr/ExprCreator.hpp 中有各类对 VARP 进行计算的函数,此处不详细介绍

    3.3.2 常量转化

    执行 Varibale 中的计算图,得到数据,并将 Variable 转换为指定的类型

    1. enum InputType {
    2. INPUT = 0,
    3. CONSTANT = 1,
    4. TRAINABLE = 2,
    5. };
    6. bool fix(InputType type) const;

    分三种类型

  • INPUT : 输入类型,在 Variable 保存时仅保留维度信息,不存储数据

  • CONSTANT : 常量类型,在 Variable 保存时同时保留维度信息与存储数据
  • TRAINABLE : 可训练类型,在 Variable 保存时同时保留维度信息与存储数据,在转换 mnn 模型为可训练模型时,按该类型去决定 Variable 是否需要更新值

示例:

  1. #include <MNN/expr/ExprCreator.hpp>
  2. using namespace MNN::Express;
  3. void demo() {
  4. auto varp = _Input({1, 3, 224, 224}, NHWC);
  5. {
  6. // Init value init
  7. auto ptr = varp->writeMap<float>();
  8. auto size = varp->getInfo()->size;
  9. for (int i=0; i<size; ++i) {
  10. ptr[i] = (float)i / 100.0f;
  11. }
  12. }
  13. auto input = varp * _Scalar<float>(1.0f/255.0f);
  14. output = input * input + input;
  15. // fix input 之后,1.0f / 255.0f 的预处理不会保存到计算图里面
  16. input.fix(VARP::INPUT);
  17. // graph.mnn 描述 x * x + x 这个计算图
  18. Variable::save({output}, "graph.mnn");
  19. // fix output 之后,保存输出的数值而非计算图
  20. output.fix(VARP::CONSTANT);
  21. Variable::save({varp}, "output.mnn");
  22. }

4. 模型推理

4.1 总流程

image.png

4.2 创建 / 销毁

4.2.1 Executor 创建

MNN中Executor给用户提供接口来配置推理后端、线程数等属性,以及做性能统计、算子执行的回调函数、内存回收等功能。 提供一个全局的Exector对象,用户不用创建或持有对象即可直接使用。

新建Exector示例:

  1. NNForwardType type = MNN_FORWARD_CPU;
  2. MNN::BackendConfig backend_config; // default backend config
  3. // static std::shared_ptr<Executor> newExecutor(MNNForwardType type,
  4. // const BackendConfig& config,
  5. // int numberThread);
  6. std::shared_ptr<MNN::Express::Executor> executor(
  7. MNN::Express::Executor::newExecutor(type, backend_config, 4));
  8. MNN::Express::ExecutorScope scope(executor);

使用默认全局Exector示例:

  1. // NNForwardType type = MNN_FORWARD_CPU;
  2. MNN::BackendConfig backend_config; // default backend config
  3. MNN::Express::Executor::getGlobalExecutor()->setGlobalExecutorConfig(type, backend_config, 4);

4.2.2 加载MNN模型创建 Module

  1. const std::string model_file = "/tmp/mymodule.mnn"; // model file with path
  2. const std::vector<std::string> input_names{"input_1", "input_2", "input_3"};
  3. const std::vector<std::string> output_names{"output_1"};
  4. Module::Config mdconfig; // default module config
  5. std::unique_ptr<Module> module; // module
  6. module.reset(Module::load(input_names, output_names, model_filename.c_str(), &mdconfig));
  • input_names 和 output_names 可以为空,MNN 会在模型中搜索输入输出,顺序需要自行用 4.3.1 中的 getInfo 查询

4.2.3 基于已有的 Module 创建 Module

  1. std::unique_ptr<Module> module_shallow_copy;
  2. module_shallow_copy.reset(Module::clone(module.get()));
  • 调用 Module 的 clone 方法可以复制新的 Module
  • 复制后的 Module 与 原 Module 共享常量内存
  • 常用于同一个模型的多实例并发

    4.2.4 销毁

    如4.2.2所示,一般用share_ptr保存Module指针,使用结束可不用专门处理,自动会销毁。

    4.3 使用

    4.3.1 信息查询

    调用 getInfo 函数可获取Module信息
  1. const Info* getInfo() const;

Info 定义如下

  1. struct Info {
  2. // Input info load from model
  3. std::vector<Variable::Info> inputs;
  4. // The Module's defaultFormat, NCHW or NHWC
  5. Dimensionformat defaultFormat;
  6. // Runtime Info
  7. std::shared_ptr<MNN::Express::Executor::RuntimeManager> runTimeManager;
  8. // Input Names By Order
  9. std::vector<std::string> inputNames;
  10. // Output Names By Order
  11. std::vector<std::string> outputNames;
  12. // Version of MNN which build the model
  13. std::string version;
  14. };

可参考 tools/cpp/GetMNNInfo.cpp 使用

4.3.2 推理

示例代码:

  1. int dim = 224
  2. std::vector<VARP> inputs(3);
  3. inputs[0] = _Input({1, dim}, NHWC, halide_type_of<int>());
  4. inputs[1] = _Input({1, dim}, NHWC, halide_type_of<int>());
  5. inputs[2] = _Input({1, dim}, NHWC, halide_type_of<int>());
  6. // set input data
  7. std::vector<int*> input_pointer = {inputs[0]->writeMap<int>(),
  8. inputs[1]->writeMap<int>(),
  9. inputs[2]->writeMap<int>()};
  10. for (int i = 0; i < inputs[0]->getInfo->size; ++i) {
  11. input_pointer[0] = i + 1;
  12. input_pointer[1] = i + 2;
  13. input_pointer[2] = i + 3;
  14. }
  15. std::vector<VARP> outputs = module->onForward(inputs);
  16. auto output_ptr = outputs[0]->readMap<float>();

4.3.3 调试与回调函数

设置为debug模式,可以保留算子的的名称字符串,内存、算力统计信息。打开该模式需要先创建运行时管理器。

创建自定义运行时管理器

  1. MNN::ScheduleConfig config; // use default
  2. BackendConfig backendConfig; // use default backendconfig
  3. config.backendConfig = &backendConfig;
  4. std::shared_ptr<Executor::RuntimeManager> runtime_mgr(Executor::RuntimeManager::createRuntimeManager(config));

调试模式

使用自定义运行时管理器,加载模型、打开调试模式。 复用加载模型

  1. const std::string model_file = "/tmp/mymodule.mnn"; // model file with path
  2. const std::vector<std::string> input_names{"input_1", "input_2", "input_3"};
  3. const std::vector<std::string> output_names{"output_1"};
  4. Module::Config mdconfig; // default module config
  5. runtime_mgr->setMode(Interpreter::Session_Debug);
  6. std::shared_ptr<Module> user_module;
  7. user_module.reset(Module::load(inputNames, outputNames, modelName.c_str(), runtime_mgr, &mConfig));

设置与触发回调函数

  1. MNN::TensorCallBackWithInfo beforeCallBack = [&](const std::vector<MNN::Tensor*>& ntensors, const OperatorInfo* info) {
  2. // do any thing you want.
  3. auto opName = info->name();
  4. for (int i = 0; i < ntensors.size(); ++i) {
  5. auto ntensor = ntensors[i];
  6. print("input op name:%s, shape:", opName.c_str());
  7. ntensor->printShape();
  8. }
  9. return true;
  10. };
  11. MNN::TensorCallBackWithInfo callBack = [&](const std::vector<MNN::Tensor*>& ntensors, const OperatorInfo* info) {
  12. auto opName = info->name();
  13. for (int i = 0; i < ntensors.size(); ++i) {
  14. auto ntensor = ntensors[i];
  15. print("output op name:%s, shape:", opName.c_str());
  16. ntensor->printShape();
  17. }
  18. return true;
  19. };
  20. // set callback function
  21. Express::Executor::getGlobalExecutor()->setCallBack(std::move(beforeCallBack), std::move(callBack));
  22. // forward would trigger callback
  23. std::vector<VARP> outputs = user_module->onForward(inputs);