WNNX从零实现AI推理引擎终结篇(二)实现完全无依赖推理复杂模型

188 阅读7分钟

本文介绍 2022_08_07_11_WNNX从零实现AI推理引擎终结篇(二)实现完全无依赖推理复杂模型

2022_08_07_11_WNNX从零实现AI推理引擎终结篇(二)实现完全无依赖推理复杂模型

本文由在当地较为英俊的男子金天大神原创,版权所有,欢迎转载,本文首发地址 jinfagang.github.io 。但请保留这段版权信息,多谢合作,有任何疑问欢迎通过微信联系我交流:jintianiloveu

这应该是本个系列的终结篇,也是最后一篇关于建造的博客。后续如果更新,可能也只是更新一些模型应用,不会再和推理引擎有任何关系。如果我时间的话,再把AI编译器也慢慢的融合到WNNX推理引擎内,但这是后话,也不一定有什么卵用。

如果还没有看我系列文章的,可以前往知乎:

zhuanlan.zhihu.com/p/519433974

zhuanlan.zhihu.com/p/521161124

zhuanlan.zhihu.com/p/523707057

zhuanlan.zhihu.com/p/524278152

zhuanlan.zhihu.com/p/525576218

zhuanlan.zhihu.com/p/529153326

zhuanlan.zhihu.com/p/548623458

上述通过血与泪的荆棘之路,向你展示了如何自己构建一个推理引擎,包括自己定义一套推理格式,如何做模型转换器、如何设计算子、如何一步步调试算子,最终我们实现了FastDet里面的YOLO模型的推理:

今天,我们更进一步,我们要开始挑战NanoDet。这篇文章是最后一篇,也会是充满干货的一篇,走过路过的朋友们请点个赞。

NanoDetPlus概览

如果我们的框架能推理Nanodet,那就基本上达到了80%完备了。因为他几乎包含了一个复杂模型必备的所有特点,尽管他很小很轻量:

  • 它使用shufflenet,这里面包括Split Concat Permute,这些操作涉及到比较麻烦的维度转换,你习惯上来就说git clone ncnn的自然无感,让你自己去写,会骂娘;
  • 它存在比较复杂的后处理,不仅仅是Box坐标要做分布反离散,而且score的处理也需要先split再sigmoid,总之跟训练框架写法有关,处理起来有一丢丢的棘手;
  • 它存在比较复杂的GhostPAN结构,当然复杂是相对的,一个不成熟的推理框架,处理不了这玩意儿;

最后,我上一个从pytorch直接转到wnnx的图,你就明白了:

这是PAN的一部分,可以看到分支还是非常多的。

再来看Head:

我把score的后处理融合到了网络里面去了,所以你会看到最下面那一部分有点复杂,各种Split concat,稍有不慎你就推理出错了,而且你可能还不知道哪里真正出错。这才是写一个推理框架最难的地方

总之,这个模型足够快,同时也足够复杂,是检验我们推理框架是否功能完备的理想结构。 再次,我其实把测试样例难度加大了一点,如果不融合score的后处理,应该没有这么让人望而生畏。但我就是要用更大的难度测试其鲁棒性。

最后有一点需要注意的,wnnx框架的优点在于灵活,这里面的每一个算子从定义到实现,你可以完全把控,这就是自己做一个框架的好处,如果让你改mnn,我估计你会哭。

这个优势就体现在Shufflenet上:

如果是一个比较凋敝的框架,在ChannelShuffle这块,他一定是通过 Split + Concat + Permute 三个算子来实现的,而在WNNX里面直接融合成了一个算子,并且的确跟其他现代版框架一样,有一个专门为shufflenet准备的ChannelShuffle算子定义。

编写推理代码

接下来就要编写NanoDet的推理代码了。这部分懒得去写了,直接从Nanodet里面白嫖一波,实际上wnnx的推理,90%是可以直接服用ncnn或者mnn里面的代码的,具有高度的兼容性。

我们来对比一下网络推理的简介度。产品经理必看。

现代版的很多推理框架都是技术人员拍脑袋想出来的,根本不会考虑用户体验,甚至会反向优化(越复杂就显得越牛逼,例如tensorflow)。但老实说,这种想法迟早会把自己玩死(例如tensorflow)。任何产品,一定要简单即是真理。同样一个事情,10行代码能解决的事情,你为什么要写100行?。奥卡姆剃刀原理放到哪里都是正确的。

这是Nanodet里面ncnn的代码:

它用了三个文件,包括 头文件,cpp文件,以及main文件,单独这个Nanodet.cpp只是类的实现,他就用了236行代码。请注意,这里是单个文件,还不包括头文件,main文件

而在wnnx里面,我把所有都放到了一个cc文件:

可以看到,所有东西加在一起包括注释,也才 250行代码。。。

不能说简单,只能说非常简单。

我们再从细节地方来对比一下。这是ncnn的常规推理步骤:

this->Net = new ncnn::Net();
// opt
#if NCNN_VULKAN
this->hasGPU = ncnn::get_gpu_count() > 0;
#endif
this->Net->opt.use_vulkan_compute = this->hasGPU && useGPU;
this->Net->opt.use_fp16_arithmetic = true;
this->Net->load_param(param);
this->Net->load_model(bin);

ncnn::Mat input;
preprocess(image, input);

//double start = ncnn::get_current_time();

auto ex = this->Net->create_extractor();
ex.set_light_mode(false);
ex.set_num_threads(4);
#if NCNN_VULKAN
ex.set_vulkan_compute(this->hasGPU);
#endif
ex.input("data", input);

std::vector<std::vector<BoxInfo>> results;
results.resize(this->num_class);

ncnn::Mat out;
ex.extract("output", out);

简单来说,我们要创建一个Net,然后创建一个extractor,还得load param,load bin。不能说这很复杂,但只能说没有必要。

使用wnnx,一切就显得非常顺其自然:

wnn_model = new wnn::NNForward();
if (!wnn_model->load_from_file(model_f)) {
    std::cout << "model might load failed.\n";
    return;
}
wnn::Tensor input_tensor;
preprocess(img_in, input_tensor);
std::vector<wnn::Tensor> inputs;
inputs.push_back(input_tensor);

std::vector<wnn::Tensor> outputs;
wnn_model->infer(inputs, outputs);

然后你就可以从 outputs 里面拿到你需要的输出了,有几个拿几个。这跟MNN不一样,你不需要事先知道输出的维度,框架会自动推理,你可以直接拿到tensor,访问shape属性就自定知道了他的维度。

简单总结来说,即便是你用的是推理框架,即便用的是c++,你会感觉自己在用python,会感觉自己用的是pytorch。

对了,如果你想查看输出数值,直接:

std::cout << outputs[0];

你会看到:

这是不是就跟pytorch没啥两样了?

见证奇迹!

说了这么多,好像并不能正确的推理,也就并没有什么卵用

接下来就是要见证奇迹了。我们框架完备与否,就得看这个推理结果对不对了。

此时需要召唤我们的一个小助手:

github.com/jinfagang/m…

一个专门为深度学习而生的C++推理辅助库。他的功能包括但不限于:

  • 各类opencv里面的可视化,画框,human pose,segmentation,都可以;
  • 各类你需要的数据集相关信息,比如coco class names;
  • 各类你可能会用到的一些image 预处理操作;
  • 各类写Demo的时候会用到的辅助类。

例如,我们定义好了Nanodet的类之后,写一个可以同时处理 图片、文件夹、视频的demo,就这么简单:


int main(int argc, char **argv) {
  if (argc < 2) {
    std::cout << "Usage: nanodet wnnx_model_f image_f\n";
  }
  std::string model_f = argv[1];
  std::string data_f = argv[2];

  NanoDet *det_model = new NanoDet(model_f.c_str(), false);

  mjolnir::iter::ImageSourceIter<cv::Mat> sourceIter(data_f);
  while (sourceIter.ok) {
    cv::Mat img = sourceIter.next();
    auto res = det_model->detect(&img, 0.36, 0.65);
    // std::cout << "got " << res.size() << " detections. \n";
    // for (auto a : res) {
    //   a.print();
    // }
    auto res_img = mjolnir::vis::VisualizeBox(
        img, res, mjolnir::COCO_CLASSES_NO_BK, nullptr, 1, 0.4, true);

    cv::imshow("NanoDet wnnx", res_img);
    sourceIter.waitKey();
    
  }
}

这里我们调用了 mjolnir 里面的两个东西,一个是 ImageSourceIter,一个是VsiualizeBox。

ImageSourceIter 做的事情就是你可以输入一张图片文件,一个文件夹,或者一个视频,他自动给你返回你需要的cv::Mat。

这在写Demo的时候可以节省大量代码!!

然后调用VisualizeBox,你就可以看到下面这个漂亮的结果啦!!

没有错!!朋友,推理成功了!!!上面就是NanoDet跑出来的结果。虽然有一个false positive,但这并不妨碍这个框架的完备性!!因为输入是320的小图。

我们再来看看更多结果!!

可以说非常的牛逼!!

我们甚至可以推理一下视频:

视频也是非常的溜!!你敢相信,这是一个完全不依赖于其他任何框架的推理结果?包括可视化的每一行代码都是我们自己写的!!

The End

我们花了将近一个多月的时间,从0构建了一套推理框架。今天,我们在不需要任何依赖的情况下,实现了复杂模型的推理。

这让我们有充足的能力可以控制框架的每一个细节,包括随意插入算子、控制实现各种奇葩的量化算法等等。

如果你觉得本文对你有帮助,不妨点个赞吧!!

预告

下一次,我们要挑战什么东西呢?不知道,如果你觉得有什么有意思的模型,欢迎留言建议。