DirectCompute의 스레드 적용 모형
DirectCompute에 관해 가장 먼저 살펴볼 것은 그 스레드 적용 모형과 실행 모형이다. GPU가 많은 수의 처리 코어들로 이루어져 있기 때문에 병렬 알고리즘 처리에 아주 적합하다는 점은 앞에서 이미 언급했다. 작업을 수행할 처리기가 아주 많기 때문에, 특정 알고리즘을 다수의 스레드들에 효과적으로 대응시키기 위한 적당한 방법론을 갖출 필요가 있다.
전통적인 CPU 기반 알고리즘에 쓰이는 전형적인 다중스레드는 스레드들이 각자 개별적으로 실행되되 공유 메모리 공간과 수동적인 동기화를 통해서 서로 의사소통하는 방법을 사용한다. 이는 다중 프로세서 시스템들에서 오랫동안 쓰였다.
그러나 이러한 모형은 수천 개의 스레드가 동시에 돌아가야 하는 처리 패러다임에 그리 잘 맞지 않는다. DirectCompute는 일반성과 사용 편의의 균형을 도모하는 또 다른 종류의 스레드 적용 및 실행 모형을 사용한다. 이 모형에서는 스레드들을 자료 요소에 사상(대응)시키기가 더 쉽다. 이 덕분에 하나의 처리 과제를 더 작은 여러 조각으로 분할해서 GPU에서 실행하는 것이 수월해진다.
핵(Kernel) 기반 처리
계산 셰이더 단계는 핵(Kernel) 기반 처리 시스템을 구현한다. 계산 셰이더 프로그램 자체는 하나의 함수이며, 그 함수는 일종의 핵함수(Kernel Function)처럼 작용한다. 작업의 단위는 알고리즘마다 다르지만, 현재 적재된 핵의 각 인스턴스가 각각의 단일한 입력 자료 집합을 처리하는 식으로 알고리즘이 실행된다는 점은 동일하다. 계산 셰이더에 주어지는 입력자료는 계산 셰이더 단계에 연결된 자원에서 온 것이다. 이러한 구조에서는 하나의 알고리즘이 GPU상의 수천 개의 스레드들에서 실행되도록 프로그래밍하기가 아주 쉽고 직관적이다. 모든 스레드가 같은 핵(Kernel)을 실행하므로, 스레드들의 행동을 일일이 동기화하는 데 힘을 빼는 대신 원하는 자료를 각자 독립적으로 처리될 자료 항목들로 분할하는 방법을 찾는 데 주력하면 된다.
작업의 배분
작업을 시작할 때 사용할 수 있는 메서드는 ID3D11DeviceContext::Dispatch() 또는 ID3D11DeviceContext::DispatchIndirect()이다.
이 함수들은 렌더링 파이프라인의 여러 그리기 메서드(Draw로 시작하는 함수)들에 비견할 수 있다.
Dispatch메서드는 부호 없는 정수 매개변수 3개를 받는데 이 세 매개변수는 원하는 처리 핵의 실행을 '배분(dispatch)' 할 스레드들의 그룹의 개수를 결정한다. 좀 더 구체적으로 이 세 매개변수는 인스턴스화할 스레드 그룹들을 담는 3차원 배열의 x, y, z 차원 크기로, 유효한 범위는 셋 모두 1에서 65535이다. 예를 들어 응용 프로그램이 Dispatch(4, 6, 2)를 호출했다면 4 x 6 x 2 = 48개의 스레드 그룹들이 만들어진다. 그 배열 안에서 각 스레드 그룹은 세 차원 색인(범위는 0 ~ 차원 크기 - 1)의 고유한 조합으로 이루어진 식별자로 식별된다.
이러한 배분 메서드를 호출할 때 지정하는 것은 인스턴스화할 스레드 그룹의 개수이지 개별 스레드들의 개수가 아님을 주의하기 바란다. 한 그룹당 인스턴스화될 스레드들의 개수는 계산 셰이더 프로그램의 주 함수 앞에서 numthreads라는 HLSL 함수 특성으로 지정한다. 배분 메서드에서처럼 이 함수 특성도 3차원 배열의 세 차원의 크기를 지정하나, 그 배열은 스레드 그룹들이 아니라 실제 스레드들을 담는 것이다.
각 차원의 크기는 셰이더 모형에 따라 다른데, cs_5_0의 경우 x와 y가 1이상이어야 하고 z는 반드시 1 ~ 64 범위 이어야 한다. 또한 그룹의 전체 스레드 개수(X * Y * Z)가 1024를 넘으면 안된다.
[numthreads(10, 10, 2)]
이 예에서는 각 스레드 그룹마다 총 10 x 10 x 2 = 200개의 스레드가 인스턴스화된다. 이 스레드들의 개별 스레드 역시 차원 색인 ( 0 ~ 차원 크기 - 1) 세 개의 고유한 조합으로 이루어진 식별자로 식별된다. 앞에서처럼 Dispatch(4, 6, 2)라고 호출한 경우, 계산 셰이더 단계 전체적으로 인스턴스화되는 스레드는 48 x 200 = 6400개이다.
스레드 식별 체계
앞에서 처럼 총 6400개의 스레드가 실행된다면 이 스레드들은 자신이 처리할 자료를 어떻게 알게 될까? 모든 인스턴스가 같은 핵(셰이더 프로그램)을 실행하므로, 셰이더 프로그램 소스 코드 자체에서 자료를 지정할 수는 없다. 따라서 처리할 자료 집합을 스레드에게 알려주는 다른 어떤 메커니즘이 필요하다.
앞에서 이야기한 기하학적 스레드 조직화 방식이 바로 그러한 메커니즘의 기반이 된다. 스레드들을 그렇게 조직화하는 데에는 다 이유가 있었던 것이다. 앞에서 보았듯이, 하나의 배분 호출에 관여하는 스레드 그룹들 중 특정 스레드 그룹을 3차원 좌표 형태로 지정하는 것은 아주 간단한 문제이며, 특정 스레드 그룹의 특정 스레드를 지정하는 것 역시 마찬가지로 간단하다.
결과적으로, 한 배분 호출의 모든 스레드를 각각 고유한 식별자로 간단하게 식별할 수 있다.
셰이더 프로그램을 작성할 때 개발자는 셰이더 함수의 입력 매개변수로 쓰일 일단의 시스템 값 의미소들을 지정할 수 있는데, 그런 시스템 값 의미소들 중에는 현재 실행되는 셰이더 프로그램 인스턴스(스레드)를 식별하는 데 사용할 수 있는 것들이 있다.
SV_GroupID : 이 배분 호출의 스레드 그룹들 중 현재 스레드가 속한 그룹의 3차원 식별자(uint3)
SV_GroupThreadID : 그 스레드 그룹 안에서의 현재 스레드의 3차원 식별자(uint3)
SV_DispatchThreadID : 전체 배분 안에서의 현재 스레드의 3차원 식별자(uint3)
SV_GroupIndex : 현재 스레드가 속한 스레드 그룹의 3차원 식별자를 1차원으로 직렬화한 색인(uint)
각 스레드의 실행마다 이러한 식별 정보가 주어지므로, 개발자는 이들을 이용해서 입력 자료 중 현재 스레드가 처리할 부분을 결정하도록 셰이더 프로그램을 작성하면 된다.
이 코드가 하는 일은 1차원 버퍼의 자원의 값을 두 배로 증가 시키는 계산 셰이더이다.
이 코드는 스레드 그룹의 크기를 numthreads 함수 특성으로 선언한다. 그 다음 부분은 계산 셰이더의 처리 핵에 해당하는 주 함수이다. 이 함수의 매개 변수는 앞에서 말한 스레드 식별용 시스템 값 의미소 특성들 중 하나이다.