References
- Programming Massively Parallel Processors
Contents
- SM 리소스의 동적 분할 (Dynamic Partitioning)
- 리소스 간의 제한사항 (limitations)
- CUDA Device Query
SM(Streaming multiprocessor)의 실행 리소스는 레지스터(registers), 공유 메모리(shared memory), 스레드 블록 슬롯(thread block slots), 스레드 슬롯(thread slot)을 포함합니다. 이 리소스들은 동적으로 분할되고 스레드에게 할당되어 수행될 때 사용됩니다.
스레드 슬롯과 블록 슬롯
Fermi 아키텍처의 경우에는 1536개의 스레드 슬롯이 있습니다. 이 스레드 슬롯은 분할되어 런타임에 스레드 블록에 할당됩니다. 만약 각 블록이 512개의 스레드로 구성되어 있다면, 1536개의 스레드 슬롯은 분할되어 3개의 블록에 할당될 것입니다. 이 경우 각 SM은 스레드 슬롯 수의 제약 때문에 3개의 블록까지만 수용할 수 있습니다.
만약 블록이 256개의 스레드로 구성된다면, 1536개의 스레드 슬롯은 6개의 블록에 할당될 수 있습니다. 블록 간에 스레드 슬롯을 동적으로 분할할 수 있는 기능 때문에 SM은 매우 다양하게 사용될 수 있습니다. 각 블록이 적은 스레드로 구성된 많은 블록을 실행할 수 있고, 많은 수의 스레드로 구성된 적은 수의 블록들을 실행할 수도 있습니다. 이는 고정된 분할(Fixed Partitioning) 기법과 대조되는데, 고정된 분할 기법에서는 각 블록들이 실제 필요한 리소스와 무관하게 고정된 양의 리소스를 할당받습니다. 고정된 분할은 블록이 적은 수의 스레드로 구성될 때 스레드 슬롯의 낭비를 초래하고, 고정된 분할에서 허용한 개수보다 많은 수의 스레드 슬롯이 필요한 블록은 생성할 수 없습니다.
리소스를 동적으로 분할하는 것은 리소스 간 제한사항에서 미묘한 상호작용이 발생하여 리소스를 충분히 활용하지 못하는 경우가 발생할 수도 있습니다. 이러한 상호작용은 블록 슬롯과 스레드 슬롯 사이에서 발생할 수 있습니다. 예를 들어, 각 블록이 128개의 스레드로 구성되면, 1536개의 스레드 슬롯은 12개의 블록으로 분할되어 할당됩니다. 그러나 각 SM마다 8개의 블록 슬롯만이 존재하기 때문에 8개의 블록 슬롯만 허용됩니다. 이는 결국 1024개의 스레드 슬롯만 활용된다는 것을 의미합니다. 그러므로 블록 슬롯과 스레드 슬롯을 모두 활용하려면 각 블록당 적어도 256개의 스레드가 필요합니다.
레지스터와 다른 리소스 간의 제약 사항
CUDA의 메모리 Access와 Type (예제 : matrix multiplication)
위 포스팅에서 살펴본 것처럼 CUDA 커널에 선언된 Automatic Variables(지역 변수)는 레지스터에 위치합니다. 어떤 커널은 많은 automatic variables를 사용하고 다른 커널은 많이 사용하지 않을 수 있습니다. 레지스터를 블록들 간에 동적으로 분할하여 SM은 블록들이 더 적은 레지스터를 필요로 하면 더 많은 블록을 수용할 수 있고, 레지스터가 많이 필요하다면 더 적은 블록들을 수용할 수 있습니다. 그러나 레지스터 제약과 다른 리소스 제약 사이의 잠재적인 상호작용에 대해서 알아야 합니다.
행렬 곱셈 예제에서, 각 SM은 16,384개의 레지스터를 가지고 있고, 커널 코드가 스레드당 10개의 레지스터를 사용한다고 가정해봅시다. 만약 16x16 스레드 블록을 사용한다면, 각 SM에는 몇 개의 스레드가 수행될 수 있을까요 ?
답을 내기 위해서 먼저 각 블록마다 필요한 레지스터의 개수를 구해야하는데, 이 값은 10x16x16 = 2560입니다. 6개의 블록에서 필요한 레지스터의 개수가 16,384보다 적은 15,360개가 됩니다. 또 다른 블록을 추가하면 17,920개의 레지스터가 필요하여 최대 개수를 초과합니다. 따라서, 레지스터 제한사항으로 각 SM에서 1536개의 스레드를 가진 6개의 블록을 실행할 수 있으며, 이는 SM 당 8개의 블록 슬롯과 1536개의 스레드 슬롯의 제한 범위 내에 속합니다.
이제 커널의 두 개의 automatic 변수를 추가하여 사용되는 레지스터의 수를 12개로 증가했다고 가정해봅시다. 16x16 블록으로 동일하다면 각 블록은 이제 12*16*16 = 3072개의 레지스터가 필요합니다. 6개의 블록에서 요구되는 레지스터의 수는 18,432개로 일부 CUDA 하드웨어의 레지스터 제한을 초과합니다. CUDA 런타임 시스템은 각 SM에 할당된 블록의 수를 1 감소시켜서 레지스터의 수를 15,360개로 감소시킵니다. 하지만, 블록의 수가 1 감소하면 SM에서 실행되는 스레드의 수를 1536개에서 1280개로 감소시키게 됩니다. 즉, 2개의 automatic 변수를 추가로 사용하면서 프로그램에서 SM당 워프 병렬화가 1/6 감소하였습니다. 이렇게 리소스 사용량이 약간 증가하면서 병렬 처리 및 성능이 크게 저하될 수있는 상황을 성능 절벽(performance cliff)라고 합니다.
(리소스를 조금만 증가시켜도 성능이 현격히 감소하는 결과를 초래하기 때문)
공유 메모리 제한 사항
공유 메모리도 런타임에 동적으로 분할될 수 있는 또 다른 리소스입니다.
TILING 최적화 for 메모리 Access (tiled matrix multiplication)
위 포스팅에서 타일 알고리즘을 사용했는데, 타일 알고리즘은 종종 많은 양의 공유 메모리를 필요로 합니다. 하지만, 공유 메모리 사용량이 너무 많으면 SM에서 실행되는 스레드 블록의 수가 감소될 수 있습니다. 스레드 블록이 감소되어 스레드 병렬화가 감소하면 DRAM 시스템의 메모리 액세스 대역폭을 활용하는데 영향을 미칠 수 있고, 이는 메모리 액세스 처리량이 감소하여 스레드에서 수행되는 처리량이 감소할 수 있습니다.
결과적으로 타일 알고리즘의 성능을 저하시킬 수 있습니다.
Device Query
내가 사용하는 CUDA 디바이스의 리소스의 양이 궁금하면, device query를 통하여 확인해볼 수 있습니다. 이는 CUDA에서 샘플 코드로 제공하고 있으니, 샘플 코드를 참조하시면 됩니다. 저의 경우에는 지금까지 공부한 내용들로 이해가 되는 부분만 선택하여서 출력하도록 따로 코드를 작성했습니다.
https://github.com/junstar92/parallel_programming_study/blob/master/CUDA/deviceQuery/deviceQuery.cu
개인 노트북에서 사용하고 있는 GPU의 정보인데, 다음과 같습니다.
설치된 CUDA 버전, 디바이스 Compute Capability, 블록당 공유 메모리 사이즈 등 디바이스의 여러 가지 정보들을 확인할 수 있습니다. CUDA에서 제공되는 API를 사용하여 디바이스 정보를 쿼리하는데,
int device = 0;
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, device);
cudaDeviceProp 구조체에서 제공되는 정보들은 아래 링크에서 자세하게 확인할 수 있습니다.
https://docs.nvidia.com/cuda/cuda-runtime-api/structcudaDeviceProp.html#structcudaDeviceProp
꽤나 직관적으로 보여주고 있기 때문에 큰 문제없이 해석할 수 있을 거라고 생각합니다.
다음은 RTX 3080 디바이스의 정보입니다. CUDA에서 제공되는 샘플 코드로 쿼리해봤습니다.
'NVIDIA > CUDA' 카테고리의 다른 글
1D Convolution (CUDA Constant Memory) (1) | 2021.12.13 |
---|---|
부동소수점 (Floating-Point) (0) | 2021.12.11 |
Divergent Wraps (예제 : Sum Reduction) (0) | 2021.12.09 |
Global Memory 대역폭(bandwidth) 활용 (2) | 2021.12.08 |
TILING 최적화 for 메모리 Access (tiled matrix multiplication) (2) | 2021.12.06 |
댓글