0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內(nèi)不再提示

束內(nèi)規(guī)約與塊內(nèi)規(guī)約問題

jf_pmFSk4VX ? 來源:GiantPandaCV ? 2023-11-27 17:29 ? 次閱讀

寫在前面:規(guī)約問題在 CUDA 編程中應用非常廣泛,筆者最近在研究 Faster Transformer 源碼,趁此機會結(jié)合 Nivida 官方的代碼對規(guī)約手段進行總結(jié)。

1 應用背景

關于規(guī)約的定義,相信能讀到這篇文章的讀者都不陌生,筆者在早期的文章中也介紹過一些規(guī)約方法,基本思想都是折半規(guī)約,主要應用于較大元素規(guī)模的向量規(guī)約,有興趣的讀者可以移步【CUDA編程】CUDA編程中的并行規(guī)約問題。
本文要介紹的規(guī)約場景與之前有所不同,主要應用于矩陣規(guī)約,也就是說本文假設的輸入變量的維度是 2 維的,形狀為 [batch_size, hidden_units],規(guī)約之后的輸出變量形狀為 [batch_size, ]
接下來,本文將以規(guī)約求和為例介紹兩種規(guī)約方式:束內(nèi)規(guī)約、塊內(nèi)規(guī)約。

2 束內(nèi)規(guī)約

束內(nèi)規(guī)約,也就是在一個線程束內(nèi)對某個變量進行規(guī)約。我們知道 CUDA 架構(gòu)下指令是以線程束(相鄰的 32 個線程)為基本單元執(zhí)行的,線程束內(nèi)也可以通過束內(nèi)洗牌指令進行通信,所以這提供了一個很好的束內(nèi)規(guī)約思路。下面是 Nvidia 提供的基礎的一個規(guī)約設備函數(shù)。

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;
}

這個設備函數(shù)可以求出當前線程所在線程束的指定變量的規(guī)約和,原理涉及洗牌指令的計算邏輯,不再贅述。
當矩陣寬度 hidden_units 較小時,通??梢允褂靡粋€ warp 處理一行數(shù)據(jù),一個 block 內(nèi)可以處理多行數(shù)據(jù),筆者給出具體的核函數(shù)如下:

// 一個 warp 處理一行數(shù)據(jù)
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 處理一行數(shù)據(jù),所以一個 block 可以處理 warp_num 行數(shù)據(jù),總共需要 grid_size 個 block。
核函數(shù)內(nèi)部首先計算當前線程所在的 warp 編號 warp_id 用來定位當前處理元素在哪一行,然后確定線程在 warp 內(nèi)的編號 lane_id 用來定位該線程具體處理那些元素。由于矩陣寬度 hidden_units 實際肯定還是比 32 大的,所以不可能說一個線程只處理一個元素,因此每個線程會處理多個元素,步長為 32,例如當 hidden_units128 時,lane_id = 0 的線程將處理位置為 0、32、64、96 的四個元素,lane_id = 1 的線程將處理位置為 1、33、65、97 的四個元素,以此類推,這個計算過程是沒有并行的。循環(huán)計算一輪后,對線程束內(nèi)每個線程的 val 進行束內(nèi)規(guī)約就可以得到一行元素的規(guī)約和。

3 塊內(nèi)規(guī)約

塊內(nèi)規(guī)約,就是在一個線程塊內(nèi)求規(guī)約值,通常塊內(nèi)規(guī)約會通過束內(nèi)規(guī)約來實現(xiàn),以下是 Nvidia 提供的一個塊內(nèi)規(guī)約設備函數(shù)。

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;
}

規(guī)約思路分為兩步,首先通過束內(nèi)規(guī)約求出當前線程所在 warp 的規(guī)約值,存入 shared 中,然后把 warpSum 賦值給 threadIdx.x 小于 32 的線程內(nèi)的變量 val,這 32 個線程正好也在一個線程束內(nèi),然后再執(zhí)行一次束內(nèi)規(guī)約就得到塊內(nèi)規(guī)約值,計算思路非常巧妙。
另外針對塊內(nèi)規(guī)約的問題,官方 cub 庫其實提供了 API,開發(fā)者可以導入頭文件 cub/cub.cuh 后直接使用,注意低版本的 cuda 不支持此 API。我們來看下 API 的調(diào)用方式。

#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;
}

除了必要的待規(guī)約變量、block_size 以外,還需要傳入一個計算函數(shù),筆者給出了示例 SumOp
當矩陣寬度 hidden_units 較大時,通??梢允褂靡粋€ block 處理一行數(shù)據(jù),筆者給出具體的核函數(shù)如下:

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 處理一行數(shù)據(jù),總共需要 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 的四個元素,以此類推,這個計算過程是沒有并行的。循環(huán)計算一輪后,對 block 內(nèi)每個線程的 val 進行塊內(nèi)規(guī)約就可以得到一行元素的規(guī)約和。

4 向量化數(shù)據(jù)提升訪存帶寬

使用向量化操作能夠提升內(nèi)存讀寫的帶寬,而 CUDA 里也提供了一系列數(shù)據(jù)類型來支持向量化操作,如 float2、float4,就是將 2 個或 4 個 float 數(shù)據(jù)作為一個整體。為了增加代碼的復用性,筆者這里封裝了一個 Packed 數(shù)據(jù)結(jié)構(gòu),用于對不同的數(shù)據(jù)類型進行打包。

template <typename T, int pack_size>
struct alignas(sizeof(T) * pack_size) Packed
{
    __device__ Packed()
    {
        // do nothing
    }
    union
    {
        T elem[pack_size]; // 這里聯(lián)合體只有一個成員,為了方便后期擴展
    };
};

結(jié)構(gòu)體內(nèi)有一個 elem 數(shù)組變量,整個結(jié)構(gòu)的內(nèi)存對齊設置為 sizeof(T) * pack_size,說白了其實就是把 pack_sizeT 類型的數(shù)據(jù)“捆綁”在一起組成一個新的數(shù)據(jù)結(jié)構(gòu),讀寫內(nèi)存的時候只需要一次讀寫就可以讀 pack_size 個數(shù)據(jù),目的是減小內(nèi)存讀寫次數(shù)。
那么這個 pack_size 能不能無限大呢?顯然不能,CUDA 里最大支持 128 bit 的訪問粒度,也就是說對于 float 類型(占 4 個字節(jié),32 bit),一次最多讀寫 4 個,也就是說 float 的 pack_size 最多取到 4,本文筆者的示例代碼中數(shù)據(jù)類型都以 float 為例,pack_size4。

4.1 pack 后的束內(nèi)規(guī)約示例代碼

matrix2DWarpReduceSum 改寫為 pack 版的核函數(shù)也很簡單,計算思路都是一致的,只不過原來一次訪問一個元素,現(xiàn)在一次訪問一個 pack 的元素,在執(zhí)行核函數(shù)之前筆者加了一個斷言,保證 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 數(shù)量
  uint32_t num_packs = hidden_units / pack_size;
  assert(hidden_units % pack_size == 0);
  matrix2DWarpReduceSumPack<<>>(d_x, d_y, hidden_units, num_packs);
}

核函數(shù)內(nèi)部就一句核心代碼,將 const T* 指針轉(zhuǎn)換成 const Packed*。

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

然后用 pack_id 索引一次取一個 pack 的數(shù)據(jù),注意這里對 pack 索引的時候不要寫錯了。跟前面一樣,相鄰的線程處理相鄰的 pack 數(shù)據(jù),這是為了全局內(nèi)存的合并訪問。加法計算次數(shù)還是那么多次,因為 Packed 結(jié)構(gòu)體并不能直接參與計算,還是要用 elem 里面的元素計算,這個核函數(shù)也就節(jié)省了訪存次數(shù)而已。

4.2 pack 后的塊內(nèi)規(guī)約示例代碼

matrix2DBlockReduceSumPack 核函數(shù)的實現(xiàn)就更簡單了,直接上代碼。

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 小結(jié)

深度學習算子的開發(fā)過程中,規(guī)約是一個非常常見的場景,以 Softmax 為例就有 reduceMax 和 reduceSum 的應用,本文給出了兩種規(guī)約實現(xiàn)方式,可供讀者參考使用。實際開發(fā)過程中,規(guī)約計算一般是隱藏在其他 kernel 中的,并不會奢侈到單獨寫個規(guī)約 kernel,所以要求開發(fā)人員領會思路活學活用。


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 編程
    +關注

    關注

    88

    文章

    3565

    瀏覽量

    93536
  • 矩陣
    +關注

    關注

    0

    文章

    418

    瀏覽量

    34475
  • 變量
    +關注

    關注

    0

    文章

    613

    瀏覽量

    28306

原文標題:【CUDA編程】束內(nèi)規(guī)約與塊內(nèi)規(guī)約問題

文章出處:【微信號:GiantPandaCV,微信公眾號:GiantPandaCV】歡迎添加關注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關推薦

    配電自動化系統(tǒng)數(shù)據(jù)采集和遠動規(guī)約的研究

    配電自動化系統(tǒng)數(shù)據(jù)采集和遠動規(guī)約的研究摘要:介紹了配電自動化通信結(jié)構(gòu)和通信規(guī)約,分析討論配電自動化系統(tǒng)中數(shù)據(jù)的傳輸特點,論述了遠動規(guī)約在配電自動化系統(tǒng)的數(shù)據(jù)傳輸過程中的應用,并對規(guī)約轉(zhuǎn)
    發(fā)表于 08-08 09:50

    IEC61850-9-2通信規(guī)約

    現(xiàn)在網(wǎng)上只有許繼IEC61850-9-2LE版通信規(guī)約,大神們誰有IEC62850-9-2的通信規(guī)約的資料嗎?
    發(fā)表于 04-27 10:36

    嵌入式系統(tǒng)的通信規(guī)約管理平臺該怎么設計?

    眾所周知,通信的雙方必須遵守相同的協(xié)議,報文才能互相識別。目前,不同行業(yè)間的通信協(xié)議千差萬別。為解決不同通信協(xié)議間的計算機系統(tǒng)通信問題,人們普遍采用的措施是一個具體規(guī)約對應一段程序。如果出現(xiàn)新規(guī)約
    發(fā)表于 09-18 06:55

    水利rtu通信規(guī)約

    `  水利rtu通信規(guī)約,為保證數(shù)據(jù)通信系統(tǒng)中通信雙方能有效和可靠地通信而規(guī)定的雙方應共同遵守的一系列約定,包括:數(shù)據(jù)的格式、順序和速率、鏈路管理、流量調(diào)節(jié)和差錯控制等。  水利rtu通信規(guī)約
    發(fā)表于 09-07 11:47

    有哪位大佬做過基于單片機的104規(guī)約解析嗎

    有大佬做過基于單片機的104規(guī)約解析嗎?單片機作為從站,通過104規(guī)約與主機通訊。
    發(fā)表于 09-05 14:12

    有大佬做過基于單片機的104規(guī)約解析嗎?

    有大佬做過基于單片機的104規(guī)約解析嗎?單片機作為從站,通過104規(guī)約與主機通訊。
    發(fā)表于 05-12 15:54

    [電能表規(guī)約調(diào)試助手]1.0版.

    關于[電能表規(guī)約調(diào)試助手]1.0版(軟件功能和特色:     1、作為一般的串口調(diào)試軟件(不要選中[規(guī)約調(diào)試])     2、規(guī)約調(diào)試 完
    發(fā)表于 03-28 00:15 ?0次下載

    嵌入式系統(tǒng)的通信規(guī)約管理平臺設計

    論述設計通信規(guī)約管理平臺的必要性與可行性;借鑒操作系統(tǒng)的PCB 思想,結(jié)合面向?qū)ο蟮姆椒▽W提出通信規(guī)約管理平臺設計的核心思想——用戶填寫靜態(tài)規(guī)約說明書。規(guī)約管理平臺
    發(fā)表于 05-15 15:46 ?12次下載

    基于CAN總線的綜自通訊規(guī)約設計

    介紹一種基于CAN總線的牽引變電站自動化系統(tǒng)通訊規(guī)約的設計,CAN通訊規(guī)約采用標準幀,報文采用主動發(fā)送和發(fā)送查詢兩種處理形式。該設計在城市輕軌與地鐵牽引變電站中的應
    發(fā)表于 12-25 16:26 ?39次下載

    101規(guī)約通信流程及各功能碼作用

    101通訊規(guī)約,電力行業(yè)101通信規(guī)約,主要介紹了101規(guī)約通信流程及各功能碼作用,使用方法等
    發(fā)表于 10-28 11:21 ?54次下載

    電力101規(guī)約(2002版)報文解析

    電力101規(guī)約(2002版)報文解析~~~~
    發(fā)表于 01-08 14:39 ?0次下載

    104規(guī)約調(diào)試和學習小記下載資料

    04規(guī)約是廠站與配網(wǎng)主站進行通訊的規(guī)約,以以太網(wǎng)為載體,服務模式是平衡傳輸。
    發(fā)表于 03-22 16:23 ?7次下載

    軟件的順序語句自動化規(guī)約與驗證研究

    算法以及對所規(guī)約的語義自動生成證明腳本的算法。利用C++和 Python并通過交互式定理證明器abelle2017在基準數(shù)據(jù)中隨機選擇10個程序進行測試,結(jié)果表明,與完全人工操作相比,該算法具有較高的驗證效率,可實現(xiàn)順序語句的自動化
    發(fā)表于 06-03 14:31 ?5次下載

    Modbus RTU通訊規(guī)約

    艾德堡HP系列數(shù)顯推拉力計_Modbus_RTU規(guī)約_HP_通信協(xié)議
    發(fā)表于 10-20 17:03 ?4次下載

    電力104規(guī)約工業(yè)智能網(wǎng)關

    計訊物聯(lián)電力104規(guī)約工業(yè)智能網(wǎng)關在實現(xiàn)電力系統(tǒng)全面監(jiān)控、智能管理方面發(fā)揮著不可替代的作用。電力104規(guī)約工業(yè)智能網(wǎng)關的基本原理電力104規(guī)約工業(yè)智能網(wǎng)關作為數(shù)據(jù)傳輸?shù)暮诵脑O備,通過將電力系統(tǒng)中
    的頭像 發(fā)表于 09-21 14:31 ?932次閱讀
    電力104<b class='flag-5'>規(guī)約</b>工業(yè)智能網(wǎng)關