如何将 Tensorflow.js 的性能疯狂提升 100%+

简介: 如何将 Tensorflow.js 的性能疯狂提升 100%+
编者按:本文由支付宝体验技术部数据智能团队成员青壁编写。文中内容是他根据在实际项目中总结而来。


在正式开始前,我们先来看看两组对比的数据。


模型首次执行性能对比  tfjs vs ant-tfjs




推理性能对比(基于MobileNetV2)



1. Web上的高性能计算


[R]. Web Worker


使用 Web Worker 可以将一些 CPU 密集型计算转移到子线程中去做执行,同理可以将计算进行分拆,创建多个线程进行并行计算。这里为各位大佬献上一个利用 Web Worker 做并行计算的库 Paralles.js。


但是即使是支持了多线程并行计算,由于 JS 动态语言的特性,即使有了 V8引擎的加持,也是远远满足不了我们执行深度学习模型的性能需要。


[G]. Asm.js


2012年,Mozilla 的工程师 Alon Zakai 在研究 LLVM 编译器时突发奇想:许多 3D 游戏都是用 C / C++ 语言写的,如果能将 C / C++ 语言编译成 JavaScript 代码,它们不就能在浏览器里运行了吗?众所周知,JavaScript 的基本语法与 C 语言高度相似。


于是,他开始研究怎么才能实现这个目标,为此专门做了一个编译器项目 Emscripten。这个编译器可以将 C / C++ 代码编译成 JS 代码,但不是普通的 JS,而是一种叫做 asm.js 的 JavaScript 变体。


C / C++ 编译成 JS 有两个最大的困难。

  • C / C++ 是静态类型语言,而 JS 是动态类型语言。
  • C / C++ 是手动内存管理,而 JS 依靠垃圾回收机制。


asm.js 就是为了解决这两个问题而设计的:它的变量一律都是静态类型,并且取消垃圾回收机制。除了这两点,它与 JavaScript 并无差异,也就是说,asm.js 是 JavaScript 的一个严格的子集,只能使用后者的一部分语法。


image.png

一旦 JavaScript 引擎发现运行的是 asm.js,就知道这是经过优化的代码,可以跳过语法分析这一步,直接转成汇编语言。另外,浏览器还会调用 WebGL 通过 GPU 执行 asm.js,即 asm.js 的执行引擎与普通的 JavaScript 脚本不同。这些都是 asm.js 运行较快的原因。据称,asm.js 在浏览器里的运行速度,大约是原生代码的50%左右。

Asm.js 相关介绍摘自阮一峰老师的《asm.js 和 Emscripten 入门教程》


即使如此,asm.js 依旧满足不了执行深度学习模型的性能需要。


[B]. WebAssembly


关于 Wasm 的文章现在实在是太多了,我这里也就不赘述了。

Wasm 相对于原生 JS 或者 asm.js 来说,速度的确很快,TensorFlow.js 官方目前也实现了基于 Wasm 的 backend。但是目前在绝大多数的机器上,Wasm 的执行速度相较于下面要说到的 WebGL 要差个3倍以上。尤其在一些大型模型上表现更为明显。


上图是当前的 Wasm 和 WebGL 的性能对比,可以看到,WebGL 比 Wasm 要快5~7 倍左右。Wasm 最根本的问题还是在 width 较大的模型上,并行度和 GPU 相比还是不够,这个是 CPU 和 GPU 的硬件结构决定的,所以说在较大的模型上 WebGL 或者未来的 WebGPU 依旧是首选的 backend。


不过随着 Wasm 对 SIMD 指令集的支持以及多线程的支持,以后在性能上与 WebGL 的差距会越来越小。


[A]. GPU


在浏览器内利用 GPU 的能力有两种方法:


  1. 十分成熟的 WebGL
  2. 尚在草案阶段的 WebGPU。当前 WebGPU 的进展十分缓慢,我们这里先抛开不谈,谈一谈如何利用 WebGL 进行前端的高性能计算。


说起 WebGL,大家的第一反应应该就是“这不是用来做图形渲染的吗?和高性能计算有啥关系?”。各位看官莫急,且往下看。


在 Web 上面利用 WebGL 做高性能计算其实是依赖于一个 offscreen 的 canvas。canvas 有许多个像素组成,每个像素的颜色可以有 RGBA 四个维度表示,每个维度范围为0-255既8位。把 RGBA 表示成数值的话,那每个像素可以存32位。这就是前端使用 GPU 计算最为核心的一点,每个像素可以存储一个32位的值, 刚刚好就是一个 int 或者 uint。


首先我们看下 WebGL 的渲染流程:


其中两个 vertex shader 和 fragment shader 为两个 GLSL 代码片段,分别处理坐标数据和颜色数据。vertex shader 和 fragment shader 的执行是以像素为单位,canvas 开始绘制的时候 vertex shader 中得到。每个需要绘制的像素的坐标,视需要可以对坐标进行各种转换,最终得到一个最终位置。这个过程中可以将数据作为输出传 fragment shader 参与下一步的计算 fragment shader 接受各种输入,最终输出一个 RGBA 颜色数据作为该像素点的颜色值。


当所有像素都绘制完成之后,画布绘制完成。


tfjs 就是利用这种方式实现了 Web 内执行模型推理的加速。

 

2. ant-tfjs的WebGL的优化


接下来从两个方面介绍下我们的优化方案:首次执行(预热)性能、推理性能。


首次执行(预热)优化


模型的首次执行是指模型第一次在手机上执行的时间,原因是由于目前的 tfjs 计算使用 GPU(WebGL) 进行计算加速。(WebAssembly 目前性能不够,所以 WebGL 是目前唯一可用的方案)。


熟悉 WebGL 开发的朋友应该都清楚,WebGL 的计算主要依赖于 Shader(着色器),一般一个模型中的计算节点(op)至少依赖一个 FragmentShader。而对于一个中等 MobilenetV2 的模型来说,一般会存在大几十甚至100+个计算节点,那么我们在首次运行的时候需要对 Shader 进行编译、执行、寻址、缓存等等各种操作,所以会导致首次执行性能很慢。而由于首次执行时更是涉及到了模型文件的各种编码(在 GPU 内完成),所以导致 Shader 数量巨大。


而从图上可以看到,ant-tfjs 相对于官方的 tfjs 在首次执行性能上有了巨大的提升,80%~100%+。具体是怎么做到的呢?且听我娓娓道来。

 

一个模型的执行其实就是一张有向无环图的顺序执行,每次的执行就是图中的一个计算节点。下面以一个典型的计算节点(卷积)举例:

计算节点可以表示为:Y = Conv(X, W)。Conv 为卷积算法,接受 X 和 W 两个输入(实际可能还会有bias、preluActivation等变量输入)。X 是上一个节点(xxx OP)的输出,在 GPU 内表现为一块已编码的 texture,而 W 则是从我们模型文件中直接读取的数据。

 

由于计算要在 GPU 内进行,所以需要把模型文件内的数据上传至 GPU 内(通过texture存储),由于 texture 的数据存储的特殊性,所以需要对原始模型数据进行编码,使之能够适应 GPU 的计算要求。所以在首次执行时,tfjs 会对每一个节点的W(权重)数据进行上传、编码操作,这样就产生了大量的 Shader。


所以,我们只要对模型文件进行预先编码,在离线环境对模型数据进行排布,这样就能省去了 GPU 内数据编码这一步,从而获得了巨大的性能收益。

 

推理优化


影响推理性能的因素事实上非常多,从 WebGL 渲染管线到 JS 执行,方方面面都会拖慢性能,这里先举上几个🌰:

  • 计算节点过多,导致频繁切换 WebGL program;
  • 数据在 GPU 内的内存布局不合理,导致 L1 cache 频繁 miss;
  • 分支过多,影响 GPU 计算单元并行化;
  • 对 GPU 并行化特性未充分利用;
  • JS 代码未针对 jitless 做优化,导致 iOS 小程序环境下 JS 代码执行过慢;
  • ...

 

问题分析清楚了,那么再去解决就事半功倍了。

 

图优化


图优化手段还是挺多的,具体可以参考 TVM,我们这里实际上是用到了 OP FUSION。将一些可以融合的节点在图结构上进行融合(nOP -> 1OP),基于新的计算结点实现新的 OP。这样一来大大减少了 OP 的数量,进而减少了 Program 的数量,所以提升了推理性能。在低端手机上效果尤为明显。


向量化


GPU 计算能力强大的原因就在于它的访存带宽及计算并行化,官方的 tfjs 在并行化计算上做的并不足,在一些高频率、高计算量的 OP 上仍然采用的是逐点计算的方法。所以针对这些 OP 进行向量化优化,相对充分利用 GPU 的并行化能力,对提升推理性能的优化起到很大的作用。

 

jitless 优化


jitless 优化主要针对于 iOS 小程序无 JIT 的场景,就是一些常规的 JS 性能优化方案,这里不做赘述了。

 

优化内存布局


相对于并行计算的优化,访存的效率更容易成为推理性能的瓶颈。相对一个内存布局糟糕的方案,一个优秀的内存布局性能可以是其数倍。比如各种推理引擎常见的 IM2COL 算法,就是一种优化卷积操作访存连续性的算法。官方 tfjs 的内存布局是对一个矩阵中的元素进行2x2的 pack,即将相邻的两个 row 和相邻的两个 colpack到一个像素点内。这种布局方法实际上并未充分利用 GPU 的 cache 机制,尤其在矩阵乘法这种 high traffic 的场景下,会导致 cache 频繁 miss,这样就带来了糟糕的性能。


 

所以,通过以上的种种操作,我们最终将 ant-tfjs 的性能(预热、推理)相对于官方 tfjs 最高提升了100%+。并且在宠物宝项目上取得了很好的效果。

 

更多的优化方案


目前的计算虽然是基本已经向量化了,但是依旧有一定的优化空间。例如:


  • 提升每次渲染时的 texture bandwidth,继而提升访存及并行效率;
  • 并行渲染。


我们优化还在继续,我们有信息能够将性能再提升一个数量级。希望未来在低端手机上也能看到 30fps 的 mobilenet。


如果你对前端智能化或者 AntTF.js 感兴趣,想参与 AntTF.js 的开发,欢迎发送邮件至:diforce-talent@antgroup.com。

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
6月前
|
缓存 编解码 JavaScript
在JavaScript小游戏开发中,如何优化游戏性能,比如减少重绘、提高动画流畅度?
提升JavaScript游戏性能的关键点包括:使用requestAnimationFrame优化动画流畅度;减少DOM操作,利用DocumentFragment或虚拟DOM;使用Canvas/WebGL高效渲染;优化图像资源,压缩图片和使用雪碧图;分层渲染与视口裁剪减少无效绘制;借助Web Workers进行后台计算;缓存计算结果;合理添加事件监听器并采用事件委托;定期进行性能分析以找到并解决瓶颈。不断测试与调整是优化的关键。
100 4
|
6月前
|
数据采集 并行计算 JavaScript
实战指南:在 Node.js 中利用多线程提升性能
在 Node.js 的世界中,多线程技术一直是一个受到广泛关注的领域。最初,Node.js 设计为单线程模式。随着技术发展,Node.js 引入了多线程支持,进而利用多核处理器的强大性能,提升了应用性能。接下来的内容将深入探讨 Node.js 如何实现多线程,以及在何种场合应该采用这种技术。
|
6月前
|
负载均衡 JavaScript 算法
Node.js 多进程的概念、原理、优势以及如何使用多进程来提高应用程序的性能和可伸缩性
Node.js 多进程的概念、原理、优势以及如何使用多进程来提高应用程序的性能和可伸缩性
154 1
|
4天前
|
算法 JavaScript 前端开发
垃圾回收机制对 JavaScript 性能的影响有哪些?
【10月更文挑战第29天】垃圾回收机制对JavaScript性能有着重要的影响。开发者需要了解不同垃圾回收算法的特点和性能开销,通过合理的代码优化和内存管理策略,来降低垃圾回收对性能的负面影响,提高JavaScript程序的整体性能。
|
3天前
|
JavaScript 前端开发
利用事件循环提高 JavaScript 程序的性能
本文介绍了事件循环在JavaScript中的工作原理,以及如何通过合理利用事件循环来优化程序性能,包括异步操作、任务优先级和避免阻塞等技巧。
|
5月前
|
JavaScript 前端开发
事件委托是JS技巧,通过绑定事件到父元素利用事件冒泡,减少事件处理器数量,提高性能和节省内存。
【6月更文挑战第27天】事件委托是JS技巧,通过绑定事件到父元素利用事件冒泡,减少事件处理器数量,提高性能和节省内存。例如,动态列表可共享一个`click`事件处理器,通过`event.target`识别触发事件的子元素,简化管理和响应动态内容变化。
44 0
|
22天前
|
存储 JavaScript 前端开发
JavaScript数组去重的八种方法详解及性能对比
在JavaScript开发中,数组去重是一个常见的操作。本文详细介绍了八种实现数组去重的方法,从基础的双重循环和 indexOf() 方法,到较为高级的 Set 和 Map 实现。同时,分析了每种方法的原理和适用场景,并指出了使用 Set 和 Map 是目前最优的解决方案。通过本文,读者可以深入理解每种方法的优缺点,并选择最合适的数组去重方式。
39 0
|
2月前
|
缓存 JavaScript 中间件
优化Express.js应用程序性能:缓存策略、请求压缩和路由匹配
在开发Express.js应用时,采用合理的缓存策略、请求压缩及优化路由匹配可大幅提升性能。本文介绍如何利用`express.static`实现缓存、`compression`中间件压缩响应数据,并通过精确匹配、模块化路由及参数化路由提高路由处理效率,从而打造高效应用。
133 11
|
3月前
|
缓存 前端开发 JavaScript
超时空加速秘籍:揭秘JavaScript前端开发中的性能魔法,让您的Web应用瞬间穿越到未来!
【8月更文挑战第27天】本文介绍了一系列实用的JavaScript性能优化方法并提供了示例代码,包括减少DOM操作、使用事件委托、避免阻塞主线程、异步加载资源、利用浏览器缓存、代码分割以及使用Service Worker等技术,帮助开发者有效提升Web应用性能和用户体验。
49 2
|
3月前
|
UED 存储 数据管理
深度解析 Uno Platform 离线状态处理技巧:从网络检测到本地存储同步,全方位提升跨平台应用在无网环境下的用户体验与数据管理策略
【8月更文挑战第31天】处理离线状态下的用户体验是现代应用开发的关键。本文通过在线笔记应用案例,介绍如何使用 Uno Platform 优雅地应对离线状态。首先,利用 `NetworkInformation` 类检测网络状态;其次,使用 SQLite 实现离线存储;然后,在网络恢复时同步数据;最后,通过 UI 反馈提升用户体验。
82 0