一、束内原语
原语,对于开发者们并不陌生。而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提供了三个接口函数,即:
- int __any_sync(unsigned mask, int predicate)
其实如果开发者经验相对丰富一些的话,就很容易猜测出这个函数的意思,它通过mask掩码来指定使用参与线程中的perdicate值来判断返回结果,即如果任何一个线程中的predicate不为零,则所有的参与线程都返回非零。这类似于逻辑操作中的OR操作。 - int __all_sync(unsigned mask, int predicate)
这个与上面的恰恰相反,如果参与线程中有任何一个predicate值不为零,则所有的参与线程都返回零。它类似于逻辑操作的AND操作 - 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
五、总结
从历史的经验来看,任何操作控制为了更贴近实际应用,往往会分层分类的进行不同的接口处理。所以在实际开发如果遇到某种与其它类似的已知情况时,往往确实是会提供相应的机制。这也算是经验的一种推理和延伸吧。