如何将 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。

相关实践学习
在云上部署ChatGLM2-6B大模型(GPU版)
ChatGLM2-6B是由智谱AI及清华KEG实验室于2023年6月发布的中英双语对话开源大模型。通过本实验,可以学习如何配置AIGC开发环境,如何部署ChatGLM2-6B大模型。
相关文章
|
5月前
|
监控 负载均衡 JavaScript
有哪些有效的方法可以优化Node.js应用的性能?
有哪些有效的方法可以优化Node.js应用的性能?
316 69
|
11月前
|
算法 JavaScript 前端开发
垃圾回收机制对 JavaScript 性能的影响有哪些?
【10月更文挑战第29天】垃圾回收机制对JavaScript性能有着重要的影响。开发者需要了解不同垃圾回收算法的特点和性能开销,通过合理的代码优化和内存管理策略,来降低垃圾回收对性能的负面影响,提高JavaScript程序的整体性能。
|
8月前
|
前端开发 JavaScript Java
JavaScript闭包深入剖析:性能剖析与优化技巧
JavaScript 闭包是强大而灵活的特性,广泛应用于数据封装、函数柯里化和事件处理等场景。闭包通过保存外部作用域的变量,实现了私有变量和方法的创建,提升了代码的安全性和可维护性。然而,闭包也可能带来性能问题,如内存泄漏和执行效率下降。为优化闭包性能,建议采取以下策略:及时解除对不再使用的闭包变量的引用,减少闭包的创建次数,使用 WeakMap 管理弱引用,以及优化闭包结构以减少作用域链查找的开销。在实际开发中,无论是 Web 前端还是 Node.js 后端,这些优化措施都能显著提升程序的性能和稳定性。
215 70
|
7月前
|
JavaScript 前端开发 算法
JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)
Array.sort() 是一个功能强大的方法,通过自定义的比较函数,可以处理各种复杂的排序逻辑。无论是简单的数字排序,还是多字段、嵌套对象、分组排序等高级应用,Array.sort() 都能胜任。同时,通过性能优化技巧(如映射排序)和结合其他数组方法(如 reduce),Array.sort() 可以用来实现高效的数据处理逻辑。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7月前
|
JavaScript 前端开发 Java
深入理解 JavaScript 中的 Array.find() 方法:原理、性能优势与实用案例详解
Array.find() 是 JavaScript 数组方法中一个非常实用和强大的工具。它不仅提供了简洁的查找操作,还具有性能上的独特优势:返回的引用能够直接影响原数组的数据内容,使得数据更新更加高效。通过各种场景的展示,我们可以看到 Array.find() 在更新、条件查找和嵌套结构查找等场景中的广泛应用。 在实际开发中,掌握 Array.find() 的特性和使用技巧,可以让代码更加简洁高效,特别是在需要直接修改原数据内容的情形。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一
|
11月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
403 77
|
7月前
|
前端开发 JavaScript 大数据
关于JavaScript性能问题的误解
JavaScript 是单线程语言,代码逐行执行,遇到大数据量计算可能影响性能。前端同事担心遍历大量数据会导致性能问题,但实际上,即使遍历1000、10000条数据,耗时也较少。测试代码执行时间有三种方法:Date.now、console.time 和 performance.now,其中 performance.now 精度最高。开发中不必过度担忧遍历带来的性能损耗,保持代码清晰更重要。
|
11月前
|
存储 缓存 JavaScript
如何优化Node.js应用的内存使用以提高性能?
通过以上多种方法的综合运用,可以有效地优化 Node.js 应用的内存使用,提高性能,提升用户体验。同时,不断关注内存管理的最新技术和最佳实践,持续改进应用的性能表现。
534 62
|
11月前
|
JavaScript 前端开发 数据处理
模板字符串和普通字符串在浏览器和 Node.js 中的性能表现是否一致?
综上所述,模板字符串和普通字符串在浏览器和 Node.js 中的性能表现既有相似之处,也有不同之处。在实际应用中,需要根据具体的场景和性能需求来选择使用哪种字符串处理方式,以达到最佳的性能和开发效率。
262 63

热门文章

最新文章