《Cooperative Groups Flexible CUDA Thread Programming》笔记

更方便、细粒度的线程协作

这是《Cooperative Groups: Flexible CUDA Thread Programming》这篇博客的学习笔记。

动机

在 CUDA 中线程之间分享数据和协作工作是非常常见的。CUDA 为此提供了一个同步函数 __syncthreads(),但是这个函数只能在 block 间同步。有时我们会需要更细粒度的线程协作。

所以 CUDA 推出了 Cooperative Groups programming model,这可以认为是原先 CUDA programming model 的一个扩展。

Cooperative Groups 基础

使用 Cooperative Groups 需要加头文件 #include <cooperative_groups.h>,并且所有的命名都在 cooperative_groups:: 命名空间下。

Cooperative Groups 中的基础类型是 thread_group,这是一个指向一组线程的 handle,这个 handle 只能被该组的线程访问。一个 group 有一些简单的接口,如 unsigned size() 来查询 group 内的线程数量,unsigned thread_rank() 来查询当前线程在 group 中的 id(在 0size() - 1 之间)等。

对于一个 group,可以用下面的语句来同步。

1
2
g.sync();           // synchronize group g
cg::synchronize(g); // an equivalent way to synchronize g

创建 group

很显然,我们不用自己创建,block 本身就符合一个 group 概念。所以我们可以通过

1
thread_block block = this_thread_block();

来拿到指向该 block 的 handle。我们对这个 group 同步的话就和之前的 __syncthreads() 是一样的,所以下面的所有语句作用是相同的。

1
2
3
4
5
__syncthreads();
block.sync();
cg::synchronize(block);
this_thread_block().sync();
cg::synchronize(this_thread_block());

thread_block 相比上面的 thread_group,多了

1
2
dim3 group_index();  // 3-dimensional block index within the grid
dim3 thread_index(); // 3-dimensional thread index within the block

这两个值,等同于先前的 blockIdxthreadIdx

想要把 group 继续细分,则可以使用 cg::tiled_partition() 函数,如我们可以用下面的代码把整个 block 分为 32 个线程的块,然后再分为 4 个线程一组的块:

1
2
thread_group tile32 = cg::tiled_partition(this_thread_block(), 32);
thread_group tile4 = tiled_partition(tile32, 4);

下面是一个 reduce sum 的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <cooperative_groups.h>

using namespace cooperative_groups;
__device__ int reduce_sum(thread_group g, int *temp, int val)
{
    int lane = g.thread_rank();

    // Each iteration halves the number of active threads
    // Each thread adds its partial sum[i] to sum[lane+i]
    for (int i = g.size() / 2; i > 0; i /= 2)
    {
        temp[lane] = val;
        g.sync(); // wait for all threads to store
        if(lane<i) val += temp[lane + i];
        g.sync(); // wait for all threads to load
    }
    return val; // note: only thread 0 will return full sum
}

__device__ int thread_sum(int *input, int n) 
{
    int sum = 0;

    for(int i = blockIdx.x * blockDim.x + threadIdx.x;
        i < n / 4; 
        i += blockDim.x * gridDim.x)
    {
        int4 in = ((int4*)input)[i];
        sum += in.x + in.y + in.z + in.w;
    }
    return sum;
}

__global__ void sum_kernel_32(int *sum, int *input, int n)
{
    int my_sum = thread_sum(input, n); 

    extern __shared__ int temp[];

    auto g = this_thread_block();
    auto tileIdx = g.thread_rank() / 32;
    int* t = &temp[32 * tileIdx];
    
    auto tile32 = tiled_partition(g, 32);  
    int tile_sum = reduce_sum(tile32, t, my_sum);

    if (tile32.thread_rank() == 0) atomicAdd(sum, tile_sum);
}

同时,对于 warp 来说,一个 warp 内的线程可能会发生 diverge,即 warp divergence。这时 SM 会用 active masks 来屏蔽没有激活的线程。而 Cooperative Groups 提供了 coalesced_threads() 函数来创建一个 coalesced threads group。

1
2
3
4
5
6
7
auto block = this_thread_block();

if (block.thread_rank() % 2) {
    coalesced_group active = coalesced_threads();
    ...
    active.sync();
}

很显然,最大的 coalesced threads group 就是一整个 warp。

针对 warp 的优化

对齐到 warp 大小

我们可以把 group size 写到模板参数里,使用静态的 group 定义,这样 thread 的大小就在编译时已知了:

1
2
thread_block_tile<32> tile32 = tiled_partition<32>(this_thread_block());
thread_block_tile<4>  tile4  = tiled_partition<4> (this_thread_block());

虽然我们可以随便定 group 的大小,但是当我们把 size 定到 warp 大小时,编译器会把同步做到 warp level,效率更高。

使用 warp level 指令

同时我们可以使用下面的 warp level 指令来提速:

1
2
3
4
5
6
7
8
9
.shfl()
.shfl_down()
.shfl_up()
.shfl_xor()
.any()
.all()
.ballot()
.match_any()
.match_all()
使用 Hugo 构建
主题 StackJimmy 设计