- 编译器
- 深度学习
- 模型优化
【免费下载链接】tvm
Open deep learning compiler stack for cpu, gpu and specialized accelerators
本文围绕仓库中的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 | |_ _ _ _ _ _ _ _ _ _|把两张图与源码对应起来:
- 框架模型 → ONNX 模型 → Relay:构建脚本 build_graph_lib.py 下载预训练的
resnet50-v2-7.onnx,经relay.frontend.from_onnx转换为 Relay 计算图; - TVM Compiler Stack 产出
graph.o:relay.build针对 wasm32 目标生成目标文件; llvm-ar打包libgraph_wasm32.a:把graph.o归档为静态库,供 Rust 侧链接;- WASM Graph Builder 产出
wasm_graph.wasm:wasm-graphcrate(cdylib)把静态库、graph.json与graph.params一起链接/内嵌为最终的.wasm模块; - WASM Graph Loader 加载执行:
wasm-runtimecrate 用 wasmtime 嵌入 API 实例化wasm_graph.wasm,完成一次推理。
项目状态与算子支持矩阵
项目处于非常早期的实验阶段,README 给出的当前算子/模型支持矩阵如下:
| Model Name | Status |
|---|---|
| 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-wasiTVM
需要先完成 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)函数中:
下载并加载模型:从 ONNX 模型库下载
resnet50-v2-7.onnx(脚本内固定了该模型的 URL 与 commit 版本),用onnx.load读入;同时下载一张示例图片imagenet_cat.png用于测试;图像预处理:把图片 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";Relay 编译:
relay.frontend.from_onnx(onnx_model, shape_dict)得到mod与params,随后针对 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;产出三个构建产物(输出目录
wasm-graph/lib/):graph.o:factory.get_lib().save(obj_file)保存编译出的目标文件(build_graph_lib.py);libgraph_wasm32.a:调用llvm-ar rcs把graph.o归档成静态库,归档器通过环境变量LLVM_AR指定,缺省回退为llvm-ar-10(build_graph_lib.py);graph.json与graph.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.json与lib/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 线性内存地址反序列化出输入张量,转成DLTensor后set_input("data", ...),执行executor.run(),取 0 号输出,再序列化写回 wasm 内存并返回输出字节数。由于执行器非多线程,这里用Mutex保证单次持锁执行。
张量与 DLTensor 的桥接在 wasm-graph/src/types.rs:自定义Tensor(dtype/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(getopts、ndarray、csv、image,以及本地的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)封装了完整的四步交互:
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实例化;set_input(graph.rs):从实例导出中取memory,把输入Tensor序列化为 JSON 字节,先memory.grow((in_size >> 16) as u32 + 1)(以 64KB 页为单位扩容、至少一页),再memory.write写入 wasm 线性内存,记录写入地址wasm_addr与长度input_size;run(graph.rs):通过instance.get_func("run")拿到 wasm-graph 导出的run函数,以(wasm_addr, input_size)两个i32参数调用,返回输出字节数out_size;若返回 0 则panic!("graph run failed!");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 支持的情况下也能工作。切换方式有三步:
- 修改 apps/wasm-standalone/wasm-graph/.cargo/config,把构建目标从
wasm32-wasi改为wasm32-unknown-unknown; - 在 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 行的注释即保留了这一套替代实现; - 注意:没有 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-rt、rust/tvm-sys这套 Rust 侧运行时进一步原生化。
附录:系统依赖安装
Rust(最新版本)
Linux 用户:在终端执行以下命令,然后按屏幕提示完成安装:
curl https://sh.rustup.rs -sSf | shWindows 用户:下载并运行 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
相关推荐
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考