什么是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),吞吐量会严重下降。
-
全局内存原子操作延迟大。
-
cpp
__global__ void histogram_atomic(const int *data,int *hist,int data_size,int numBins){
//data是原数据,hist是直方图频率数组,data_size的数据大小,numBins是bin的个数;这里对10取余,所以numBins传10
int tid = blockDim.x * blockIdx.x + threadIdx.x;//为了简单,只用一维的block和thread
if (tid < data_size) {
int bin = data[tid] % numBins;//计算本线程是数据属于哪个bin
atomicAdd(&hist[bin], 1);//原子加
}
}
CUB 方法
HistogramEven --- 等宽区间直方图(单通道)
cpp
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)
分为等宽区间,并对每个样本执行:
cpp
bin = floor((sample - lower_level) * (num_levels-1) / (upper_level - lower_level))
注意:若类型为整数,则进行整数除法处
cpp
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
-
num_levels = numBins + 1 = 11
-
这个参数指示 分割点数量,不是 bin 数量。
-
分割点个数 = bin 数 + 1,所以实际生成 10 个 bin。
-
-
min = 0, max = numBins = 10
- 数据落在
[0, 10)
区间,最大值 10 是上界,不包含 10 本身。
- 数据落在
-
区间计算公式(每个 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 | 空或部分数据落入? |
对比测试:
cpp
#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);
}
cpp
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规约代码
cpp
__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,对整体进行切割,计算局部再规约
cpp
__global__ void histogram_warp_shared(const int* data, int* hist, int data_size, int numBins) {
extern __shared__ unsigned int sharedHist[];//通过<<<>>>里动态分配;动态分配必须加extern
int 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 unroll
for (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 unroll
for (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]);
}
}
调用
cpp
histogram_warp_shared<<<10,10 , 10 * sizeof(int) >>>(d_data, d_hist, DATA_SIZE, 10);
测试100个数据的平均分布的结果
cpp
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还是加速了许多的