0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

束内规约与块内规约问题

jf_pmFSk4VX 来源:GiantPandaCV 2023-11-27 17:29 次阅读

写在前面:规约问题在 CUDA 编程中应用非常广泛,笔者最近在研究 Faster Transformer 源码,趁此机会结合 Nivida 官方的代码对规约手段进行总结。

1 应用背景

关于规约的定义,相信能读到这篇文章的读者都不陌生,笔者在早期的文章中也介绍过一些规约方法,基本思想都是折半规约,主要应用于较大元素规模的向量规约,有兴趣的读者可以移步【CUDA编程】CUDA编程中的并行规约问题。
本文要介绍的规约场景与之前有所不同,主要应用于矩阵规约,也就是说本文假设的输入变量的维度是 2 维的,形状为 [batch_size, hidden_units],规约之后的输出变量形状为 [batch_size, ]
接下来,本文将以规约求和为例介绍两种规约方式:束内规约块内规约

2 束内规约

束内规约,也就是在一个线程束内对某个变量进行规约。我们知道 CUDA 架构下指令是以线程束(相邻的 32 个线程)为基本单元执行的,线程束内也可以通过束内洗牌指令进行通信,所以这提供了一个很好的束内规约思路。下面是 Nvidia 提供的基础的一个规约设备函数。

template <typename T>
__inline__ __device__
T warpReduceSum(T val)
{
  for(int mask = 16; mask > 0; mask >>= 1)
    val += __shfl_xor_sync(FINAL_MASK, val, mask, 32);
  return val;
}

这个设备函数可以求出当前线程所在线程束的指定变量的规约和,原理涉及洗牌指令的计算逻辑,不再赘述。
当矩阵宽度 hidden_units 较小时,通常可以使用一个 warp 处理一行数据,一个 block 内可以处理多行数据,笔者给出具体的核函数如下:

// 一个 warp 处理一行数据
template<typename T>
__global__ void matrix2DWarpReduceSum(const T* inp, T*out, const uint32_t hidden_units) {
    uint32_t tid = threadIdx.x;
    uint32_t lane_id = tid % 32;
    uint32_t warp_id = tid / 32;
    uint32_t warp_num = blockDim.x / 32;
    uint32_t offset = blockIdx.x * warp_num * hidden_units + warp_id * hidden_units;
    T val = 0.0f;
    for (uint32_t i=lane_id; i32) {
        val += inp[offset + i];
    }
    __syncwarp();
    T warpSum;
    warpSum = warpReduceSum(val);
    if (lane_id == 0) {
      out[blockIdx.x * warp_num + warp_id] = warpSum;
    }
}

template<typename T>
void launchMatrix2DWarpReduceSum(const T* d_x, T* d_y, const uint32_t batch_size, const uint32_t hidden_units) {
  constexpr uint32_t warp_num = BLOCK_SIZE / 32;
  uint32_t gird_size = (batch_size - 1) / (warp_num) + 1;
  matrix2DWarpReduceSum<<>>(d_x, d_y, hidden_units);
}

先确定 block_size,这里笔者直接取 128,由于是一个 warp 处理一行数据,所以一个 block 可以处理 warp_num 行数据,总共需要 grid_size 个 block。
核函数内部首先计算当前线程所在的 warp 编号 warp_id 用来定位当前处理元素在哪一行,然后确定线程在 warp 内的编号 lane_id 用来定位该线程具体处理那些元素。由于矩阵宽度 hidden_units 实际肯定还是比 32 大的,所以不可能说一个线程只处理一个元素,因此每个线程会处理多个元素,步长为 32,例如当 hidden_units128 时,lane_id = 0 的线程将处理位置为 0、32、64、96 的四个元素,lane_id = 1 的线程将处理位置为 1、33、65、97 的四个元素,以此类推,这个计算过程是没有并行的。循环计算一轮后,对线程束内每个线程的 val 进行束内规约就可以得到一行元素的规约和。

3 块内规约

块内规约,就是在一个线程块内求规约值,通常块内规约会通过束内规约来实现,以下是 Nvidia 提供的一个块内规约设备函数。

template <typename T>
__inline__ __device__
T blockReduceSum(T val)
{
  static __shared__ T shared[32]; 
  int lane = threadIdx.x & 0x1f; 
  int wid = threadIdx.x >> 5;  

  val = warpReduceSum(val);

  if(lane == 0)
    shared[wid] = val;
  __syncthreads();
  
  val = (threadIdx.x < (blockDim.x >> 5 )) ? shared[lane] : (T)0.0f;
  val = warpReduceSum(val);
  return val;
}

规约思路分为两步,首先通过束内规约求出当前线程所在 warp 的规约值,存入 shared 中,然后把 warpSum 赋值给 threadIdx.x 小于 32 的线程内的变量 val,这 32 个线程正好也在一个线程束内,然后再执行一次束内规约就得到块内规约值,计算思路非常巧妙。
另外针对块内规约的问题,官方 cub 库其实提供了 API开发者可以导入头文件 cub/cub.cuh 后直接使用,注意低版本的 cuda 不支持此 API。我们来看下 API 的调用方式。

#include 

template<typename T>
struct SumOp {
  __device__ __forceinline__ T operator()(const T& a, const T& b) const { return a + b; }
};

template<template<typename> class ReductionOp, typename T, int block_size>
__inline__ __device__ T BlockAllReduce(T val) {
  typedef cub::BlockReduce BlockReduce;
  __shared__ typename BlockReduce::TempStorage temp_storage;
  __shared__ T result_broadcast;
  T result = BlockReduce(temp_storage).Reduce(val, ReductionOp());
  if (threadIdx.x == 0) { result_broadcast = result; }
  __syncthreads();
  return result_broadcast;
}

除了必要的待规约变量、block_size 以外,还需要传入一个计算函数,笔者给出了示例 SumOp
当矩阵宽度 hidden_units 较大时,通常可以使用一个 block 处理一行数据,笔者给出具体的核函数如下:

template<typename T>
__global__ void matrix2DBlockReduceSum(const T* inp, T*out, const uint32_t hidden_units) {
  T val = 0.0f;
  uint32_t offset = blockIdx.x * hidden_units;
  for (uint32_t i=threadIdx.x; i(val);
  if (threadIdx.x == 0) {
    out[blockIdx.x] = blockSum;
  }
}

template<typename T>
void launchMatrix2DBlockReduceSum(const T* d_x, T* d_y, const uint32_t batch_size, const uint32_t hidden_units) {
  uint32_t gird_size = batch_size;
  matrix2DBlockReduceSum<<>>(d_x, d_y, hidden_units);
}

同样,block_size 这里笔者直接取 128,由于是一个 block 处理一行数据,总共需要 batch_size 个 block。
由于矩阵宽度 hidden_units 实际肯定还是比 block_size 大的,所以不可能说一个线程只处理一个元素,因此每个线程会处理多个元素,步长为 block_size,例如当 hidden_units512 时,lane_id = 0 的线程将处理位置为 0、128、256、384 的四个元素,lane_id = 1 的线程将处理位置为 1、129、257、385 的四个元素,以此类推,这个计算过程是没有并行的。循环计算一轮后,对 block 内每个线程的 val 进行块内规约就可以得到一行元素的规约和。

4 向量化数据提升访存带宽

使用向量化操作能够提升内存读写的带宽,而 CUDA 里也提供了一系列数据类型来支持向量化操作,如 float2、float4,就是将 2 个或 4 个 float 数据作为一个整体。为了增加代码的复用性,笔者这里封装了一个 Packed 数据结构,用于对不同的数据类型进行打包。

template <typename T, int pack_size>
struct alignas(sizeof(T) * pack_size) Packed
{
    __device__ Packed()
    {
        // do nothing
    }
    union
    {
        T elem[pack_size]; // 这里联合体只有一个成员,为了方便后期扩展
    };
};

结构体内有一个 elem 数组变量,整个结构的内存对齐设置为 sizeof(T) * pack_size,说白了其实就是把 pack_sizeT 类型的数据“捆绑”在一起组成一个新的数据结构,读写内存的时候只需要一次读写就可以读 pack_size 个数据,目的是减小内存读写次数。
那么这个 pack_size 能不能无限大呢?显然不能,CUDA 里最大支持 128 bit 的访问粒度,也就是说对于 float 类型(占 4 个字节,32 bit),一次最多读写 4 个,也就是说 float 的 pack_size 最多取到 4,本文笔者的示例代码中数据类型都以 float 为例,pack_size4

4.1 pack 后的束内规约示例代码

matrix2DWarpReduceSum 改写为 pack 版的核函数也很简单,计算思路都是一致的,只不过原来一次访问一个元素,现在一次访问一个 pack 的元素,在执行核函数之前笔者加了一个断言,保证 hidden_units 能够被 pack_size 整除,具体代码如下。

template <int pack_size, typename T>
__global__ void matrix2DWarpReduceSumPack(const T* d_x, T* d_y, const uint32_t hidden_units, const uint32_t num_packs) {
  const uint32_t warp_id = threadIdx.x / 32;
  const uint32_t lane_id = threadIdx.x & 0x1f;
  const uint32_t warp_num = blockDim.x / 32;
  const uint32_t offset = blockIdx.x * warp_num * hidden_units + warp_id * hidden_units;
  const Packed* buf = reinterpret_cast<const Packed*>(d_x + offset);
  Packed pack;
  T val = 0.0f;
  for (uint32_t pack_id=lane_id; pack_id32) {
    pack = buf[pack_id];
    for (uint32_t i=0; i(val);
  if (lane_id == 0) {
    d_y[blockIdx.x * warp_num + warp_id] = warpSum;
  }
}

template<typename T>
void launchMatrix2DWarpReduceSumPack(const T* d_x, T* d_y, const uint32_t batch_size, const uint32_t hidden_units) {
  constexpr uint32_t warp_num = BLOCK_SIZE / 32;
  uint32_t gird_size = (batch_size - 1) / (warp_num) + 1;
  constexpr uint32_t pack_size = 4;
  // 一行元素的 pack 数量
  uint32_t num_packs = hidden_units / pack_size;
  assert(hidden_units % pack_size == 0);
  matrix2DWarpReduceSumPack<<>>(d_x, d_y, hidden_units, num_packs);
}

核函数内部就一句核心代码,将 const T* 指针转换成 const Packed*

const Packed* buf = reinterpret_cast<const Packed*>(d_x + offset);

然后用 pack_id 索引一次取一个 pack 的数据,注意这里对 pack 索引的时候不要写错了。跟前面一样,相邻的线程处理相邻的 pack 数据,这是为了全局内存的合并访问。加法计算次数还是那么多次,因为 Packed 结构体并不能直接参与计算,还是要用 elem 里面的元素计算,这个核函数也就节省了访存次数而已。

4.2 pack 后的块内规约示例代码

matrix2DBlockReduceSumPack 核函数的实现就更简单了,直接上代码。

template <int pack_size, typename T>
__global__ void matrix2DBlockReduceSumPack(const T* d_x, T* d_y, const uint32_t hidden_units, const uint32_t num_packs) {
  T val = 0.0f;
  uint32_t offset = blockIdx.x * hidden_units;
  const Packed* buf = reinterpret_cast<const Packed*>(d_x + offset);
  Packed pack;
  for (uint32_t pack_id=threadIdx.x; pack_idfor (uint32_t i=0; i(val);
  if (threadIdx.x == 0) {
    d_y[blockIdx.x] = blockSum;
  }
}

template<typename T>
void launchMatrix2DBlockReduceSumPack(const T* d_x, T* d_y, const uint32_t batch_size, const uint32_t hidden_units) {
  uint32_t gird_size = batch_size;
  constexpr uint32_t pack_size = 4;
  assert(hidden_units % pack_size == 0);
  uint32_t num_packs = hidden_units / pack_size;
  matrix2DBlockReduceSumPack<<>>(d_x, d_y, hidden_units, num_packs);
}

5 小结

深度学习算子的开发过程中,规约是一个非常常见的场景,以 Softmax 为例就有 reduceMax 和 reduceSum 的应用,本文给出了两种规约实现方式,可供读者参考使用。实际开发过程中,规约计算一般是隐藏在其他 kernel 中的,并不会奢侈到单独写个规约 kernel,所以要求开发人员领会思路活学活用。


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 编程
    +关注

    关注

    88

    文章

    3615

    浏览量

    93710
  • 矩阵
    +关注

    关注

    0

    文章

    423

    浏览量

    34541
  • 变量
    +关注

    关注

    0

    文章

    613

    浏览量

    28365

原文标题:【CUDA编程】束内规约与块内规约问题

文章出处:【微信号:GiantPandaCV,微信公众号:GiantPandaCV】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    配电自动化系统数据采集和远动规约的研究

    配电自动化系统数据采集和远动规约的研究摘要:介绍了配电自动化通信结构和通信规约,分析讨论配电自动化系统中数据的传输特点,论述了远动规约在配电自动化系统的数据传输过程中的应用,并对规约
    发表于 08-08 09:50

    IEC61850-9-2通信规约

    现在网上只有许继IEC61850-9-2LE版通信规约,大神们谁有IEC62850-9-2的通信规约的资料吗?
    发表于 04-27 10:36

    嵌入式系统的通信规约管理平台该怎么设计?

    众所周知,通信的双方必须遵守相同的协议,报文才能互相识别。目前,不同行业间的通信协议千差万别。为解决不同通信协议间的计算机系统通信问题,人们普遍采用的措施是一个具体规约对应一段程序。如果出现新规约
    发表于 09-18 06:55

    水利rtu通信规约

    `  水利rtu通信规约,为保证数据通信系统中通信双方能有效和可靠地通信而规定的双方应共同遵守的一系列约定,包括:数据的格式、顺序和速率、链路管理、流量调节和差错控制等。  水利rtu通信规约
    发表于 09-07 11:47

    有哪位大佬做过基于单片机的104规约解析吗

    有大佬做过基于单片机的104规约解析吗?单片机作为从站,通过104规约与主机通讯。
    发表于 09-05 14:12

    有大佬做过基于单片机的104规约解析吗?

    有大佬做过基于单片机的104规约解析吗?单片机作为从站,通过104规约与主机通讯。
    发表于 05-12 15:54

    [电能表规约调试助手]1.0版.

    关于[电能表规约调试助手]1.0版(软件功能和特色:     1、作为一般的串口调试软件(不要选中[规约调试])     2、规约调试 完
    发表于 03-28 00:15 0次下载

    嵌入式系统的通信规约管理平台设计

    论述设计通信规约管理平台的必要性与可行性;借鉴操作系统的PCB 思想,结合面向对象的方法学提出通信规约管理平台设计的核心思想——用户填写静态规约说明书。规约管理平台
    发表于 05-15 15:46 12次下载

    基于CAN总线的综自通讯规约设计

    介绍一种基于CAN总线的牵引变电站自动化系统通讯规约的设计,CAN通讯规约采用标准帧,报文采用主动发送和发送查询两种处理形式。该设计在城市轻轨与地铁牵引变电站中的应
    发表于 12-25 16:26 39次下载

    101规约通信流程及各功能码作用

    101通讯规约,电力行业101通信规约,主要介绍了101规约通信流程及各功能码作用,使用方法等
    发表于 10-28 11:21 54次下载

    电力101规约(2002版)报文解析

    电力101规约(2002版)报文解析~~~~
    发表于 01-08 14:39 0次下载

    104规约调试和学习小记下载资料

    04规约是厂站与配网主站进行通讯的规约,以以太网为载体,服务模式是平衡传输。
    发表于 03-22 16:23 7次下载

    软件的顺序语句自动化规约与验证研究

    算法以及对所规约的语义自动生成证明脚本的算法。利用C++和 Python并通过交互式定理证明器abelle2017在基准数据中随机选择10个程序进行测试,结果表明,与完全人工操作相比,该算法具有较高的验证效率,可实现顺序语句的自动化
    发表于 06-03 14:31 5次下载

    Modbus RTU通讯规约

    艾德堡HP系列数显推拉力计_Modbus_RTU规约_HP_通信协议
    发表于 10-20 17:03 4次下载

    电力104规约工业智能网关

    计讯物联电力104规约工业智能网关在实现电力系统全面监控、智能管理方面发挥着不可替代的作用。电力104规约工业智能网关的基本原理电力104规约工业智能网关作为数据传输的核心设备,通过将电力系统中
    的头像 发表于 09-21 14:31 989次阅读
    电力104<b class='flag-5'>规约</b>工业智能网关