在WebAssembly中使用Paddle Inference完成模型推理
LI Rui

WebAssembly(简称Wasm)技术作为一个跨平台且安全高效的二进制执行方案正逐渐流行起来,在浏览器、嵌入式等领域均有落地。而PaddlePaddle作为一大深度学习框架,提供了完善的深度学习解决方案。我们可以将两者结合起来,在WebAssembly Runtime中给模块提供Host Funtion来调用Paddle Inference推理模型。

WebAssembly与WASI

WebAssembly是一种在栈式虚拟机中执行的二进制格式,可由C++/Rust/Grain(一门直接编译到Wasm的语言)等语言编译而成。目前主流浏览器已经提供Wasm支持,编译后的模块可以直接在浏览器中执行,还能够与网页交互。这使得在网页中跑Python(如RustPython)甚至虚拟机(如开源项目v86)成为可能。

而WASI(The WebAssembly System Interface)是WebAssembly模块访问文件、套接字等资源的接口。这将WebAssembly从浏览器中搬出来,我们可以自己写一个Wasm Runtime,为模块提供相应的接口。这样模块就可以像其他程序一样读写文件、发送TCP包,我们的Runtime也可以对模块能够访问的文件、IP等资源进行限制。于是我们便有了一个可执行文件的沙箱,并且如果我们的Runtime可以跨平台,那么编写的模块也能在不同平台上运行。在Wasm Runtime中这就是一个个Host Function,Wasm二进制格式存在一个Import段用于表示需要引入的函数。

在设计嵌入式、Serverless等场景的时候,我们可以选择使用Wasm作为安全的二进制格式。Wasm模块可以被动态分发到系统中,按需执行并限制访问的资源。当需要访问文件等敏感资源时,Wasm模块调用Host Funtcion发出请求,由Runtime判断是否提供资源。

WasmEdge

WasmEdge是C++编写的轻量高效WebAssembly Runtime,同时也是CNCF的沙箱项目,可以用于Serverless、嵌入式、微服务等场景。

Linux等平台可以使用安装脚本:

1
curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash

Windows可以使用Winget完成安装:

1
winget install wasmedge

我们可以使用Rust编译Wasm模块并在WasmEdge中执行:

Paddle Inference

Paddle Inference是飞桨生态中的一环,作为原生推理库可以将飞桨框架训练出的模型直接部署到云端和服务器上。Paddle Inference支持C++/C/Python/Golang等语言,支持x86/ARM等架构,且对模型计算过程有很好的优化。

这次我们将使用C++预测库,我也曾在往期的在线分享中介绍过将C++预测库制作成Node.js下的插件供js程序调用,原理是使用N-API完成Node.js与C++双方的数据类型转换。

为WasmEdge添加自定义Host Function

完整代码在 https://github.com/KernelErr/WasmEdge/tree/pp_inference

Wasm模块本身不能进行打开Socket等操作,需要调用由Runtime提供的Host Function完成。我们的目标就是为WasmEdge新增一个名为pp_yolov3的host function,将Paddle Inference C++ YOLOv3 Demo移植过来。

Runtime此时相当于一个中间件,调用Paddle Inference库完成推理,将结果复制到模块的线性内存中。为WasmEdge添加Host Function比较简单,我们fork一份源码,做出下面的修改:

  • 修改CMake规则,添加Paddle Inference相关内容(CMakeLists.txt#L70-L277)
  • 添加自定义函数(wasifunc.cpp#L2087-L2169、wasimodule.cpp#L75)
  • 修改对应的头文件和类声明等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Expect<uint32_t>
WasiPaddleYolov3::body(Runtime::Instance::MemoryInstance *MemInst,
uint32_t ResPtr, uint32_t MaxResLength, uint32_t ResLengthPtr) {
using paddle_infer::Config;
using paddle_infer::CreatePredictor;
using paddle_infer::Predictor;

Config config;
config.SetModel("/path/to/model.pdmodel", "/path/to/model.pdiparams");
config.EnableMKLDNN();
auto predictor = CreatePredictor(config);

const int height = 608;
const int width = 608;
const int channels = 3;
std::vector<int> input_shape = {1, channels, height, width};
std::vector<float> input_data(1 * channels * height * width);
for (size_t i = 0; i < input_data.size(); ++i) {
input_data[i] = i % 255 * 0.13f;
}
std::vector<int> input_im_shape = {1, 2};
std::vector<float> input_im_data(1 * 2, 608);
std::vector<float> out_data;

if (auto Res = run(predictor.get(), input_data, input_shape, input_im_data,
input_im_shape, &out_data);
unlikely(!Res)) {
return __WASI_ERRNO_CANCELED;
}

auto copyOutputData =
[&MemInst](uint8_t_ptr Base, uint32_t Length,
const std::vector<float> &Data) {
for (uint32_t Item = 0; Item < Length && Item < Data.size(); Item++) {
auto *p = MemInst->getPointer<float *>(
Base + sizeof(float) * Item, sizeof(float));
*p = Data[Item];
}
};

auto *const ResLength = MemInst->getPointer<__wasi_size_t *>(
ResLengthPtr, sizeof(__wasi_size_t *));
copyOutputData(*(MemInst->getPointer<uint8_t_ptr *>(ResPtr, sizeof(uint8_t))),
MaxResLength, out_data);
*ResLength = out_data.size();

return __WASI_ERRNO_SUCCESS;
}

在最关键的WasiPaddleYolov3::body函数体中,大体上和正常调用Paddle Inference类似,不同体现在我们获取参数和返回结果上。由于Wasm模块的内存是由Runtime提供的线性内存,我们读入参数和复制结果的时候需要使用特定的函数去拿到指针。作为参数的指针在Wasm模块中也需要提前申请好内存。

编译和执行

这里我们下载manylinux_cpu_avx_mkl_gcc8.2预编译库,并解压。在clone下来的WasmEdge文件夹中通过下面的命令编译:

1
2
3
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DPADDLE_LIB=PATH_TO_INFERENCE -DPP_WITH_MKL=ON -DPP_WITH_GPU=OFF -DPP_WITH_STATIC_LIB=OFF -DPP_USE_TENSORRT=OFF .. && make -j

PATH_TO_INFERENCE需要替换为预编译库的解压文件夹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() { 
const MAX_LENGTH : u32 = 1024;
let mut buf: Vec<f32> = vec![0f32; MAX_LENGTH as usize];
let mut buf_ptr = buf.as_mut_ptr() as u32;
let mut res_len: u32 = 0;
unsafe {
let res = wasi_host::pp_yolov3(&mut buf_ptr, MAX_LENGTH, &mut res_len);
println!("Res: {}", res);
}
println!("Res_len: {}", res_len);
println!("Buf: {:?}", buf);
}

mod wasi_host {
#[link(wasm_import_module = "wasi_snapshot_preview1")]
extern "C" {
pub fn pp_yolov3(
res: *mut u32,
max_len: u32,
res_len: *mut u32,
) -> u32;
}
}
wasmedge ./target/wasm32-wasi/debug/wasm_yolov3.wasm

总结

将Paddle Inference和WasmEdge结合起来可以赋予Wasm模块进行模型推理的能力,以上的例子只是一个初步的尝试。读者可以将图片读入等功能一并添加进来,构建完整的推理程序。这样做可以将AI能力提供给所有部署的模块,而不需要在每个模块中重复代码。得益于WebAssembly的能力,Serverless等场景进行AI推理将更为简单。

  • 本文标题:在WebAssembly中使用Paddle Inference完成模型推理
  • 本文作者:LI Rui
  • 创建时间:2022-08-20 20:39:00
  • 本文链接:https://www.lirui.tech/post/2022/708210e8fe54.html
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-SA 许可协议。转载请注明出处!