这篇文章上次修改于 1635 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

二 、创建会话之创建Interpreter

欢迎转载,如需转载请注明原文地址 。谢谢!

在使用模型进行推理时,首先是通过解释器(Interpreter) , 加载模型并持有模型数据,然后使用解释器创建 Session,Session持有模型推理中生成的数据。

创建 Interpreter

Interpreter 类的将需要存储的数据放在了内部变量Content* mNet 中,加载模型即为了将模型文件内容填充到该指针指向的Content对象中的buffer中,buffer为MNN实现的内存池对象。后面的分析模型读取的流程。

创建Interpreter有两种方式:

  • 从磁盘文件加载

    /**
     * @brief create net from file.
     * @param file  given file.

*/
static Interpreter createFromFile(const char file);


* 从内存加载

/**

  • @brief create net from buffer.
  • @param buffer given data buffer.
  • @param size size of data buffer.
  • @return created net if success, NULL otherwise.

*/
static Interpreter createFromBuffer(const void buffer, size_t size);




### 1. 从磁盘文件中加载模型

下面我们从源码看下创建 Interpreter 究竟做了哪些工作。

找到这两个函数实现的位置`MNN\source\core\Interpreter.cpp`

首先看从文件中加载模型:

Interpreter Interpreter::createFromFile(const char file) {

//检查传入的文件名字符串是否为空
if (nullptr == file) {
    MNN_PRINT("NULL file for create interpreter");
    return nullptr;
}
//new 一个 FileLoader 的对象并用unique_ptr持有该对象的指针。
std::unique_ptr<FileLoader> loader(new FileLoader(file));
//检查模型加载类是否可用
if (!loader->valid()) {
    MNN_PRINT("Create interpreter failed, open %s error\n", file);
    return nullptr;
}
bool result = loader->read();
if (!result) {
    MNN_PRINT("Read file error\n");
    return nullptr;
}
if (loader->size() == 0) {
    MNN_PRINT("Create interpreter failed, %s is empty\n", file);
    return nullptr;
}
auto net     = new Content;
bool success = loader->merge(net->buffer);
if (!success) {
    return nullptr;
}
loader.reset();
return createFromBufferInternal(net);

}


#### 创建loader对象

在加载模型时,使用了 FileLoader 类。我们看下FileLoader类的实现。
//new 一个 FileLoader 的对象并用unique_ptr持有该对象的指针。
std::unique_ptr<FileLoader> loader(new FileLoader(file));

`\MNN\source\core\FileLoader.hpp`

class MNN_PUBLIC FileLoader {
public:

//通过一个文件路径构造 FileLoader 对象
FileLoader(const char* file);

~FileLoader();

//读取文件中的内容
bool read();

bool valid() const {
    return mFile != nullptr;
}
inline size_t size() const {
    return mTotalSize;
}

bool merge(AutoStorage<uint8_t>& buffer);

private:

std::vector<std::pair<size_t, void*>> mBlocks;
FILE* mFile                 = nullptr;
static const int gCacheSize = 4096;
size_t mTotalSize           = 0;

};

//使用 fopen 以二进制只读的方式打开文件,存储文件句柄到内部变量mFile
leLoader::FileLoader(const char* file) {

mFile = fopen(file, "rb");

}


createFromFile函数创建的loader对象就是是打开模型文件,并存储句柄到loader的内部变量mFile中。

#### 从loader中读取数据

bool result = loader->read();


看一下FileLoader中的read函数的实现

bool FileLoader::read() {

//分配一块size为gCacheSize = 4096,32位对齐(align = 32)的内存块。
auto block = MNNMemoryAllocAlign(gCacheSize, MNN_MEMORY_ALIGN_DEFAULT);
if (nullptr == block) {
    MNN_PRINT("Memory Alloc Failed\n");
    return false;
}
auto size  = fread(block, 1, gCacheSize, mFile);
mTotalSize = size;
mBlocks.push_back(std::make_pair(size, block));

while (size == gCacheSize) {
    block = MNNMemoryAllocAlign(gCacheSize, MNN_MEMORY_ALIGN_DEFAULT);
    if (nullptr == block) {
        MNN_PRINT("Memory Alloc Failed\n");
        return false;
    }
    size = fread(block, 1, gCacheSize, mFile);
    if (size > gCacheSize) {
        MNN_PRINT("Read file Error\n");
        MNNMemoryFreeAlign(block);
        return false;
    }
    mTotalSize += size;
    mBlocks.push_back(std::make_pair(size, block));
}

if (ferror(mFile)) {
    return false;
}
return true;

}

//MNN 使用自己封装的内存池进行堆内存的分配与释放,可以看到源码MNN\source\core\MNNMemoryUtils.c 中具体实现,主要为了可以自定义align的大小,可以避免了使用 #pragma pack 宏可能造成的全局影响。在实现中也是通过调用系统函数malloc, calloc, free三个函数进行内存的分配与释放。MNN内存池的实现源码考虑后面单独分析。[记录1]

auto block = MNNMemoryAllocAlign(gCacheSize, MNN_MEMORY_ALIGN_DEFAULT);

// 使用 fread 从模型文件 mFile 中读取gCacheSize(4096)字节数据到 block 中
//fread(voidbuff,int size, int count, FILE stream), 从stream中读取count个大小为size字节的数据到buff, 返回值为实际读取到的字节大小

auto size = fread(block, 1, gCacheSize, mFile);

// std::vector<std::pair<int, void*>> mBlocks;
// 使用block大小和地址创建一个pair存储到内部变量mBlocks中。

mBlocks.push_back(std::make_pair(size, block));


后面通过while循环,当实际读取到的大小不到gCacheSize时,说明文件读取结束。所有模型数据被以4096字节一个block的形式放置到FileLoader类中mBlocks变量中。

#### 创建 Content 对象

auto net = new Content;


 Content 结构体的定义 

struct Content {

AutoStorage<uint8_t> buffer;
const Net* net = nullptr;
std::vector<std::unique_ptr<Session>> sessions;
std::map<const Tensor*, const Session*> tensorMap;

};


可以看到Content包含了一个 AutoStorage 的自动内存管理对象 buff。Net 对象的指针,Net 类是使用FloatBuffers 的SDL定义并自动生成的,存储Session指针的vector, 存储 Tensor 与 Session 指针的map。

#### 模型文件内容拷贝到 Content 中

// 将loader中存储的模型内容复制到 Content 的 buffer 中。
bool success = loader->merge(net->buffer);


看下loader->merge的流程:

bool FileLoader::merge(AutoStorage<uint8_t>& buffer) {

// 首先调用buffer的reset函数,分配mTotalSize(模型文件总大小)的空间。
buffer.reset((int)mTotalSize);
// 判断内存是否分配成功,未成功返回false并打印内存分配失败。
if (buffer.get() == nullptr) {
    MNN_PRINT("Memory Alloc Failed\n");
    return false;
}
// 获取存储空间的指针
auto dst   = buffer.get();
// 将FileLoader中读取到的模型内容拷贝到 buffer 中(AutoStorage可以自动进行内存的管理)。
int offset = 0;
for (auto iter : mBlocks) {
    ::memcpy(dst + offset, iter.second, iter.first);
    offset += iter.first;
}
return true;

}


在将模型文件内容拷贝到 Content* mNet 后, 使用loader.reset(),销毁unique_ptr中存储的对象,销毁调用FileLoader类的析构函数,负责关闭文件句柄,同时释放读取文件中分配的blocks。

#### 创建Interpreter对象

// 调用createFromBufferInternal 函数通过Content创建Interpreter对象
return createFromBufferInternal(net);


看一下这个函数的具体实现

Interpreter Interpreter::createFromBufferInternal(Content net) {

if (nullptr == net) {
    MNN_PRINT("Buffer is null for create interpreter\n");
    return nullptr;
}
// 验证内存中的数据是否是合法flatbuffers对象。
flatbuffers::Verifier verify((const uint8_t*)(net->buffer.get()), net->buffer.size());
if (false == VerifyNetBuffer(verify)) {
    MNN_PRINT("Invalidate buffer to create interpreter\n");
    delete net;
    return nullptr;
}
// 此处使用 flatbuffer 直接从内存中构建Net对象
net->net = GetNet(net->buffer.get());
if (nullptr == net->net->oplists()) {
    MNN_ERROR("Model has no oplist\n");
    delete net;
    return nullptr;
}
return new Interpreter(net);

}


在从内存中的模型数据构建Interpreter对象时,使用了FlatBuffers库进行对象的反序列化,关于FlatBuffers可以参考这篇文章了解  https://halfrost.com/flatbuffers_schema/  。

关于Net的定义在文件`\MNN\schema\default\MNN.fbs` 中, 使用 flatc 编译生成 MNN_generated.h文件。

FlatBuffers定义的数据结构后面再单独分析。[记录2]

Net的定义:

table Net {

bizCode: string;
extraTensorDescribe: [TensorDescribe];
gpulibrary: GpuLibrary;
oplists: [Op];
outputName: [string];
preferForwardType: ForwardType = CPU;
sourceType: NetSource = CAFFE;
tensorName: [string];
tensorNumber: int = 0;

}

// GetNet 函数为 FlatBuffers 自动生成的函数,用来将内存中的数据直接序列化成Net对象。返回一个Net指针。
net->net = GetNet(net->buffer.get());


然后调用Interpreter(Content* net) 构造函数使用Content构造一个Interpreter对象。该构造函数只是简单的将net指针赋值给内部的mNet变量。

Interpreter::Interpreter(Content* net) {

MNN_ASSERT(nullptr != net);
mNet      = net;

}


### 2. 从内存加载模型文件

在分析完从硬盘文件加载模型之后再看从内存直接加载就很容易理解,就是从硬件加载的函数去掉了使用FileLoader从文件读取的过程。这里就不赘述了。

Interpreter Interpreter::createFromBuffer(const void buffer, size_t size) {

if (nullptr == buffer || 0 == size) {
    MNN_PRINT("Buffer is null for create interpreter\n");
    return nullptr;
}
auto net = new Content;
net->buffer.reset((int)size);
if (nullptr == net->buffer.get()) {
    MNN_ERROR("Memory not enought!\n");
    return nullptr;
}
::memcpy(net->buffer.get(), buffer, size);

return createFromBufferInternal(net);

}




## 总结