cuda 프로그래밍 학습 - CUDA 공유 메모리 성능 최적화 (9)

머리말

참조:

Gao Sheng의 블로그
"CUDA C 프로그래밍 권위 있는 가이드"
및 CUDA 공식 문서
CUDA 프로그래밍: 기본 및 실습 Fan Zheyong

기사의 모든 코드는 내 GitHub에서 사용할 수 있으며 향후 천천히 업데이트됩니다.

기사와 설명영상 동시 공개 'AI 지식이야기' B국 : 밥 삼공기 먹으러 나간다

1: 공유 메모리

공유 메모리는 프로그래머가 직접 조작할 수 있는 일종의 캐시로 두 가지 주요 기능이 있습니다: (1) 커널 기능에서 전역 메모리
에 대한 액세스 수를 줄이고 스레드 블록의 효율적인 내부 통신을 실현하는 것입니다.
하나는 전역 메모리 액세스의 통합을 개선하는 것입니다.

다음은
배열에 있는 모든 요소의 합을 계산해야 하는 경우 C++ An array x with N elements, 즉
sum = x[0] + x[1] + ... + x[로 작성된 축소 계산입니다. 엔-1]

#include<stdint.h>
#include<cuda.h>
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <math.h>
#include <stdio.h>

#define CHECK(call)                                   \
do                                                    \
{
      
                                                           \
    const cudaError_t error_code = call;              \
    if (error_code != cudaSuccess)                    \
    {
      
                                                       \
        printf("CUDA Error:\n");                      \
        printf("    File:       %s\n", __FILE__);     \
        printf("    Line:       %d\n", __LINE__);     \
        printf("    Error code: %d\n", error_code);   \
        printf("    Error text: %s\n",                \
            cudaGetErrorString(error_code));          \
        exit(1);                                      \
    }                                                 \
} while (0)



#ifdef USE_DP
typedef double real;
#else
typedef float real;
#endif

const int NUM_REPEATS = 20;
void timing(const real* x, const int N);
real reduce(const real* x, const int N);

int main(void)
{
    
    
    const int N = 100000000;
    const int M = sizeof(real) * N;
    real* x = (real*)malloc(M);
    for (int n = 0; n < N; ++n)
    {
    
    
        x[n] = 1.23;
    }

    timing(x, N);

    free(x);
    return 0;
}

void timing(const real* x, const int N)
{
    
    
    real sum = 0;

    for (int repeat = 0; repeat < NUM_REPEATS; ++repeat)
    {
    
    
        cudaEvent_t start, stop;
        CHECK(cudaEventCreate(&start));
        CHECK(cudaEventCreate(&stop));
        CHECK(cudaEventRecord(start));
        cudaEventQuery(start);

        sum = reduce(x, N);

        CHECK(cudaEventRecord(stop));
        CHECK(cudaEventSynchronize(stop));
        float elapsed_time;
        CHECK(cudaEventElapsedTime(&elapsed_time, start, stop));
        printf("Time = %g ms.\n", elapsed_time);

        CHECK(cudaEventDestroy(start));
        CHECK(cudaEventDestroy(stop));
    }

    printf("sum = %f.\n", sum);
}

real reduce(const real* x, const int N)
{
    
    
    real sum = 0.0;
    for (int n = 0; n < N; ++n)
    {
    
    
        sum += x[n];
    }
    return sum;
}

여기에 이미지 설명 삽입

2: 스레드 동기화 메커니즘

다중 스레드 프로그램의 경우 서로 다른 두 스레드의 명령 실행 순서가 코드에 표시된 순서와 다를 수 있습니다.
커널 함수에서 명령문의 실행 순서가 나타나는 순서와 일치하도록 하려면 일종의 동기화 메커니즘을 사용해야 합니다. CUDA에서는 동기화 함수 __syncthreads가 제공됩니다. 이 함수는 커널 함수에서만 사용할 수 있으며 가장 간단한 사용법은 매개변수 없이 사용하는 것입니다:
__syncthreads();
이 함수는 스레드 블록의 모든 스레드가 이전 명령문 다음에 오는 명령문을 실행하기 전에 명령문을 완전히 실행하도록 보장할 수 있습니다. 그러나 이 기능은 같은 스레드 블록에 있는 스레드에만 해당되며 다른 스레드 블록에 있는 스레드의 실행 순서는 여전히 불확실합니다.

3: 스레드 동기화를 사용하여 계산을 줄입니다.

배열 요소의 수가 2의 거듭제곱이라고 가정하면(이 가정은 나중에 제거함) 배열의 후반부에 있는 각 요소를 전반부에 있는 해당 배열 요소에 추가할 수 있습니다. 이 프로세스가 반복되면 결과로 생성되는 첫 번째 배열 요소는 원래 배열에 있는 요소의 합계입니다. 이것은 소위 이진 감소 방법입니다.

3.1 글로벌 메모리 조건에서의 축소 계산

void __global__ reduce_global(real* d_x, real* d_y)
{
    
    
    const int tid = threadIdx.x;
 //定义指针X,右边表示 d_x 数组第  blockDim.x * blockIdx.x个元素的地址
 //该情况下x 在不同线程块,指向全局内存不同的地址---》使用不同的线程块对dx数组不同部分分别进行处理   
    real* x = d_x + blockDim.x * blockIdx.x;

    //blockDim.x >> 1  等价于 blockDim.x /2,核函数中,位操作比 对应的整数操作高效
    for (int offset = blockDim.x >> 1; offset > 0; offset >>= 1)
    {
    
    
        if (tid < offset)
        {
    
    
            x[tid] += x[tid + offset];
        }
        //同步语句,作用:同一个线程块内的线程按照代码先后执行指令(块内同步,块外不用同步)
        __syncthreads();
    }

    if (tid == 0)
    {
    
    
        d_y[blockIdx.x] = x[0];
    }
}

3.2 정적 공유 메모리 조건에서의 감소 계산

void __global__ reduce_shared(real* d_x, real* d_y)
{
    
    
    const int tid = threadIdx.x;
    const int bid = blockIdx.x;
    const int n = bid * blockDim.x + tid;
    //定义了共享内存数组 s_y[128],注意关键词  __shared__
    __shared__ real s_y[128];
    s_y[tid] = (n < N) ? d_x[n] : 0.0;
    __syncthreads();
    //归约计算用共享内存变量替换了原来的全局内存变量。这里也要记住: 每个线程块都对其中的共享内存变量副本进行操作。在归约过程结束后,每一个线程
    //块中的 s_y[0] 副本就保存了若干数组元素的和
    for (int offset = blockDim.x >> 1; offset > 0; offset >>= 1)
    {
    
    

        if (tid < offset)
        {
    
    
            s_y[tid] += s_y[tid + offset];
        }
        __syncthreads();
    }

    if (tid == 0)
    {
    
    
        d_y[bid] = s_y[0];
    }
}

3.3 동적 공유 메모리 조건에서의 감소 계산

이전 커널 함수에서 공유 메모리 배열을 정의할 때 고정 길이(128)를 지정했습니다. 우리 프로그램은 이 길이가 커널 함수의 실행 구성 매개변수 block_size(즉, 커널 함수의 blockDim.x)와 같다고 가정합니다. 공유 메모리 변수를 정의할 때 실수로 배열의 길이를 잘못 쓰면 오류가 발생하거나 커널 기능의 성능이 저하될 수 있습니다.

이 오류의 가능성을 줄이는 한 가지 방법은 동적 공유 메모리를 사용하는 것입니다.

  1. 커널 함수를 호출하는 실행 구성의 세 번째 매개변수를 기록합니다.
<<<grid_size, block_size, sizeof(real) * block_size>>>
前面2个参数网格大小和线程块大小,
第三个参数就是核函数中每个线程块需要 定义的动态共享内存的字节数
  1. 동적 공유 메모리를 사용하려면 커널 함수에서 공유 메모리 변수 선언도 변경해야 합니다.
extern __shared__ real s_y[];这是动态声明
__shared__ real s_y[128]; 这是静态声明

감소 계산 프로그램 코드

#include<stdint.h>
#include<cuda.h>
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <math.h>
#include <stdio.h>

#define CHECK(call)                                   \
do                                                    \
{
      
                                                           \
    const cudaError_t error_code = call;              \
    if (error_code != cudaSuccess)                    \
    {
      
                                                       \
        printf("CUDA Error:\n");                      \
        printf("    File:       %s\n", __FILE__);     \
        printf("    Line:       %d\n", __LINE__);     \
        printf("    Error code: %d\n", error_code);   \
        printf("    Error text: %s\n",                \
            cudaGetErrorString(error_code));          \
        exit(1);                                      \
    }                                                 \
} while (0)

#ifdef USE_DP
typedef double real;
#else
typedef float real;
#endif

const int NUM_REPEATS = 100;
const int N = 100000000;
const int M = sizeof(real) * N;
const int BLOCK_SIZE = 128;

void timing(real* h_x, real* d_x, const int method);

int main(void)
{
    
    
    real* h_x = (real*)malloc(M);
    for (int n = 0; n < N; ++n)
    {
    
    
        h_x[n] = 1.23;
    }
    real* d_x;
    CHECK(cudaMalloc(&d_x, M));

    printf("\nUsing global memory only:\n");
    timing(h_x, d_x, 0);
    printf("\nUsing static shared memory:\n");
    timing(h_x, d_x, 1);
    printf("\nUsing dynamic shared memory:\n");
    timing(h_x, d_x, 2);

    free(h_x);
    CHECK(cudaFree(d_x));
    return 0;
}

void __global__ reduce_global(real* d_x, real* d_y)
{
    
    
    const int tid = threadIdx.x;
 //定义指针X,右边表示 d_x 数组第  blockDim.x * blockIdx.x个元素的地址
 //该情况下x 在不同线程块,指向全局内存不同的地址---》使用不同的线程块对dx数组不同部分分别进行处理   
    real* x = d_x + blockDim.x * blockIdx.x;

    //blockDim.x >> 1  等价于 blockDim.x /2,核函数中,位操作比 对应的整数操作高效
    for (int offset = blockDim.x >> 1; offset > 0; offset >>= 1)
    {
    
    
        if (tid < offset)
        {
    
    
            x[tid] += x[tid + offset];
        }
        //同步语句,作用:同一个线程块内的线程按照代码先后执行指令(块内同步,块外不用同步)
        __syncthreads();
    }

    if (tid == 0)
    {
    
    
        d_y[blockIdx.x] = x[0];
    }
}

void __global__ reduce_shared(real* d_x, real* d_y)
{
    
    
    const int tid = threadIdx.x;
    const int bid = blockIdx.x;
    const int n = bid * blockDim.x + tid;
    //定义了共享内存数组 s_y[128],注意关键词  __shared__
    __shared__ real s_y[128];
    //将全局内存中的数据复制到共享内存中
    //共享内存的特 征:每个线程块都有一个共享内存变量的副本
    s_y[tid] = (n < N) ? d_x[n] : 0.0;
    //调用函数 __syncthreads 进行线程块内的同步
    __syncthreads();
    //归约计算用共享内存变量替换了原来的全局内存变量。这里也要记住: 每个线程块都对其中的共享内存变量副本进行操作。在归约过程结束后,每一个线程
    //块中的 s_y[0] 副本就保存了若干数组元素的和
    for (int offset = blockDim.x >> 1; offset > 0; offset >>= 1)
    {
    
    

        if (tid < offset)
        {
    
    
            s_y[tid] += s_y[tid + offset];
        }
        __syncthreads();
    }

    if (tid == 0)
    {
    
    
        d_y[bid] = s_y[0];
    }
}

void __global__ reduce_dynamic(real* d_x, real* d_y)
{
    
    
    const int tid = threadIdx.x;
    const int bid = blockIdx.x;
    const int n = bid * blockDim.x + tid;
    //声明 动态共享内存 s_y[]  限定词 extern,不能指定数组大小
    extern __shared__ real s_y[];
    s_y[tid] = (n < N) ? d_x[n] : 0.0;
    __syncthreads();

    for (int offset = blockDim.x >> 1; offset > 0; offset >>= 1)
    {
    
    

        if (tid < offset)
        {
    
    
            s_y[tid] += s_y[tid + offset];
        }
        __syncthreads();
    }

    if (tid == 0)
    {
    
    //将每一个线程块中归约的结果从共享内存 s_y[0] 复制到全局内 存d_y[bid]
        d_y[bid] = s_y[0];
    }
}

real reduce(real* d_x, const int method)
{
    
    
    int grid_size = (N + BLOCK_SIZE - 1) / BLOCK_SIZE;
    const int ymem = sizeof(real) * grid_size;
    const int smem = sizeof(real) * BLOCK_SIZE;
    real* d_y;
    CHECK(cudaMalloc(&d_y, ymem));
    real* h_y = (real*)malloc(ymem);

    switch (method)
    {
    
    
    case 0:
        reduce_global << <grid_size, BLOCK_SIZE >> > (d_x, d_y);
        break;
    case 1:
        reduce_shared << <grid_size, BLOCK_SIZE >> > (d_x, d_y);
        break;
    case 2:
        reduce_dynamic << <grid_size, BLOCK_SIZE, smem >> > (d_x, d_y);
        break;
    default:
        printf("Error: wrong method\n");
        exit(1);
        break;
    }

    CHECK(cudaMemcpy(h_y, d_y, ymem, cudaMemcpyDeviceToHost));

    real result = 0.0;
    for (int n = 0; n < grid_size; ++n)
    {
    
    
        result += h_y[n];
    }

    free(h_y);
    CHECK(cudaFree(d_y));
    return result;
}

void timing(real* h_x, real* d_x, const int method)
{
    
    
    real sum = 0;

    for (int repeat = 0; repeat < NUM_REPEATS; ++repeat)
    {
    
    
        CHECK(cudaMemcpy(d_x, h_x, M, cudaMemcpyHostToDevice));

        cudaEvent_t start, stop;
        CHECK(cudaEventCreate(&start));
        CHECK(cudaEventCreate(&stop));
        CHECK(cudaEventRecord(start));
        cudaEventQuery(start);

        sum = reduce(d_x, method);

        CHECK(cudaEventRecord(stop));
        CHECK(cudaEventSynchronize(stop));
        float elapsed_time;
        CHECK(cudaEventElapsedTime(&elapsed_time, start, stop));
        printf("Time = %g ms.\n", elapsed_time);

        CHECK(cudaEventDestroy(start));
        CHECK(cudaEventDestroy(stop));
    }

    printf("sum = %f.\n", sum);
}

결과 비교:
전역 메모리 소요 시간은 25ms, 계산 결과가 잘못됨, 1.23*10^8이어야 함, 그 뒤에 많은 소수점이 있음
여기에 이미지 설명 삽입
정적 공유 메모리, 소요 시간 28ms, 결과도 틀림
여기에 이미지 설명 삽입

동적 공유 메모리는
29ms 소요
여기에 이미지 설명 삽입
결론:
(1) 글로벌 메모리의 액세스 속도는 모든 메모리 중 가장 느리 므로 사용을 최소화해야 합니다. 모든 장치 메모리 중에서 레지스터가 가장 효율적이지만 스레드 협력이 필요한 문제에서는 단일 스레드에만 표시되는 레지스터를 사용하는 것만으로는 충분하지 않습니다. 전체 스레드 블록에서 볼 수 있는 공유 메모리를 사용해야 합니다.
(2) 동적 공유 메모리를 사용하는 커널 함수와 정적 공유 메모리를 사용하는 커널 함수는 실행 시간의 차이가 거의 없습니다. 따라서 동적 공유 메모리를 사용하면 프로그램 성능에는 영향을 미치지 않지만 때로는 프로그램 유지 관리성을 향상시킬 수 있습니다.
(3) 전역 메모리에 대한 액세스를 향상시키기 위해 공유 메모리를 사용한다고 해서 반드시 커널 기능의 성능이 향상되는 것은 아닙니다. 따라서 CUDA 프로그램을 최적화할 때 일반적으로 서로 다른 최적화 방식을 테스트하고 비교하는 것이 필요합니다
.
(4) 누적 계산에서 소위 "큰 숫자가 작은 숫자를 먹는다" 현상이 발생하기 때문에 계산 결과 SUM에 오류가 있습니다. 단정밀도 부동 소수점 숫자에는 6개 또는 7개의 정확한 유효 숫자만 있습니다. 위 함수 reduce에서 변수 sum의 값을 3천만 이상으로 누적한 후 1.23에 더하면 값이 더 이상 증가하지 않습니다(작은 숫자는 큰 숫자에 의해 "먹히지만" 큰 숫자는 다양성이 아닙니다
).
다음과 같은 일부 현재 솔루션: Kahan 합산 알고리즘

추천

출처blog.csdn.net/qq_40514113/article/details/130989649