这类问题的最佳做法是提供完整的代码,使得其他人可以直接编译和运行,而无需添加或更改任何内容。一般来说,SO期望您提供完整的
this。由于您的问题还涉及性能问题,因此您还应在完整的代码中包含实际的计时测量方法。
修复错误:
您的代码中至少有两个错误,其中@Jez已经指出了一个。在进行“部分约简”之后,还有另一个错误:
if ( blockSize >= 128 ) {
if ( threadIdx.x < 64 ) {
sum[threadIdx.x] += sum[threadIdx.x + 64];
}
}
我们需要在继续进行之前加上
__syncthreads();
。通过以上更改,我能够让您的内核生成可重复的结果,与我的 naive host 实现相匹配。另外,由于您有像这样不在线程块内评估的条件代码:
if ( threadIdx.x < 32 ) {
在条件代码块中使用 __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 并行规约样例代码 和 相关pdf 可能是您的好复习资料。
时间/性能分析:
我恰好有一台GT 640,cc3.5设备。 当我在它上面运行bandwidthTest
时,我观察到设备间传输大约为32GB/s。 这个数字代表了当设备内核访问设备内存时可实现带宽的合理近似上限。 此外,当我添加基于cudaEvent
的计时并围绕您所展示的内容构建一个样例代码,并使用模拟数据,我观察到吞吐量约为16GB/s,而不是5GB/s。 因此,您的实际测量技术在这里将是有用的信息(事实上,完整的代码可能是分析您的内核计时和您的计时之间差异所需的)。
问题仍然存在,那么它可以改进吗? (假设~32GB/s是近似的上限)。
你的问题:
我访问字节的方式会不会造成银行冲突?如果是,我可以避免冲突吗?
由于您的内核实际上将字节有效地加载为32位数量(uchar4
),并且每个线程正在加载相邻、连续的32位数量,因此我认为您的内核没有任何银行冲突访问问题。
我的reinterpret_cast用法正确吗?
是的,它看起来是正确的(下面是我的示例代码,经过上述修复,验证了您的内核生成的结果与朴素的主机函数实现相匹配)。
是否有更好的方法进行8位无符号计算?
有的,正如@njuffa所指出的那样,在这种情况下,SIMD intrinsic可以处理这个问题,事实证明,只需要单个指令(__vsadu4()
, 请参见下面的示例代码)。
还有哪些优化方法可以使用?(我想会有很多,因为我是一个完全的新手)
使用@MichalHosala提出的cc3.0 warp-shuffle减少方法。
使用SIMD内置函数__vsadu4()
来简化和改善对字节数量的处理,由@njuffa提出。
重新组织数据库向量数据以列主要存储。这使我们可以放弃普通的并行约简方法(即1中提到的方法),转而使用直接的for-loop读取内核,一个线程计算整个向量比较。在这种情况下(cc3.5 GT640),这使得我们的内核达到了设备的大约内存带宽。
这里是代码和样例运行,展示了3种实现方式:你的原始实现(加上上述“修复”,以获得正确的结果),opt1内核修改了你的实现,包括上述列表中的1和2项,而opt2内核则使用了上述列表中的2和3项。根据我的测量,你的内核达到了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 ) {
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[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();
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();
}
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 ) {
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();
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);
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");
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");
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!
$
一些注意事项:
- 上述代码,特别是您的内核,必须编译为cc3.0或更高版本,以我设置测试用例的方式。这是因为我在一个单一的1D网格中创建了100,000个块,因此我们不能直接在cc2.0设备上运行此代码。
- 对于opt2内核,在不同的设备上运行时,可以通过修改网格和块参数进行微调。我将它们设置为64和512,这些值不应该是关键因素(尽管块应该是VECTOR_SIZE/4线程或更多),因为算法使用网格遍历循环来覆盖整个向量集。GT640只有2个SM,因此在这种情况下,64个线程块足以让设备保持繁忙(甚至32个也可以)。您可能需要修改这些参数以在更大的设备上获得最佳性能。
BYTE
是如何定义的? - Michal Hosala