คัดลอกอาร์เรย์ได้เร็วขึ้นเมื่อใช้เธรดน้อยลงใน CUDA

ฉันทดสอบสองวิธีที่แตกต่างกันในการคัดลอกอาร์เรย์ 2D ในเคอร์เนล CUDA

อันแรกเปิดตัวบล็อกของเธรด TILE_DIM x TILE_DIM แต่ละบล็อกจะคัดลอกไทล์ของอาเรย์โดยกำหนดหนึ่งเธรดต่อองค์ประกอบ:

__global__ void simple_copy(float *outdata, const float *indata){

int x = blockIdx.x * TILE_DIM + threadIdx.x;
int y = blockIdx.y * TILE_DIM + threadIdx.y;

int width = gridDim.x * TILE_DIM;

outdata[y*width + x] = indata[y*width + x];

}

ส่วนที่สองนำมาจาก บล็อก NVIDIA คล้ายกับเคอร์เนลก่อนหน้า แต่ใช้เธรด TILE_DIM x BLOCK_ROWS ต่อบล็อก แต่ละเธรดจะวนซ้ำหลายองค์ประกอบของเมทริกซ์:

__global__ void fast_copy(float *outdata, const float *indata)
{
int x = blockIdx.x * TILE_DIM + threadIdx.x;
int y = blockIdx.y * TILE_DIM + threadIdx.y;
int width = gridDim.x * TILE_DIM;

for (int k = 0 ; k < TILE_DIM ; k += BLOCK_ROWS)
    outdata[(y+k)*width + x] = indata[(y+k)*width + x];
}

ฉันทำการทดสอบเพื่อเปรียบเทียบทั้งสองวิธีนี้ เคอร์เนลทั้งสองทำการเข้าถึงหน่วยความจำส่วนกลางร่วมกัน แต่อันที่สองดูเหมือนจะเร็วกว่าอย่างเห็นได้ชัด

ตัวสร้างโปรไฟล์ภาพ NVIDIA ยืนยันการทดสอบนี้

ดังนั้นเคอร์เนลตัวที่สองจะจัดการเพื่อให้ได้สำเนาที่เร็วขึ้นได้อย่างไร

นี่คือรหัสทั้งหมดที่ฉันใช้ทดสอบเมล็ด:

#include <stdio.h>
#include <stdlib.h>
#include <cuda.h>
#include <conio.h>

#define TILE_DIM 32
#define BLOCK_ROWS 8

/* KERNELS */

__global__ void simple_copy(float *outdata, const float *indata){

int x = blockIdx.x * TILE_DIM + threadIdx.x;
int y = blockIdx.y * TILE_DIM + threadIdx.y;

int width = gridDim.x * TILE_DIM;

outdata[y*width + x] = indata[y*width + x];

}
//###########################################################################

__global__ void fast_copy(float *outdata, const float *indata)
{
int x = blockIdx.x * TILE_DIM + threadIdx.x;
int y = blockIdx.y * TILE_DIM + threadIdx.y;
int width = gridDim.x * TILE_DIM;

for (int k = 0 ; k < TILE_DIM ; k += BLOCK_ROWS)
    outdata[(y+k)*width + x] = indata[(y+k)*width + x];
}
//###########################################################################

/* MAIN */

int main(){

float *indata,*dev_indata,*outdata1,*dev_outdata1,*outdata2,*dev_outdata2;
cudaEvent_t start, stop;
float time1,time2;
int i,j,k;

int n_iter = 100;

int N = 2048;

cudaEventCreate(&start);
cudaEventCreate(&stop);


dim3 grid(N/TILE_DIM, N/TILE_DIM);
dim3 threads1(TILE_DIM,TILE_DIM);
dim3 threads2(TILE_DIM,BLOCK_ROWS);

// Allocations

indata = (float *)malloc(N*N*sizeof(float));
outdata1 = (float *)malloc(N*N*sizeof(float));
outdata2 = (float *)malloc(N*N*sizeof(float));

cudaMalloc( (void**)&dev_indata,N*N*sizeof(float) );
cudaMalloc( (void**)&dev_outdata1,N*N*sizeof(float) );
cudaMalloc( (void**)&dev_outdata2,N*N*sizeof(float) );

// Initialisation

for(j=0 ; j<N ; j++){
        for(i=0 ; i<N ; i++){
            indata[i + N*j] = i + N*j;
        }
}

// Transfer to Device
cudaMemcpy( dev_indata, indata, N*N*sizeof(float),cudaMemcpyHostToDevice );

// Simple copy
cudaEventRecord( start, 0 );
for(k=0 ; k<n_iter ; k++){
    simple_copy<<<grid, threads1>>>(dev_outdata1,dev_indata);
}
cudaEventRecord( stop, 0 );

cudaEventSynchronize( stop );
cudaEventElapsedTime( &time1, start, stop );
printf("Elapsed time with simple copy: %f\n",time1);

// Fast copy
cudaEventRecord( start, 0 );
for(k=0 ; k<n_iter ; k++){
    fast_copy<<<grid, threads2>>>(dev_outdata2,dev_indata);
}
cudaEventRecord( stop, 0 );

cudaEventSynchronize( stop );
cudaEventElapsedTime( &time2, start, stop );
printf("Elapsed time with fast copy: %f\n",time2);

// Transfer to Host

cudaMemcpy( outdata1, dev_outdata1, N*N*sizeof(float),cudaMemcpyDeviceToHost );
cudaMemcpy( outdata2, dev_outdata2, N*N*sizeof(float),cudaMemcpyDeviceToHost );

// Check for error
float error = 0;
for(j=0 ; j<N ; j++){
        for(i=0 ; i<N ; i++){
            error += outdata1[i + N*j] - outdata2[i + N*j];
        }
}
printf("error: %f\n",error);

/*// Print the copied matrix
printf("Copy\n");
for(j=0 ; j<N ; j++){
        for(i=0 ; i<N ; i++){
            printf("%f\t",outdata1[i + N*j]);
        }
        printf("\n");
}*/

cudaEventDestroy( start );
cudaEventDestroy( stop );

free(indata);
free(outdata1);
free(outdata2);

cudaFree(dev_indata);
cudaFree(dev_outdata1);
cudaFree(dev_outdata2);

cudaDeviceReset();

getch();

return 0;
 }

//###########################################################################

person Tatore    schedule 27.09.2013    source แหล่งที่มา


คำตอบ (2)


การเปิดเธรดต้องใช้เวลา GPU บ้าง เธรดที่น้อยลงและการทำงานต่อเธรดที่มากขึ้นหมายถึงค่าใช้จ่ายในการเปิดเธรดที่น้อยลง นั่นเป็นสาเหตุที่ fast_copy() เร็วขึ้น

แต่แน่นอนว่าคุณยังต้องมีเธรดและบล็อกจำนวนเพียงพอเพื่อใช้งาน GPU ได้อย่างเต็มที่

ในความเป็นจริงบล็อกต่อไปนี้ขยายแนวคิดนี้เพิ่มเติม ใช้จำนวนบล็อก/เธรดคงที่ในการทำงานกับขนาดที่กำหนดเองโดยใช้ลูป Grid-stride มีการกล่าวถึงข้อดีหลายประการของวิธีนี้

https://developer.nvidia.com/content/cuda-pro-tip-write-flexible-kernels-grid-stride-loops

person kangshiyin    schedule 27.09.2013

ฉันคิดว่าคุณจะพบคำตอบโดยการเปรียบเทียบไมโครโค้ดของเมล็ดทั้งสอง

เมื่อฉันคอมไพล์เคอร์เนลเหล่านี้สำหรับ SM 3.0 คอมไพเลอร์จะคลี่ลูปในเคอร์เนลตัวที่สองโดยสมบูรณ์ (เนื่องจากรู้ว่ามันจะวนซ้ำ 4x) นั่นอาจอธิบายความแตกต่างด้านประสิทธิภาพได้ - ฮาร์ดแวร์ CUDA สามารถใช้รีจิสเตอร์เพื่อครอบคลุมเวลาแฝงของหน่วยความจำตลอดจนเวลาแฝงของคำสั่ง Vasily Volkov ได้นำเสนอที่ยอดเยี่ยม "ประสิทธิภาพที่ดีขึ้นเมื่อมีอัตราการเข้าพักต่ำ" ในหัวข้อเมื่อสองสามปีที่แล้ว (http://www.cs.berkeley.edu/~volkov/volkov10-GTC.pdf)

person ArchaeaSoftware    schedule 27.09.2013
comment
ผลลัพธ์การทำโปรไฟล์แสดงว่าการปิดใช้งานการคลายลูปไม่ได้ทำให้ fast_copy() ช้าลง แต่ฉันคิดว่าสไลด์ที่คุณให้มานั้นให้คำอธิบายที่ถูกต้องสำหรับคำถามนี้ - person kangshiyin; 29.09.2013