本文介绍 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没啥两样了?
见证奇迹!
说了这么多,好像并不能正确的推理,也就并没有什么卵用。
接下来就是要见证奇迹了。我们框架完备与否,就得看这个推理结果对不对了。
此时需要召唤我们的一个小助手:
一个专门为深度学习而生的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构建了一套推理框架。今天,我们在不需要任何依赖的情况下,实现了复杂模型的推理。
这让我们有充足的能力可以控制框架的每一个细节,包括随意插入算子、控制实现各种奇葩的量化算法等等。
如果你觉得本文对你有帮助,不妨点个赞吧!!
预告
下一次,我们要挑战什么东西呢?不知道,如果你觉得有什么有意思的模型,欢迎留言建议。