Warp의 상위 스레드 그룹, 워프들의 집합.
하나의 블록에 포함된 각 스레드는 자신만의 고유한 스레드 번호 (Thread ID)를 가진다.
: 서로 다른 블록에 포함된 스레드들은 같은 스레드 번호를 가질 수 있다.
각 블록은 자신만의 고유한 block ID가 존재
: 스레드를 정확히 지칭하기 위해서는 block ID와 thread ID를 모두 사용해야 한다.
블록 내 스레드는 1차원, 2차원, 3차원 형태로 배치될 수 있다.
가장 상위 계층
: 블록들의 그룹.
그리드 내 블록 또한 1차원, 2차원, 3차원 형태로 배치될 수 있다.
커널이 호출되면 그리드가 생성.
: 하나의 그리드는 하나의 커널 호출과 1:1 대응.
블록의 형태 정보를 담고 있는 구조체.
: 각 차원의 크기를 담고 있다.
: 사용하지 않는 차원의 크기는 역시 1
커널이 실행될 때 그리드 및 블록 형태가 결정, 한 그리드 내 모든 블록은 동일한 형태를 가진다.
: blockDim
은 그리드 내 모든 스레드가 공유.
Warp는 연속된 32개의 스레드로 구성
: 연속성은 x, y, z 순으로 결정.
: 즉, 우선적으로 (0,0,0)~(31,0,0) 스레드가 하나의 워프를 구성한다.
: 만ㅇ갹 x차원의 길이가 워프의 크기보다 작다면, (0,0,0)~(0,31,0) 스레드가 하나의 워프를 이루게 된다.
커널의 성능에 큰 영향을 미치는 요소 중 하나인 메모리 접근 패턴을 이해하려면 워프를 구성하는 스레드를 정확히 인지하는 것이 중요.
: 워프 수준에서 스레드 사이의 작업을 분배할 때도 중요하다.
x차원 최대 길이는 2^31 -1
: 제한 없다고 생각해도 됨
y, z차원의 경우 최대 길이는 2^16-1
x, y 차원 최대 크기는 2^10, z차원 최대 크기는 2^6
한 블록이 가질 수 있는 최대 스레드의 개수는 1024개.
스레드 레이아웃
: 스레드의 배치 형태, 그리드와 블록의 형태로 정의
: 커널 호출 시 설정, <<<>>>
실행 구성 문법을 통해 전달.
kernel<<<그리드의 형태, 블록의 형태>>>()
: <<<1, n>>>
의 의미는 (1,1,1) 크기의 그리드를 사용, (n,1,1) 크기의 블록 사용.
dim3
: x, y, z 멤버 변수를 가지는 구조체, 각 차원의 크기를 담는 역할.
: 일반적으로 <<<>>>
안에 dim3
구조체를 사용해서 전달.
dim3 dimGrid(4, 1, 1);
dim3 dimBlock(8, 1, 1);
kernel<<<dimGrid, dimBlock>>>();
%%cu
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
__global__ void checkIndex(void)
{
printf("threadIdx : (%d %d %d) blockIdx : (%d %d %d) threadDim : (%d %d %d) gridDim : (%d %d %d)\n", threadIdx.x, threadIdx.y, threadIdx.z, blockIdx.x, blockIdx.y, blockIdx.z, blockDim.x, blockDim.y, blockDim.z, gridDim.x, gridDim.y, gridDim.z);
}
int main(void)
{
dim3 dimBlock(3, 1, 1);
dim3 dimGrid(2, 1, 1);
printf("dimGrid.x = %d dimGrid.y = %d dimGrid.z = %d\n", dimGrid.x, dimGrid.y, dimGrid.z);
printf("dimBlock.x = %d dimBlock.y = %d dimBlock.z = %d\n", dimBlock.x, dimBlock.y, dimBlock.z);
checkIndex<<<dimGrid, dimBlock>>>();
cudaDeviceSynchronize();
return 0;
}
dimGrid.x = 2 dimGrid.y = 1 dimGrid.z = 1
dimBlock.x = 3 dimBlock.y = 1 dimBlock.z = 1
threadIdx : (0 0 0) blockIdx : (1 0 0) threadDim : (3 1 1) gridDim : (2 1 1)
threadIdx : (1 0 0) blockIdx : (1 0 0) threadDim : (3 1 1) gridDim : (2 1 1)
threadIdx : (2 0 0) blockIdx : (1 0 0) threadDim : (3 1 1) gridDim : (2 1 1)
threadIdx : (0 0 0) blockIdx : (0 0 0) threadDim : (3 1 1) gridDim : (2 1 1)
threadIdx : (1 0 0) blockIdx : (0 0 0) threadDim : (3 1 1) gridDim : (2 1 1)
threadIdx : (2 0 0) blockIdx : (0 0 0) threadDim : (3 1 1) gridDim : (2 1 1)
: 각 스레드가 자신의 threadIdx와 blockIdx, blockDim, gridDim을 출력한다.
%%cu
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// The size of the vector
#define NUM_DATA 1024
// Simple vector sum kernel
__global__ void vecAdd(int* _a, int* _b, int* _c)
{
int tID = threadIdx.x;
_c[tID] = _a[tID] + _b[tID];
}
int main(void)
{
int* a, * b, * c, * hc;
int *da, *db, *dc;
int memSize = sizeof(int) * NUM_DATA;
printf("%d elements, memSize = %d bytes\n", NUM_DATA, memSize);
// memory allocation on the host_side
a = new int[NUM_DATA]; memset(a, 0, memSize);
b = new int[NUM_DATA]; memset(b, 0, memSize);
c = new int[NUM_DATA]; memset(c, 0, memSize);
hc = new int[NUM_DATA]; memset(hc, 0, memSize);
// Data generation
for (int i = 0 ; i < NUM_DATA; i++)
{
a[i] = rand() % 10;
b[i] = rand() % 10;
}
//Vector sum on the host
for (int i = 0 ; i < NUM_DATA ; i++)
hc[i] = a[i] + b[i];
// Memory allocation on te device-side
cudaMalloc(&da, memSize); cudaMemset(da, 0, memSize);
cudaMalloc(&db, memSize); cudaMemset(db, 0, memSize);
cudaMalloc(&dc, memSize); cudaMemset(dc, 0, memSize);
// Data copy : Host -> Device
cudaMemcpy(da, a, memSize, cudaMemcpyHostToDevice);
cudaMemcpy(db, b, memSize, cudaMemcpyHostToDevice);
// Kernel call
vecAdd <<<1, NUM_DATA>>> (da, db, dc);
// Copy results : Device -> Host
cudaMemcpy(c, dc, memSize, cudaMemcpyDeviceToHost);
// Release device Memory
cudaFree(da);
cudaFree(db);
cudaFree(dc);
// Check results
bool result = true;
for (int i = 0 ; i < NUM_DATA ; i++)
{
if (hc[i] != c[i])
{
printf("[%d] The result is not matched! (%d, %d)\n", i, hc[i], c[i]);
result = false;
}
}
if (result)
printf("GPU works well!\n");
// Release host memory
delete[] a;
delete[] b;
delete[] c;
return 0;
}
NUM_DATA
가 1024보다 크다면?
: 블록의 최대 thread수가 1024개이기 때문에, 스레드 레이아웃을 잡아줘야 한다.
: 블록을 2차원, 3차원으로 늘리는 방법은 쓸 수 없다. 최대 1024개의 스레드만 가질 수 있기 때문.
1차원 그리드, 1차원 블록을 사용한다고 가정해보자. 우리는 커널 vecAdd()
를 호출할 때 다음과 같이 호출해야 한다.
vecAdd <<<ceil(NUM_DATA/1024), 1024>>> (da, db, dc);
: ceil()
은 올림연산을 수행하는 함수.
NUM_DATA
가 4224인 경우
: 마지막 블록 뒤쪽의 스레드들은 담당할 데이터가 없다.
: 그 스레드들은 연산에 참여하지 않도록 하는 조치가 필요하다. (다음 장에서 배움)
: 스레드 레이아웃을 그림으로 그려보면 다음과 같다.
스레드 레이아웃을 적용한 상태에서 프로그램을 실행시키면 다음과 같다. NUM_DATA
는 1030으로 설정한 후 실행.
1030 elements, memSize = 4120 bytes
[1024] The result is not matched! (11, 0)
[1025] The result is not matched! (3, 0)
[1026] The result is not matched! (13, 0)
[1027] The result is not matched! (12, 0)
[1028] The result is not matched! (13, 0)
[1029] The result is not matched! (13, 0)
: 어디가 문제일까?
문제는 vecAdd()
커널에 있다. 다음은 3장에서 작성한 vecAdd()
커널이다.
// Simple vector sum kernel
__global__ void vecAdd(int* _a, int* _b, int* _c)
{
int tID = threadIdx.x;
_c[tID] = _a[tID] + _b[tID];
}
: 한 블록 내에서는 모든 스레드가 서로 다른 번호를 갖지만, 다른 블록에는 동일한 번호를 갖는 스레드가 있다.
: 이 커널 함수에는 블록을 고려하지 않았따.
: 스레드가 속한 블록 번호를 고려해 담당할 데이터의 번호를 결정해야함. 이를 스레드 인덱싱이라고 한다.