基于 TVM 编译栈的 WebAssembly 独立深度学习推理:wasm-standalone 项目实战解析
2026/9/23 18:38:42 网站建设 项目流程
  • 编译器
  • 深度学习
  • 模型优化

【免费下载链接】tvm

Open deep learning compiler stack for cpu, gpu and specialized accelerators

项目地址:https://gitcode.com/gh_mirrors/tvm7/tvm
点击查看免费下载

本文围绕仓库中的apps/wasm-standalone实验性项目,完整讲解如何把深度学习框架导出的 ONNX 模型,经 TVM Relay 编译成 WebAssembly 图(.wasm),再借助 Rust 生态的 wasmtime 运行时在宿主机上加载并执行推理。读完本文,你将掌握 WASM 图生成与图加载两端的分工、完整的 ResNet50 端到端构建与测试流程,以及源码层面的关键实现原理。

背景与动机:把 TVM 编译栈搬到 WebAssembly

TVM 是一个面向 CPU、GPU 与专用加速器的开源深度学习编译栈,其运行时本身就支持将 WASM 作为可选硬件后端(这一点在本项目的构建脚本中体现为llvm -mtriple=wasm32-unknown-unknown这一 target)。wasm-standalone的动机正是把 WebAssembly 的可移植性与沙箱安全性,和 TVM 的领域专用优化能力结合起来:由 TVM 负责把框架模型编译成针对 WASM 后端自动优化的图,由 WebAssembly 负责提供跨平台、可隔离的执行环境,从而构建一个灵活且自动优化的、面向"所有深度学习框架"的图编译器。

需要特别说明的是,项目自身在 README 中明确标注为experimental(实验性):它目前只作为"在 WebAssembly 运行时上运行深度学习框架"的概念验证(PoC),并非生产级方案。本文所有结论均以当前仓库中的代码与文档为准。

整体架构:框架模型如何变成 wasm 图

README 用两张示意图描述了完整的系统全景,整个链路分为"WASM 图生成"与"WASM 图加载"两个阶段。

WASM 图生成(对应仓库中apps/wasm-standalone/wasm-graph目录):

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ | | | | | | | Framework Model | ---> | ONNX Model | ---> | TVM Relay Python API | |_ _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ _ _ _| || \/ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ | | | | | WASM Graph Builder | | TVM Compiler Stack | | (TVM runtime) | |_ _ _ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ _ _| || || \/ _ _ _ _ _ _ _ _ _ || _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ | | \/ | | llvm-ar | | | wasm_graph.wasm | <--- | libgraph_wasm32.a | <------- | graph.o | |_ _ _ _ _ _ _ _ _| |_ _ _ _ _ _ _ _ _ _| |_ _ _ _ _|

WASM 图加载(对应仓库中apps/wasm-standalone/wasm-runtime目录):

_ _ _ _ _ _ _ _ _ _ _ | | | WASM Graph Loader | | (WASM runtime) | |_ _ _ _ _ _ _ _ _ _ _| || \/ _ _ _ _ _ _ _ _ _ _ | | | wasm_graph.wasm | |_ _ _ _ _ _ _ _ _ _|

把两张图与源码对应起来:

  1. 框架模型 → ONNX 模型 → Relay:构建脚本 build_graph_lib.py 下载预训练的resnet50-v2-7.onnx,经relay.frontend.from_onnx转换为 Relay 计算图;
  2. TVM Compiler Stack 产出graph.orelay.build针对 wasm32 目标生成目标文件;
  3. llvm-ar打包libgraph_wasm32.a:把graph.o归档为静态库,供 Rust 侧链接;
  4. WASM Graph Builder 产出wasm_graph.wasmwasm-graphcrate(cdylib)把静态库、graph.jsongraph.params一起链接/内嵌为最终的.wasm模块;
  5. WASM Graph Loader 加载执行wasm-runtimecrate 用 wasmtime 嵌入 API 实例化wasm_graph.wasm,完成一次推理。

项目状态与算子支持矩阵

项目处于非常早期的实验阶段,README 给出的当前算子/模型支持矩阵如下:

Model NameStatus
ResNet50✔️
LeNet—(未完成)

同时 README 给出明确提示:目前仅在 Ubuntu 系统上测试过,测试环境需准备Ubuntu 16.04+。在 Windows 或 macOS 上运行本示例不在当前支持范围内。

环境准备(Pre-installation)

开始构建前需要准备三样东西:Rust 工具链、TVM 安装、LLVM。

Rust 与 wasm32-wasi 目标

安装 Rust 后(见文末附录),先为 Rust 添加wasm32-wasi目标,这是把 Rust cdylib 编译成 WASI 版 WebAssembly 模块的前提:

rustup target add wasm32-wasi

TVM

需要先完成 TVM 的安装。具体步骤请参考仓库内 docs/install/from_source.rst(源码编译安装文档),或使用仓库conda/docker/目录下提供的现成构建脚本与镜像。

LLVM

编译 wasm32 目标要求LLVM 10.0 或更高版本。构建时通过LLVM_AR环境变量指定归档工具(示例中为llvm-ar-10)。

第一步:构建 ResNet50 的 WebAssembly 图

构建 DL 库(WebAssembly 格式)的命令如下:

cd apps/wasm-standalone/wasm-graph/tools && LLVM_AR=llvm-ar-10 python ./build_graph_lib.py -O3

这条命令做了四件事,全部封装在 build_graph_lib.py 的build_graph_lib(opt_level)函数中:

  1. 下载并加载模型:从 ONNX 模型库下载resnet50-v2-7.onnx(脚本内固定了该模型的 URL 与 commit 版本),用onnx.load读入;同时下载一张示例图片imagenet_cat.png用于测试;

  2. 图像预处理:把图片 resize 到224x224,转成float32,将 HWC 布局转成 ONNX 期望的 CHW 布局np.transpose(img_data, (2, 0, 1)),再按 ImageNet 规范做归一化((img_data / 255 - mean) / stddev,mean/std 分别为[0.485, 0.456, 0.406][0.229, 0.224, 0.225]),最后添加 batch 维度得到 NCHW 输入(1, 3, 224, 224),输入名固定为"data"

  3. Relay 编译relay.frontend.from_onnx(onnx_model, shape_dict)得到modparams,随后针对 wasm32 目标编译(build_graph_lib.py):

    target = "llvm -mtriple=wasm32-unknown-unknown -mattr=+simd128" with tvm.transform.PassContext(opt_level=opt_level): factory = relay.build( mod, target=target, params=params, runtime=tvm.relay.backend.Runtime("cpp", {"system-lib": True}), )

    关键点有两个:一是 target 明确指定了 wasm32 架构并开启simd128向量指令集;二是运行时选用cpp后端且开启system-lib(即把图函数作为系统库符号注册,后续由 Rust 侧的SystemLibModule直接调用)。opt_level由命令行-O/--opt-level参数控制,默认 0,最高 3;

  4. 产出三个构建产物(输出目录wasm-graph/lib/):

    • graph.ofactory.get_lib().save(obj_file)保存编译出的目标文件(build_graph_lib.py);
    • libgraph_wasm32.a:调用llvm-ar rcsgraph.o归档成静态库,归档器通过环境变量LLVM_AR指定,缺省回退为llvm-ar-10(build_graph_lib.py);
    • graph.jsongraph.params:分别保存计算图结构与参数(build_graph_lib.py)。

第二步:构建 wasm-graph 包

wasm-graph是一个 Rustcdylib库(无main,对外只导出 FFI 符号),它负责把上一步的静态库与图/参数内嵌进最终的.wasm模块。构建命令:

cd apps/wasm-standalone/wasm-graph && cargo build --release cp ./target/wasm32-wasi/release/wasm_graph.wasm ./lib/wasm_graph_resnet50.wasm

第一条命令在 release 模式下编译,第二条把生成的wasm_graph.wasm复制为带模型标识的wasm_graph_resnet50.wasm

链接静态库的关键配置在 wasm-graph/.cargo/config:

[build] target = "wasm32-wasi" rustflags = ["-C", "link-arg=--whole-archive", "-C", "link-arg=-lgraph_wasm32"]

即:默认构建目标为wasm32-wasi,链接时用--whole-archive强制把libgraph_wasm32.a中由 TVM 生成的全部符号纳入最终模块(避免因"无引用"被裁剪掉)。

依赖关系见 wasm-graph/Cargo.toml:crate-type = ['cdylib'];依赖本地 Rust cratetvm-sys../../../rust/tvm-sys)与tvm-graph-rt../../../rust/tvm-graph-rt),以及serde/serde_json/ndarray/lazy_static;release 配置开启 LTO 并选择体积优先的opt-level = 's'

入口实现在 wasm-graph/src/lib.rs,其结构清晰地解释了"图如何被内嵌并执行":

  • 通过include_str!include_bytes!编译期lib/graph.jsonlib/graph.params直接内嵌进二进制(lib.rs);
  • lazy_static构造全局单例:SYSLIB: SystemLibModule(对应 TVM 的 system-lib 模式)与GRAPH_EXECUTOR: Mutex<GraphExecutor>。初始化时先调用__wasm_call_ctors(),其注释指出这是"调用TVMBackendRegisterSystemLibSymbolAPI 所必需的"(lib.rs);
  • 对外导出唯一函数run(wasm_addr: i32, in_size: i32) -> i32(lib.rs):从 wasm 线性内存地址反序列化出输入张量,转成DLTensorset_input("data", ...),执行executor.run(),取 0 号输出,再序列化写回 wasm 内存并返回输出字节数。由于执行器非多线程,这里用Mutex保证单次持锁执行。

张量与 DLTensor 的桥接在 wasm-graph/src/types.rs:自定义Tensordtype/shape/strides/data)通过as_dltensor()构造出DLTensor(CPU 设备、FP32/INT32/INT8 三种数据类型映射),也实现了From<DLTensor>反向转换。内存 I/O在 wasm-graph/src/utils.rs:load_input从裸指针按字节切片后用serde_json::from_slice反序列化,store_output则把输出张量serde_json::to_vec后逐字节写入 wasm 内存——由此可见,PoC 阶段选择JSON 作为跨 wasm 边界的数据协议,这是一种便于调试、性能上偏简化的设计取舍。

第三步:运行推理测试

测试程序test_graph_resnet50是一个独立的 Rust 二进制,位于 apps/wasm-standalone/wasm-runtime/tests/test_graph_resnet50,其依赖见 Cargo.toml(getoptsndarraycsvimage,以及本地的wasm-runtime)。先编译(需要 Rust):

cd apps/wasm-standalone/wasm-runtime/tests/test_graph_resnet50 && cargo build

查看命令行用法:

~# ./target/debug/test_graph_resnet50 -h Usage: ./target/debug/test_graph_resnet50 [options] Options: -g, --wasm-graph-file FILE_PATH set the path to wasm graph file -i, --input-data-file FILE_PATH set the path to input image file -l, --label-class-file FILE_PATH set the path to label class file -h, --help print this help menu

三个核心选项的含义与实现(main.rs):

选项作用
-g指定wasm_graph_resnet50.wasm文件路径,GraphExecutor::instantiate用它实例化模块
-i指定输入图片路径,image::open打开后进入预处理流程
-l指定标签类文件路径(如synset.csv),用于把 argmax 结果映射为类别名

接下来执行推理。先把 wasm 图文件复制到当前目录,再准备一张测试图片和标签文件(原 demo 通过下载获取cat.png(一张 256×256 的测试图片)与synset.csv(ImageNet 的类别 id,类别名映射表);也可以自行准备等价的本地文件):

$ cp ../../../wasm-graph/lib/wasm_graph_resnet50.wasm ./ $ wget -O cat.png <demo-cat-image-url> $ wget -O synset.csv <demo-synset-label-url> $ ./target/debug/test_graph_resnet50 -g ./wasm_graph_resnet50.wasm -i ./cat.png -l ./synset.csv

成功时的输出如下:

original image dimensions: (256, 256) resized image dimensions: (224, 224) input image belongs to the class `tiger cat`

测试端的预处理逻辑在 main.rs 的data_preprocess:打印原始尺寸 →resize_exact(224, 224)(Nearest 插值)→ 按 RGB 通道用 ImageNet 统计量归一化(此处用 0-255 整数域的常量(123, 117, 104)(58.395, 57.12, 57.375),与构建脚本中 0-1 域的 mean/std 互为 255 倍缩放关系,数值上略有出入)→ 展平为(H,W,C)permuted_axes([2, 0, 1])转成 CHW → 构造Tensor分类输出output_assert(main.rs):取输出向量的 argmax,用csv解析synset.csv建立(类 id, 类名)映射,打印类别。

WASM Graph Loader 与 wasmtime 的交互原理

wasm-runtimecrate 是"加载端",其依赖(wasm-runtime/Cargo.toml)核心是wasmtime = "0.28.0"wasmtime-wasi = "0.28.0"GraphExecutor(graph.rs)封装了完整的四步交互:

  1. instantiate(graph.rs):创建Engine时开启Config::new().wasm_simd(true);构造Linker并注册 WASI(wasmtime_wasi::add_to_linker),通过WasiCtxBuilder::new().inherit_stdio().inherit_args()让模块继承宿主的标准输入输出与命令行参数;最后Module::from_file加载.wasm并用linker.instantiate实例化;
  2. set_input(graph.rs):从实例导出中取memory,把输入Tensor序列化为 JSON 字节,先memory.grow((in_size >> 16) as u32 + 1)(以 64KB 页为单位扩容、至少一页),再memory.write写入 wasm 线性内存,记录写入地址wasm_addr与长度input_size
  3. run(graph.rs):通过instance.get_func("run")拿到 wasm-graph 导出的run函数,以(wasm_addr, input_size)两个i32参数调用,返回输出字节数out_size;若返回 0 则panic!("graph run failed!")
  4. get_output(graph.rs):从wasm_addr处读取out_size字节,serde_json::from_slice反序列化回Tensor

由此可见,wasm-graph(图内)与 wasm-runtime(图外)通过"wasm 线性内存地址 + JSON 字节流"这一对偶协议完成通信:一端写、一端读,run的返回值充当握手信息。这也再次印证了本项目作为 PoC 的实验性质。

无 WASI 的纯 wasm32 变体

README 特别说明:本示例在没有 WASI 支持的情况下也能工作。切换方式有三步:

  1. 修改 apps/wasm-standalone/wasm-graph/.cargo/config,把构建目标从wasm32-wasi改为wasm32-unknown-unknown
  2. 在 apps/wasm-standalone/wasm-runtime/src/graph.rs 的instantiate取消注释"raw wasm engine"代码(即不使用Linker/WasiCtx,直接用Engine::new(Config::new().wasm_simd(true))+Store::new(&engine, ())的无状态 Store 方案),改以纯 wasm32 模式运行;该文件第 31-32、51-61 行的注释即保留了这一套替代实现;
  3. 注意:没有 WASI 支持时 SIMD 可能不可用,此时还需要删除 build_graph_lib.py 中 target 字符串里的-mattr=+simd128,重新生成不带 simd128 的图库。

后续工作(Future Work)

README 记录的规划方向包括:

  • 更多网络支持(More networks support):TODO,当前支持矩阵中 LeNet 尚处于未完成状态;
  • 性能基准(Performance benchmark):已完成的优化项是WebAssembly simd128 支持(Done);进行中的是面向 llvm target 的 AutoTVM 增强;
  • 原生 TVM Rust runtime 支持(Native TVM Rust runtime support):TODO,即把rust/tvm-graph-rtrust/tvm-sys这套 Rust 侧运行时进一步原生化。

附录:系统依赖安装

Rust(最新版本)

  • Linux 用户:在终端执行以下命令,然后按屏幕提示完成安装:

    curl https://sh.rustup.rs -sSf | sh
  • Windows 用户:下载并运行 Rust 官方 Windows 安装程序(RUST-INIT.EXE),然后按屏幕提示操作。

安装完成后,配合前文的rustup target add wasm32-wasi即可获得 wasm32-wasi 的交叉编译能力。整个流程涉及的软件与版本要求可概括为:Rust(含 wasm32-wasi target)、TVM(按 docs/install/from_source.rst 安装)、LLVM 10.0+、Ubuntu 16.04+ 测试环境——四者齐备后,即可按"编译 ONNX → 打包 wasm → cargo 构建 → 运行测试"的路径,把 ResNet50 完整跑在 WebAssembly 之上。

  • 编译器
  • 深度学习
  • 模型优化

【免费下载链接】tvm

Open deep learning compiler stack for cpu, gpu and specialized accelerators

项目地址:https://gitcode.com/gh_mirrors/tvm7/tvm
点击查看免费下载
上一篇:如何掌握Type-Challenges中的Pick类型工具:从入门到精通
下一篇:攻克Chrome浏览器系统弹窗:Selenium自动化测试的终极解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询