如何将 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盲盒。
相关文章
|
3月前
|
算法 JavaScript 前端开发
垃圾回收机制对 JavaScript 性能的影响有哪些?
【10月更文挑战第29天】垃圾回收机制对JavaScript性能有着重要的影响。开发者需要了解不同垃圾回收算法的特点和性能开销,通过合理的代码优化和内存管理策略,来降低垃圾回收对性能的负面影响,提高JavaScript程序的整体性能。
|
3月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
214 77
|
3月前
|
存储 缓存 JavaScript
如何优化Node.js应用的内存使用以提高性能?
通过以上多种方法的综合运用,可以有效地优化 Node.js 应用的内存使用,提高性能,提升用户体验。同时,不断关注内存管理的最新技术和最佳实践,持续改进应用的性能表现。
157 62
|
3月前
|
JavaScript 前端开发
如何使用时间切片来优化JavaScript动画的性能?
如何使用时间切片来优化JavaScript动画的性能?
|
3月前
|
存储 缓存 监控
如何使用内存监控工具来优化 Node.js 应用的性能
需要注意的是,不同的内存监控工具可能具有不同的功能和特点,在使用时需要根据具体工具的要求和操作指南进行正确使用和分析。
88 31
|
2月前
|
JSON 缓存 负载均衡
Node.js 的性能
Node.js 的性能
73 12
|
3月前
|
JavaScript 前端开发
如何在不影响性能的前提下使用JavaScript库来实现复杂的动画效果?
如何在不影响性能的前提下使用JavaScript库来实现复杂的动画效果?
|
3月前
|
JavaScript 前端开发 数据处理
模板字符串和普通字符串在浏览器和 Node.js 中的性能表现是否一致?
综上所述,模板字符串和普通字符串在浏览器和 Node.js 中的性能表现既有相似之处,也有不同之处。在实际应用中,需要根据具体的场景和性能需求来选择使用哪种字符串处理方式,以达到最佳的性能和开发效率。
118 60
|
3月前
|
JavaScript 前端开发
CSS3 动画和 JavaScript 动画的性能比较
具体的性能表现还会受到许多因素的影响,如动画的复杂程度、浏览器的性能、设备的硬件条件等。在实际应用中,需要根据具体情况选择合适的动画技术。
|
3月前
|
JavaScript 前端开发
利用事件循环提高 JavaScript 程序的性能
本文介绍了事件循环在JavaScript中的工作原理,以及如何通过合理利用事件循环来优化程序性能,包括异步操作、任务优先级和避免阻塞等技巧。

热门文章

最新文章