柳州网站建设,十五年品牌  Tel:4006-130-670 15877267431 / 13788620417

柳州网站建设之如何玩转 WebGL 并行计算

 二维码 2
发表时间:2021-08-20 15:53

微信图片_20210820155000.jpg


如今在 Web 端使用 WebGL 进行高性能计算已有不少实践,例如在端智能领域中的 tensorflow.js,再比如可视化领域中的 Stardust.js。在本文中,我们将介绍以下内容:


使用 GPU 进行通用计算(GPGPU)的历史


当前在 Web 端使用图形 API 实现 GPGPU 的技术原理,以及前端开发者可能遇到的难点


相关业界实践,包括布局计算、动画插值等


局限性与未来展望


一   什么是 GPGPU


由于硬件结构不同,GPU 与 CPU 擅长执行不同类型的计算任务。CPU 通过复杂的 Cache 设计实现低延迟,包含复杂的控制逻辑(分支预测),ALU 只占一小部分。而 GPU 为高吞吐量而生,包含大量 ALU。因此在单指令流多数据流(SIMD)场景下,GPU 的运算速度远超 CPU,并且这种差距还在不断拉大。


而一些现代 GPU 上甚至有专门负责张量计算、光线追踪的硬件(Tensor/RT Core),例如 Nvidia 的图灵架构。这使得在处理这些计算复杂度极高的任务时能获得更大的性能提升。


这里就需要引出一个概念,用 GPU 进行除渲染外的通用计算:General-Purpose computation on Graphics Processing Units,即 GPGPU。


自 2002 年提出以来,在实时加解密、图片压缩、随机数生成等计算领域都能看到它的身影,GPU Gems/Pro 上也有专门的章节介绍。经由 Nvidia 提出的 CUDA(Compute Unified Device Architecture) 这一统一计算架构,开发者可以使用 C、Java、Python 等语言编写自己的并行计算任务代码。


那么在 Web 端我们应该如何使用 GPU 的计算能力呢?


二   用 WebGL 实现并行计算的原理


在现代化的图形 API(Vulkan/Metal/Direct3D)中提供了 Compute Shader 供开发者编写计算逻辑。考虑到 WebGPU 仍在开发中,目前在 Web 端能使用的图形渲染 API 只有 WebGL1/2,它们都不支持 Compute Shader(WebGL 2.0 Compute 已废弃),因此只能“曲线救国”。在本文的最后一节我们将展望未来的技术手段。


我们先忽略具体的 API 用法,从 CPU 和 GPU 的角度看两者在并行计算过程中是如何协作的,前者也常被称作 host,后者为 device。**步为数据初始化,需要从 CPU 内存中拷贝数据到 GPU 内存中,在 WebGL 中会通过纹理绑定完成。第二步 CPU 需要准备提交给 GPU 的指令和数据,完成计算程序的编译,在 WebGL 中通过调用一系列 API 实现。在第三步中将计算逻辑分配给 GPU 各个核心执行,因此这段逻辑也叫做“核函数”。最后把计算结果从 GPU 内存中拷贝回 CPU 内存,在 WebGL1 中通过读取纹理中像素值完成。


下面我们从 GPU 编程模型和执行模型入手,顺便引出线程和线程组的概念,这也是 GPU 可数据并行的关键。下图展示了网格与线程组的层次关系,并不局限于 DirectCompute。


通过 dispatch(x, y, z) 分配一个 3 维的线程网格(Grid),其中的线程共享全局内存空间;


网格中包含了许多线程组(Work Group、Thread Group、Thread Block、本地工作组不同叫法),每一个线程组中又包含了许多线程,线程组也是 3 维的,一般在 Shader 中通过 numthreads(x, y, z) 指定。它们可以通过共享内存或同步原语进行通信;


Shader 程序最终会运行在每一个线程上。对于每一个线程,可以获取自己在线程组中的 3 维坐标,也可以获取线程组在整个线程网格中的 3 维坐标,以此映射到不同的数据上,实现数据并行的效果;


再回到硬件视角,线程对应 GPU 中的 CUDA 核心,线程组对应 SM(Streaming Multiprocessor),网格就是 GPU。


1   WebGL1 纹理映射


下图来自「GPGPU 编程技术 - 从 GLSL、CUDA 到 OpenCL」,这也是经典的 GPGPU 计算流程。


微信图片_20210820155254.jpg


通常来说图形渲染 API 最终的输出目标就是屏幕,显示渲染结果。但是在 GPGPU 场景中我们只是希望在 CPU 侧读取最终的计算结果。因此会使用到渲染 API 提供的离屏渲染功能,即渲染到纹理,其中的关键技术就是使用帧缓存对象(Framebuffer Object/FBO)作为渲染对象。纹理用来存储输入参数和计算结果,因此在创建时我们通常需要开启浮点数扩展 OES_texture_float,该扩展在 WebGL2 中已经内置。


并行计算发生在光栅化阶段,我们将计算逻辑(核函数)写在 Fragment Shader 中,Vertex Shader 仅负责映射纹理坐标,因此 Geometry 可以使用一个 Quad(4个顶点)或者全屏三角形(3个顶点)。对于每一个像素点来说,它的工作并无变化,平时执行的渲染逻辑此时成了一种计算过程,像素值也成了计算结果。


但这种方式存在一个明显的限制,对于所有线程,纹理缓存要么是只读的,要么就是只写的,没法实现一个线程在读纹理,另一个在写纹理。本质上是由 GPU 的硬件设计决定的,如果想要实现多个线程同时对同一个纹理进行读/写操作,需要设计复杂的同步机制避免读写冲突,势必会影响到线程并行执行的效率。因此在经典 GPGPU 的实现中,通常我们会准备两个纹理,一个用来保存输入数据,一个用来保存输出数据。


除此之外,该方法并不支持线程间同步和共享内存这些特性,因此一些并行算法无法实现,例如 Bellman-Ford 单源最短路径算法。


上图中也提到了乒乓技术,很多算法需要连续运行多次,例如 G6 中使用的布局算法需要迭代多次达到稳定状态。上一次迭代中输出的计算结果,需要作为下一次迭代的输入。在实际实现中,我们会分配两张纹理缓存,每次迭代后对输入和输出纹理进行交换,实现类似乒乓的效果。


值得注意的是,由于 readPixels(在 CPU 侧读取纹理中的数据)非常慢,除了获取最终结果,过程中应当尽可能减少对它的调用,尽可能让数据留在 GPU 中。


这里我们不再展开 WebGL1 API 的实际用法,详细使用方式可以参考相关教程。


2   WebGL2 Transform Feedback


首先不得不提到已废弃的 WebGL 2.0 Compute(底层为 OpenGL ES 3.1),在草案中能看到例如用于线程间同步的 memoryBarrier 和 shared memory 这些高级特性,但最终工作组还是转向了 WebGPU。


WebGL2 中提供了另一种在 Vertex Shader 中进行并行计算的手段,即 Transform Feedback,它会跳过光栅化管线因此也不需要 Fragment Shader 参与(实际实现中提供一个空 Shader 即可)。


该方案和 WebGL1 的纹理映射方法有以下不同点:


不需要 Fragment Shader 参与,因此可以通过全局变量开启 gl.enable(gl.RASTERIZER_DISCARD);


计算逻辑写在 Vertex Shader 中,不再需要晦涩的纹理映射,可以直接使用 Buffer 读写数据;


读取结果时可以直接使用 getBufferSubData。不过不变的是,该方法依然很慢


来源:阿里技术


会员登录
登录
其他帐号登录:
留言
回到顶部