把 ONNX 模型编译成 WebAssembly,并不等于获得了可用的 AI 推理服务。
真正决定性能与可维护性的,通常不是“模型能不能跑”,而是 张量如何跨越 ABI、运行时如何提供算子、推理是否发生了重复拷贝。
如果目标是掌握 WebAssembly 在 AI 推理中的工程应用,应先理解 WASI Preview 2 与组件模型,再处理模型格式、算子后端、内存和并发问题。
一、先判断:什么时候需要 WebAssembly 推理
WebAssembly(Wasm)适合解决的问题是:在不依赖完整操作系统环境的前提下,把一段可移植代码安全地运行在浏览器、边缘节点、插件系统或多语言服务中。
AI 推理中常见的场景包括:
- 浏览器本地运行文本分类、图像预处理、Embedding 或小型生成模型;
- 在 SaaS 平台中加载第三方模型插件,限制插件访问文件、网络和系统资源;
- 在边缘节点运行固定模型,减少 Python 运行时和系统依赖;
- 把推理前处理、后处理、Tokenizer 或特征工程封装成跨语言组件;
- 在 Rust、Go、C++ 服务中嵌入一段受控的推理逻辑。
结论一:WebAssembly 更适合“可移植、受约束、模型规模受控”的推理环节,而不是默认替代 CUDA 推理服务。
机制在于,Wasm 运行时通过线性内存、验证过的指令集和宿主能力边界隔离执行代码;但它本身不提供 GPU 算子、模型仓库或高性能张量库。模型最终仍然要依赖 CPU SIMD、GPU API 或宿主侧推理引擎。
例如,浏览器中的 onnxruntime-web 可以使用 WebAssembly Execution Provider 执行部分 ONNX 模型,但大模型的内存占用、算子支持和浏览器线程策略仍然决定了它是否适合生产使用。
二、必须分清的四个概念
1. WebAssembly 核心模块
Wasm Module 是由 Wasm 指令、线性内存、导入和导出组成的二进制模块。核心规范提供 i32、i64、f32、f64 等基础类型,SIMD 扩展提供固定宽度的 v128 向量类型。
它解决的是“代码如何被安全执行”,不解决:
- 文件如何读取;
- 网络如何访问;
- 模型如何加载;
- Tensor 如何表示;
- GPU 如何调用。
因此,一个只导出 malloc、run、free 的 C ABI 模块,能够运行,但跨语言调用者必须自己约定指针、长度、数据布局和错误码。
2. WASI
WASI(WebAssembly System Interface)是 Wasm 访问宿主能力的接口集合,例如文件、随机数、时钟和网络。
WASI Preview 2 以组件模型为基础,并使用 WIT(Wasm Interface Type)描述接口。它的重点不是“把 POSIX 搬进 Wasm”,而是建立语言无关、可组合的接口契约。
结论二:WASI Preview 2 解决的是能力与接口治理,不是推理加速本身。
机制是:组件通过导入接口声明自己需要什么能力,宿主决定是否提供。例如,模型组件可以只获得只读文件系统和时钟能力,而不能直接访问任意网络。
3. Component Model
组件模型(WebAssembly Component Model)在核心模块之上定义了更高层的类型和组合方式,包括字符串、列表、记录、变体、资源等。
一个简化的推理接口可以写成:
package ai:inference;
interface runtime {
resource session;
// 加载模型,返回一个由组件管理的 session 资源
open: func(model: list<u8>) -> result<session, string>;
// 输入与输出使用 f32 列表表示
run: func(s: borrow<session>, input: list<f32>)
-> result<list<f32>, string>;
}
world inference {
export runtime;
}
这个接口比裸指针 ABI 更容易生成 Rust、Go 或 JavaScript 胶水代码,也更容易做版本演进。
但它有一个重要边界:list<u8> 和 list<f32> 代表跨组件边界的数据传递,具体实现可能包含内存分配和拷贝。对于每次推理都传输几十 MB 输入的模型,这个接口可能比模型计算本身更慢。
4. 推理接口与后端
WASI-NN(WebAssembly System Interface for Neural Networks)试图定义加载神经网络图、创建执行上下文和执行推理的接口。它与 WASI Preview 2 不是同一个概念,也不能假设所有 WASI 运行时都完整支持它。
结论三:WASI-NN 是推理能力接口,WASI Preview 2 是组件与宿主能力体系,二者需要分别验证。
机制上,WASI-NN 可以把“模型图”和“执行后端”分离:模块只描述推理请求,运行时再选择 CPU、TensorFlow Lite 或其他后端。但不同实现对算子、模型格式和硬件加速的支持并不一致。
工程上应把以下内容写进兼容性矩阵:
- ONNX、TFLite 或自定义格式;
- 动态维度是否支持;
Conv、MatMul、Gather、LayerNorm等算子是否支持;- FP32、FP16、INT8 是否支持;
- CPU SIMD、线程和 GPU 是否可用;
- 模型加载是否发生在模块内还是宿主侧。
三、AI 推理在 Wasm 中是怎样完成的
一次完整推理可以拆成五层:
请求输入
↓
Tokenizer / 图像预处理
↓
张量 ABI:形状、类型、布局、生命周期
↓
推理后端:ONNX Runtime / TFLite / 自定义算子
↓
Wasm 运行时:内存、线程、文件、宿主能力
1. 模型不是普通文件
模型通常包括权重、图结构、词表和额外元数据。把模型打包进 .wasm 可以简化分发,但会造成模块体积膨胀、启动时间增加,并可能阻止权重缓存。
更常见的做法是:
- Wasm 模块只包含推理逻辑;
- 通过 WASI 文件接口或宿主导入加载模型;
- 启动时创建 session;
- 多次复用 session 和已分配的张量缓冲区。
结论四:推理服务必须区分“模型加载路径”和“单次执行路径”。
如果每次请求都重新解析模型、分配权重并创建执行图,即使算子本身很快,整体延迟也会很高。一个可验证的工程检查项是:压测时分别记录冷启动耗时、模型加载耗时和稳态单次推理耗时,而不是只看平均请求延迟。
2. 内存拷贝常常比算子更值得优化
Wasm 线性内存本质上是连续字节区域。宿主调用模块时,常见流程是:
宿主 Tensor
→ 拷贝到 Wasm 线性内存
→ Wasm 推理
→ 从线性内存拷贝输出
→ 宿主 Tensor
如果输入是图像或长文本,拷贝成本可能很明显。组件模型提供了更安全的类型契约,但不会自动消除所有数据复制。
可以采用以下策略:
- 复用输入输出缓冲区;
- 避免每层算子都创建新的数组;
- 统一 Tensor 的 dtype、shape 和 row-major 布局;
- 让预处理与推理处于同一个组件或同一块线性内存中;
- 对大模型考虑宿主侧加载权重,Wasm 只负责受控计算。
3. SIMD 和线程必须单独验证
WebAssembly SIMD 使用 v128 指令表达向量计算,可以帮助矩阵乘法、卷积和激活函数获得更高吞吐。但 SIMD 只提供指令能力,不保证后端自动生成高效代码。
浏览器线程还受安全策略影响。使用 SharedArrayBuffer 的多线程 WebAssembly 通常需要页面具备跨源隔离配置,例如:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
服务器侧则要确认运行时是否启用了线程、内存上限和对应的编译选项。不能仅因为模块编译成功,就认为运行环境一定支持线程。
四、工程选型:浏览器、WASI 组件还是原生后端
| 方案 | 适合场景 | 优点 | 主要边界 |
|---|---|---|---|
| 浏览器 Wasm | 小型分类、Embedding、预处理 | 无需上传数据,部署简单 | 内存、线程、算子和浏览器策略受限 |
| WASI Preview 2 组件 | 插件化推理、边缘服务、跨语言模块 | 接口清晰,能力隔离,易组合 | 推理后端生态仍需逐项验证 |
| Wasm + 宿主推理库 | 需要隔离业务逻辑,模型由宿主管理 | 可复用成熟推理引擎 | ABI 与 Tensor 拷贝设计复杂 |
| 原生 CPU/GPU 服务 | 大模型、高吞吐、低延迟 | 后端和硬件支持完整 | 部署依赖多,隔离成本更高 |
以 Wasmtime 27.0.0 这一类支持组件模型的运行时为例,可以把 WIT 接口编译成宿主语言绑定,再由宿主决定文件目录、资源限制和日志接口。对于需要严格租户隔离的插件系统,这种架构通常比允许第三方直接加载 Python 包更容易治理。
但如果模型依赖 CUDA、FlashAttention 或特定 GPU 算子,优先选择原生推理服务。Wasm 可以负责 Tokenizer、请求校验和后处理,不必强行承载核心矩阵计算。
五、一个真实容易踩到的坑
坑:组件接口很安全,但性能反而下降
触发条件是:开发者使用 list<f32> 作为每层输入输出,并在组件之间频繁传输大张量。
现象通常包括:
- CPU 利用率不高,但请求延迟明显;
- profile 显示大量内存分配和复制;
- 小输入性能正常,批量输入后吞吐快速下降;
- 换成单体核心模块后性能明显改善。
规避方式是:
- 先用组件模型表达稳定的业务边界;
- 将高频、大数据量的 Tensor 计算收敛到同一组件内部;
- 对跨边界数据使用句柄、资源或批量接口;
- 用
perf、运行时 profiling 和内存分配统计确认瓶颈; - 不要用“组件调用次数”替代实际性能测试。
另一个常见坑是模型算子不兼容。模型能被转换成 ONNX,并不代表目标 Wasm 后端能够执行全部算子。应在构建阶段固定模型版本、输入 shape、dtype 和算子清单,并对每个模型执行真实推理回归。
六、AI 工具如何帮助学习与开发
AI 编程工具适合加速三类工作:
- 根据 WIT 接口生成 Rust、Go、JavaScript 胶水代码;
- 根据模型图列出算子、输入输出 shape 和 dtype;
- 生成覆盖冷启动、并发、异常输入和资源限制的测试样例。
但它不能替代运行时验证。尤其要警惕以下错误:
- 把 WASI Preview 2 当成完整 POSIX 环境;
- 把 WASI-NN 的某个实现能力当成标准保证;
- 忽略浏览器的跨源隔离要求;
- 用
list<f32>传输大张量,却声称“零拷贝”; - 根据模型结构猜测算子一定受支持。
更可靠的做法是让 AI 先生成“假设清单”,然后逐项用官方规范、运行时文档和基准测试确认。AI 可以加快查资料和写样板代码,但不能替你证明 ABI、内存和后端行为。
七、系统学习路线
第一阶段:掌握核心模块
先学习:
- 线性内存、表、导入导出;
wasm32-wasi编译目标;- Rust 或 C/C++ 到 Wasm 的编译流程;
- 内存分配、指针长度协议和错误传播;
- SIMD、线程和模块实例化。
动手项目可以是一个不依赖框架的文本预处理模块,明确测量字符串传递和内存分配成本。
第二阶段:学习 WASI Preview 2 与组件模型
重点掌握:
- WIT 的接口、world、record、variant、resource;
wit-bindgen、组件适配器和宿主绑定;- 文件、时钟、随机数等 WASI 能力;
- 组件版本演进与错误建模。
动手实现一个“模型文件读取 + Tokenizer + 结构化结果返回”的组件,不要一开始就接大模型。
第三阶段:进入推理后端
选择一种格式深入:
- ONNX Runtime Web:理解浏览器 Wasm 后端、SIMD 和线程;
- TensorFlow Lite:理解静态图、量化和嵌入式部署;
- WASI-NN 实现:理解推理接口与具体运行时支持之间的差异。
此阶段必须建立模型兼容性测试,而不是只验证一个样例输入。
第四阶段:做工程化压测
至少测量:
- 冷启动与稳态延迟;
- 模型加载时间;
- 输入输出拷贝耗时;
- 单实例与多实例内存;
- 并发数变化下的吞吐;
- SIMD、线程开启前后的差异;
- 不同运行时之间的算子和性能差异。
进阶资料应优先阅读 WebAssembly Core Specification、WASI 规范、Component Model 与 WIT 文档,以及 Wasmtime、ONNX Runtime Web、WasmEdge 等实现的版本文档,而不是只看框架示例。
总结:WebAssembly AI 推理的核心不是“把模型编译成 Wasm”,而是同时设计推理后端、Tensor ABI、内存生命周期和 WASI 能力边界。学习顺序应是核心模块 → 组件模型 → 推理后端 → 性能与兼容性验证。
在实际项目中,你更倾向于让 Wasm 承担完整推理,还是只负责 Tokenizer、预处理和插件隔离?