大家好~本文使用WebGPU的计算着色器,实现了奇偶排序。

奇偶排序是冒泡排序的并行版本,在1996年由J Kornerup提出。它解除了每轮冒泡间的串行依赖以及每轮冒泡内部的串行依赖,使得冒泡操作可以并行执行

最终版本的代码在这里

介绍奇偶排序算法

假设待排序的数组为Arr1

在奇数步中,Arr1中奇数项与相邻的右边一项比较和交换;

在偶数步中,Arr1中奇数项与相邻的左边一项比较和交换;

直到一步中没有交换项,则停止

举例来说的话,如下图所示:

在每步中,红框内的两项进行比较和交换;

直到一步中没有交换项,则停止

分析时间复杂度

与冒泡排序一样,总的比较次数不变,依然为O(n^2)次

但因为为并行执行,所以时间复杂度降低为O(log2(n^2))=O(n)

需求

排序的需求如下所示:

  • 对一个包含128个数字的数组进行升序的排序

初步设计

因为数组可以两两分为64个组,每个组并行执行操作,所以计算着色器只使用一个workgroup,包含64个局部单位,每个局部单位对应一个组;

在每个局部单位中:

启动一个while循环,执行每步操作,然后同步,最后判断所有局部单位在该步骤中是否有交换操作,如果都没有的话则停止循环

“执行每步操作”时判断该步骤是奇数还是偶数步,从而取对应的两项来比较和交换

代码实现

经过上面的设计后,现在我们来实现代码

计算着色器代码如下所示:

//64个局部单位
const workgroupSize = 64; // 局部单位之间的共享变量,用于存放128个数字
var<workgroup> sharedData: array<f32,128>;
// 局部单位之间的共享变量,用于标志所有局部单位在该步骤中是否有交换操作(只要有任意一个局部单位在该步骤中有交换操作,则该标志为true)
var<workgroup> isSwap: bool;
// 局部单位之间的共享变量,用于记录步骤数,从而判断是奇数还是偶数步
var<workgroup> stepCount: u32; struct BeforeSortData {
data : array<f32, 128>
}
struct AfterSortData {
data : array<f32, 128>
} //待排序的数组
@binding(0) @group(0) var<storage, read> beforeSortData : BeforeSortData;
//排序后的数组
@binding(1) @group(0) var<storage, read_write> afterSortData : AfterSortData; @compute @workgroup_size(workgroupSize, 1, 1)
fn main(
@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>,
) {
//将待排序的数据读取到共享变量中 var index = GlobalInvocationID.x * 2;
sharedData[index] = beforeSortData.data[index];
sharedData[index+ 1 ] = beforeSortData.data[index + 1]; //初始化共享变量 isSwap = false;
stepCount = 0; //同步
workgroupBarrier(); //开始循环
while(true){
var firstIndex:u32;
var secondIndex:u32; //判断该步骤是奇数还是偶数步,从而得到对应的两项的序号 //偶数步
if(stepCount % 2 == 0){
firstIndex = index + 1;
secondIndex = index + 2;
}
//奇数步
else{
firstIndex = index;
secondIndex = index + 1;
} //确保没超过边界
if(secondIndex < 128){
//将大的一项交换到后面,从而实现升序
if(sharedData[firstIndex] > sharedData[secondIndex]){
var temp = sharedData[firstIndex];
sharedData[firstIndex] = sharedData[secondIndex];
sharedData[secondIndex] = temp; isSwap = true;
}
} stepCount += 1; workgroupBarrier(); //如果该步骤中没有交换操作的话则停止循环
if(!isSwap){
break;
}
} //将排序后的数据传给返回给CPU端的Storage Buffer中,从而可在CPU端得到排序后的结果
afterSortData.data[index] = sharedData[index];
afterSortData.data[index + 1] = sharedData[index + 1];
}

本来我是想像设置workgroupSize一样,将128设为const的,如下所示:

const workgroupSize = 64;
const itemCount = 128; var<workgroup> sharedData: array<f32,itemCount>;

但是运行时会报错!照理说根据WGSL的文档,是不应该报错的!所以不清楚是我没搞清楚还是WGSL目前的bug?

(另外,如果使用override而不是const,也会报错!)

如果改为使用workgroupSize,则不会报错。代码如下所示:

const workgroupSize = 64;

var<workgroup> sharedData: array<f32,workgroupSize>;

这是因为workgroupSize在@workgroup_size中使用了。代码如下所示:

@compute @workgroup_size(workgroupSize, 1, 1)

发现问题

运行代码后,会报错:

1 warning(s) generated while compiling the shader:
:74:5 warning: 'workgroupBarrier' must only be called from uniform control flow
workgroupBarrier();
^^^^^^^^^^^^^^^^ :77:5 note: control flow depends on non-uniform value
if(!isSwap){
^^ :77:9 note: reading from workgroup storage variable 'isSwap' may result in a non-uniform value
if(!isSwap){
^^^^^^

这是因为WGSL会进行Uniformity analysis检查,确保像“workgroupBarrier”这种barries是在uniform control flow中安全地调用

WGSL在检查时发现:因为isSwap被多个局部单位读写,所以为"non-uniform value",导致所在的control flow为non-uniform,从而报错

更多关于Uniformity的资料在这里:

uniformity

Add the uniformity analysis to the WGSL spec

uniformity issues

改进设计

现在需要去掉isSwap的if判断

因为isSwap的if判断是用来结束循环的,那么在去掉它之后我们就需要新的结束条件

因为总共有128个数字要排序,所以最多进行128步即可完成所有的排序

相关代码实现

所以去掉isSwap,把循环终止条件修改下,并且重构一下代码

相关代码改为:


fn _swap(firstIndex:u32, secondIndex:u32){
var temp = sharedData[firstIndex];
sharedData[firstIndex] = sharedData[secondIndex];
sharedData[secondIndex] = temp;
} fn _oddSort(index:u32) {
var firstIndex = index;
var secondIndex = index + 1; if(sharedData[firstIndex] > sharedData[secondIndex]){
_swap(firstIndex, secondIndex);
}
} fn _evenSort(index:u32) {
var firstIndex = index + 1;
var secondIndex = index + 2; if(secondIndex <128 && sharedData[firstIndex] > sharedData[secondIndex]){
_swap(firstIndex, secondIndex);
}
} @compute @workgroup_size(workgroupSize, 1, 1)
fn main(
@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>,
) {
... var firstIndex:u32;
var secondIndex:u32; for (var i: u32 = 0; i < 128; i += 1) {
//偶数步
if(stepCount % 2 == 0){
_evenSort(index);
}
//奇数步
else{
_oddSort(index);
} stepCount +=1 ; workgroupBarrier(); }
...
}

现在就能够正确运行了

改进设计

现在我们通过判断步骤数是偶数还是奇数来进行对应的排序,这会造成“Warp Divergence”的优化问题:

不同的局部单位会进入不同的分支(偶数步或者奇数步),造成同一时刻除了正在执行的分支以外,其余分支都被阻塞了,十分影响性能

如下图所示:

参考资料:

CUDA编程——Warp Divergence

所以我们可以去掉stepCount和相关的判断,改为在每次循环中分别执行奇数排序和偶数排序,并把循环次数减半

相关代码实现

修改后的相关代码为:

for (var i: u32 = 0; i < 64; i += 1) {
_oddSort(index);
workgroupBarrier(); _evenSort(index);
workgroupBarrier();
}

限制

现在程序有如下的限制:

  • 只有一个workgroup,意味着只能排序最多“局部单位数量*2”个数字

总结

感谢大家的学习~

计算着色器是SIMT架构,也就是说每条指令都是并行执行的。这与CPU的串行思维不同,所以我们要切换为并行的思维,并在需要同步的时候同步

另外,计算着色器的代码因为处在并行环境下,所以需要仔细优化

有下面的一些优化建议:

  • 减少Warp Divergence

    • 尽量减少if判断
    • if中尽量使用常数判断,如if(const value < 2)
  • 减少Bank Conflict
    • 对共享变量内存尽量连续地读写

      如对本文的共享数组sharedData,尽量连续的读写相邻的序号
  • 减少同步操作
    • 如果已知确定的循环次数,可以展开循环,这样可以减少同步操作和循环的指令开销

参考资料

啥是Parallel Reduction

CUDA(六). 从并行排序方法理解并行化思维——冒泡、归并、双调排序的GPU实现

uniformity

CUDA编程——Warp Divergence

WebGPU的计算着色器实现冒泡排序的更多相关文章

  1. WebGPU 计算管线、计算着色器(通用计算)入门案例:2D 物理模拟

    目录 1. WebGL 2. WebGPU 2.1. 适配器(Adapter)和设备(Device) 2.2. 着色器(Shaders) 2.3. 管线(Pipeline) 2.4. 并行(Paral ...

  2. DirectX11 With Windows SDK--27 计算着色器:双调排序

    前言 上一章我们用一个比较简单的例子来尝试使用计算着色器,但是在看这一章内容之前,你还需要了解下面的内容: 章节 26 计算着色器:入门 深入理解与使用缓冲区资源(结构化缓冲区/有类型缓冲区) Vis ...

  3. DirectX11 With Windows SDK--26 计算着色器:入门

    前言 现在开始迎来所谓的高级篇了,目前计划是计算着色器部分的内容视项目情况,大概会分3-5章来讲述. DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群 ...

  4. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十三章:计算着色器(The Compute Shader)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十三章:计算着色器(The Compute Shader) 代码工程 ...

  5. DirectX11 Windows Windows SDK--28 计算着色器:波浪(水波)

    前言 有关计算着色器的基础其实并不是很多.接下来继续讲解如何使用计算着色器实现水波效果,即龙书中所实现的水波.但是光看代码可是完全看不出来是在做什么的.个人根据书中所给的参考书籍找到了对应的实现原理, ...

  6. DirectX11 With Windows SDK--29 计算着色器:内存模型、线程同步;实现顺序无关透明度(OIT)

    前言 由于透明混合在不同的绘制顺序下结果会不同,这就要求绘制前要对物体进行排序,然后再从后往前渲染.但即便是仅渲染一个物体(如上一章的水波),也会出现透明绘制顺序不对的情况,普通的绘制是无法避免的.如 ...

  7. CesiumJS 2022^ 原理[5] - 着色器相关的封装设计

    目录 1. 对 WebGL 接口的封装 1.1. 缓冲对象封装 1.2. 纹理与采样参数封装 1.3. 着色器封装 1.4. 上下文对象与渲染通道 1.5. 统一值(uniform)封装 1.6. 渲 ...

  8. OpenGL管线(用经典管线代说着色器内部)

    图形管线(graphics pipeline)向来以复杂为特点,这归结为图形任务的复杂性和挑战性.OpenGL作为图形硬件标准,是最通用的图形管线版本.本文用自顶向下的思路来简单总结OpenGL图形管 ...

  9. 【OPENGL】第三篇 着色器基础(二)

    在这一小节,主要学习GLSL的基本数据类型以及控制结构.GLSL具备了C++和Java的很多特性,我们会先了解所有着色阶段共有的特性,再了解各个着色器的专属特性. 1.着色器的基本结构 一个着色器程序 ...

随机推荐

  1. flask实现python方法转换服务

    一.flask安装 pip install flask 二.flask简介: flask是一个web框架,可以通过提供的装饰器@server.route()将普通函数转换为服务 flask是一个web ...

  2. 世界排名前三的Linux桌面发行版

    linux操作系统 1.MX Linux 2.Manjaro 3. Linux Mint 1.MX Linux https://mxlinux.org 中文用户不太友好 2.Manjaro https ...

  3. 一个全新的Vue拖拽特性实现:“调整尺寸”部分

    关于拖拽 CabloyJS提供了完备的拖拽特性,可以实现移动和调整尺寸两大类功能,这里对调整尺寸的开发进行阐述 关于移动的开发,请参见:拖拽:移动 演示 开发步骤 下面以模块test-party为例, ...

  4. vue传值的几种方式

    props:适用于 父组件 ==> 子组件 通信 由父组件传值子组件在props中接收即可: (由父组件给子组件传递 函数类型 的props可实现 子组件 ==> 父组件 传递数据,较为繁 ...

  5. Hyperledger Fabric 智能合约开发及 fabric-sdk-go/fabric-gateway 使用示例

    前言 在上个实验 Hyperledger Fabric 多组织多排序节点部署在多个主机上 中,我们已经实现了多组织多排序节点部署在多个主机上,但到目前为止,我们所有的实验都只是研究了联盟链的网络配置方 ...

  6. python各版本下载

    python2源码压缩包 Python-2.7.9.tgz   Python-2.7.10.tgz Python-2.7.11.tgz Python-2.7.12.tgz Python-2.7.13. ...

  7. MySQL 千万数据库深分页查询优化,拒绝线上故障!

    文章首发在公众号(龙台的技术笔记),之后同步到博客园和个人网站:xiaomage.info 优化项目代码过程中发现一个千万级数据深分页问题,缘由是这样的 库里有一张耗材 MCS_PROD 表,通过同步 ...

  8. 什么是AR技术?AR的价值究竟有多大?

    什么是AR技术? AR技术,解释来说就是增强现实(Augmented Reality),是一种实时地计算摄影机影像的位置及角度并加上相应图像.3D模型的技术,它的目标是把虚拟世界嵌套进真实世界进行互动 ...

  9. 程序员必备,一款让你提高工作效率N倍的神器uTools

    下载地址:https://www.aliyundrive.com/s/f7PU7QxdxEz uTools 是什么? uTools = your tools(你的工具集) uTools 是一个极简.插 ...

  10. 基于thinkphp6 layui的优秀极速后台开发框架推荐

    很多时候我们在做项目开发的时候,苦于没有好一点的轮子,自己动手开发的话,太耗费时间了,如果采用VUE的话,学习成本跟调试也比较麻烦, 而且有时候选用的东西甲方也不太容易接受,现在给大家介绍一款优秀的极 ...