我对Cuda相对较新,我正在尝试编写一个内核,用于计算查询向量和大型向量数据库之间的绝对差值之和。两者的元素必须是8位无符号整数。我的内核是基于nvidias样本并行缩减内核的,我也读过这个thread。
我只得到大约5GB / s,这比快速CPU好多了,甚至没有接近我的DDR5 GT640的理论带宽大约80GB / s。
我的数据集包含1024字节的查询向量,100,000 x 1024字节的数据库
我有100,000个128个线程的块,如果每个块访问相同的1024字节query_vector,那会不会导致性能下降?由于每个块都访问相同的内存位置。
blockSize和共享内存都设置为128和128 * sizeof(int),128是#define'd为THREADS_PER_BLOCK
template<UINT blockSize> __global__ void reduction_sum_abs( BYTE* query_vector, BYTE* db_vector, uint32_t* result )
{
extern __shared__ UINT sum[];
UINT db_linear_index = (blockIdx.y*gridDim.x) + blockIdx.x ;
UINT i = threadIdx.x;
sum[threadIdx.x] = 0;
int* p_q_int = reinterpret_cast<int*>(query_vector);
int* p_db_int = reinterpret_cast<int*>(db_vector);
while( i < VECTOR_SIZE/4 ) {
/* memory transaction */
int q_int = p_q_int[i];
int db_int = p_db_int[db_linear_index*VECTOR_SIZE/4 + i];
uchar4 a0 = *reinterpret_cast<uchar4*>(&q_int);
uchar4 b0 = *reinterpret_cast<uchar4*>(&db_int);
/* sum of absolute difference */
sum[threadIdx.x] += abs( (int)a0.x - b0.x );
sum[threadIdx.x] += abs( (int)a0.y - b0.y );
sum[threadIdx.x] += abs( (int)a0.z - b0.z );
sum[threadIdx.x] += abs( (int)a0.w - b0.w );
i += THREADS_PER_BLOCK;
}
__syncthreads();
if ( blockSize >= 128 ) {
if ( threadIdx.x < 64 ) {
sum[threadIdx.x] += sum[threadIdx.x + 64];
}
}
/* reduce the final warp */
if ( threadIdx.x < 32 ) {
if ( blockSize >= 64 ) { sum[threadIdx.x] += sum[threadIdx.x + 32]; } __syncthreads();
if ( blockSize >= 32 ) { sum[threadIdx.x] += sum[threadIdx.x + 16]; } __syncthreads();
if ( blockSize >= 16 ) { sum[threadIdx.x] += sum[threadIdx.x + 8 ]; } __syncthreads();
if ( blockSize >= 8 ) { sum[threadIdx.x] += sum[threadIdx.x + 4 ]; } __syncthreads();
if ( blockSize >= 4 ) { sum[threadIdx.x] += sum[threadIdx.x + 2 ]; } __syncthreads();
if ( blockSize >= 2 ) { sum[threadIdx.x] += sum[threadIdx.x + 1 ]; } __syncthreads();
}
/* copy the sum back to global */
if ( threadIdx.x == 0 ) {
result[db_linear_index] = sum[0];
}
}
如果我使用4行代码运行内核并将实际的绝对差值计算注释掉,我可以得到大约4倍的带宽,显然它会导致错误的答案,但我相信至少有很大一部分时间在那里度过。
我是否有可能像访问字节一样创建银行冲突?如果可以,我可以避免冲突吗?
我对reinterpret_cast
的使用是否正确?
有没有更好的方法进行8位无符号计算?
还有哪些(我会假设很多,因为我是一个完整的新手)可以进行优化吗?
由于
修改
我的机器规格如下:
Windows XP 2002 SP3
intel 6600 2.40GHz
2GB ram
GT640 GDDR5 1gb
visual c ++ 2010 express
答案 0 :(得分:7)
这类问题的良好做法是提供某人可以编译和运行的完整代码,而无需添加任何内容或更改任何内容。一般来说,SO期望this。由于您的问题也与性能有关,因此您还应在完整的代码中包含实际的时序测量方法。
修复错误:
您的代码中至少有2个错误,其中一个错误已经指出@Jez。在此之后&#34;部分减少&#34;步骤:
if ( blockSize >= 128 ) {
if ( threadIdx.x < 64 ) {
sum[threadIdx.x] += sum[threadIdx.x + 64];
}
}
我们需要__syncthreads();
才能继续进行余下的工作。通过上述更改,我能够让您的内核生成与我的天真主机实现相匹配的可重复结果。此外,由于你有像这样的条件代码,它不会在整个threadblock中评估相同的代码:
if ( threadIdx.x < 32 ) {
it is illegal在条件代码块中包含__syncthreads()
语句:
if ( blockSize >= 64 ) { sum[threadIdx.x] += sum[threadIdx.x + 32]; } __syncthreads();
(同样对于后续行做同样的事情)。所以建议修复它。有几种方法可以解决这个问题,其中一种方法是切换到使用volatile
类型指针来引用共享数据。由于我们现在在warp中运行,volatile
限定符会强制编译器执行我们想要的操作:
volatile UINT *vsum = sum;
if ( threadIdx.x < 32 ) {
if ( blockSize >= 64 ) vsum[threadIdx.x] += vsum[threadIdx.x + 32];
if ( blockSize >= 32 ) vsum[threadIdx.x] += vsum[threadIdx.x + 16];
if ( blockSize >= 16 ) vsum[threadIdx.x] += vsum[threadIdx.x + 8 ];
if ( blockSize >= 8 ) vsum[threadIdx.x] += vsum[threadIdx.x + 4 ];
if ( blockSize >= 4 ) vsum[threadIdx.x] += vsum[threadIdx.x + 2 ];
if ( blockSize >= 2 ) vsum[threadIdx.x] += vsum[threadIdx.x + 1 ];
}
CUDA parallel reduction sample code和associated pdf对您来说可能是一个很好的评论。
时间/完整分析:
我碰巧有一台GT 640,cc3.5设备。当我在其上运行bandwidthTest
时,我观察到大约32GB / s的设备到设备传输。当设备内核访问设备存储器时,该数字代表可实现带宽的合理近似上限。此外,当我添加基于cudaEvent
的时序并围绕您显示的示例代码构建示例代码时,使用模拟数据,我观察到吞吐量约为16GB / s,而不是5GB / s。因此,您的实际测量技术在这里将是有用的信息(实际上,可能需要一个完整的代码来分析我的内核时序和时间之间的差异。)
问题仍然存在,是否可以改进? (假设~32GB / s是近似上限)。
您的问题:
我是否有可能以访问字节的方式创建银行冲突?如果可以,我可以避免冲突?
由于你的内核实际上有效地将字节加载为32位数量(uchar4
),并且每个线程正在加载相邻的连续32位数量,我不相信有任何银行 - 内核的冲突访问问题。
我对reinterpret_cast的使用是否正确?
是的,它似乎是正确的(我的示例代码,上面提到的修复,验证内核产生的结果与天真的主机函数实现相匹配。)
有没有更好的方法进行8位无符号计算?
在这种情况下,正如@njuffa所指出的那样,SIMD intrinsics可以处理这个,事实证明,只需一条指令(__vsadu4()
,请参阅下面的示例代码)
还有什么其他的(我会假设很多,因为我是一个完整的新手)我可以做出优化吗?
使用@MichalHosala提出的cc3.0 warp-shuffle减少方法
利用SIMD instrinsic __vsadu4()
来简化和改进@njuffa提出的字节数量处理。
将数据库矢量数据重新组织为列主存储。这允许我们省去普通的并行缩减方法(即使如第1项中所述)并切换到直接的for循环读取内核,一个线程计算整个矢量比较。这允许我们的内核在这种情况下达到设备的大约内存带宽(cc3.5 GT640)。
以下是代码和示例运行,显示了3个实现:您的原始实现(加上上面命名的&#34;修复&#34;以使其产生正确的结果),一个opt1内核修改您的包含项目上面列表中的1和2,以及使用上面列表中的2和3替换您的方法的opt2内核。根据我的测量,你的内核达到16GB / s,大约是GT640带宽的一半,opt1内核以大约24GB / s的速度运行(增加的内容大致相当于上面的第1和第2项),以及opt2内核通过数据重组,以大约全带宽(36GB / s)运行。
$ cat t574.cu
#include <stdio.h>
#include <stdlib.h>
#define THREADS_PER_BLOCK 128
#define VECTOR_SIZE 1024
#define NUM_DB_VEC 100000
typedef unsigned char BYTE;
typedef unsigned int UINT;
typedef unsigned int uint32_t;
template<UINT blockSize> __global__ void reduction_sum_abs( BYTE* query_vector, BYTE* db_vector, uint32_t* result )
{
extern __shared__ UINT sum[];
UINT db_linear_index = (blockIdx.y*gridDim.x) + blockIdx.x ;
UINT i = threadIdx.x;
sum[threadIdx.x] = 0;
int* p_q_int = reinterpret_cast<int*>(query_vector);
int* p_db_int = reinterpret_cast<int*>(db_vector);
while( i < VECTOR_SIZE/4 ) {
/* memory transaction */
int q_int = p_q_int[i];
int db_int = p_db_int[db_linear_index*VECTOR_SIZE/4 + i];
uchar4 a0 = *reinterpret_cast<uchar4*>(&q_int);
uchar4 b0 = *reinterpret_cast<uchar4*>(&db_int);
/* sum of absolute difference */
sum[threadIdx.x] += abs( (int)a0.x - b0.x );
sum[threadIdx.x] += abs( (int)a0.y - b0.y );
sum[threadIdx.x] += abs( (int)a0.z - b0.z );
sum[threadIdx.x] += abs( (int)a0.w - b0.w );
i += THREADS_PER_BLOCK;
}
__syncthreads();
if ( blockSize >= 128 ) {
if ( threadIdx.x < 64 ) {
sum[threadIdx.x] += sum[threadIdx.x + 64];
}
}
__syncthreads(); // **
/* reduce the final warp */
if ( threadIdx.x < 32 ) {
if ( blockSize >= 64 ) { sum[threadIdx.x] += sum[threadIdx.x + 32]; } __syncthreads();
if ( blockSize >= 32 ) { sum[threadIdx.x] += sum[threadIdx.x + 16]; } __syncthreads();
if ( blockSize >= 16 ) { sum[threadIdx.x] += sum[threadIdx.x + 8 ]; } __syncthreads();
if ( blockSize >= 8 ) { sum[threadIdx.x] += sum[threadIdx.x + 4 ]; } __syncthreads();
if ( blockSize >= 4 ) { sum[threadIdx.x] += sum[threadIdx.x + 2 ]; } __syncthreads();
if ( blockSize >= 2 ) { sum[threadIdx.x] += sum[threadIdx.x + 1 ]; } __syncthreads();
}
/* copy the sum back to global */
if ( threadIdx.x == 0 ) {
result[db_linear_index] = sum[0];
}
}
__global__ void reduction_sum_abs_opt1( BYTE* query_vector, BYTE* db_vector, uint32_t* result )
{
__shared__ UINT sum[THREADS_PER_BLOCK];
UINT db_linear_index = (blockIdx.y*gridDim.x) + blockIdx.x ;
UINT i = threadIdx.x;
sum[threadIdx.x] = 0;
UINT* p_q_int = reinterpret_cast<UINT*>(query_vector);
UINT* p_db_int = reinterpret_cast<UINT*>(db_vector);
while( i < VECTOR_SIZE/4 ) {
/* memory transaction */
UINT q_int = p_q_int[i];
UINT db_int = p_db_int[db_linear_index*VECTOR_SIZE/4 + i];
sum[threadIdx.x] += __vsadu4(q_int, db_int);
i += THREADS_PER_BLOCK;
}
__syncthreads();
// this reduction assumes THREADS_PER_BLOCK = 128
if (threadIdx.x < 64) sum[threadIdx.x] += sum[threadIdx.x+64];
__syncthreads();
if ( threadIdx.x < 32 ) {
unsigned localSum = sum[threadIdx.x] + sum[threadIdx.x + 32];
for (int i = 16; i >= 1; i /= 2)
localSum = localSum + __shfl_xor(localSum, i);
if (threadIdx.x == 0) result[db_linear_index] = localSum;
}
}
__global__ void reduction_sum_abs_opt2( BYTE* query_vector, UINT* db_vector_cm, uint32_t* result)
{
__shared__ UINT qv[VECTOR_SIZE/4];
if (threadIdx.x < VECTOR_SIZE/4) qv[threadIdx.x] = *(reinterpret_cast<UINT *>(query_vector) + threadIdx.x);
__syncthreads();
int idx = threadIdx.x + blockDim.x*blockIdx.x;
while (idx < NUM_DB_VEC){
UINT sum = 0;
for (int i = 0; i < VECTOR_SIZE/4; i++)
sum += __vsadu4(qv[i], db_vector_cm[(i*NUM_DB_VEC)+idx]);
result[idx] = sum;
idx += gridDim.x*blockDim.x;}
}
unsigned long compute_host_result(BYTE *qvec, BYTE *db_vec){
unsigned long temp = 0;
for (int i =0; i < NUM_DB_VEC; i++)
for (int j = 0; j < VECTOR_SIZE; j++)
temp += (unsigned long) abs((int)qvec[j] - (int)db_vec[(i*VECTOR_SIZE)+j]);
return temp;
}
int main(){
float et;
cudaEvent_t start, stop;
BYTE *h_qvec, *d_qvec, *h_db_vec, *d_db_vec;
uint32_t *h_res, *d_res;
h_qvec = (BYTE *)malloc(VECTOR_SIZE*sizeof(BYTE));
h_db_vec = (BYTE *)malloc(VECTOR_SIZE*NUM_DB_VEC*sizeof(BYTE));
h_res = (uint32_t *)malloc(NUM_DB_VEC*sizeof(uint32_t));
for (int i = 0; i < VECTOR_SIZE; i++){
h_qvec[i] = rand()%256;
for (int j = 0; j < NUM_DB_VEC; j++) h_db_vec[(j*VECTOR_SIZE)+i] = rand()%256;}
cudaMalloc(&d_qvec, VECTOR_SIZE*sizeof(BYTE));
cudaMalloc(&d_db_vec, VECTOR_SIZE*NUM_DB_VEC*sizeof(BYTE));
cudaMalloc(&d_res, NUM_DB_VEC*sizeof(uint32_t));
cudaMemcpy(d_qvec, h_qvec, VECTOR_SIZE*sizeof(BYTE), cudaMemcpyHostToDevice);
cudaMemcpy(d_db_vec, h_db_vec, VECTOR_SIZE*NUM_DB_VEC*sizeof(BYTE), cudaMemcpyHostToDevice);
cudaEventCreate(&start); cudaEventCreate(&stop);
// initial run
cudaMemset(d_res, 0, NUM_DB_VEC*sizeof(uint32_t));
cudaEventRecord(start);
reduction_sum_abs<THREADS_PER_BLOCK><<<NUM_DB_VEC, THREADS_PER_BLOCK, THREADS_PER_BLOCK*sizeof(int)>>>(d_qvec, d_db_vec, d_res);
cudaEventRecord(stop);
cudaDeviceSynchronize();
cudaEventSynchronize(stop);
cudaEventElapsedTime(&et, start, stop);
cudaMemcpy(h_res, d_res, NUM_DB_VEC*sizeof(uint32_t), cudaMemcpyDeviceToHost);
unsigned long h_result = 0;
for (int i = 0; i < NUM_DB_VEC; i++) h_result += h_res[i];
printf("1: et: %.2fms, bw: %.2fGB/s\n", et, (NUM_DB_VEC*VECTOR_SIZE)/(et*1000000));
if (h_result == compute_host_result(h_qvec, h_db_vec)) printf("Success!\n");
else printf("1: mismatch!\n");
// optimized kernel 1
cudaMemset(d_res, 0, NUM_DB_VEC*sizeof(uint32_t));
cudaEventRecord(start);
reduction_sum_abs_opt1<<<NUM_DB_VEC, THREADS_PER_BLOCK>>>(d_qvec, d_db_vec, d_res);
cudaEventRecord(stop);
cudaDeviceSynchronize();
cudaEventSynchronize(stop);
cudaEventElapsedTime(&et, start, stop);
cudaMemcpy(h_res, d_res, NUM_DB_VEC*sizeof(uint32_t), cudaMemcpyDeviceToHost);
h_result = 0;
for (int i = 0; i < NUM_DB_VEC; i++) h_result += h_res[i];
printf("2: et: %.2fms, bw: %.2fGB/s\n", et, (NUM_DB_VEC*VECTOR_SIZE)/(et*1000000));
if(h_result == compute_host_result(h_qvec, h_db_vec)) printf("Success!\n");
else printf("2: mismatch!\n");
// convert db_vec to column-major storage for optimized kernel 2
UINT *h_db_vec_cm, *d_db_vec_cm;
h_db_vec_cm = (UINT *)malloc(NUM_DB_VEC*(VECTOR_SIZE/4)*sizeof(UINT));
cudaMalloc(&d_db_vec_cm, NUM_DB_VEC*(VECTOR_SIZE/4)*sizeof(UINT));
for (int i = 0; i < NUM_DB_VEC; i++)
for (int j = 0; j < VECTOR_SIZE/4; j++)
h_db_vec_cm[(j*NUM_DB_VEC)+i] = *(reinterpret_cast<UINT *>(h_db_vec + (i*VECTOR_SIZE))+j);
cudaMemcpy(d_db_vec_cm, h_db_vec_cm, NUM_DB_VEC*(VECTOR_SIZE/4)*sizeof(UINT), cudaMemcpyHostToDevice);
cudaMemset(d_res, 0, NUM_DB_VEC*sizeof(uint32_t));
cudaEventRecord(start);
reduction_sum_abs_opt2<<<64, 512>>>(d_qvec, d_db_vec_cm, d_res);
cudaEventRecord(stop);
cudaDeviceSynchronize();
cudaEventSynchronize(stop);
cudaEventElapsedTime(&et, start, stop);
cudaMemcpy(h_res, d_res, NUM_DB_VEC*sizeof(uint32_t), cudaMemcpyDeviceToHost);
h_result = 0;
for (int i = 0; i < NUM_DB_VEC; i++) h_result += h_res[i];
printf("3: et: %.2fms, bw: %.2fGB/s\n", et, (NUM_DB_VEC*VECTOR_SIZE)/(et*1000000));
if(h_result == compute_host_result(h_qvec, h_db_vec)) printf("Success!\n");
else printf("3: mismatch!\n");
return 0;
}
$ nvcc -O3 -arch=sm_35 -o t574 t574.cu
$ ./run35 t574
1: et: 6.34ms, bw: 16.14GB/s
Success!
2: et: 4.16ms, bw: 24.61GB/s
Success!
3: et: 2.83ms, bw: 36.19GB/s
Success!
$
一些注意事项:
答案 1 :(得分:1)
有一件事立即引起我的注意:
if ( blockSize >= 128 ) {
if ( threadIdx.x < 64 ) {
sum[threadIdx.x] += sum[threadIdx.x + 64];
}
}
第一个条件在任何地方都是正确的,而第二个条件仅在前两个经线中。因此,您可以将订单切换为:
if ( threadIdx.x < 64 ) {
if ( blockSize >= 128 ) {
sum[threadIdx.x] += sum[threadIdx.x + 64];
}
}
这将允许除前两个之外的所有warp更快完成执行。
接下来的事情 - 您可以使用__shfl_xor
指令非常显着地降低warp级别:
/* reduce the final warp */
if ( threadIdx.x < 32 ) {
auto localSum = sum[threadIdx.x] + sum[threadIdx.x + 32]);
for (auto i = 16; i >= 1; i /= 2)
{
localSum = localSum + __shfl_xor(localSum, i);
}
if (threadIdx.x == 0) result[db_linear_index] = localSum;
}
我不是说它就是这样,你的代码没有问题,但这些是我能够很容易发现的问题。我甚至没有使用我的解决方案测试性能,但我相信它应该会改进。
修改强> 您似乎也不必要四次写入共享内存:
/* sum of absolute difference */
sum[threadIdx.x] += abs( (int)a0.x - b0.x );
sum[threadIdx.x] += abs( (int)a0.y - b0.y );
sum[threadIdx.x] += abs( (int)a0.z - b0.z );
sum[threadIdx.x] += abs( (int)a0.w - b0.w );
为什么不简单地执行以下操作?
/* sum of absolute difference */
sum[threadIdx.x] += abs( (int)a0.x - b0.x )
+ abs( (int)a0.y - b0.y )
+ abs( (int)a0.z - b0.z );
+ abs( (int)a0.w - b0.w );