Почему при сокращении используется регистровая память медленнее, чем общая память?

#cuda

Вопрос:

Я оценил две производительности ядра:

 #include <chrono>
#include <cuda_runtime.h>
#include <stdio.h>

void initData_int(int *p, int size){
    for (int t=0; t<size; t  ){
        p[t] = (int)(rand()amp;0xff);
    }
}

__global__ void reduceShfl(int *in, int* out, int size)
{
    extern __shared__ int smem[];
    int tid = threadIdx.x;
    int idx = threadIdx.x   blockIdx.x*blockDim.x*4;
    smem[tid] = 0;
    if (tid>=size) return;
    int tmp = 0; 
    if (idx   blockDim.x*3 <= size){
        int a = in[idx];
        int b = in[idx blockDim.x];
        int c = in[idx 2*blockDim.x];
        int d = in[idx 3*blockDim.x];
        tmp = a   b   c   d;
    }
    smem[tid] = tmp;
    __syncthreads();

    if (blockDim.x >= 1024 amp;amp; tid < 512){
        smem[tid]  = smem[tid   512];
    }
    __syncthreads();
    if (blockDim.x >= 512 amp;amp; tid < 256){
        smem[tid]  = smem[tid   256];
    }
    __syncthreads();
    if (blockDim.x >= 256 amp;amp; tid < 128){
        smem[tid]  = smem[tid   128];
    }
    __syncthreads();
    if (blockDim.x >= 128 amp;amp; tid < 64){
        smem[tid]  = smem[tid   64];
    }
    __syncthreads();

    if (blockDim.x >= 64 amp;amp; tid < 32){
        smem[tid]  = smem[tid   32];
    }
    __syncthreads();
    
    int tmpsum = smem[tid]; 
    tmpsum  = __shfl_xor_sync(0xffffffff, tmpsum, 16);
    tmpsum  = __shfl_xor_sync(0xffffffff, tmpsum, 8);
    tmpsum  = __shfl_xor_sync(0xffffffff, tmpsum, 4);
    tmpsum  = __shfl_xor_sync(0xffffffff, tmpsum, 2);
    tmpsum  = __shfl_xor_sync(0xffffffff, tmpsum, 1);
    if (tid==0)
        out[blockIdx.x] = tmpsum;
}

__global__ void reduceShmUnroll(int *in, int *out, int num)
{
    extern __shared__ int smem[];
    int tid = threadIdx.x;
    int idx = threadIdx.x   blockIdx.x*blockDim.x*4;
    
    if (tid >= num) return;
    int tmp=0;
    if(idx   blockDim.x*3 <= num)
    {
        int a = in[idx];
        int b = in[idx   blockDim.x];
        int c = in[idx   blockDim.x*2];
        int d = in[idx   blockDim.x*3];
        tmp = a   b   c   d;
    }   
    smem[tid] = tmp;
    __syncthreads();

    if (blockDim.x >= 1024 amp;amp; tid < 512){
        smem[tid]  = smem[tid   512];
    }
    __syncthreads();
    if (blockDim.x >= 512 amp;amp; tid < 256){
        smem[tid]  = smem[tid 256];
    }
    __syncthreads();
    if (blockDim.x >= 256 amp;amp; tid < 128){
        smem[tid]  = smem[tid 128];
    }
    __syncthreads();
    if (blockDim.x >= 128 amp;amp; tid < 64){
        smem[tid]  = smem[tid 64];
    }
    __syncthreads();
    if (tid < 32){
        volatile int *vsmem = smem;
        vsmem[tid]  = vsmem[tid 32];
        vsmem[tid]  = vsmem[tid 16];
        vsmem[tid]  = vsmem[tid 8];
        vsmem[tid]  = vsmem[tid 4];
        vsmem[tid]  = vsmem[tid 2];
        vsmem[tid]  = vsmem[tid 1];
    }

    if (tid == 0) out[blockIdx.x] = smem[0];
}

int main(int agrc, char **argv)
{
    int size = 1<<24;
    int nBytes = size*sizeof(int);
    int *a_h = (int*)malloc(nBytes);
    initData_int(a_h, size);

    int blocksize = 1024;
    int gridsize = (size-1)/blocksize 1;
    dim3 block(blocksize, 1);
    dim3 grid((size-1)/blocksize 1, 1);
    int *a_d, *b_d;
    cudaMalloc((int**)amp;a_d, nBytes);
    cudaMalloc((int**)amp;b_d, grid.x*sizeof(int));
    cudaMemcpy(a_d, a_h, nBytes, cudaMemcpyHostToDevice);
    int *tmp = (int*)malloc(gridsize*sizeof(int));
    memset(tmp, 0, grid.x/4);
    cudaMemcpy(a_d, a_h, nBytes, cudaMemcpyHostToDevice);
    auto s_0 = std::chrono::system_clock::now();
    reduceShfl<<<grid, block, blocksize*sizeof(int)>>>(a_d, b_d, size);
    cudaMemcpy(tmp, b_d, grid.x/4*sizeof(int), cudaMemcpyDeviceToHost);
    cudaDeviceSynchronize();
    int res_1 = 0;
    for (int i=0; i<grid.x/4; i  ){
        res_1  = tmp[i];
    }
    auto e_0 = std::chrono::system_clock::now();
    std::chrono::duration<double> diff = e_0 - s_0;
    printf("Result from reduceShfl is: %d and time cost is /.n", res_1, diff.count());

    memset(tmp, 0, grid.x/4);
    cudaMemcpy(a_d, a_h, nBytes, cudaMemcpyHostToDevice);
    s_0 = std::chrono::system_clock::now();
    reduceShmUnroll<<<grid, block, blocksize*sizeof(int)>>>(a_d, b_d, size);
    cudaMemcpy(tmp, b_d, grid.x/4*sizeof(int), cudaMemcpyDeviceToHost);
    cudaDeviceSynchronize();
    int res_0 = 0;
    for (int i=0; i<grid.x/4; i  ){
        res_0  = tmp[i];
    }
    e_0 = std::chrono::system_clock::now();
    diff = e_0 - s_0;
    printf("Result from reduceShmUnroll is: %d and time cost is /.n", res_0, diff.count());

    cudaFree(a_d);
    cudaFree(b_d);
    free(a_h);
    free(tmp);
    return 0;
}
 

Основное различие заключается в последнем уменьшении деформации, reduceShmUnroll использовании общей памяти и reduceShfl выполнении перетасовки деформации, которая использует регистровую память.
Но я обнаружил, что reduceShfl это медленнее, чем reduceShmUnroll .

 Result from reduceShfl is: 2139353471 and time cost is 0.000533.
Result from reduceShmUnroll is: 2139353471 and time cost is 0.000485.
 

Что-то не так с моим кодом?

Комментарии:

1. Для более точного измерения времени выполнения ядра лучше использовать события CUDA . Кроме того, перед запуском измерений синхронизации ядра рекомендуется немного прогреть устройство. Первые вызовы ядра, выполняемые в новом контексте, могут выполняться немного медленнее, чем в противном случае. Даже в этом случае будет небольшая разница, поэтому вы можете посмотреть лучшее и среднее время 10 или более одинаковых вызовов ядра.

Ответ №1:

Что-то не так с моим кодом?

Да, я бы сказал, что с вашим кодом что-то не так.

Основная проблема, которую я вижу, заключается в том, что вы проводите неверное сравнение. В ядре общей памяти вы ограничиваете последнюю операцию по уменьшению деформации последней деформацией. В ядре shuffle вы не являетесь:

общее ядро mem:

 __syncthreads();
if (tid < 32){  // this is missing from your shuffle kernel
    volatile int *vsmem = smem;
    vsmem[tid]  = vsmem[tid 32];
    vsmem[tid]  = vsmem[tid 16];
    vsmem[tid]  = vsmem[tid 8];
    vsmem[tid]  = vsmem[tid 4];
    vsmem[tid]  = vsmem[tid 2];
    vsmem[tid]  = vsmem[tid 1];
}
 

перетасовать ядро:

 __syncthreads();

int tmpsum = smem[tid]; 
tmpsum  = __shfl_xor_sync(0xffffffff, tmpsum, 16);
tmpsum  = __shfl_xor_sync(0xffffffff, tmpsum, 8);
tmpsum  = __shfl_xor_sync(0xffffffff, tmpsum, 4);
tmpsum  = __shfl_xor_sync(0xffffffff, tmpsum, 2);
tmpsum  = __shfl_xor_sync(0xffffffff, tmpsum, 1);
if (tid==0)
    out[blockIdx.x] = tmpsum;
 

Когда я ограничиваю ваше ядро shuffle так же, как ограничено общее ядро mem (чтобы ненужные искажения не выполняли ненужную работу), я наблюдаю примерно одинаковое время выполнения (разница примерно в 1%) между двумя ядрами, когда я профилирую с nvprof на V100:

                 0.38%  222.76us         1  222.76us  222.76us  222.76us  reduceShmUnroll(int*, int*, int)
                0.37%  220.55us         1  220.55us  220.55us  220.55us  reduceShfl(int*, int*, int)
 

Именно этого я и ожидал. Для такого ограниченного использования нет оснований полагать, что совместное использование mem или перетасовка будут быстрее или медленнее.

Как активность в общей памяти, так и активность в случайном перемещении имеют ограничения по пропускной способности. Поэтому трудно предсказать, что будет быстрее, потому что это зависит от того, что еще происходит в вашем коде. Если ваш код привязан к пропускной способности общей памяти, и вы преобразуете часть этой активности в перетасовку с деформацией, вы, скорее всего, увидите выгоду от перетасовки с деформацией. То же самое утверждение можно сделать и в другом направлении. Для этого конкретного раздела этого конкретного кода, когда он написан правильно/сопоставимо/эквивалентно, вы не связаныпо-разному по пропускной способности общей памяти или по пропускной способности в случайном порядке, поэтому правильное ожидание не имеет разницы в производительности, заменяя одно другим.

Комментарии:

1. Я добавляю строку, которую вы рекомендовали. перетасовка деформации происходит быстрее, чем предыдущая. Но все равно медленнее, чем в общей памяти. Я считаю, что это определяется как оборудованием (2080 ti, которое я использую), так и действиями с кодом.

2. Спасибо, Роберт. Есть ли у вас соответствующие материалы, рекомендуемые для определения того, связано ли приложение пропускной способностью общей памяти или пропускной способностью в случайном порядке?

3. Научитесь пользоваться профилировщиками. Это не то, что можно осветить в комментариях. вы можете начать работу с профилировщиками здесь и здесь .