주의 : 이 문서는 초심자 튜토리얼이 아닙니다. 기본 개념 정도는 안다고 가정합니다. 초심자는 [ Vulkan Tutorial ] 이나 [ Vulkan Samples Tutorial ] 을 보면서 같이 보시기 바랍니다.

주의 : 허락받고 번역한 것이 아니라 언제든 내려갈 수 있습니다.

주의 : 번역이 개판이므로 이상하면 원문을 참고하세요.

원문 : Leveraging asynchronous queues for concurrent execution, gpuopen.



동시성( concurrency ) 및 그것을 저해하는 것에 대해서 이해하는 것은 최신( modern ) GPU 들을 최적화할 때 매우 중요합니다. DirectX 12 나 Vulkan 과 같은 최신 API 들은 태스크( task )들을 비동기적으로 스케줄링할 수 있는 기능을 제공하는데, 이는 상대적으로 적은 노력으로 GPU 를 더욱 많이 활용( utilization )할 수 있도록 해 줍니다.


Why concurrency is important


렌더링은 embarrassingly parallel( 역주 : 병행 작업을 처리하기 위해서 노력을 거의 할 필요가 없는 작업을 의미. 작업이 서로 독립적일 때 이런 상황이 발생함. 출처 : Embarrassingly parallel, Wikipedia ) 태스크입니다. 메시 내의 모든 삼각형들은 병행적으로 변환될( transformed ) 수 있으며, 겹치지 않는 삼각형들은 병행적으로 래스터화될( rasterized ) 수 있습니다. 결과적으로 GPU 들은 매우 많은 작업들을 병행적으로 수행할 수 있도록 설계됩니다. 예를 들어 Radeon Fury X GPU 는 64 개의 컴퓨트 유닛( compute unit, CU )들로 구성되어 있으며, 각각은 4 개의 Single-Instruction-Multiple-Data( SIMD ) 유닛들을 가지고 있으며, 각각의 SIMD 들은 64 개의 스레드들로 구성된 블락( block )을 실행합니다. 우리는 그 블락들을 "웨이브프런트( wavefront )" 라 부릅니다. 메모리 접근에 대한 지연( latency )는 쉐이더를 실행할 때 엄청난 스톨( stall )을 야기할 수 있는데, 이 지연을 줄이기( hide ) 위해서 10 개까지의 웨이브프런트들이 각 SIMD 에서 스케줄링될 수 있습니다.


실행중에 실제 웨이브프런트의 개수가 이론적인 최대치보다 작아지는 데는 몇 가지 이유가 존재합니다. 가장 일반적인 이유는 다음과 같습니다:


  • 셰이더는 많은 Vector General Purpose Register( VGPR ) 들을 사용합니다. 예를 들어 셰이더가 128 개 이상의 VGPR 을 사용한다면, 단 하나만의 웨이브프런트만이 SIMD 를 위해서 스케줄링될 수 있습니다( 그렇게 되는 이유와 그리고 셰이더가 실행할 수 있는 웨이브프런트들을 계산하는 방법에 대해 세부적으로 알고자 한다면, GPR 사용을 최적화하기 위해서 CodeXL 을 사용하는 방법에 대해서 다루는 기사를 참고하세요 ).
  • LDS 요구사항 : 만약 셰이더가 32KiB 의 LDS 를 사용하고 스레드 그룹당 64 개의 스레드들을 사용한다면, 이는 CU 당 동시에 스케줄링될 수 있는 웨이브프런트가 2 개밖에 안 된다는 것을 의미합니다( 역주 : LDS 는 Local Data Share 의 머리글자입니다. LDS 는 각 CU 를 위한 칩에 위치한 램을 의미합니다. 32 KiB 의 LDS 가 있을 때 CU 당 두 개의 LDS 만 쓸 수 있다고 한 건 다음과 같이 계산하기 때문입니다. 64(CU) * 4(SIMD) * 64(Thread) = 16384 = 16 KiB 이므로 32 KiB 일때 CU 당 2 개 인 것입니다 ).
  • 만약 컴퓨트 셰이더( compute shader )가 충분한 웨이브프런트를 생성하지 않거나, 스크린상에 몇 개 안 되는 픽셀만을 그리는 드로 콜( draw call )을 날리는 작은 지오메트리들이 많다면, 모든 CU 들을 포화상태로 만드는데 충분한 웨이브프런트들을 생성하기 위해 스케줄링되는 작업들이 별로 없을 것입니다.
  • 모든 프레임은 동기화 지점( sync point )를 가지고 있으며 올바른 렌더링 결과를 보장하기 위한 배리어( barrier )들을 가지고 있습니다. 이것들은 GPU 를 놀게 만듭니다( idle ).


비동기 컴퓨트는 그러한 GPU 리소스들에 다가가기 위해서 사용될 수 있으며, 그렇지 않으면 그 리소스들은 테이블에 그냥 남아 있게 될 것입니다.


아래에 있는 두 개의 이미지들은 일반적인 프레임에서 Radeon RX480 GPU 의 셰이더 엔진 중 하나에 대해 무슨 일이 발생하고 있는지를 가시화해주는 스크린샷입니다. 그 그래프들은 게임의 잠재적인 성능을 확인하기 위해서 AMD 내부적으로 사용하고 있는 도구를 통해서 생성되었습니다.


이미지의 위쪽에 있는 섹션들은 하나의 CU 내부의 서로 다른 부분들에 대한 활용도를 보여 줍니다. 아래쪽에 있는 섹션들은 서로 다른 셰이더 타입들을 위해 얼마나 많은 웨이브프런트들이 생성되었는지를 보여줍니다.


첫 번째 이미지는 G-Buffer 렌더링을 위해서 0.25 ms 까지 사용하는 것을 보여 줍니다. 위쪽 부분에서 GPU 는 매우 바빠 보입니다. 특히 익스포트 유닛( export unit )이 그렇습니다. 그러나 CU 내에 있는 어떠한 컴포넌트들도 완전히 포화상태가 아니라는 것에 주의를 기울일 필요가 있습니다.



두 번째 이미지는 깊이만 렌더링하는데 0.5 ms 를 사용하는 것을 보여 줍니다. 왼쪽 절반은 PS 를 사용하지 않으며, 이것은 CU 활용도를 매우 낮게 만듭니다. 중간쯤에서는 일부 PS 웨이브( 역주 : 웨이브프런트의 약자인듯 )들이 생성됩니다. 아마도 알파 테스트를 통해서 반투명 지오메트리를 렌더링하기 때문인 것으로 보입니다( 하지만 그런 그래프들에서 그 이유를 보여 주는 것은 아닙니다 ). 오른쪽 1/4 에서는 생성되는 전체 웨이브 개수가 0 으로 떨어집니다. 이는 렌더타깃들이 다음 드로 콜들에서 텍스쳐로서 사용되기 때문에 GPU 가 기존 태스크들이 끝날 때까지 대기하기 때문일 수 있습니다.



Improved performance through higher GPU utilization


위의 이미지들에서 볼 수 있듯이, 일반적인 프레임에서 GPU 리소스들이 많이 놀고 있습니다. 새로운 API 들은 GPU 에서 태스크가 스케줄링되는 방식을 개발자가 더 많이 제어할 수 있는 방법을 제공하도록 설계되어 있습니다. 차이가 하나 있다면, 거의 대부분의 콜들은 묵시적으로 독립된 것이라는 가정을 깔고 있다는 것입니다. 드로 연산이 이전의 결과에 언제 의존하게 되느냐와 같은 정확성을 보장하기 위해서 배리어를 지정하는 것은 개발자의 책임하에 있습니다. 배리어에 대한 배칭( batching )을 강화하기 위해서 작업들( workloads )을 섞음( shuffling )으로서, 응용프로그램들은 GPU 활용도를 높일 수 있고, 각 프레임에서 배리어를 위해 소요되는 GPU idle time 을 줄일 수 있습니다( 역주 : lock 이 덜 걸리게 독립적인 작업들을 잘 분류한다는 것을 의미 ).


GPU 활용도를 높이는 추가적인 방법은 비동기 컴퓨트( asynchronous compute )입니다: 프레임의 특정 위치에서 다른 작업들과 함께 컴퓨트 셰이더를 순차적으로 실행하는 대신에, 비동기 컴퓨트는 다른 작업들과 동시에 실행되는 것을 허용됩니다. 이는 위의 그래프들에서 보이는 일부 간격들을 채울 수 있으며 추가적인 성능 향상을 제공합니다.


개발자로 하여금 어떤 작업들을 병행적으로 실행할 수 있는지 지정할 수 있도록 하기 위해서, 새로운 API 들은 응용프로그램이 태스크를 스케줄링할 수 있는 다중의 큐들을 정의할 수 있도록 합니다.


세 종류의 큐가 존재합니다:


  • Copy Queue ( DirectX 12 ) / Transfer Queue ( Vulkan ) : PCIe 버스를 통해 데이터를 전송하는 DMA( 역주 : Direct Memory Access ).
  • Compute Queue ( DirectX 12 와 Vulkan ) : 컴퓨트 셰이더를 실행하거나 데이터를 복사하는데, 로컬 메모리를 선호합니다.
  • Direct Queue ( DirectX 12 ) / Graphics Queue ( Vulkan ) : 이 큐는 아무 일이나 수행할 수 있어서, 기존 API 들의 메인 디바이스와 유사합니다.


응용프로그램은 동시성 활용을 위해서 다중의 큐를 생성할 수 있습니다: DirectX 12 에서는 임의의 개수의 큐가 각각의 타입을 위해서 생성될 수 있지만, Vulkan 에서는 드라이버가 지원되는 큐의 개수를 열거해 줄 것입니다.


GCN 하드웨어는 단일 지오메트리 프런트엔드를 포함하고 있어서, DirectX 12 에서 다중의 direct queue 를 생성하더라도 추가적인 성능 향상이 존재하지는 않을 것입니다. Direct queue 에 스케줄링된 모든 커맨드 리스트들은 같은 하드웨어 큐에 직렬화될 것입니다. GCN 하드웨어는 다중 컴퓨트 엔진을 지원하지만, 하나 이상의 컴퓨트 큐를 응용프로그램에서 사용한다고 해서 특별한 성능 향상을 확인할 수는 없습니다. 하드웨어가 지원하는 것보다 더 많은 큐를 생성하지 않는 것이 일반적으로 중요합니다. 그래야 커맨드 리스트 실행에 대한 더 직접적인 제어가 가능합니다.


Build a task graph based engine


어떤 작업이 비동기적으로 스케줄링되어야 하는지 어떻게 결정할까요? 프레임은 태스크에 대한 그래프로 간주되어야 합니다. 여기에서 각 태스크는 다른 태스크들에 대한 의존성을 가지고 있습니다. 예를 들어 여러 개의 섀도우 맵들은 독립적으로 생성될 수 있습니다. 그리고 이것들은 섀도우 맵을 입력으로 사용하여 Variance Shadow Map( VSM )을 생성하는 컴퓨트 셰이더를 사용하는 처리 단계를 포함할 수 있겠죠. 그림자가 드리워진 광원을 동시에 처리하는 타일 기반 라이팅 셰이더도 모든 섀도우 맵들이 만들어지고 G-Buffer 의 처리가 끝난 후에 시작될 수 있습니다. 이 경우 VSM 생성은 다른 섀도우 맵들이 렌더링되고 있는 동안 실행되거나 G-Buffer 를 렌더링하는 동안 배치될( batched ) 수 있습니다.


이와 유사하게, Ambient Occlusion 은 깊이 버퍼에 의존합니다. 하지만 섀도우나 타일 기반 라이팅에는 독립적이죠. 그러므로 비동기 컴퓨트 큐에서 실행하는 것도 좋은 선택입니다.


게임 개발자들이 비동기 컴퓨트의 이점을 취할 수 있는 최적의 시나리오를 찾을 수 있게 도왔던 경험에서, 수동으로 태스크를 병행적으로 실행하도록 지정하는 것이 이 처리를 자동화하려고 시도하는 것보다 더 낫다는 것을 발견했습니다. 컴퓨트 태스크만이 비동기적으로 스케줄링되기 때문에, 가능한 한 많은 렌더링 작업에 대해 컴퓨트 패스를 구현하는 것을 추천합니다. 그래야지 어떤 태스크들이 실행중에 겹치게 되는지를 결정하는데 있어서 더 많은 자유도를 가질 수 있게 됩니다.


마지막으로 작업을 컴퓨트 패스로 옮길 때, 응용프로그램은 각 커맨드 리스트들이 충분히 커지도록 해야 합니다. 이는, 커맨드 리스트를 쪼개는 비용과 서로 다른 큐에서 태스크를 동기화하는 연산에 요구되는 펜스( fence ) 상에서의 스톨을 만회해서, 비동기 컴퓨트로부터 성능 향상을 얻을 수 있도록 해 줄 것입니다.


How to check if queues are working expected


응용프로그램에서 비동기 큐들이 원하는 대로 동작하고 있는지 확실하게 하기 위해서는 GPUView 를 사용하는 것을 추천합니다. GPUView 는 어떤 큐들이 사용되고 있는지, 얼마나 많은 작업들이 각 큐에 포함되어 있는지, 그리고 가장 중요하게는 작업들이 실제로 서로에 대해서 병행적으로 실행되고 있는지를 가시화해 줍니다.


Windows 10 환경에서, 거의 대부분의 응용프로램들은 적어도 하나의 3D 그래픽스 큐와 카피 큐를 보여 줄 것입니다. 카피 큐는 페이징( paging )을 위해 Windows 에 의해 사용됩니다. 아래 이미지에서, GPU 에 데이터를 업로드하기 위한 추가적인 카피 큐를 사용하는 응용프로그램의 한 프레임을 볼 수 있습니다. 렌더링을 시작하기 전에 데이터를 스트리밍하고 동적 상수 버퍼를 업로드하기 위해 카피 큐를 사용하는, 개발중인 게임에서 가지고 온 것입니다. 이 게임 빌드에서 그래픽스 큐는 렌더링을 시작하기 전에 복사가 완료될 때까지 대기할 필요가 있습니다. 아래 그림에서도 보이듯이, 카피 큐는 복사를 시작하기 전에 이전 프레임의 렌더링이 완료될 때까지 대기합니다:



이 경우에, 카피 큐를 사용하는 데는 어떠한 성능 이점도 없습니다. 왜냐하면 업로드된 데이터 상에서 더블 버퍼링이 구현되어 있지 않기 때문입니다. 데이터에 대해 더블 버퍼링이 수행된 후에, 그제서야 업로드가 발생할 것입니다. 그 동안에 이전 프레임은 여전히 3D 큐에 의해서 처리되고 있는 중이며, 3D 큐에서의 그 차이( gap )가 제거됩니다. 이러한 변경은 전체 프레임 시간을 거의 10 % 정도 줄여줍니다.


두 번째 예는 컴퓨트 큐를 엄청나게 사용하는 게임인 Ashes of the Singularity 의 벤치마크 씬에서의 두 프레임을 보여 줍니다.



비동기 컴퓨트 큐가 프레임의 대부분을 위해서 사용됩니다. 그래픽스 큐가 컴퓨트 큐를 기다리기 위해서 스톨되지 않는 것을 보실 수 있습니다. 그것은 비동기 컴퓨트가 성능 향상을 제공하는 가장 좋은 수단임을 확인할 수 있는 좋은 시작점입니다.


What could possibly go wrong?


비동기 커퓨트를 사용할 때, 서로 다른 큐에 존재하는 커맨드 리스트들이 병행적으로 실행됨에도 불구하고 그것들은 여전히 같은 GPU 리소스들을 공유할 수 있다는 데 주의할 필요가 있습니다.


  • 만약 리소스들이 시스템 메모리에 존재한다면, 그래픽스 큐나 컴퓨트 큐에서 그것들에 접근하는 것은 DMA 큐 성능 등에 영향을 줄 수 있습니다.
  • 로컬 메모리에 접근( 예를 들어, 텍스쳐 패칭( fetching ), UAV 에 대한 쓰기, rasterization-heavy task 수행 )하는 그래픽스 큐나 컴퓨트 큐는 대역폭 한계 때문에 서로에게 영향을 줄 수 있습니다.
  • 같은 CU 를 공유하는 스레드는 GPR 과 LDS 를 공유하게 되며, 그래서 이용가능한 모든 리소스들을 사용하는 태스크들은 같은 CU 상에서 수행되는 비동기 작업들을 방해할 수 있습니다.
  • 서로 다른 큐들은 그것들의 캐시들을 공유합니다. 만약 다중 큐가 같은 캐시를 활용한다면, 이는 더 많은 캐시가 버려지거나( trashing ) 성능을 저하시키는 결과를 산출할 수 있습니다.


Due to the reasons above it is recommended to determine bottlenecks for each pass and place passes with complementary bottlenecks next to each other( 역주 : 병목을 없애기 위해서는 동시에 처리해야 할 패스를 잘 선택해서 보완해야 한다는 의미인듯 ):


  • DLS 와 ALU 를 심하게 사용하는 컴퓨트 셰이더는 보통 비동기 컴퓨트 큐를 위한 좋은 후보입니다.
  • 깊이만 그리는 렌더링 패스들은 보통 그것 다음에 실행하는 컴퓨트 태스크를 가지고 있는 좋은 후보입니다.
  • 효율적인 비동기 컴퓨트를 위한 일반적인 해결책은 프레임 N 의 포스트 프로세싱과 프레임 N+1 의 섀도우 맵 렌더링을 겹치는 것입니다.
  • 가능한 한 많은 프레임들을 컴퓨트로 포팅( porting )하면 다음에 스케줄링될 수 있는 태스크들을 시험할 때 더욱 유연한 결과를 산출할 것입니다.
  • 태스크를 서브 태스크들로 나누고 삽입하는 것은 배리어를 줄이고 효율적인 비동기 컴퓨트를 위한 기회를 창출할 것입니다( 예를 들어 "각 광원을 위해 섀도우 맵을 클리어하고, 섀도우를 렌더링하고, VSM 을 계산하는 것" 대신에 "모든 섀도우 맵을 클리어하고, 모든 섀도우 맵을 렌더링하고, 모든 섀도우 맵을 위해 VSM 을 계산하는 것" ). 

비동기 컴퓨트는 적합하게 사용되지 않을 때는 성능을 저하시킨다는 점에 주의해야만 합니다. 이런 경우를 피하기 위해서는, 각 태스크들을 위해서 비동기 컴퓨트 사용이 쉽게 활성화되거나 비활성화될 수 있도록 하는 것을 권장합니다. 이는 여러분이 성능 이점을 측정하고 응용프로그램이 여러 하드웨어에서 최적으로 실행될 수 있도록 해 줄 것입니다.


Stephan Hodes is a member of the Developer Technology Group at AMD. Links to third party sites are provided for convenience and unless explicitly stated, AMD is not responsible for the contents of such linked sites and no endorsement is implied.

+ Recent posts