在上一篇文章中,我们展示了如何使用 OpenVINO 构建一个道路分割的机器学习推理任务。在这个过程中,我们观察到两个有趣且值得进一步完善的工作:
在示例中使用到了 wasi-nn crate,其为 WASI-NN 提案提供了 Rust 接口实现,从而大大降低了使用 Rust 语言构建基于 WebAssembly 技术的机器学习任务的流程复杂度。不过,wasi-nn crate 提供的接口是 unsafe 的,更适合作为底层API 用于构建更高层的库。因此,我们可以基于 wasi-nn crate 创建一个提供 safe 接口的库。
在对输入图片进行预处理的时候,我们使用到了 opencv crate 。但是,因为 opencv crate 无法编译为 wasm 模块,所以就不得不将图片预处理模块独立出来,单独作为一个项目来实现。
对于上述两个观察,我们尝试做了初步的尝试:
借鉴 Rust 和 WebAssembly 社区开发者的一些尝试,我们对 wasi-nn crate 中定义的unsafe 接口进行了抽象和安全封装,构建了 wasmedge-nn crate 原型。本文的后续部分将演示如何使用 wasmedge-nn crate 替换 wasi-nn crate,重新构建上一篇文章中所使用的道路分割 Wasm 推理模块。
Rust 社区中著名的图像处理库之一 image crate 提供了我们所需的图片预处理的基本能力;此外,由于其是 Rust 原生实现,所以基于这个库来构建我们需要的图像处理库是可以编译为 wasm 模块的。
下面,我们继续使用道路分割示例,具体演示一下我们的改进方案。
wasmedge-nn crate 的安全接口
在上一篇文章中,我们已经使用了 wasi-nn crate 中定义的五个主要的接口,他们分别对应 WASI-NN 提案中的接口。我们对照着看一下改进后的接口。下图中,蓝色框图中是我们要使用的 wasmedge-nn crate 的 nn 模块中定义的接口,绿色框图为相对应的 wasi-nn crate 中定义的接口,箭头显示了它们之间的映射关系。关于 wasmedge-nn crate 的设计细节,感兴趣的同学可以先行阅读源码,后续我们会在另外一篇文章进行讨论,所以这里就不进行过多的阐述了。
基于wasmedge-nn构建wasm推理模块
接下来,我们就通过代码来展示如何使用 wasmedge-nn 提供的接口和相关数据结构,重新实现 wasm 推理模块。
下面的示例代码是使用 wasmedge-nn crate 提供的安全接口重新构建的 wasm 推理模块。通过代码中的注释,可以很容易地发现:接口的调用顺序与使用 wasi-nn 接口的调用顺序保持一致;而最明显的不同之处在于,因为 wasmedge-nn 中定义的安全接口,所以示例代码中不再有 unsafe 字样出现。正如在上一篇文章中所阐述,示例代码中所展示的接口调用顺序可以看作一个模板:如果更换一个模型来完成一个新的推理任务,下面的代码几乎不需要任何改动。感兴趣的同学可以尝试使用其它的模型来试试。下面示例的完整代码可以在这里找到。use std::env;
use wasmedge_nn::{
cv::image_to_bytes,
nn::{ctx::WasiNnCtx, Dtype, ExecutionTarget, GraphEncoding, Tensor},
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
let model_xml_name: &str = &args[1];
let model_bin_name: &str = &args[2];
let image_name: &str = &args[3];
// 加载图片,并转换为字节序列
println!("Load image file and convert it into tensor ...");
let bytes = image_to_bytes(image_name.to_string(), 512, 896, Dtype::F32)?;
// 创建 Tensor 实例,包括数据、维度、类型等信息
let tensor = Tensor {
dimensions: &[1, 3, 512, 896],
r#type: Dtype::F32.into(),
data: bytes.as_slice(),
};
// 创建 WASI-NN Context 实例
let mut ctx = WasiNnCtx::new()?;
// 加载模型文件及其它推理过程需要的配置信息
println!("Load model files ...");
let graph_id = ctx.load(
model_xml_name,
model_bin_name,
GraphEncoding::Openvino,
ExecutionTarget::CPU,
)?;
// 初始化执行环境
println!("initialize the execution context ...");
let exec_context_id = ctx.init_execution_context(graph_id)?;
// 为执行环境提供输入
println!("Set input tensor ...");
ctx.set_input(exec_context_id, 0, tensor)?;
// 执行推理计算
println!("Do inference ...");
ctx.compute(exec_context_id)?;
// 获取推理计算的结果
println!("Extract result ...");
let mut out_buffer = vec![0u8; 1 * 4 * 512 * 896 * 4];
ctx.get_output(exec_context_id, 0, out_buffer.as_mut_slice())?;
// 导出计算结果到指定的二进制文件
println!("Dump result ...");
dump(
"wasinn-openvino-inference-output-1x4x512x896xf32.tensor",
out_buffer.as_slice(),
)?;
Ok(())
}
这里需要说明的是,最后导出的 .tensor 二进制文件用于后续可视化推理结果数据。由于示例代码是通过命令行来执行,在某些环境下(比如Docker)无法直接通过 API 调用展示推理结果,所以这里就只是导出推理结果。对于其他类型的推理任务,比如使用分类模型,在不需要可视化显示的情况下,就可以考虑直接打印分类结果,而无需导出到文件。作为参考,这里我们提供一段Python代码(引用自WasmEdge-WASINN-examples/openvino-road-segmentation-adas),通过读取导出的 .tensor 文件,可视化推理结果数据。import matplotlib.pyplot as plt
import numpy as np
# 读取保存推理结果的二进制文件,并将其转换为原始维度
data = np.fromfile("wasinn-openvino-inference-output-1x4x512x896xf32.tensor", dtype=np.float32)
print(f"data size: {data.size}")
resized_data = np.resize(data, (1,4,512,896))
print(f"resized_data: {resized_data.shape}, dtype: {resized_data.dtype}")
# 准备用于可视化的数据
segmentation_mask = np.argmax(resized_data, axis=1)
print(f"segmentation_mask shape: {segmentation_mask.shape}, dtype: {segmentation_mask.dtype}")
# 绘制并显示
plt.imshow(segmentation_mask[0])
基于 image crate 的图像预处理函数
除了提供安全的接口用于执行推理任务,通过 cv 模块,wasmedge-nn crate 提供了基本的图像预处理函数 image_to_bytes。这个函数的实现借鉴了 image2tensor 开源项目的设计,主要用于将输入图片转换为满足推理任务要求的字节序列,在后续步骤中进一步构建 Tensor 变量作为推理模块接口函数的输入。由于当前的后端仅支持 OpenVINO,图像处理的需求还比较简单,所以这个 cv 模块仅仅包含了这一个图像预处理函数。use image::{self, io::Reader, DynamicImage};
// 将图片文件转换为特定尺寸,并转换为指定类型的字节序列
pub fn image_to_bytes(
path: impl AsRef<Path>,
nheight: u32,
nwidth: u32,
dtype: Dtype,
) -> CvResult<Vec<u8>> {
// 读取图片
let pixels = Reader::open(path.as_ref())?.decode()?;
// 转换为特定的尺寸
let dyn_img: DynamicImage = pixels.resize_exact(nwidth, nheight, image::imageops::Triangle);
// 转换为BGR格式
let bgr_img = dyn_img.to_bgr8();
// 转换为指定类型的字节序列
let raw_u8_arr: &[u8] = &bgr_img.as_raw()[..];
let u8_arr = match dtype {
Dtype::F32 => {
// Create an array to hold the f32 value of those pixels
let bytes_required = raw_u8_arr.len() * 4;
let mut u8_arr: Vec<u8> = vec![0; bytes_required];
for i in 0..raw_u8_arr.len() {
// Read the number as a f32 and break it into u8 bytes
let u8_f32: f32 = raw_u8_arr[i] as f32;
let u8_bytes = u8_f32.to_ne_bytes();
for j in 0..4 {
u8_arr[(i * 4) + j] = u8_bytes[j];
}
}
u8_arr
}
Dtype::U8 => raw_u8_arr.to_vec(),
};
Ok(u8_arr)
}
有了安全的 wasmedge-nn crate, 与支持将 OpenCV 编译成 Wasm 的图像处理库,使用 Rust 与 WebAssembly 进行 AI 推理就变得非常简单。接下来只需按照第一篇文章的说明运行 OpenVINO 模型就可以了。
总结
wasi-nn crate 为 Rust 开发者提供了基础性的底层接口,在使用 WasmEdge Runtime 内建的WASI-NN 支持的场景下,大大降低了接口调用的复杂性;在此基础之上,通过提供安全封装的接口,wasmedge-nn crate 进一步完善了推理任务的用户接口定义;同时,通过进一步的抽象,将面向推理任务的前端接口与面向推理引擎的后端接口进行了解耦,从而实现前、后端之间的松耦合。
此外,通过 cv 模块提供的、基于 image crate 的图像预处理函数,允许图像预处理模块和推理计算模块编译在同一个 Wasm模块中,从而实现从原始图像到推理任务的输入张量、再到推理计算、最后到计算结果导出的流水线化。
关于 wasmedge-nn crate 的细节,我们会在下一篇文章中进行详细阐述。感兴趣的同学也可以前往 wasmedge-nn GitHub repo 进一步了解。我们也欢迎对 WasmEdge + AI感兴趣的开发者和研究员反馈你们的意见和建议;同时,也欢迎将你们的实践经验和故事分享到我们的 WasmEdge-WASINN-examples 开源项目。谢谢!