[Multi-Core & GPU Programming] HW4 - CUDA Conv2d

"CUDA Conv2d"

[Objective]

CUDA를 활용한 직접적인 2D Convolution 구현과 이미지를 행렬로 변환하는 im2col 기법 및 행렬 곱셈(GEMM)을 결합한 방식을 각각 구현하여, GPU 병렬 처리 환경에서 효율적인 메모리 접근 패턴과 고성능 연산 최적화 이해하기

[Theory]

▣ Convolution
· Input 데이터의 특정 영역과 Filter(Kernel)간의 요소별 곱셈 후 합계를 구하는 연산

▣ Parallel Conv2d
· GPU에서 각 출력 Pixel 값을 독립적으로 계산 가능 → 병렬성 극대화
· 효율적인 성능을 내기 위해서는 Shared Memory 활용 및 Coalesced Memory Access 활용 필수

▣ Parallel im2col combined with Matmul
· im2col(Image-To-Column) : Input 이미지에서 Convolution 연산이 수행될 모든 Patch를 추출하여 2D 행렬의 열로 펼치는 과정
    → 다차원 데이터인 이미지가 2차원 행렬 구조로 재구성

· GEMM : 변환된 이미지 행렬과 Filter를 펼친 행렬을 행렬 곱셈 연산으로 계산
    → GPU는 행렬 곱셈에 최적화

[Implementation]

▣ im2col
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include "cuda_runtime.h"
#define TILE_WIDTH 8
 
/**
 * @brief Launches the im2col operation, which rearranges image blocks into columns.
 *
 * This function is typically used in convolutional neural networks (CNNs) to
 * transform input image data into a format suitable for matrix multiplication.
 *
 * @param x        Pointer to the input tensor of shape (N, C, H, W), where:
 *                 - N: Batch size
 *                 - C: Number of channels
 *                 - H: Height of the input
 *                 - W: Width of the input
 * @param N        Number of images in the batch.
 * @param C        Number of channels in the input tensor.
 * @param H        Height of the input tensor.
 * @param W        Width of the input tensor.
 * @param kH       Height of the convolution kernel (filter).
 * @param kW       Width of the convolution kernel (filter).
 * @param padH     Padding applied to the height dimension.
 * @param padW     Padding applied to the width dimension.
 * @param strideH  Stride along the height dimension.
 * @param strideW  Stride along the width dimension.
 * @param dilH     Dilation factor for the height dimension.
 * @param dilW     Dilation factor for the width dimension.
 * @param out      Pointer to the output tensor, which stores the rearranged
 *                 image blocks in column format.
*/
__global__ void im2col_kernel(const float* x, int N, int C, int H, int W,
                              int kH, int kW, int padH, int padW, int strideH, int strideW,
                              int dilH, int dilW, int outH, int outW, float* out) {
        int idx = blockIdx.x * blockDim.x + threadIdx.x;
 
        int rows = C * kH * kW;
        int cols = outH * outW;
        int total = rows * cols;
 
        if (idx < total) {
                int w_out = idx % outW;
                int h_out = (idx / outW) % outH;
                int q = (idx / (outH * outW)) % kW;
                int p = (idx / (outH * outW * kW)) % kH;
                int c = idx / (outH * outW * kH * kW);
 
                int h_in = h_out * strideH - padH + p * dilH;
                int w_in = w_out * strideW - padW + q * dilW;
 
                if (h_in >= 0 && h_in < H && w_in >= 0 && w_in < W) {
                        int in_idx = (c * H + h_in) * W + w_in;
                        out[idx] = x[in_idx];
                } else {
                        out[idx] = 0;
                }
        }
}
 
void launch_im2col(const float* x, int N, int C, int H, int W,
                   int kH, int kW, int padH, int padW, int strideH, int strideW,
                   int dilH, int dilW, float* out)
{
        int outH = (H + 2 * padH - dilH * (kH - 1- 1/ strideH + 1;
        int outW = (W + 2 * padW - dilW * (kW - 1- 1/ strideW + 1;
 
        int rows = C * kH * kW;
        int cols = outH * outW;
        int total = rows * cols;
        int block = 256;
        int grid = (total + block - 1/ block;
 
        im2col_kernel<<<grid, block>>>(x, N, C, H, W, kH, kW, padH, padW, strideH, strideW, dilH, dilW, outH, outW, out);
}
 
cs

> im2col_kernel
① 각 Thread가 im2col 행렬(Unrolled Input Features)의 하나의 원소(c, p, q, h_out, w_out)를 담당
    · idx : 1차원 Block과 Thread Index를 사용하여 현재 Thread의 고유 ID를 계산

② im2col 행렬(Unrolled Input Features) 행렬 크기 계산
    · rows : im2col 결과 행렬(Unrolled Input Features)의 행 개수 → 12(= 3 x 2 x 2)
    · cols : im2col 결과 행렬(Unrolled Input Features)의 열 개수 → 4(= 2 x 2) (Batch Size = 1 → N = 1)
    · total : im2col 결과 행렬(Unrolled Input Features)의 전체 원소 개수 → 48(= 12 x 4)

③ 범위 밖 Thread를 제외한 후, idx를 이용해 c, p, q, h_out, w_out을 계산
    · w_out : Output Feature Map에서의 열 Index
    · h_out : Output Feature Map에서의 행 Index
    · q : Filter 내부의 열 Index
    · p : Filter 내부의 행 Index
    · c : Input Channel 번호

④ 실제 Input Image에서 읽을 위치 계산
    · h_in : Input Image에서 실제로 읽을 행 Index
    · w_in : Input Image에서 실제로 읽을 열 Index

⑤ Input Image 안쪽이면 해당 Index의 값을 복사하고, Input Image 바깥쪽이면 Padding 영역이므로 0을 할당

> launch_im2col
① Output Feature Map 크기 계산
    · outH : Output Feature Map의 Height → 2
    · outW : Output Feature Map의 Width → 2

② im2col 결과 행렬(Unrolled Input Features) 크기 계산
    · rows : im2col 결과 행렬(Unrolled Input Features)의 행 개수 → 12(= 3 x 2 x 2)
    · cols : im2col 결과 행렬(Unrolled Input Features)의 열 개수 → 4(= 2 x 2)
    · total : im2col 결과 행렬(Unrolled Input Features)의 전체 원소 개수 → 48(= 12 x 4)

③ CUDA Block 크기 설정 → 한 Block 안에서 실행할 Thread 개수 설정
④ CUDA Grid 크기 계산 → im2col 결과 행렬의 전체 원소 개수만큼 Thread 필요
⑤ GPU에서 im2col_kernel 실행

▣ matmul
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
 * @brief Launches a matrix multiplication operation on the provided matrices.
 *
 * This function performs the matrix multiplication operation C = A * B, where:
 * - A is an MxK matrix.
 * - B is a KxN matrix.
 * - C is the resulting MxN matrix.
 *
 * @param A Pointer to the first input matrix (MxK).
 * @param B Pointer to the second input matrix (KxN).
 * @param C Pointer to the output matrix (MxN) where the result will be stored.
 * @param M Number of rows in matrix A and matrix C.
 * @param N Number of columns in matrix B and matrix C.
 * @param K Number of columns in matrix A and rows in matrix B.
 */
__global__ void matmul_kernel(const float* A, const float* B, float* C, int M, int N, int K) {
        __shared__ float tileA[TILE_WIDTH][TILE_WIDTH];
        __shared__ float tileB[TILE_WIDTH][TILE_WIDTH];
 
        int col = blockIdx.x * blockDim.x + threadIdx.x;
        int row = blockIdx.y * blockDim.y + threadIdx.y;
        float sum = 0;
 
        for (int t = 0; t < (K + TILE_WIDTH - 1/ TILE_WIDTH; t++) {
                if (row < M && t * TILE_WIDTH + threadIdx.x < K) {
                        tileA[threadIdx.y][threadIdx.x] = A[row * K + t * TILE_WIDTH + threadIdx.x];
                } else {
                        tileA[threadIdx.y][threadIdx.x] = 0;
                }
 
                if (t * TILE_WIDTH + threadIdx.y < K && col < N) {
                        tileB[threadIdx.y][threadIdx.x] = B[(t * TILE_WIDTH + threadIdx.y) * N + col];
                } else {
                        tileB[threadIdx.y][threadIdx.x] = 0;
                }
                __syncthreads();
 
                for (int k = 0; k < TILE_WIDTH; k++) {
                        sum += tileA[threadIdx.y][k] * tileB[k][threadIdx.x];
                }
                __syncthreads();
        }
        if (row < M && col < N) {
                C[row * N + col] = sum;
        }
}
// Tiling X Version
/*__global__ void matmul_kernel(const float* A, const float* B, float*C, int M, int N, int K) {
        int col = blockIdx.x * blockDim.x + threadIdx.x;
        int row = blockIdx.y * blockDim.y + threadIdx.y;
        float sum = 0;
        if (row < M && col < N) {
                for (int k = 0; k < K; k++) {
                        sum += A[row * K + k] * B[k * N + col];
                }
                C[row * N + col] = sum;
        }
}*/
 
 
void launch_matmul(const float* A, const float* B, float* C, int M, int N, int K)
{
        dim3 block(TILE_WIDTH, TILE_WIDTH);
        dim3 grid((N + TILE_WIDTH - 1/ TILE_WIDTH, (M + TILE_WIDTH - 1/ TILE_WIDTH);
 
        matmul_kernel<<<grid, block>>>(A, B, C, M, N, K);
}
 
cs

> matmul_kernel
① Shared Memory 선언
② 각 Thread가 담당할 결과 행렬 C의 Column과 Row Index를 계산하고, 곱셈 결과를 누적할 변수 선언
③ A, B 행렬의 일부 Tile을 Shared Memory에 Load한 후, 동기화 수행
④ Shared Memory에 저장된 Tile 값을 이용해 부분 내적을 수행하고 sum에 누적한 후, 다음 Tile 계산 전에 동기화 수행
⑤ 누적된 sum을 마지막에 결과 행렬 C에 저장

> launch_matmul
① Block 하나에 Thread를 (TILE_WIDTH x TILE_WIDTH)개 배치 → Block 하나가 결과 행렬 C의 Tile 영역을 담당
② 결과 행렬 C 전체를 처리할 수 있는 Block 개수를 계산
③ GPU에서 matmul_kernel 실행

▣ direct_conv2d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
 * @brief Launches a 2D convolution operation using the direct convolution method.
 *
 * @param x Pointer to the input tensor of shape (N, C, H, W), where:
 *          - N: Batch size
 *          - C: Number of input channels
 *          - H: Height of the input
 *          - W: Width of the input
 * @param w Pointer to the weight tensor of shape (K, C, kH, kW), where:
 *          - K: Number of output channels
 *          - C: Number of input channels
 *          - kH: Height of the kernel
 *          - kW: Width of the kernel
 * @param y Pointer to the output tensor of shape (N, K, outH, outW), where:
 *          - outH: Computed output height
 *          - outW: Computed output width
 * @param N Number of input batches.
 * @param C Number of input channels.
 * @param H Height of the input tensor.
 * @param W Width of the input tensor.
 * @param K Number of output channels.
 * @param kH Height of the convolution kernel.
 * @param kW Width of the convolution kernel.
 * @param padH Padding applied to the height dimension.
 * @param padW Padding applied to the width dimension.
 * @param strideH Stride applied to the height dimension.
 * @param strideW Stride applied to the width dimension.
 * @param dilH Dilation applied to the height dimension of the kernel.
 * @param dilW Dilation applied to the width dimension of the kernel.
 */
__global__ void conv2d_direct_kernel(const float* x, const float* w, float* y,
                                     int N, int C, int H, int W,
                                     int K, int kH, int kW,
                                     int padH, int padW, int strideH, int strideW,
                                     int dilH, int dilW, int outH, int outW) {
        int idx = blockIdx.x * blockDim.x + threadIdx.x;
        int total = K * outH * outW;
 
        if (idx < total) {
                int w_out = idx % outW;
                int h_out = (idx / outW) % outH;
                int k = idx / (outH * outW);
 
                float sum = 0;
 
                for (int c = 0; c < C; c++) {
                        for (int p = 0; p < kH; p++) {
                                for (int q = 0; q < kW; q++) {
                                        int h_in = h_out * strideH - padH + p * dilH;
                                        int w_in = w_out * strideW - padW + q * dilW;
 
                                        if (h_in >= 0 && h_in < H && w_in >= 0 && w_in < W) {
                                                int x_idx = (c * H + h_in) * W + w_in;
                                                int weight_idx = ((k * C + c) * kH + p) * kW + q;
 
                                                sum += x[x_idx] * w[weight_idx];
                                        }
                                }
                        }
                }
                y[idx] = sum;
        }
}
 
void launch_conv2d_direct(const float* x, const float* w, float* y,
                          int N, int C, int H, int W,
                          int K, int kH, int kW,
                          int padH, int padW, int strideH, int strideW,
                          int dilH, int dilW)
{
        int outH = (H + 2 * padH - dilH * (kH - 1- 1/ strideH + 1;
        int outW = (W + 2 * padW - dilW * (kW - 1- 1/ strideW + 1;
 
        int block = 256;
        int grid = (K * outH * outW + block - 1/ block;
 
        conv2d_direct_kernel<<<grid, block>>>(x, w, y, N, C, H, W, K, kH, kW, padH, padW, strideH, strideW, dilH, dilW, outH, outW);
}
 
cs

> conv2d_direct_kernel
① 각 Thread가 담당할 1D Index 계산
② 전체 출력 원소 개수 계산
③ 현재 Thread가 계산할 출력의 Column과 Row, Output Channel Index를 계산하고, 곱셈 결과를 누적할 변수 선언
④ 모든 Input Channel을 반복하면서 Input Image의 Column과 Row Index를 계산
⑤ Padding 때문에 입력 범위를 벗어난 경우를 제외하고, Input과 Weight의 1차원 Index 계산 후 서로 곱하여 누적
⑥ 최종 결과를 Output에 저장

> launch_conv2d_direct
① 출력의 Row와 Column 개수를 계산
② Block 하나에 Thread 256개를 배치
③ 전체 결과 원소를 처리할 수 있는 Block 개수 계산
④ GPU에서 conv2d_direct_kernel 실행

[Result]

· Direct Kernel은 Shared Memory Reuse가 없어 Global Memory에 반복적으로 접근하지만, im2col 행렬 생성 비용 X
    → im2col + matmul 방식보다 빠름
· Filter Size가 7x7일때도 Direct Conv2d보다 im2col + matmul의 성능이 더 낮음
    ∵ Batch Size가 1로 고정되어 있기 때문에 im2col + matmul의 효율↓

∴ Batch Size가 증가
    → im2col Matrix Column↑
    → GEMM 크기↑
    → GPU 병렬성↑
    → im2col + matmul의 성능↑

[Discussion]

· HW3과 마찬가지로 실행중인 Process를 Kill하여 여유 Process를 보유하고 있어야 함

<im2col_kernel에서 for문 사용>
· 각 Thread가 Output Feature의 한 원소를 담당 = im2col의 한 Column 담당
    → 각 Thread가 Kernel 전체를 다 돌며, 각 Thread당 (kH x kW)번의 연산 반복

<matmul_kernel에서 Tiling X>
· Naïve 방식이 Tiling 방식보다 빠름
    → 연산량에 비해 Shared Memory로 복사하여 Load하고, 모든 Thread가 대기하도록 동기화하는 것에서 Overhead 발생
    → Tiling에서 for 구문 안의 if-else로 인해 Warp Divergence가 반복적으로 발생하여 연산 Unit이 낭비되어 성능 저하


<TILE_WIDTH = 16>                    <TILE_WIDTH = 32>
· im2col은 K차원이 매우 작아서 TILE_WIDTH를 늘리면 Thread 낭비 발생

[Reference]

· HW4 - CUDA Conv2d (MGP) - Yongjun Park
· 7_cuda_dnn_mgp_2026 (MGP) - Yongjun Park
· [Multi-Core & GPU Programming] HW3 - CUDA LoRA

댓글

이 블로그의 인기 게시물

[Mini-NPU RTL] NN Reference Model

[Mini-NPU RTL] TPU (Study Paper)