CUDA 的块间同步方法

简介: CUDA 的块间同步方法

CUDA 块间同步方法有以下三种

① Lock-Based Synchronization


② Lock-Free Synchronization


③ __threadfence()



基于锁的块间同步

CUDA 基于锁的同步的基本思想是使用一个全局互斥量变量来计算到达同步点的线程块的数量。如下代码所示,在 barrier 函数 __gpu_sync() 中,在一个块完成它的计算之后,它的一个线程 (这里人为设置为 0 号线程,我们称之为主导线程) 将自动地向 g_mutex 添加 1 (原子操作)。然后,主导线程将重复将 g_mutex 和一个目标值 goalVal 进行比较。如果 g_mutex 等于 goalVal,那么就意味着同步完成,即每个线程块都可以进行下一阶段的计算。在该设计中,当第一次调用 barrier 函数时,将 goalVal 设置为内核中的块数 N 。然后,当连续调用 barrier 函数时,goalVal的值每次递增 N 。这种设计比保持 goalVal 常量并在每个 barrier 之后重新设置 g_mutex 更有效,因为前者节省了指令的数量并避免了条件分支 。

// lock-based
__device__ volatile int g_mutex;
// GPU lock-based synchronization function
__device__ void __gpu_sync(int goalVal)
{
  // thread ID in a block
  int tid_in_block = threadIdx.x * blockDim.y + threadIdx.y;
  // only thread 0 is used for synchronization
  if (tid_in_block == 0)
  {
    atomicAdd((int*) &g_mutex, 1);
    // only when all blocks add 1 go g_mutex
    // will g_mutex equal to goalVal
    while (g_mutex != goalVal)
    {
      // Do nothing here
    }
  }
  __syncthreads();
}

无锁的块间同步

在 CUDA 基于锁的同步中,互斥量变量 g_mutex 是通过原子函数 atomicAdd() 添加的。这意味着 g_mutex 的添加只能按顺序执行,即使这些操作是在不同的块中执行的。因此,提出一种完全避免使用原子操作的无锁同步方法。这种方法的基本思想是为每个线程块分配一个同步变量,这样每个块就可以独立地记录其同步状态,而不必争用单个全局互斥锁变量。如下代码所示,我们的无锁同步方法使用两个数组 Arrayin 和 Arrayout 来协调来自不同块的同步请求。在这两个数组中,每个元素都映射到内核中的一个线程块,即,将元素 i 映射到线程块 i ,算法大致分为三个步骤:



一开始,当 block i 准备好通信时,它的主导线程 (线程 0) 将 Arrayin 中的元素 i 设置为目标值 goalVal。block i 中的主导线程然后忙等 Arrayout 的元素 i 被设置为 goalVal 。

然后人为的让 block 1中的前 N 个(N 等于块数)线程重复检查 Arrayin 中的所有元素是否等于 goalVal ,线程 i 负责检查 Arrayin 中的第 i 个元素(一对一检查)。将 Arrayin 中的所有元素设置为 goalVal 后,每个检查线程将 Arrayout 中的相应元素设置为 goalVal 。注意,在更新 Arrayout的元素之前,block 1 的每个检查线程都会调用块内 barrier 函数  __syncthreads()。

最后,当 block i 的主导线程看到 Arrayout 中的对应元素被设置为 goalVal 时,该 block 将继续执行。

// lock-free
__device__ void __gpu_sync(int goalVal, volatile int *Arrayin, volatile int *Arrayout)
{
  // thread ID in a block
  int tid_in_blk = threadIdx.x * blockDim.y + threadIdx.y;
  int nBlockNum = gridDim.x * gridDim.y;
  int bid = blockIdx.x * gridDim.y + blockIdx.y;
  // only thread 0 is used for synchonization
  if (tid_in_blk == 0)
  {
    Arrayin[bid] = goalVal;
  }
  if (bid == 1)
  {
    if (tid_in_blk < nBlockNum)
    {
      while (Arrayin[tid_in_blk] != goalVal)
      {
        // Do nothing here
      }
    }
    __syncthreads();
    if (tid_in_blk < nBlockNum)
    {
      Arrayout[tid_in_blk] = goalVal;
    }
  }
  if (tid_in_blk = 0)
  {
    while (Arrayout[bid] != goalVal)
    {
      // Do nothing here
    }
  }
  __syncthreads();
}

从以上代码可以看出,CUDA 无锁同步中没有原子操作。所有的操作都可以并行执行。不同线程块的同步由单个块 (block 1) 中的 N 个线程来控制,可以通过调用块内 barrier 函数 __syncthreads() 来有效地同步。



__threadfence()

最后,值得注意的是,另外一种保证 CUDA 块间同步通信的正确性的办法是使用 __threadfence() (CUDA 2.2中引入了一个新的函数 )。这个函数将阻塞调用线程,直到之前对 全局内存 或 共享内存 的写入对其他线程可见为止。但是使用 __threadfence() 也会引起一定的额外开销,所以需要进行实际测试和权衡。



相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
目录
相关文章
|
缓存 Java 编译器
volatile的内存语义
volatile的内存语义
63 0
|
并行计算 异构计算
如何将cuda上的变量转到cpu上面?
在这个示例中,我们首先将x张量对象创建在GPU上。然后,我们使用.cpu()方法将其移动到CPU上,并将其分配给一个新的变量x_cpu。现在,我们可以在CPU上使用x_cpu变量并打印它。 请注意,将张量移动到不同的设备(如从GPU到CPU)可能会涉及到数据的复制,因此需要确保不会频繁地在不同的设备之间移动数据以避免性能下降。
1818 0
|
存储 缓存 安全
基础篇:深入JMM内存模型解析volatile、synchronized的内存语义
总线锁定:当某个CPU处理数据时,通过锁定系统总线或者是内存总线,让其他CPU不具备访问内存的访问权限,从而保证了缓存的一致性
97 0
|
Java
【synchronized】同步方法与同步块
【synchronized】同步方法与同步块
115 0
【synchronized】同步方法与同步块
|
安全
多线程编程核心技术-对象及变量的并发访问-synchronize同步方法(2)(上)
多线程编程核心技术-对象及变量的并发访问-synchronize同步方法(2)(上)
111 0
多线程编程核心技术-对象及变量的并发访问-synchronize同步方法(2)(上)
|
存储 并行计算 算法
初识CUDA网格与线程块
初识CUDA网格与线程块
646 0
初识CUDA网格与线程块
|
安全
synchronized同步方法及同步块
synchronized同步方法及同步块
118 0
|
并行计算 索引
初识CUDA使用线程索引
初识CUDA使用线程索引
288 0
|
存储 并行计算 安全
原子操作CUDA中的原子操作
原子操作CUDA中的原子操作
285 0