Skip to main content

CUDA 기반 matmul 성능 최적화하기

·3 mins· loading · loading ·
CS Multicore-GPU-Programming cuda
Soeun Uhm
Author
Soeun Uhm
problem-solving engineer, talented in grit.
Table of Contents
Multicore-GPU-Programming - This article is part of a series.
Part 12: This Article

Optimizing Memory Access
#

img
캐시를 통한 전역 메모리 접근 흐름

L2 캐시에 대한 기본 전송 흐름은 32byte 입니다. 즉, 한번의 메모리 전송으로 최대 연속된 32byte의 데이터를 읽거나 쓸 수 있습니다. L1 캐시의 캐시 라인 크기는 128byte 입니다. 각 캐시 라인은 디바이스 메모리에서 크기가 같은 메모리 블럭에 대응됩니다. 캐시 라인이 128byte 인 L1 캐시를 예로 들면, 128byte 단위로 나누어진 디바이스 메모리 블록과 대응됩니다. 만약 각 스레드가 int 변수와 같은 4byte 데이터에 접근한다면, 한 워프에서 접근해야 하는 총 데이터 크기는 128byte가 됩니다. 그리고 워프 내 스레드들의 메모리 접근 요청 위치가 연속된 메모리 주소(메모리 공간)이면서 캐시 라인과 정확하게 그 위치가 일치한다면 이 요청은 한 번의 L1캐시 전송으로 처리됩니다. 하지만 이러한 조건이 맞지 않으면, 워프의 데이터 요청을 처리하기 위해 메모리 전송 횟수가 증가합니다.

Aligned memory acess & Coalesced memory access
#

글로벌 메모리 접근을 최적화하기 위해 기억해야 할 것은 정렬된(aligned) 메모리 접근과 병합된(coalesced) 메모리 접근입니다. 정렬된 메모리 접근이란 요청한 데이터의 시작점이 캐시에 대응하는 데이터 블록의 시작 지점과 일치하는 경우입니다. 병합된 메모리 접근은 32개의 스레드가 연속적인 데이터에 접근하는 것 입니다.

  1. 정렬 & 병합된 메모리 접근

image
정렬 & 병합된 메모리 접근

가장 이상적인 정렬 & 병합된 메모리 접근 방식입니다. 워프 내 32개 스레드가 시작 주소가 128이고 연속된 128byte에 접근하는 경우로, 캐시 라인의 크기가 128byte 인 L1 캐시에 대한 데이터 전송은 한 번의 메모리 전송으로 수행할 수 있습니다. 캐시 라인 크기가 32byte인 L2 캐시의 경우, 4번의 메모리 전송으로 요청된 메모리 접근을 처리할 수 있습니다.

  1. 정렬 X & 병합된 메모리 접근

image
정렬되지 않은 병합된 메모리 접근

이 경우 접근하는 메모리 영역은 연속적이지만, 시작 메모리 주소가 128의 배수가 아닙니다. 따라서 L1캐시라면 2번, L2캐시라면 5번의 메모리 전송을 필요로 합니다.

  1. 정렬 X & 병합 X 인 메모리 접근

image
정렬되지 않고 병합되지 않은 메모리 접근

이 경우 워프 내 스레드의 메모리 접근 패턴이 불규칙하고, 여러 캐시 라인에 접근해야 합니다. 만약 워프 내 모든 스레드가 서로 다른 캐시 라인에 접근한다면 최대 32번의 메모리 전송을 요구할 수도 있습니다. 즉, 이상적인 경우보다 최대 32배의 메모리 전송 부하가 발생할 수 있습니다.

Matmul and Coalescing
#

image

2가지 방법 중, 어느 것이 더 빠를까요? 정답은 N의 한 칼럼마다 스레드를 할당하는 방식입니다.

  1. 열마다 스레드 할당(Coalescing)
image image

워프 내에 있는 스레드가 같은 캐시 라인에 접근하면, 메모리에 대한 접근은 연속적으로 이루어집니다. 따라서 병합된 메모리 접근이며, 메모리에 한 번만 접근하면 됩니다.

  1. 행마다 스레드 할당(No Coalescing)
image image

이때는 여러 번(4번)의 메모리 접근을 해야 합니다. 따라서 병합된 메모리 접근을 할 수 있도록 코드를 작성해야 합니다.

Dynamic shared memory allocation
#

__global__ void MatrixMulKernel(float* M, float* N, float* P, int Width)
{
    // Declare shared memory variable
    __shared__ float subTileM[TILE_WIDTH][TILE_WIDTH];
    __shared__ float subTileN[TILE_WIDTH][TILE_WIDTH];
    ...
}

우리는 지금까지 정적 공유 메모리 할당을 했습니다. 즉, 하나의 스레드 당 하나의 원소를 할당했습니다. 만약 우리가 아래와 같이 서로 다른 블럭 사이즈로 커널을 여러 번 런칭하고 싶으면 어떻게 해야 할까요?

// Launch kernel with multiple block sizes
SomeKernel<<<N/128, 128>>>(parameters..);
SomeKernel<<<N/64, 64>>>(parameters..);
SomeKernel<<<N/32, 32>>>(parameters..);

이때 extern 키워드를 사용하면 됩니다. 커널 런칭을 할 때, 공유 메모리 사이즈를 명시해주어야 합니다.

__global__ void someKernel(int* A)
{
    extern __shared__ int sm[];
}

Reference
#

Multicore-GPU-Programming - This article is part of a series.
Part 12: This Article

Related

CUDA Memories : 레지스터, 공유 메모리, 글로벌 메모리
·11 mins· loading · loading
CS Multicore-GPU-Programming cuda
언제, 어떤 메모리를 사용해야 할까?
CUDA와 Nvidia GPU 아키텍처: 스레드 계층, 메모리 계층 및 GPU 캐시 구조 이해하기
·10 mins· loading · loading
CS Multicore-GPU-Programming cuda
CUDA 의 스레드 계층와 GPU 하드웨어의 관계, 메모리 계층, GPU 캐시 구조
CPU-GPU 통신 및 CUDA를 활용한 이미지 프로세싱 기법
·10 mins· loading · loading
CS Multicore-GPU-Programming cuda
CUDA 프로그래밍 작성에 필수적인 CUDA 스레드 계층구조와 스레드 인덱싱