Firefox本地AI运行时性能优化实战

本文详细介绍了Mozilla如何通过将Transformers.js默认的onnxruntime-web替换为原生C++版本,显著提升Firefox本地AI运行时的推理速度。优化涉及WASM限制突破、多线程矩阵运算、编译图缓存等核心技术,实现了2-10倍的性能提升。

加速Firefox本地AI运行时

去年我们推出了Firefox AI运行时,这个引擎默默支撑着PDF.js生成替代文本等特性,以及最近推出的智能标签分组功能。系统虽然能工作,但速度未达我们预期。

本文阐述了我们如何通过替换Transformers.js默认使用的onnxruntime-web(改用现在内置于Firefox的原生C++版本)来加速推理过程。

起点

Transformers.js是Hugging Face Python库的JavaScript对应版本。其底层依赖onnxruntime-web,这是ONNX Runtime的WebAssembly(WASM)构建版本。

典型的推理周期包括:

  • JavaScript中的预处理(标记化、张量整形)
  • WASM中的模型执行
  • 回到JavaScript的后处理

即使有预热缓存,这个流程仍需跨越多个层级。真正的热点是矩阵乘法,在CPU上运行时使用通用SIMD实现。

为什么纯WASM不够

WASM SIMD很棒,但无法超越硬件特定指令,比如Apple Silicon上的NEON或现代Intel芯片上的AVX-512。

Firefox Translations(使用Bergamot)已经证明深入原生代码可以加速:它使用WASM内置函数,这些小型钩子让WASM能够调用使用这些内部函数编译的C++代码。这个昵称为gemmology的项目效果显著。

我们尝试将此技巧移植到ONNX,但大量运算符使得逐个重写难以维护。而且每次冷启动仍需支付JS/WASM预热开销。

切换到ONNX C++

Transformers.js通过一个极小的接口与ONNX Runtime通信。它创建一个会话,推送一个张量,然后拉取结果。这使得在不触及功能代码的情况下交换后端变得简单。

我们实现此目标的步骤是:

  1. 将ONNX Runtime C++直接引入Firefox代码树
  2. 通过薄WebIDL层将其暴露给JavaScript
  3. 将Transformers.js连接到新后端

从PDF替代文本等功能的视角来看,没有任何变化,它仍然调用await pipeline(...)。但在底层,张量现在直接进入原生代码。

将ONNX Runtime集成到构建系统

上游ONNX运行时不支持我们所有的构建配置,且代码量庞大。因此我们选择不将其加入代码树。相反,可以使用配置标志提供已编译的ONNX运行时版本。它最终会自动从Taskcluster下载(我们在那里为一系列支持的配置构建它)或由下游开发人员提供。这提供了灵活性,同时不拖慢我们通常的构建过程且维护需求低。

在Taskcluster上构建ONNX需要一些配置更改和上游补丁。目标是在速度和二进制大小之间找到平衡,同时与Firefox仓库的原生代码要求兼容。

最值得注意的是:

  • 在没有异常和RTTI支持的情况下构建需要一些上游补丁
  • 默认构建配置设置为MinSizeRel,编译使用LTO

成效

由于原生后端是即插即用的替代品,我们可以逐个功能启用它并收集实际数据。早期基准测试显示推理速度提升2到10倍,且零WASM预热开销。

例如,在首次运行时可能滞后的智能标签分组主题建议,现在相当迅捷,这是我们为Firefox 142逐步迁移到此后端的第一个功能。

用于PDF.js替代文本功能的图像到文本模型也受益于此更改。在相同硬件上,延迟从3.5秒降至350毫秒。

下一步计划

我们正在整个夏季逐步将此新后端推广到更多功能,因此所有基于Transformers.js构建的功能都能利用它。

而且手头有了C++ API,我们计划解决一些长期存在的痛点,并启用GPU支持。

这些更改将随我们定制的ONNX Runtime一起发布,并在未来为基于Transformers.js的功能在我们的运行时中提供最佳性能。

1. DequantizeLinear实现多线程

DequantizeLinear操作是单线程的,常常主导推理时间。虽然上游工作最近合并了一项改进(PR #24818),但我们构建了一个补丁来将工作分散到多个核心,让编译器自动向量化内部循环。结果几乎是线性的加速,尤其是在多核机器上。

2. 矩阵转置实现多线程

类似地,在执行推理任务时,通常需要转置非常大的矩阵(数十兆字节)。此操作原本使用嵌套for循环简单完成。切换到多线程缓存感知的平铺转置方案,并利用SIMD,使得能够充分利用现代硬件,将此操作加速超线性因子,通常是分配给此任务的线程数的两倍,例如使用4个线程实现8倍加速。

这可以解释为:朴素的for循环虽然自动向量化,但对CPU缓存的使用效果不佳。

3. 缓存编译图

在推理运行之前,ONNX Runtime会为当前平台编译模型图。在大型模型(如Qwen 2.5 0.5B)上,每次启动可能耗时长达五秒。

我们可以动态地将编译图与权重分开缓存,节省从几毫秒到整整五秒的时间。

4. 使用GPU

目前,我们仅集成了基于CPU的提供程序。下一步是支持GPU加速的ONNX后端,这将需要更多努力。这是因为GPU支持需要额外的沙盒化,以安全地与底层硬件交互。

结论

这次迁移的有趣之处在于,我们能够在逐步迁移功能的同时如此大幅度地提升性能,而且所有这些都是在完全隔离的情况下完成的,无需更改任何功能代码。

虽然从用户体验的角度来看,加速已经可见,但我们相信未来可以并且将会发生许多改进,进一步提高基于ML的功能的效率,并使它们对更广泛的受众更加易用。

有想法、问题或错误报告?在Discord的firefox-ai频道(https://discord.gg/TBZXDKnz)联系我们,或在Bugzilla上提交问题,我们洗耳恭听。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计