CUDA 노트 - 컨볼루션 계산

1. 1차원 컨벌루션       

         1차원 컨볼루션의 계산은 소스 데이터와 컨볼루션 커널에 따른 각 요소 주변의 값과 컨볼루션 커널의 가중 곱의 합이며, 일반적으로 컨볼루션 커널은 홀수이다.

        아래 그림은 소스 데이터로 16개 요소의 배열과 5개 요소의 컨볼루션 커널을 보여줍니다.

         컨볼루션을 계산하는 과정은 아래 그림과 같으며, 각 노드는 해당 컨볼루션 커널과 중첩 가중치 계산을 수행하여 배열의 경계 위치에서 컨볼루션 커널이 유효 범위를 초과(경계 판단)하여 값이 해당 위치는 0이고; 

        소스 배열을 N, 출력 컨볼루션 배열을 P, 두 배열의 길이를 크기로 설정합니다.

        컨볼루션 커널 배열을 M, 길이를 size_kernel로 설정합니다.

2. CPU 코드 구현:

void convolution_1D_basic_kernel(float* N, float* P, int size,
    float* M, int size_kernel) {
    int half_width_kernel = size_kernel / 2;
    for (int i = 0; i < size; i++) {
        float p_value = 0;
        int begin_pos = i - half_width_kernel;
        for (int j = begin_pos; j < begin_pos + size_kernel; j++) {
            if (j >= 0 && j < size) {
                p_value += N[j] * M[j - begin_pos];
            }            
        }
        P[i] = p_value;
    }
}

        코드 내용:

        1. 컨볼루션 커널의 half_width_kernel을 계산합니다 컨볼루션 커널이 현재 5로 설정되어 있을 때 half_width_kernel은 2입니다.

        2. 전체 소스 어레이를 반복합니다.

                2.1 각 배열 및 컨볼루션 커널의 가중치를 저장하기 위해 임시 변수 p_value를 선언합니다.

                2.2 현재 i 위치의 컨볼루션 계산 시작 위치를 계산합니다. begin_pos는 i - half_width_kernel입니다.

                2.3 begin_pos에서 시작하여 각 값과 컨볼루션 커널의 가중 값을 계산합니다. 전제 조건은 소스 배열 [0, size)의 유효 범위 내에 있어야 하며 순회 횟수는 5입니다.

                2.4 계산 결과를 해당 P[i]에 씁니다.

3. GPU 코드는 다음과 같습니다.

3.1 일반 구현

__global__ void convolution_1D_basic_kernel(float* N, float* P, int size,
    float* M, int size_kernel) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;

    float p_value = 0;
    int begin_pos = i - (size_kernel / 2);
    for (int j = begin_pos; j < begin_pos + size_kernel; j++) {
        if (j >= 0 && j < size) {
            p_value += N[j] * M[j - begin_pos];
        }
    }
    P[i] = p_value;
}

        그리드는 하나의 차원만 사용하고 블록은 하나의 차원, 즉 convolution_1D_basic_kernel<<<1,16>>>을 사용한다고 가정합니다.

        코드 내용:

        1. 현재 스레드 ID와 스레드 블록 ID 및 스레드 블록 크기에 따라 전체 스레드 그리드에서 현재 스레드의 첨자 i를 계산합니다.

        2. 현재 스레드는 i에 해당하는 N[i]의 컨볼루션 값만 계산하면 되며, 현재 위치 i의 컨볼루션 계산을 계산하기 위한 시작 위치는 begin_pos가 i - (size_kernel / 2)이며, begin_pos에서 시작하여 size_kernel을 통과합니다. 컨볼루션의 값 합계;

        참고: 순회 첨자는 N의 유효한 범위 내에 있어야 합니다.

        3. 결과를 P[i]에 씁니다.

        불충분:

        1. 제어 흐름 다양성이 있을 것입니다. 경계 계산 범위가 다르기 때문에 if 문에서 다른 결정이 있을 것입니다.

        2. 메모리 대역폭 위의 코드에서 N 어레이가 항상 액세스되고 있음을 알 수 있습니다. 메모리;

3.2 상수 메모리 사용

        컨볼루션에서 컨볼루션 커널의 크기는 크지 않고 컨볼루션 계산에서 컨볼루션 커널의 내용은 변경되지 않으며 각 스레드는 컨볼루션 커널에 액세스합니다.

        컨볼루션 커널은 계산에서 전역 메모리 액세스 수를 개선하고 액세스 저장 지연을 개선할 수 있는 상수 메모리 및 캐시 저장소를 사용할 수 있고 사용해야 합니다.

        상수 메모리에 저장된 변수는 모든 함수 정의 외부에서 선언된 전역 변수여야 합니다. 키워드 " __constant__ "를 사용하고 " cudaMemcpyToSymbol "을 사용하여 상수 메모리에 배치할 데이터를 장치의 상수 메모리에 복사합니다.

#define MAX_MASK_WIDTH 10
__constant__ float M[MAX_MASK_WIDTH];

        이전 코드와의 차이점은 M이 더 이상 커널 함수에 매개변수로 전달되지 않는다는 점입니다.

__global__ void convolution_1D_basic_kernel(float* N, float* P, int size,
    int size_kernel) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;

    float p_value = 0;
    int begin_pos = i - (size_kernel / 2);
    for (int j = begin_pos; j < begin_pos + size_kernel; j++) {
        if (j >= 0 && j < size) {
            p_value += N[j] * M[j - begin_pos];
        }
    }
    P[i] = p_value;
}

3.3 블록 사용

        3.1 및 3.2의 스레드 구성은 1차원 스레드입니다.

convolution_1D_basic_kernel<<<1,16>>>(N, P, size, size_kernel);

         청킹 알고리즘에서 모든 스레드는 협력하여 입력 요소를 온칩 메모리에 로드하고 해당 요소가 나중에 사용될 때 온칩 메모리에 직접 액세스합니다.

        요소를 온칩 메모리에 넣는 가장 직접적인 방법은 컴퓨팅 전에 스레드 블록에 필요한 모든 입력 데이터를 공유 메모리에 로드하는 것입니다.

        4개의 스레드 블록을 사용하여 스레드 블록에서 4개의 스레드가 1차원 컨벌루션을 계산하는 데 사용됩니다.

convolution_1D_basic_kernel<<<4,4>>>(N, P, size, size_kernel);

        스레드 블록에는 4개의 스레드가 있으며, 4개의 스레드가 사용하는 요소만 온칩 메모리에 로드하면 액세스 속도는 향상되지만 워프 차별화 문제는 여전히 저장됩니다.

        워프 차별화 문제를 해결하기 위해서는 온칩 메모리에 몇 가지 요소를 더 로드해야 하는데, 이 숫자는 컨볼루션 커널의 범위인 "size_kernel/2*2"와 관련이 있습니다. 아래 그림은 4개의 스레드 블록의 계산 요구를 나타냅니다.공유 메모리에 저장된 데이터 내용, 노란색 부분은 현재 스레드 계산에 필요한 추가 요소(가장자리 요소)이고 녹색 부분은 해당 중앙입니다. 현재 스레드의 요소(내부 요소);

        각 블록의 공유 저장소 크기는 왼쪽 가장자리 요소, 중간 내부 요소 및 오른쪽 가장자리 데이터를 저장할 수 있어야 합니다. ), 중앙의 내부 요소(스레드 수 4) 및 오른쪽의 에지 데이터(size_kernel/2==2), 총 8;

        이와 같이 각 스레드를 계산할 때 아래 그림과 같이 공유 메모리의 시작 위치부터 계산할 수 있으며, 경계 조건의 판단은 생략하고,

       아래 그림은 스레드 2와 3을 기준으로 왼쪽(첨자 0과 1)의 모서리 요소를 계산하고 스레드 번호 0과 1을 기준으로 오른쪽(첨자 6과 7)의 모서리 요소를 계산합니다.

        왼쪽 가장자리 요소 로드

int halo_index_left = (blockIdx.x - 1) * blockDim.x + threadIdx.x;
if (threadIdx.x >= blockDim.x - n){
    N_ds[threadIdx.x - (blockDim.x - n)] =
         (halo_index_left < 0) ? 0 : N[halo_index_left];
}

        현재 스레드 블록 halo_index_left에 필요한 요소의 시작 위치를 계산합니다(이전 스레드 블록의 내부 요소이므로 blockIdx.x - 1 사용), 이때 n은 size_kernl / 2, n 스레드를 사용하여 로드 왼쪽 가장자리 요소의 n;

        내부 요소 로드

N_ds[n + threadIdx.x] = N[blockIdx.x * blockDim.x + threadIdx.x];

        오른쪽 가장자리 요소 로드

int halo_index_right = (blockIdx.x + 1) * blockDim.x + threadIdx.x;
if (threadIdx.x < n){
    N_ds[n + blockDim.x + threadIdx.x] =
         (halo_index_right >= size) ? 0 : N[halo_index_right];
}

        자세한 코드는 다음과 같습니다.

__global__ void convolution_1D_basic_kernel(float* N, float* P, int size,
    int size_kernel) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    __shared__ float N_ds[TILE_SIZE + MAX_MASK_WIDTH - 1];

    int n = size_kernel / 2;
    
    int halo_index_left = (blockIdx.x - 1) * blockDim.x + threadIdx.x;
    if (threadIdx.x >= blockDim.x - n){
        N_ds[threadIdx.x - (blockDim.x - n)] =
             (halo_index_left < 0) ? 0 : N[halo_index_left];
    }

    N_ds[n + threadIdx.x] = N[blockIdx.x * blockDim.x + threadIdx.x];

    int halo_index_right = (blockIdx.x + 1) * blockDim.x + threadIdx.x;
    if (threadIdx.x < n){
        N_ds[n + blockDim.x + threadIdx.x] =
             (halo_index_right >= size) ? 0 : N[halo_index_right];
    }    

    __syncthreads();

    float p_value = 0;
    
    for (int j = 0; j < size_kernel; j++) {
        p_value += N[threadIdx.x + j] * M[j];
    }
    P[i] = p_value;
}

        추가 복잡성을 도입하여 어레이 N에 대한 DRAM 액세스 수를 줄입니다. 궁극적인 목표는 산술 연산과 메모리 액세스 사이의 비율을 증가시켜 얻은 성능이 더 이상 DRAM의 대역폭에 의해 제한되거나 부분적으로 제한되지 않도록 하는 것입니다.

3.4 다른 공유 블록 사용

        내부 요소만 공유 메모리 블록에 저장되고 가장자리 요소는 전역 메모리에 직접 액세스합니다 컨볼루션 커널의 크기가 내부 요소의 수보다 훨씬 적은 상대적으로 작기 때문에 코드가 비교적 간단합니다. ;

__global__ void convolution_1D_basic_kernel(float *N, float *P,
	int Mask_Width, int Width){
	int i = blockIdx.x * blockDim.x + threadIdx.x;
	__shared__ float N_ds[TILE_SIZE];
	
	N_ds[threadIdx.x] = N[i];
	
	__syncthreads();
	
	int this_tile_start_point = blockIdx.x + blockDim.x;
	int next_tile_start_point = (blockIdx.x + 1) * blockDim.x;
	int N_start_point = i - (Mask_Width / 2);
	float Pvalue = 0;
	for (int j = 0; j < Mask_Width; j++) {
		int N_index = N_start_point + j;
		if (N_index >= 0 && N_index < Width) {
			if (N_index >= this_tile_start_point 
                && N_index < next_tile_start_point) {
				Pvalue += N_ds[threadIdx.x + j - (Mask_Width/2)]*M[j];
			} else {
				Pvalue += N[N_index] * M[j];
			}
		}		
	}
	P[i] = Pvalue;
}

참조

CUDA 3D 컨볼루션 - ijpq - 블로그 정원 개요 https://www.cnblogs.com/ijpq/p/15405106.html

추천

출처blog.csdn.net/liushao1031177/article/details/124044206