并行编程实战——CUDA编程的Warp Vote

一、束内原语

原语,对于开发者们并不陌生。而CUDA也提供了不同情况下的原语操作,其中一个就是束内原语。包括前面才分析过的Warp Shuffle和将要分析的Warp Vote。

所以束内原语,其实就是Warp内的原语。它提供了在Warp内的原子性、不可再细分的基本操作,提供了最基础的同步操作。

二、Warp Vote

在Warp Shuffle中是对束内线程的数据交换进行处理,那么Warp Vote则对束内所有线程的布尔条件(predicate,谓词操作结果)进行归约操作。它同样也存在着新旧版本的接口更迭。老的版本在CUDA9.0后已经不再支持(可以理解为加强为了同步版本,在算力大于7.0时,应该使用同步版本)。

Warp Vote函数允许在Warp(线程束)内的所有线程进行归约-广播(reduction-and-broadcast)操作。它们接收同一Warp内每个线程的整数谓词(predicate)作为输入,并将这些值进行布尔值判断(和0比较)。比较结果会通过它们在Warp的活跃线程中进行归约(合并),并将其具体的每次返回值广播给所有参与的线程。

三、相关的函数接口

CUDA中的Warp Vote提供了三个接口函数,即:

  1. int __any_sync(unsigned mask, int predicate)
    其实如果开发者经验相对丰富一些的话,就很容易猜测出这个函数的意思,它通过mask掩码来指定使用参与线程中的perdicate值来判断返回结果,即如果任何一个线程中的predicate不为零,则所有的参与线程都返回非零。这类似于逻辑操作中的OR操作。
  2. int __all_sync(unsigned mask, int predicate)
    这个与上面的恰恰相反,如果参与线程中有任何一个predicate值不为零,则所有的参与线程都返回零。它类似于逻辑操作的AND操作
  3. unsigned __ballot_sync(unsigned mask, int predicate)
    这个是一个详细的操作结果返回。它获取通过mask指定参与线程的predicate,组成一个32位的无符号整数,其中每一位对应着相应活动Lane(线程)N 的谓词结果(零或非零)。

上面的参数很简单,此处不再说明。但需要说明的是,虽然其提供了原语操作,但未提供内存栅栏机制也不保证内存序。

这些束内的Vote函数,在大规模的数据统计和并行算法中应用非常广泛。典型的应用就是并行归约和流压缩。

四、例程

下面看一个例程:

c 复制代码
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>

__global__ void warpVoteKernel() {
    int lane = threadIdx.x & 0x1f;
    int pred = (lane % 2 == 0) ? 1 : 0;
    unsigned mask = 0xffffffff;

    int anyRet = __any_sync(mask, pred);
    int allRet = __all_sync(mask, pred);
    unsigned ballotRet = __ballot_sync(mask, pred);

    // lane 0 display result
    if (lane == 0) {
        printf("__any_sync:  %d \n", anyRet);
        printf("__all_sync:  %d \n", allRet);
        printf("__ballot_sync: 0x%08x\n", ballotRet);
    }

    printf("Thread %2d: pred = %d\n", lane, pred);
}

int main() {
    warpVoteKernel << <1, 32 >> > ();
    cudaDeviceSynchronize();

    return 0;
}

上面的代码很简单,通过设置奇偶ID线程的谓词值来进行测试。最后统计结果,为了方便打印,只在lane 0中打印结果。

运行结果:

复制代码
__any_sync:  1
__all_sync:  0
__ballot_sync: 0x55555555
Thread  0: pred = 1
Thread  1: pred = 0
Thread  2: pred = 1
Thread  3: pred = 0
Thread  4: pred = 1
Thread  5: pred = 0
Thread  6: pred = 1
Thread  7: pred = 0
Thread  8: pred = 1
Thread  9: pred = 0
Thread 10: pred = 1
Thread 11: pred = 0
Thread 12: pred = 1
Thread 13: pred = 0
Thread 14: pred = 1
Thread 15: pred = 0
Thread 16: pred = 1
Thread 17: pred = 0
Thread 18: pred = 1
Thread 19: pred = 0
Thread 20: pred = 1
Thread 21: pred = 0
Thread 22: pred = 1
Thread 23: pred = 0
Thread 24: pred = 1
Thread 25: pred = 0
Thread 26: pred = 1
Thread 27: pred = 0
Thread 28: pred = 1
Thread 29: pred = 0
Thread 30: pred = 1
Thread 31: pred = 0

五、总结

从历史的经验来看,任何操作控制为了更贴近实际应用,往往会分层分类的进行不同的接口处理。所以在实际开发如果遇到某种与其它类似的已知情况时,往往确实是会提供相应的机制。这也算是经验的一种推理和延伸吧。

相关推荐
fpcc1 小时前
并行编程实战——CUDA编程的Warp Shuffle
c++·cuda
xiaoye-duck2 小时前
《算法题讲解指南:优选算法-滑动窗口》--13 水果成篮
c++·算法
智者知已应修善业2 小时前
【冰雹猜想过程逆序输出】2025-4-19
c语言·c++·经验分享·笔记·算法
wefg12 小时前
【算法】倍增思想(快速幂)
数据结构·c++·算法
锅包一切2 小时前
一、C++ 发展与程序创建
开发语言·c++·后端·学习·编程
power 雀儿3 小时前
LibTorch激活函数&LayerNorm归一化
c++·人工智能
枷锁—sha3 小时前
【CTFshow-pwn系列】03_栈溢出【pwn 051】详解:C++字符串替换引发的血案与 Ret2Text
开发语言·网络·c++·笔记·安全·网络安全
YXXY3134 小时前
C++11的介绍(上)
c++
sycmancia4 小时前
C++——析构函数的调用顺序、const修饰对象、类成员
开发语言·c++