从 WebAssembly 角度改进 WASI-NN | WASI-NN 系列文章2

上一篇文章中,我们展示了如何使用 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 cratenn 模块中定义的接口,绿色框图为相对应的 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 开源项目。谢谢!

{{o.name}}
{{m.name}}

猜你喜欢

转载自my.oschina.net/u/4532842/blog/5565103
nn