当前位置: 首页 > news >正文

cuda编程笔记(15)--使用 CUB 和 atomicAdd 实现 histogram

什么是histogram?

直方图(histogram)是一种统计分布的表示方法。

它的核心思想:

  • 把数据按数值范围分成若干个 区间(bin)。

  • 统计每个区间里有多少个数据。

  • 最终得到一个数组,表示“每个区间的数量”。

假设我有 10 个数字:

[1, 2, 2, 3, 5, 5, 5, 8, 9, 10]

如果我按下面的区间划分:

  • Bin 0: 0–2

  • Bin 1: 3–5

  • Bin 2: 6–8

  • Bin 3: 9–10

那么 histogram 结果就是:

Bin 0: 3   (1, 2, 2)
Bin 1: 4   (3, 5, 5, 5)
Bin 2: 2   (8)
Bin 3: 1   (9, 10)

如果画出来,就是典型的直方图柱状图。

在 GPU 上计算 histogram,就是并行统计每个数据属于哪个 bin,然后更新计数。

  • 常见场景:图像灰度统计(0–255)、点云密度分析、特征分布统计等。

  • 例如一张 8-bit 灰度图的 histogram 有 256 个 bin,每个 bin 统计有多少像素是该灰度值。

cuda编程笔记(5)--原子操作_cuda atomiccas-CSDN博客

实际上,上文中的示例代码:统计字符流里的字符出现频率,就是一个典型的histogram

难点

在 GPU 上计算 histogram 的主要挑战是:

  • 数据竞争:多个线程可能同时更新同一个 bin(原子冲突)。

  • 访存效率:全局内存的写入延迟很高,如果每次更新都直接写全局内存,就会很慢。

  • 因此就有了 atomicAdd 方法(直接原子更新) 和 CUB 方法(先分块私有化,再合并) 的不同优化思路。

我们用“10以内的统计分布”来举例,这是一个非常简单的 histogram 问题。

atomicAdd 方法

每个线程直接计算自己数据的 bin 索引,然后对全局 histogram 数组执行:

  • 优点:实现简单,几乎一行代码搞定。

  • 缺点:

    • 同一个 bin 高并发时会产生原子冲突(atomic contention)。

    • 对于热点分布(hot-spot distribution),吞吐量会严重下降。

    • 全局内存原子操作延迟大。

__global__ void histogram_atomic(const int *data,int *hist,int data_size,int numBins){//data是原数据,hist是直方图频率数组,data_size的数据大小,numBins是bin的个数;这里对10取余,所以numBins传10int tid = blockDim.x * blockIdx.x + threadIdx.x;//为了简单,只用一维的block和threadif (tid < data_size) {int bin = data[tid] % numBins;//计算本线程是数据属于哪个binatomicAdd(&hist[bin], 1);//原子加}
}

CUB 方法

HistogramEven — 等宽区间直方图(单通道)

template<typename SampleIteratorT,typename CounterT,typename LevelT,typename OffsetT>
static cudaError_t HistogramEven(void* d_temp_storage,size_t& temp_storage_bytes,SampleIteratorT d_samples,CounterT* d_histogram,int num_levels,LevelT lower_level,LevelT upper_level,OffsetT num_samples,cudaStream_t stream = 0,bool debug_synchronous = false
);
  • d_temp_storage:设备上用于中间计算的临时缓冲区指针。如果设为 nullptr,函数只负责返回所需缓冲区大小,不执行计算。

  • temp_storage_bytes:输入输出参数,用于读取或写入临时缓冲区大小(Byte 单位)。

  • d_samples:输入数据指针(device memory),可以是指针或迭代器。

  • d_histogram:输出直方图数组指针(device memory),长度应为 num_levels - 1

  • num_levels:区间边界数量,因此输出 bin 数是 num_levels - 1

  • lower_level, upper_level:数据样本的取值范围 [lower_level, upper_level)。区间宽度为 (upper_level - lower_level) / (num_levels - 1)

  • num_samples:输入样本数量。

  • stream:可选 CUDA 流。

  • debug_synchronous:可选调试标志,帮助调试但会影响性能。

这个方法会把 [lower_level, upper_level) 分为等宽区间,并对每个样本执行:

bin = floor((sample - lower_level) * (num_levels-1) / (upper_level - lower_level))

注意:若类型为整数,则进行整数除法处

void histogram_cub(const int* data, int* hist, int data_size, int numBins) {void* d_temp_storage = nullptr;size_t temp_storage_bytes = 0;//注意numBins+1需要加1,因为numBins传进去是10,我们也确实有10个区间cub::DeviceHistogram::HistogramEven(d_temp_storage, temp_storage_bytes,data, hist,numBins+1, 0, numBins,data_size);cudaMalloc(&d_temp_storage, temp_storage_bytes);cub::DeviceHistogram::HistogramEven(d_temp_storage, temp_storage_bytes,data, hist,numBins+1, 0, numBins,data_size);cudaFree(d_temp_storage);
}

这里主要探讨一下num_levels 这个参数:

如果传递11,也即numBins+1

  1. num_levels = numBins + 1 = 11

    • 这个参数指示 分割点数量,不是 bin 数量。

    • 分割点个数 = bin 数 + 1,所以实际生成 10 个 bin。

  2. min = 0, max = numBins = 10

    • 数据落在 [0, 10) 区间,最大值 10 是上界,不包含 10 本身。

  3. 区间计算公式(每个 bin 区间宽度):

width=(max−min)/numBins​​=1

所以每个 bin 的范围是:

Bin index区间
0[0, 1)
1[1, 2)
2[2, 3)
3[3, 4)
4[4, 5)
5[5, 6)
6[6, 7)
7[7, 8)
8[8, 9)
9[9, 10)

这样整数 0~9 恰好落在 10 个 bin 中,每个整数对应一个 bin。

如果传了10

width=(max−min)/(numLevels - 1)​​=10/(9)=1.111

CUB 会自动计算 bin 宽度:1.111

区间划分(错误的情况)

Bin index区间范围
0[0, 1.111)
1[1.111, 2.222)
2[2.222, 3.333)
3[3.333, 4.444)
4[4.444, 5.555)
5[5.555, 6.666)
6[6.666, 7.777)
7[7.777, 8.888)
8[8.888, 10]
9空或部分数据落入?

对比测试:

#ifndef __CUDACC__
#define __CUDACC__
#endif
#include <cuda_runtime.h>
#include <device_launch_parameters.h>
#include <cublas_v2.h>
#include<cub/cub.cuh>
#include <iostream>
#include<cstdio>
#include <random>#include <vector>
const int DATA_SIZE = 100;void error_handling(cudaError_t res) {if (res !=cudaSuccess) {std::cout << "error!" << std::endl;}
}
__global__ void histogram_atomic(const int* data, int* hist, int data_size, int numBins) {int tid = blockDim.x * blockIdx.x + threadIdx.x;if (tid < data_size) {int bin = data[tid]; // 数据已经在 [0,numBins-1]if(bin>=0&&bin<10)atomicAdd(&hist[bin], 1);}
}void histogram_cub(const int* data, int* hist, int data_size, int numBins) {void* d_temp_storage = nullptr;size_t temp_storage_bytes = 0;//注意numBins+1需要加1,因为numBins传进去是10,我们也确实有10个区间cub::DeviceHistogram::HistogramEven(d_temp_storage, temp_storage_bytes,data, hist,numBins+1, 0, numBins,data_size);cudaMalloc(&d_temp_storage, temp_storage_bytes);cub::DeviceHistogram::HistogramEven(d_temp_storage, temp_storage_bytes,data, hist,numBins+1, 0, numBins,data_size);cudaFree(d_temp_storage);
}void get_uniform_data(int* h_data, int data_size) {std::mt19937 gen(std::random_device{}()); // 梅森旋转算法引擎,种子来自硬件随机数std::uniform_int_distribution<int> dist(0, 9); // 均匀分布,范围 [0, 9]for (int i = 0; i < data_size; i++) {h_data[i] =dist(gen);}
}
void gen_hotspot_data(int* h_data, int data_size) {for (int i = 0; i < data_size; i++) {h_data[i] = 0; // 所有数据落到 bin=0}
}
void gen_skewed_data(int* h_data, int data_size) {std::mt19937 gen(std::random_device{}());std::uniform_int_distribution<int> dist(0, 9);for (int i = 0; i < data_size; i++) {int temp = dist(gen);if (temp  < 8) { // 80% 概率h_data[i] = 0; // 热点 bin}else {h_data[i] = temp;}}
}int main() {int h_data[DATA_SIZE];int* d_data;cudaMalloc(&d_data, DATA_SIZE * sizeof(int));int h_hist[10];int* d_hist;cudaMalloc(&d_hist, 10 * sizeof(int));cudaEvent_t start, stop;cudaEventCreate(&start);cudaEventCreate(&stop);float atomicTime, cubTime;//均匀分布memset(h_hist, 0, 10 * sizeof(int));cudaMemset(d_hist, 0, 10 * sizeof(int));get_uniform_data(h_data, DATA_SIZE);cudaMemcpy(d_data, h_data, DATA_SIZE *sizeof(int), cudaMemcpyHostToDevice);cudaEventRecord(start);histogram_atomic<<<10,10>>>(d_data, d_hist, DATA_SIZE, 10);cudaEventRecord(stop);cudaEventSynchronize(stop);cudaEventElapsedTime(&atomicTime, start, stop);cudaMemcpy(h_hist, d_hist, 10 * sizeof(int), cudaMemcpyDeviceToHost);for (int i = 0; i < 10; i++)std::cout << h_hist[i] << " ";std::cout << std::endl;memset(h_hist, 0, 10 * sizeof(int));cudaMemset(d_hist, 0, 10 * sizeof(int));cudaEventRecord(start);histogram_cub(d_data, d_hist, DATA_SIZE, 10);cudaEventRecord(stop);cudaEventSynchronize(stop);cudaEventElapsedTime(&cubTime, start, stop);cudaMemcpy(h_hist, d_hist, 10 * sizeof(int), cudaMemcpyDeviceToHost);for (int i = 0; i < 10; i++)std::cout << h_hist[i] << " ";std::cout << std::endl;std::cout << "对于平均分布 atomic的时间是:" << atomicTime << "ms cub的时间是:" << cubTime << "ms" << std::endl;memset(h_data, -1, DATA_SIZE * sizeof(int));//热点分布memset(h_hist, 0, 10 * sizeof(int));cudaMemset(d_hist, 0, 10 * sizeof(int));gen_hotspot_data(h_data, DATA_SIZE);cudaMemcpy(d_data, h_data, DATA_SIZE * sizeof(int), cudaMemcpyHostToDevice);cudaEventRecord(start);histogram_atomic << <10, 10 >> > (d_data, d_hist, DATA_SIZE, 10);cudaEventRecord(stop);cudaEventSynchronize(stop);cudaEventElapsedTime(&atomicTime, start, stop);cudaMemcpy(h_hist, d_hist, 10 * sizeof(int), cudaMemcpyDeviceToHost);for (int i = 0; i < 10; i++)std::cout << h_hist[i] << " ";std::cout << std::endl;memset(h_hist, 0, 10 * sizeof(int));cudaMemset(d_hist, 0, 10 * sizeof(int));cudaEventRecord(start);histogram_cub(d_data, d_hist, DATA_SIZE, 10);cudaEventRecord(stop);cudaEventSynchronize(stop);cudaEventElapsedTime(&cubTime, start, stop);cudaMemcpy(h_hist, d_hist, 10 * sizeof(int), cudaMemcpyDeviceToHost);for (int i = 0; i < 10; i++)std::cout << h_hist[i] << " ";std::cout << std::endl;std::cout << "对于热点分布 atomic的时间是:" << atomicTime << "ms cub的时间是:" << cubTime << "ms" << std::endl;cudaFree(d_data);cudaFree(d_hist);cudaEventDestroy(start);cudaEventDestroy(stop);
}
5 8 13 12 10 11 7 10 9 15
5 8 13 12 10 11 7 10 9 15
对于平均分布 atomic的时间是:4.31718ms cub的时间是:3.11603ms
100 0 0 0 0 0 0 0 0 0
100 0 0 0 0 0 0 0 0 0
对于热点分布 atomic的时间是:0.014336ms cub的时间是:0.206144ms

平均分布情况

  • 数据分布在 0~9 的各个 bin 上,atomicAdd 被 相对均匀地分布到各个线程

  • GPU 上的 atomicAdd 竞争较少,每个 block/warp 访问不同的 hist 元素

  • CUB 内部实现 先做 block-local hist,再全局归并,减少了全局 atomic 冲突

  • 因此对于平均分布,CUB 会比直接 global atomicAdd 略快(测试结果也是 atomic: 4.24ms,CUB: 3.22ms)


热点分布情况

  • 数据大部分都落在 bin 0,atomicAdd 全部线程都要写 hist[0]

  • 直接 global atomicAdd

    • 虽然冲突多,但因为数据量很小(100 个样本),atomicAdd 冲突不严重

    • GPU 的 kernel launch 和 CUB 的临时存储 malloc 等开销甚至比 atomicAdd 执行时间更大

    • 因此 atomicAdd 时间非常短(0.015ms)

  • CUB

    • 内部需要先分配 temp storage,block-local hist,最后全局归并

    • 即便数据量小,CUB 的 kernel launch、临时存储和 block 内 hist 合并操作开销明显

    • 因此对于小数据量或高度集中数据,CUB 会慢于直接 atomicAdd(你测试是 0.164ms)

尝试用warp对atomicAdd进行优化

首先是标准的warp规约代码

__inline__ __device__
int warpReduceSum(int val) {unsigned mask = __activemask();for (int offset = warpSize / 2; offset > 0; offset >>= 1) {val += __shfl_down_sync(mask, val, offset);}return val;
}
  • warp 大小始终是 32,永远不会变小。

  • 如果最后一组线程不足 32 个,CUDA 仍然分配一个 warp,但其中未使用的线程是 inactive,不会执行你的指令。

  • 当使用 __shfl_sync 等 warp 内通信时,如果包含了 inactive 线程,就必须通过 mask 来保证正确性。

__activemask() 函数

获取当前 warp 活跃线程掩码

然后通过共享内存+warp,对整体进行切割,计算局部再规约

__global__ void histogram_warp_shared(const int* data, int* hist, int data_size, int numBins) {extern __shared__ unsigned int sharedHist[];//通过<<<>>>里动态分配;动态分配必须加externint tid = blockDim.x * blockIdx.x + threadIdx.x;int lane = threadIdx.x % warpSize;int warpId = threadIdx.x / warpSize;//初始化,可能块内线程数不够numBins,所以用了循环for (int i = threadIdx.x; i < numBins; i += blockDim.x) {sharedHist[i] = 0;}__syncthreads();//warp内累加;localBin是本线程存储的hist数组unsigned int localBin[10];//这里设置numBins的大小,这里确定是10了
#pragma unrollfor (int i = 0; i < 10; i++) localBin[i] = 0;//遍历所有数据(可能线程总数不够data_size),处理本线程负责的区域for (int i = tid; i < data_size; i += gridDim.x * blockDim.x) {int d = data[i];if (d >= 0 && d < numBins) localBin[d]++;}//warp内累加
#pragma unrollfor (int i = 0; i < numBins; i++) {unsigned int val = warpReduceSum(localBin[i]);if (lane == 0) atomicAdd(&sharedHist[i], val);//只有lane 0会拿到最终的和}__syncthreads();//共享内存写回全局for (int i = threadIdx.x; i < numBins; i += blockDim.x) {atomicAdd(&hist[i], sharedHist[i]);}
}

调用

histogram_warp_shared<<<10,10 , 10 * sizeof(int) >>>(d_data, d_hist, DATA_SIZE, 10);

测试100个数据的平均分布的结果

10 16 8 12 6 9 14 8 7 10
10 16 8 12 6 9 14 8 7 10
10 16 8 12 6 9 14 8 7 10
对于平均分布 atomic的时间是:4.55786ms cub的时间是:4.25782ms warp的时间是:0.113664ms

最起码warp相比atomic还是加速了许多的

http://www.dtcms.com/a/334616.html

相关文章:

  • PMP-项目管理-十大知识领域:进度管理-制定时间表、优化活动顺序、控制进度
  • 进程替换:从 “改头换面” 到程序加载的底层逻辑
  • 【深度学习计算性能】05:多GPU训练
  • TypeScript快速入门
  • MCP 大模型的扩展坞
  • 洛谷P1595讲解(加强版)+错排讲解
  • php版的FormCreate使用注意事项
  • 基于单片机的防酒驾系统设计
  • NY243NY253美光固态闪存NY257NY260
  • 24. async await 原理是什么,会编译成什么
  • 惠普声卡驱动win10装机完成检测不到声卡
  • Three.js 材质系统深度解析
  • 云原生俱乐部-RH124知识点总结(1)
  • 【CV 目标检测】Fast RCNN模型①——与R-CNN区别
  • 解锁 AI 音乐魔法,三款音乐生成工具
  • 《P4180 [BJWC2010] 严格次小生成树》
  • 服务器配置开机自启动服务
  • 基于深度强化学习的多用途无人机路径优化研究
  • 软件需求管理过程详解
  • 缓存一致性协议(Cache Coherence Protocols)与 目录协议(Directory Protocols)简介
  • 二进制为什么使用记事本读取会出乱码
  • PHP域名授权系统网站源码_授权管理工单系统_精美UI_附教程
  • RK3568 NPU RKNN(一):概念理清
  • 从通用到专业:大模型训练的两条路与遗忘难题
  • 【原理】C# 字段、属性对比及其底层实现
  • 手机版碰一碰发视频系统批量剪辑功能开发,支持OEM贴牌
  • 编写和运行 Playbook
  • 31 HTB Union 机器 - 中等难度
  • Java设计模式之《工厂模式》
  • DAY12DAY13-新世纪DL(Deeplearning/深度学习)战士:破(改善神经网络)1