在Rust上进行飞桨模型推理
LI Rui

最近团队里面的小伙伴都在说Rust语言不错,于是按照文档写了一个后端,才真切感到这门语言的易用。作为一门无GC的语言,变量在不用后即刻释放,保证写出来的程序高效。同时Rust在语法和编译器上的精心设计,使得不安全的代码在编译时便无法通过,这对于我的编程习惯有了很大的提升。也难怪一项关于“用过还想用的编程语言”调查中,Rust遥遥领先于其他语言。

在编写后端的时候,我们可以使用Paddle Serving搭建推理服务,使用Rust去调用。但能不能将这个过程变得直接一点,即允许在Rust中直接去调用Paddle Inference(飞桨原生推理库)进行推理?实际上最后的实现比我想的还要简单,下面我将详细阐述如何实现。在发文时,一个名为paddle-sys的包已经发布到了Crates上面,您可以直接调用。

编译Paddle Inference库

我们的包使用了Paddle Inference的C语言库,因为没有预编译库提供,手动编译并不算麻烦,这里分Linux和Windows两种系统进行编译。

在Linux上,编译前需要确保自己的gcc版本不能过高,实测10.2.0版本中一项依赖无法编译通过,因此我们选择在虚拟机中使用降级之后的系统进行编译。在一个新的Ubuntu 18.04系统中我们通过apt安装cmakebuild-essential包后即可进行编译工作,下面的命令展示了2.0-rc1的编译方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git clone https://github.com/paddlepaddle/Paddle
cd paddle
git checkout release/2.0-rc1
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release \
-DWITH_PYTHON=OFF \
-DWITH_MKL=OFF \
-DWITH_MKLDNN=OFF \
-DWITH_GPU=OFF \
-DWITH_NCCL=OFF \
-DON_INFER=ON \
..
make
make inference_lib_dist

这和我们利用cmake编译许多其他包无异,编译选项可以根据自己的实际需要进行修改。如果编译顺利,我们能够得到paddle_c_api.hlibpaddle_fluid_c.so两个文件。

在Windows上,我首先安装了VS 2019并启用了C++支持,同时安装了cmakeMicrosoft 生成工具 2015 更新 3Microsoft Visual C++ 2015 Redistributable 更新 3。与Linux略有不同的是,我们需要从开始菜单中提供的2015生成工具命令行进入终端,并执行下面的命令生成项目文件:

1
2
3
4
5
6
git clone https://github.com/paddlepaddle/Paddle
cd paddle
git checkout release/2.0-rc1
mkdir build
cd build
cmake .. -G "Visual Studio 14 2015" -A x64 -T host=x64 -DCMAKE_BUILD_TYPE=Release -DWITH_MKL=OFF -DWITH_GPU=OFF -DON_INFER=ON -DWITH_PYTHON=OFF

使用VS 2019打开paddle.sln文件,选择平台为x64,配置为Release,右击项目资源管理器中的inference_lib_dist项目选择生成。编译成功后,我们同样能够得到上述的两个文件。

从C到Rust

我们将使用bindgen工具来自动生成C语言或者C++的Rust FFI,在paddle-sys的0.1.0版本中,因为C++库有复杂的数据类型和特性导致难以生成成功,我们转而选择了C语言版本。通过cargo我们能一键安装bindgen,生成FFI也能一键完成,现在可能生成的方法存在小问题,在下一个fix中会更新生成的方法。

1
2
cargo install bindgen
bindgen paddle_c_api.h -o paddle_c_api.rs --dynamic-loading paddle_fluid_c

这里我们会使用libloading去加载我们的预测库,因此我们指定了dynamic-loading。到这里实际上已经完成了从C到Rust的工作,剩下的就算一些包装和测试工作。

ResNet Demo

下面我们将Paddle Inference文档中的C语言示例用Rust来写一遍,在Cargo.toml中,需要添加我们的依赖:

1
2
3
4
...
[dependencies]
paddle-sys = "0.1.0"
libc = "0.2.81"

我们的Rust源码如下:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
use paddle_sys;
use libc::{c_void, calloc, free, malloc};
use std::ffi::{CStr, CString};
use std::mem::{size_of, transmute};
use std::ptr::write;

fn main() {
#[cfg(not(target_os = "linux"))]
const LIB_PATH: &str = "paddle_fluid_c.dll";

#[cfg(target_os = "linux")]
const LIB_PATH: &str = "libpaddle_fluid_c.so";

unsafe {
let paddle = paddle_sys::bindings::paddle_fluid_c::new(LIB_PATH).unwrap();
let config = paddle.PD_NewAnalysisConfig();

let model_path = CString::new("/path/to/model").unwrap();
let param_path = CString::new("/path/to/params").unwrap();
paddle.PD_SetModel(config, model_path.as_ptr(), param_path.as_ptr());

let input_tensor = paddle.PD_NewPaddleTensor();

let input_buffer = paddle.PD_NewPaddleBuf();
match paddle.PD_PaddleBufEmpty(input_buffer) {
true => println!("PaddleBuf empty"),
false => panic!("PaddleBuf NOT empty"),
}

let batch = 1;
let channel = 3;
let height = 318;
let width = 318;
let input_shape: [i32; 4] = [batch, channel, height, width];
let input_size: usize = (batch * channel * height * width) as usize;
let input_data = calloc(size_of::<f32>(), input_size) as *mut f32;
for i in 0..input_size {
let root: *mut f32 =
transmute(transmute::<*mut f32, u64>(input_data) + (size_of::<f32>() * i) as u64);
write(root, 1.0);
}
paddle.PD_PaddleBufReset(
input_buffer,
input_data as *mut c_void,
(size_of::<f32>() * input_size) as u64,
);

let input_name = CString::new("data").unwrap();
paddle.PD_SetPaddleTensorName(input_tensor, input_name.as_ptr() as *mut i8);
paddle.PD_SetPaddleTensorDType(input_tensor, paddle_sys::bindings::PD_DataType_PD_FLOAT32);
paddle.PD_SetPaddleTensorShape(input_tensor, input_shape.as_ptr() as *mut i32, 4);
paddle.PD_SetPaddleTensorData(input_tensor, input_buffer);

let output_tensor = &mut paddle.PD_NewPaddleTensor();
let output_size: *mut i32 = malloc(size_of::<i32>()) as *mut i32;
paddle.PD_PredictorRun(config, input_tensor, 1, output_tensor, output_size, 1);

println!("Output Tensor Size: {}", &*output_size);
println!(
"Output Tensor Name: {}",
CStr::from_ptr(paddle.PD_GetPaddleTensorName(*output_tensor))
.to_str()
.unwrap()
);
println!(
"Output Tensor Dtype: {}",
paddle.PD_GetPaddleTensorDType(*output_tensor)
);

let output_buffer = paddle.PD_GetPaddleTensorData(*output_tensor);
let result_length = paddle.PD_PaddleBufLength(output_buffer) as usize / size_of::<f32>();
println!("Output Data Length: {}", result_length);
assert_eq!(result_length, 512);

free(input_data as *mut c_void);
free(output_size as *mut c_void);
paddle.PD_DeletePaddleTensor(input_tensor);
paddle.PD_DeletePaddleBuf(input_buffer);
paddle.PD_DeleteAnalysisConfig(config);
}
}

上面的代码完全复现了官方的C语言Demo,修改模型所在路径即可运行。需要注意的是paddle-sys属于External FFI bindings,因此需要使用到一些unsafe的特性,并且直接使用库的话,我们需要用C下面的数据类型和指针。

运行代码我们需要添加预测库到环境变量中确保程序能找到相应的库,请修改路径为libpaddle_fluid_c.so的实际路径:

1
2
3
cargo build
export LD_LIBRARY_PATH=/path/to/paddle_lib:$LD_LIBRARY_PATH
cargo run

写在最后

paddle-sys作为Rust上的PaddlePaddle实践,我会跟踪飞桨每个Release进行维护更新,有什么问题和建议非常欢迎提出。同时也希望能和对Rust感兴趣的小伙伴们一起,共同开发一个基于paddle-sys的安全crate,提供更加易用的方法来使用。祝愿PaddlePaddle和Rust社区蓬勃发展!

  • 本文标题:在Rust上进行飞桨模型推理
  • 本文作者:LI Rui
  • 创建时间:2020-12-25 09:22:03
  • 本文链接:https://www.lirui.tech/post/2020/883983ab27b0.html
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-SA 许可协议。转载请注明出处!