大家好~本文使用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. 第24章 Java 数据类型转换

    每日一句 井底点灯深烛伊,共郎长行莫围棋. 每日一句 What we call "failure" is not falling down, but the staying dow ...

  2. 目标检测复习之YOLO系列

    目标检测之YOLO系列 YOLOV1: blogs1: YOLOv1算法理解 blogs2: <机器爱学习>YOLO v1深入理解 网络结构 激活函数(leaky rectified li ...

  3. MySql笔记Ⅰ

    MySql part 1: 数据库概念 数据库:(DataBase, 简称DB):数据库中的数据按一定的数据模型组织.描述和储存,具有较小的冗余度.较高的数据独立性和易扩展性,并可为各种 用户共享 数 ...

  4. sqlserver2008 数据库中查询存储过程的的创建修改和执行时间,以及比较常见的系统视图和存储过程

    因为各种原因数据库中存在大量无用的存储过程,想查询存储过程的最后执行情况,处理长期不使用的存储过程 下面这条语句可以查询存储过程创建 修改和执行的最后时间: SELECT a.name AS 存储过程 ...

  5. IOI2020 国家集训队作业 泛做

    题号 题目名称 rating 算法 完成情况 CF504E Misha and LCP on Tree CF505E Mr.Kitayuta vs. Bamboos CF506E Mr.Kitayut ...

  6. Similarity calculation

    推荐算法入门(相似度计算方法大全) 一.协同过滤算法简介 在推荐系统的众多方法之中,基于用户的协同过滤是诞最早的,原理也比较简单.基于协同过滤的推荐算法被广泛的运用在推荐系统中,比如影视推荐.猜你喜欢 ...

  7. CentOS6.5修改镜像源问题

    千呼万唤使出来阿,随着centos版本不断地更新好多镜像源已经被放弃了治疗,尤其是低版本的centos,下面以CentOS6.5为例进行刨析吧! 上干货: 配置文件 vi /etc/yum.repos ...

  8. Xmind演讲主题大纲

    参考:https://www.bilibili.com/video/BV1Rb411s7VG?p=9

  9. NODE.JS exports require理解

    node.js exports 的作用是什么? 因为A.js文件想访问B.js文件中的类或函数,是不能直接访问的.为了解决这个问题 node.js 产生了 exports ,exports 实际可以理 ...

  10. Python-安装lmdb失败-解决方法

    使用pip install lmdb 时报错 ERROR: Command errored out with exit status 1: python setup.py egg_info Check ...