在上一篇文章中,我们展示了如何使用 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 开源项目。谢谢!