주의 : 답이 틀릴 수도 있습니다. 그냥 정리하는 용도로 올립니다. 혹시라도 도움이 필요한 분이 있다면 도움이 되었으면 좋겠네요.

경고 : 숙제하려고 베끼는 데 사용하지 마십시오. 본인의 미래를 망칠 뿐입니다. 나중에 저를 원망하지 마세요.

부탁 : 문제 풀이가 잘못되었으면 지적해 주셨으면 좋겠습니다.



수직으로 세운 막대의 두 곳에 질량이 m 인 물체를 끈으로 연결하고, 중심축으로 중심으로 막대를 회전시킨다고 한다. 끈의 질량은 무시하고, 끈과 막대는 한 변의 길이가 a 인 정삼각형을 이룬다. 윗 부분 끈에 작용하는 장력 세기는 T1 이라고 한다.



(a) 아랫부분 끈에 작용하는 장력 세기는 얼마인가?


(b) 물체에 작용하는 알짜 힘의 세기와 방향을 구하라.


(c) 물체의 빠르기는 얼마인가?



원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/the-mali-gpu-an-abstract-machine-part-4---the-bifrost-shader-core

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

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


최근에 Mali Bifrost 아키텍쳐 패밀리인 Mali-G71 에 대해 발표했습니다. 그것의 전체적인 렌더링 모델은 이전의 Mali GPU 들과 유사한데요, - Bifrost 패밀리는 여전히 매우 파이프라인화되어 있는 타일 기반 렌더러입니다 - 원래의 "Abstract Machine" 블로그 시리즈를 따라가기 위해서 요구되는 프로그래머블( programmable ) 셰이더 코어에서 충분한 변화가 있었습니다.

이 블로그에서 필자는 전형적인 Bifrost 셰이더 코어의 블록 레벨 아키텍쳐를 소개하고, 애플리케이션 개발자가 그 하드웨어를 사용했을 때 기대할 수 있는 성능 향상에 대해서 설명할 것입니다. 그런 성능향상은 칸텐트를 최적화하고 DS-5 Streamline 과 같은 툴들을 통해서 노출되는 하드웨어 성능 카운터들을 이해할 때 올 수 있습니다. 이 블로그에서는 여러분이 이 시리즈의 처음 두 파트를 읽었다고 가정합니다. 만약 읽지 않았다면 읽고 오세요.

GPU Architecture

Bifrost GPU 의 탑레벨 아키텍쳐는 이전의 Midgard GPU 에서와 동일합니다.

The Shader Cores

Midgard 와 마찬가지로 Bifrost 도 통합 셰이더 코어 아키텍쳐입니다. 이는 단일 클래스의 셰이더 코어가 설계에 존재하는 모든 종류의 셰이더 프로그램과 컴퓨트 커널들을 실행하는 기능을 가졌다는 것을 의미합니다.

셰이더 코어의 개수는 특정 실리콘 칩마다 다릅니다; 우리 파트너들은 성능 요구와 실리콘 면적 제한에 기반해 셰이더 코어의 개수를 선택합니다. Mali-G71 GPU 는 저사양 디바이스를 위해 하나의 코어부터 고성능 설계를 위해 32 개의 코어까지 다양합니다.

Work Dispatch

GPU 를 위한 그래픽스 작업은 큐의 쌍으로 제출됩니다. 하나는 버텍스/타일링/컴퓨트 워크로드를 위한 것이며, 다른 하나는 프래그먼트 워크로드를 위한 것입니다. 모든 작업은 각 큐에 단일 서브미션( submission )으로서 제출된( submit ) 하나의 렌더타깃을 위한 것입니다.

각 큐의 워크로드는 더 작은 조각으로 쪼개져 GPU 안의 모든 셰이더 코어들에 분배됩니다. 타일링의 경우에는 고정 함수 타일링 유닛에 분배됩니다. 두 큐로부터의 워크로드들은 동시에 셰이더 코어에 의해 처리됩니다; 예를 들어, 서로 다른 렌더 타깃을 위한 버텍스 처리와 프래그먼트 처리가 병렬적으로 실행될 수 있습니다( 파이프라인 방법론에 대한 세부사항을 원한다면 첫 번째 블로그를 확인하세요 ).

Level 2 Cache and Memory Bandwidth

시스템 내의 처리 유닛들은 성능을 증진시키고 반복되는 데이터 페치( fetch )에 의해 발생하는 메모리 대역폭을 줄이기 위해 L2 캐시를 공유합니다. L2 캐시의 크기는 우리 실리콘 파트너들에 의해 그들의 요구에 맞게 구성됩니다. 하지만 일반적으로는 GPU 의 셰이더 코어당 64 KB 입니다.

메인 메모리에 대한 GPU 외부의 버스 포트의 개수는, 그리고 그에 의한 이용가능한 메모리 대역폭은 구현된 셰이더 코어의 개수에 의존합니다. 일반적으로, 우리는 클락 당 코어 당 32 비트 픽셀을 쓸 수 있도록 하는데, 클락 사이클 당 ( 읽기와 쓰기를 위해 ) 256 비트 메모리 대역폭을 가질 수 있도록 8 코어 설계를 하는 것이 합리적입니다. AXI 포트의 개수는 Midgard 보다 증가했으며, 다운스트림( downstream ) 메모리 시스템이 그것을 지원할 수 있다면, 클락 당 더 높은 최대 대역폭에 접근할 수 있도록 하기 위해 12 개가 넘는 코어들과 함께 더 크게 구성되었습니다.

이용가능한 메모리 대역폭은 GPU( 주파수, AXI 포트 폭 ) 와 다운스트림 메모리 시스템( 주파수, AXI 데이터 폭, AXI 지연 )에 모두 의존한다는 점에 주의하십시오. 많은 설계들에서 AXI 클락은 GPU 클락보다 더 낮을 겁니다. 그러므로 이론적인 GPU 대역폭을 실제로 이용할 수는 없습니다.

The Bifrost Shader Core

Mali 셰이더 코어는 프로그래머블 코어를 둘러싼 여러 개의 고정 함수 하드웨어 블록으로써 구조화되어 있습니다. 프로그래머블 코어는 Bifrost GPU 패밀리에서 가장 크게 변화된 영역입니다. 시리즈의 이전 블로그에서 언급한 Midgard "Tripipe" 설계에서 많은 변경이 있었습니다:

Bifrost 프로그래머블 실행( Execution ) 코어는 하나 이상의 실행 엔진들 -- Mali-G71 의 경우에는 세 개 -- 과 여러 개의 공유 데이터 처리 유닛들로 구성되는데, 이것들은 메시징 구조( messaging fabric )으로 연결되어 있습니다.

The Execution Engines

실행 엔진은 실제로 프로그래머블 셰이더 명령들을 실행할 책임이 있습니다. 각 명령은 단일 복합( composite ) 산술( arithmetic ) 처리 파이프라인과 실행 엔진이 처리 중인 스레드를 위해 요구되는 스레드 상태를 모두 포함합니다.

The Execution Engines: Arithemtic Processing

Bifrost 산술 유닛은 기능 유닛 활용도( functional unit utilization )를 향상시키기 위해 쿼드 벡터화 체계( quad-vectorization scheme )를 구현합니다. 스레드들은 4 개의 번들( bundle )로 그룹화되어 있으며, 이를 쿼드라 부르는데, 각 쿼드들은 128 비트 데이터 처리 유닛의 너비를 채웁니다. 단일 스레드의 관점으로에서 볼 때, 이 아키텍쳐는 스칼라 32 비트 연산의 스트림처럼 보입니다. 이는 셰이더 컴파일러를 위한 상대적으로 직관적인 forward task 의 높은 활용도를 성취할 수 있도록 만들어 줍니다. 아래의 예제는 vec3 산술 연산이 순수한 SIMD 유닛에 매핑되는 방식을 보여 줍니다( 파이프라인은 클락 당 한 스레드를 실행합니다 ):

쿼드 기반 유닛과 비교하면 다음과 같습니다( 파이프라인이 클랑 당 네 개의 스레드를 위해 스레드 당 한 레인( lane )을 실행합니다 ):

프로그램에서의 벡터 길이와는 무관하게 유용한 작업으로 하드웨어 유닛을들을 꽉 차게 유지하도록 만드는 기능의 이점들이 이들 다이어그램에서 명확하게 강조되어 있습니다. 데이터가 32 비트 타입보다 더 작아짐으로 인해서 제공되는 성능 향상과 전력 효율성은 모바일 디바이스에서 여전히 매우 중요합니다. 그러므로 Bifrost 는 int8, int16, fp16 데이터 타입들을 위한 네이티브 서포트( native support, 역주 : 애플리케이션 실행 환경에 이미 존재하는 기능 )를 유지합니다. 이 타입들은 데이터 유닛의 128 비트 데이터 너비를 채우기 위해서 패킹( packed )될 수 있습니다. 그러므로 단일 128 비트 수학 유닛은 8x fp16/int16 연산들을 클락 사이클 당 한 번 처리할 수 있으며 16x int8 연산들을 파이클 당 한 번 처리할 수 있습니다.

The Execution Engines: Thread State

복잡한 프로그램의 성능과 성능 확장성( scalability )을 향상시키기 위해서, Bifrost 는 셰이더 프로그램들이 사용하기 위한 다소 큰 범용 목적( general-purpose ) 레지스터( register ) 파일을 구현합니다. Mali-G71 은 64x32 비트 레지스터들을 제공하는데, GPU 가 스레드를 최대로 점유하는 것을 허용하며, [ ARM Mali Compute Architecture Fundamentals ] 에서 설명한 레지스터 파일 용례( usage ) 와 스레드 카운트 사이에 존재하던 이전의 트레이드 오프( trade off )를 제거했습니다.

OpenGL ES 유니폼( uniform ) 상수와 Vulkan 의 푸시( push ) 상수를 저장하기 위해서 사용되는 빠른 상수 저장소( constant storage )의 크기가 증가되었습니다. 이는 프로그램이 상수 저장소를 대량으로 사용할 때 발생하는 캐시 접근 부하를 줄여 줍니다.

Data Processing Unit: Load/Store Unit

로드/스토어 유닛은 ( 텍스쳐를 제외한 ) 모든 범용 목적 메모리 접근을 다룹니다. 버텍스 애트리뷰트( attribute ) 페치, 베어링( varying, 역주 : 버텍스 버퍼 출력이자 프래그먼트 버퍼 입력인 변수 ) 페치, 버퍼 접근, 스레드 스택 접근 등을 포함합니다. 이는 코어 당 16 KB L1 데이터 캐시를 포함하는데, L1 캐시는 공유 L2 캐시에 의해 뒷받침됩니다.

로드/스토어 캐시는 클락 사이클 당 단일 64 바이트 캐시 라인에 접근할 수 있으며, 스레드 쿼드 사이에서의 접근은 요구되는 고유( unique ) 캐시 접근 요청들을 줄이기 위해 최적화됩니다. 예를 들어, 네 개의 스레드가 같은 캐시라인의 데이터에 접근하고 있다고 할 때, 그 데이터는 단일 사이클에 반환될 수 있습니다.

이 로드/스토어 병합 기능은 일반적인 OpenCL 컴퓨트 커널들에서 찾아 볼 수 있는 데이터 접근 패턴들을 매우 가속화해 줄 수 있습니다. 이는 일반적으로 제한된 메모리 접근이며, 알고리즘 설계에서 그것의 활용을 최대화하는 것이 핵심적인 최적화 목표입니다. 또한 Mali 산술 유닛들이 스칼라임에도 불구하고, 데이터 접근 패턴은 잘 작성된 벡터 로드를 통해 여전히 이점을 얻을 수 있으며, 그래서 여전히 가능할 때마다 벡터화된 셰이더와 커널 코드를 작성하는 것이 좋습니다.

Data Processing Unit: Varying Unit

베어링 유닛은 전용 고정 함수 베어링 보간기( dedicated fixed-function varying interpolator )입니다. 그것은 프로그래머블 산술 유닛과 같은 최적화 전략을 구현합니다; 그것은 스레드 쿼드 사이의 보간값을 벡터화해서 기능 유닛 활용도를 보장하며, 더 빠른 fp16 최적화를 지원합니다.

이 유닛은 클락 당 쿼드 당 128 비트를 보간할 수 있습니다; 즉, mediump ( fp16 ) vec4 를 보간하는 데는 네 스레드 쿼드 당 2 사이클이 걸립니다. 베어링 변수 벡터 길이를 최소화하기 위한 최적화와 fp32 보다는 fp16 을 공격적으로 사용하면 결과적으로 애플리케이션 성능을 향상시킬 수 있습니다.

Data Processing Unit: ZS/Blend

ZS 와 블렌드 유닛은 모든 타일 메모리 접근을 다룰 책임이 있으며, 둘 다 뎁스/스텐실 테스트나 컬러 블렌딩과 같은 내장 OpenGL ES 연산들을 위한 것입니다. 또한 그 타일 버퍼에 프로그램이 접근하는데 필요한 기능들은 다음과 같습니다:

LS 파이프가 타일 버퍼 접근, 베어링 보간, 로드/스토어 캐시 접근을 제어하는 모놀리식( monolithic ) 파이프라인이었던 Midgard 의 설계와는 다르게, Bifrost 는 세 개의 더 작고 더 효율적인 병렬 데이터 유닛을 구현했습니다. 이는 그 타일 버퍼 접근이 베어링 보간을 위해 비동기적으로 실행될 수 있다는 것을 의미합니다. 예를 들면, 그래픽스 알고리즘이 프로그램에서 타일 버퍼 접근을 사용하고 있다면 처리중인 리소스들에 대한 경쟁이 줄어드는 것을 확인할 수 있을 것입니다. Midgard 에서는 LS 파이프라인이 매우 무거운 경향이 있었습니다.

Data Processing Unit: Texture Unit

텍스쳐 유닛은 모든 텍스쳐 접근을 구현합니다. 그것은 코어 당 16 KB L1 데이터 캐시를 포함하며, 공유 L2 캐시에 의해 뒷받침됩니다. Mali-G71 에서는 이 블록의 아키텍쳐 성능이 이전 Midgard GPU 에서와 동일합니다; 클락 당 하나의 바이리니어( bilinear ) 필터링된 텍셀( GL_LINEAR_MIPMAP_NEAREST )을 반환합니다. 예를 들어 바이리니어 텍스쳐 룩업( lookup )을 네 개의 스레드 쿼드의 각 스레드에서 보간하는 데는 4 사이클이 걸립니다.

어떤 텍스쳐 접근 모드들은 데이터를 생성하기 위해서 다중 사이클을 요구합니다:

  • 트라이리니어( trilinear ) 필터링( GL_LINEAR_MIPMAP_LINEAR )은 텍셀당 두 개의 바이리니어 샘플들을 요구하며, 텍셀 당 2 사이클이 필요합니다.
  • 볼륨( volumetric ) 3D 텍스쳐는 2D 텍스쳐보다 두 배의 사이클을 요구합니다; 즉, 트라이리니어 필터링된 3D 텍스쳐는 4 사이클을 요구합니다. 바이리니어 필터링된 3D 텍스쳐는 2 사이클을 요구합니다.
  • 와이드( wide ) 유형의 텍스쳐 포맷( 컬러 채널 당 16 비트 이상 )은 픽셀 당 여러 개의 사이클을 요구할 수 있습니다.

Bifrost 에서 새롭게 최적화된 기능인데, 와이드 포맷 규칙의 예외는 뎁스 텍스쳐 샘플링입니다. 일반적으로 셰도우 매핑 기술이나 디퍼드 라이팅 알고리즘에 의해서 사용되는 DEPTH_COMPONENT16 이나 DEPTH_COMPONENT24 텍스쳐들로부터 샘플링하는 것은 최적화되었으며, 이제 단일 사이클 룩업입니다. 이는 Midgard 패밀리 GPU 에서보다 상대적으로 두 배의 성능을 달성합니다.

The Bifrost Geometry Flow

셰이더 코어 변경에 더해, Bifrost 는 새로운 인덱스 주도 버텍스 셰이딩( Index-Driven Vertex Shading, IDVS ) 지오메트리 처리 파이프라인을 소개합니다. 이전의 Mali GPU 들은 타일링 전에 버텍스 셰이딩을 모두 처리했고, 보통 이는 삼각형을 컬링( cull, 프러스텀 바깥인 경우나 면 방향( facing ) 테스트 실패했을 때 자름 )하기 위해서만 사용되는 베어링과 비교했을 때 계산과 대역폭을 낭비하는 결과를 낳았습니다.

IDVS 파이프라인은 버텍스 데이터를 절반으로 나눕니다; 하나는 위치를 처리하는데 사용되고, 다른 하나는 남은 베어링을 처리하는 데 사용됩니다:

이 플로우는 두 개의 중요한 최적화를 제공합니다:

  • 인덱스 버퍼가 먼저 읽히고, 버텍스 셰이딩은 인덱스 버퍼에 의해서 참조되고 있는 버텍스가 하나 이상 존재하는 버텍스들의 배치( batch )만이 제출( submit )됩니다. 이는 인덱스 버퍼에 있는 공간적 공백( gap )을 버텍스 셰이딩이 스킵할 수 있도록 해 줍니다.
  • 베어링 셰이딩은 clip-and-cull 단계에서 살아 남은 프리미티브들에 대해서만 제출됩니다; 이는 컬링된 삼각형에 대해서만 기여하고 있는 버텍스에 대한 중복 계산과 대역폭을 상당히 감소시켜 줍니다.

Bifrost 지오메트리 플로우를 최대한 활용하려면 패킹된 버텍스 버퍼들을 부분적으로 디인터리빙( deinterleave )하는 것이 좋습니다; 포지션( position )에 기여하는 애트리뷰트들을 하나의 패킹된 버퍼에 배치하고, 포지션 베어링에 기여하지 않는 애트리뷰트들은 다른 패킹된 버퍼에 배치합니다. 이는 화면에 기여하지 않거나 컬링된 버텍스들에 대해서는 비(non)-포지션 베어링들이 캐시에 올라가지 않는다는 것을 의미합니다. 필자의 동료인 stacysmith 는 이러한 유형의 지오메트리 처리 파이프라인을 이용하기 위해 버퍼 패킹을 최적화하는 것에 대한 블로그를 작성했습니다.

Performance Counters

이전의 Midgard GPU 와 마찬가지로, Bifrost 하드웨어도 수많은 성능 카운터들을 제공해 애플리케이션 개발자들이 애플리케이션을 프로우파일링하고 최적화할 수 있도록 하고 있습니다. Bifrost 아키텍쳐에 대해 애플리케이션 개발자들이 이용할 수 있는 성능 카운터에 대한 더 많은 내용을 확인하세요.

댓글과 의견을 항상 환영합니다.

Cheers,

Pete.

원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/the-mali-gpu-an-abstract-machine-part-3---the-midgard-shader-core

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

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


( 역주 : 2016 년 4 분기에 나온 Mali-G51 부터는 Bifrost 셰이더 코어입니다. Mali-T 로 시작하는 것이 Midgard 이고, Mali-$(Number)-MP 가 Utgard 입니다. 이 시리즈의 Part 4 에서는 Bifrost 에 대해 다룹니다 )

시리즈의 처음 두 블로그들에서, 필자는 Mali GPU 들에 의해 사용되는 프레임 수준 파이프라이닝과 타일 기반 렌더링 아키텍쳐에 대해서 소개했습니다. 이는 애플리케이션의 성능을 최적화할 때 그래픽스 스택의 동작을 설명하기 위해서 사용될 수 있는 멘탈 모델을 개발자들에게 제공하려는 목적으로 작성되었습니다.

이 블로그에서, 필자는 이 추상 머신의 생성을 완료하고, 최종 요소를 형성할 것입니다: 틀에박힌( stereotypical ) Mali "Midgard" GPU programmable core. 이 블로그는 여러분이 처음 두 파트들을 읽고 왔다고 가정합니다. 그러므로 읽지 않았다면 먼저 읽고 오시기 바랍니다.

GPU Architecture

Mali GPU 의 "Midgard" 패밀리들( Mali-T600, Mali-T799, Mali-T800 )은 통합( unified ) 셰이더 코어 아키텍쳐를 사용합니다. 이는 설계상으로 단일 유형의 셰이더 코어만이 존재한다는 것을 의미합니다. 이 단일 코어는 모든 유형의 프로그래머블( programmable ) 셰이더 코어를 실행하는데, 버텍스 셰이더, 프래그먼트 셰이더, 컴퓨트 커널들을 포함합니다.

셰이더 코어의 정확한 개수는 특정 실리콘( silicon ) 칩( chip )마다 다릅니다; 실리콘 파트너들은 성능 요구사항과 실리콘 면적( area ) 제한에 기반해서 얼마나 많은 셰이더 코어를 구현할지를 선택할 수 있습니다. Mali-T760 GPU 는 저사양 장치들을 위해 단일 코어부터 고성능 설계를 위해 16 코어까지 달라질 수 있습니다. 하지만 거의 대부분의 구현에서는 4 코어나 8 코어 사이를 유지합니다.

GPU 를 위한 그래픽스 작업은 큐 쌍에 들어 가는데, 버텍스/타일링 워크로드( workload )를 위한 것이 하나이고 프레그먼트 워크로드를 위한 것이 다른 하나입니다. 그것들은 하나의 렌더 타깃을 위해서 동작하며, 각 큐에 단일 서브미션( submission )으로서 서밋( submit )됩니다. 두 큐로부터의 워크로드는 GPU 에 의해 동시에 처리될 수 있습니다. 그래서 서로 다른 렌더 타깃들을 위한 버텍스 처리와 프래그먼트 처리는 병렬적으로 실행될 수 있습니다( 파이프라인 방법론에 대한 세부사항을 원한다면 첫 번째 블로그를 확인하세요 ). 단일 렌더 타깃에 대한 워크로드는 작은 조각으로 나뉘어져서 GPU 에 있는 모든 셰이더 코어들 사이에서 분배됩니다. 타일링 워크로드의 경우에는 고정 함수( fixed-function ) 타일링 유닛에 분배됩니다( 타일링에 대한 개요를 원한다면 시리즈의 두 번째 블로그를 확인하세요 ).

시스템 내의 셰이더 코어들은 L2 캐시를 공유해서 성능을 증진시키며 반복적인 데이터 페치( fetch )에 의해 발생하는 메모리 대역폭을 감소시킵니다. 코어 개수와 마찬가지로, L2 캐시의 크기는 실리콘 파트너들에 의해서 구성됩니다. 하지만 일반적으로는 GPU 의 셰이더 코어 당 32 에서 64 KB 의 범위에 있습니다. 이 캐시가 외부 메모리에 대해 가지고 있는 메모리 포트( port ) 의 개수와 버스 폭( bus width )은 구성이 가능하며, 이 역시 실리콘 파트너가 성능, 전력, 면적 요구사항을 튜닝합니다. 보통 우리는 한 클락( clock ) 당 한 코어 당 32 비트 픽셀을 쓸 수 있도록 요구하며, 8 코어 설계로 클락 사이클( cycle ) 당 ( 읽기 및 쓰기 모두를 위해 ) 총 256 비트의 메모리 대역폭을 기대하는 것이 합리적일 겁니다.

The Midgard Shader Core

Mali 셰이더 코어는 여러 개의 고정 함수 하드웨어 블록으로서 구성되는데, 이는 프로그래머들 "트라이파이프( tripipe, 삼중 파이프 )" 실행( execution ) 코어를 감쌉니다. 고정 함수 유닛들은 셰이더 연산을 위한 설정을 수행하거나 - 삼각형 래스터화( rasterizing )이나 뎁스 테스트 - 셰이더 후처리( post-shader activity )를 제어합니다 - 블렌딩( blending )이나 전체 타일의 가치있는 데이터를 렌더링의 끝에서 메모리에 쓰기. 삼중 파이프 자체는 프로그래머블 파트이며, 셰이더 프로그램의 실행에 대한 책임이 있습니다.

The Tripipe

파이프라인 설계에는 세 가지 분류의 실행 파이프라인이 존재합니다: 하나는 산술( arithmetic ) 연산을 다루며, 하나는 메모리 로드/스토어( load/store ) 및 베어링 변수( varying, 역주 : 버텍스 셰이더 출력이자 프래그먼트 셰이더 입력으로 사용되는 변수 ) 접근을, 하나는 텍스쳐 접근을 다룹니다. 셰이더 코어 당 하나의 로드/스토어 와 하나의 텍스쳐 파이프가 존재합니다. 하지만 산술 파이프라인의 개수는 다양하며, GPU 마다 다릅니다; 오늘날의 대부분의 실리콘 제폼은 두 개의 산술 파이프라인을 가지고 있을 겁니다. 하지만 Mali-T880 은 세 개를 가지고 있습니다.

Massively Multi-threaded Machine

단일 코어에서 한 번에 하나의 스레드만을 실행하도록 하는 전통적인 CPU 아키텍쳐와는 다르게, 트라이파이프는 엄청난 다중 스레드 기반 처리 엔진입니다. 수 백개의 하드웨어 스레드가 트라이파이프 내부에서 동시에 실행될 수 있으며, 셰이딩되는 각 버텍스나 프래그먼트 당 하나의 스레드가 생성될 수 있습니다. 이 엄청난 개수의 스레드들은 메모리 지연( latency )를 감추기 위해 존재합니다; 어떤 스레드가 메모리 작업을 위해 대기하고 있다해도 중요하지 않습니다. 왜냐하면 적어도 한 스레드에서는 실행이 가능할 것이고, 효율적인 실행을 유지할 수 있습니다.

Arithmetic Pipeline: Vector Core

산술 파이프라인( A-pipe ) 는 SIMD( single instruction multiple data ) 벡터 프로세싱 유닛입니다. 이는 128 비트 쿼드-워드( quad-word ) 레지스터( register )들 상에서 수행되는 산술 유닛을 가지고 있습니다. 이 레지스터들은 2 x FP64, 4 x FP32, 8 x FP16, 2 x int64, 4 x int32, 8 x int16, 16 x int8 중의 하나로서 유연하게 접근될 수 있습니다. 그러므로 단일 산술 벡터 작업을 8 개의 "mediump" 값으로 단일 연산에서 수행할 수 있습니다. 그리고 OpenCL 커널들은 클락 사이클 당 SIMD 당 16 픽셀을 처리하기 위해서 8 비트 휘도( luminance ) 데이터 상에서 동작합니다.

아키텍쳐 파이프라인의 내부 아키텍쳐를 밝힐 수는 없지만, 각 GPU 를 위한 공용 성능 데이터를 사용해서 이용 가능한 수학 유닛의 개수에 대한 아이디어를 제공할 수는 있습니다. 예를 들어 16 코어를 가진 Mali-T760 은 600 MHz 로 326 FP32 GFLOPS 를 달성합니다. 이는 이 셰이더 코어의 경우에 클락 사이클 당 34 FP32 FLOPS 입니다; 그것이 두 개의 파이프라인을 가지므로 클락 사이클 당 파이프라인당 17 FP32 FLOPS 가 됩니다. 연산( operation )의 관점에서 이용가능한 성능은 FP16/int16/int8 데이터 타입에서는 증가하고 FP64/int64 데이터 타입에서는 감소할 것입니다.

Texture Pipeline

텍스쳐 파이프라인( T-pipe )은 텍스쳐를 사용하는 모든 메모리 접근에 대한 책임이 있습니다. 텍스쳐 파이프라인은 클락 당 바이리니어 필터링된( binlinear filterd ) 하나의 텍셀( texel )를 반환할 수 있습니다; 트라이리니어 필터링( trilinear filtering )은 메모리로에 있는 두 개의 서로 다른 밉맵( mipmap )로부터 샘플( sample )을 로드할 것을 요구하므로, 완료되려면 두 번째 클락 사이클이 요구됩니다.

Load/Store Pipeline

로드/스토어 파이프라인( LS-pipe )은 텍스쳐링과 관련이 없는 모든 메모리 접근에 대한 책임이 있습니다.

그래픽스 워크로드의 경우, 이는 버텍스 애트리뷰트 인풋( vertex attribute input ) 당 읽기, 버텍스 셰이딩 동안 계산된 버텍스 아웃풋( output ) 당 쓰기, 그리고 프래그먼트 셰이딩 동안 버텍스 셰이더에 의해 작성되고 베어링 변수로 보간될 수 있는 버텍스 아웃풋 값 당 읽기를 의미합니다.

일반적으로 모든 명령( instruction )들은 단일 메모리 접근 연산이며, 그것들은 벡터 연산임에도 불구하고 산술 연산처럼 단일 사이클에서 전체 "highp" vec4  베어링을 로드할 수 있습니다.

Early ZS Testing and Late ZS Testing

OpenGL ES 명세의 "fragment operations" 은 -- 그것은 뎁스 테스트와 스텐실 테스트를 포함합니다 -- 파이프라인의 끝에서 프래그먼트 세이딩이 완료된 후에 발생합니다. 이는 명세를 매우 단순하게 만들지만, 무엇인가를 셰이딩하는데 많은 시간을 소비해야 하고 그것이 ZS 테스트에 의해서 무효화( killed )된 것으로 밝혀진다면 프레임의 끝으로 던져져 버리기만 함을 내포합니다. 그것을 그냥 버려 버리는( discard ) 컬러링 프래그먼트들에서는 성능향상과 에너지 절약에 도움이 되므로, ZS 테스트를 미리 할 수가 있습니다( 예를 들어 프래그먼트 셰이딩 전에 ). 늦은 ZS 테스트는 ( 예를 들어 프래그먼트 셰이딩 이후에 ) 피할 수 없습니다( 즉, "discard" 를 호출할 수도 있는 프래그먼트에 대한 의존성같은 것들은 트라이파이프에 존재할 때까지는 비결정된 뎁스 상태를 가집니다 ).

전통적인 이른 Z 구조와 더불어, 우리는 프래그먼트를 중지시킬 수 있는 일부 오우버드로( overdraw ) 제거( removal ) 기능을 가지고 있습니다. 그것은 이미 래스터화되었지만 출력 화면에는 유용한 방식으로 기여하지 않는다면, 실제 렌더링을 막을 수 있습니다. 내 동료인 seanellis 는 이 기술에 대해 살펴 보는 훌륭한 블로그를 작성했습니다 - Killing Pixels - A New Optimization for Shading on Arm Mali GPUs - 그러므로 여기에서 깊게 살펴 보지는 않겠습니다.

Memory System

이 섹션은 나중에 추가되었습니다. 그래서 만약 이 블로그를 이전에 이미 읽었다면 이 섹션을 기억하지 못할 것입니다. 여러분이 이상하게 아니니까 걱정하지 마세요. 우리는 OpenCL 커널과 OpenGL ES 컴퓨트 셰이더를 작성하는 개발자들에게 GPU 캐시 구조에 대해서 더 많은 정보를 달라는 요청을 받아왔습니다. 캐시 지역성( locality )을 최적화하기 위해서는 데이터 구조체와 버퍼들을 깔고 가는게 실제로 이득이 되기 때문입니다. 사실들은 다음과 같습니다:

  • 셰이더 코어 당 두 개의 16 KB L1 데이터 캐시; 하나는 텍스쳐 접근을 위해 하나는 범용 메모리 접근을 위해.
  • 모든 셰이더 코어들에 의해 공유되는 단일 논리 L2 캐시. 이 캐시의 크기는 다양하며, 실리콘 통합자( integrator )에 의해서 구성됩니다. 하지만 일반적으로는 인스턴스화된( instantiated ) 셰이더 코어 당 32 KB 와 64 KB 의 사이입니다.
  • 두 개의 캐시 레벨은 모두 64 byte 캐시 라인을 사용합니다.

GPU Limits

  • 단순한 모델에 기반해, GPU 성능의 근본적인 속성들의 일부를 알아내는 것이 가능합니다:
  • GPU 는 클락 당 셰이더 코어 당 하나의 버텍스를 제출합니다.
  • GPU 는 클락 당 셰이더 코어 당 하나의 프래그먼트를 제출합니다.
  • GPU 는 클락 당 셰이더 코어당 하나의 픽셀을 리타이어( retire, 역주 : 명령이 분기 예측 실패같은 것들 없이 실제로 완료되는 것을 의미 )합니다.
  • 클락 당 파이프 당 하나의 명령을 제출할 수 있습니다. 그러므로 일반적인 셰이더 코어에 대해서는 만약 가능하다면 병렬적으로 4 개의 명령들을 제출할 수 있습니다:
    • A-pipe 당 17 FP32 연산.
    • LS-pipe 당 한 벡터 로드, 한 벡터 스토어, or one vector varying.
    • T-pipe 당 한 바이리니어 필터 텍셀.
  • 클라 당 코어 당 GPU 는 일반적으로 32 비트 DDR 접근( 읽기와 쓰기 )을 가지게 됩니다[ 구성 가능 ].
만약 Mali-T769 MP8 을 600 MHz 로 스케일링하면, 이론적인 최대 성능은 다음과 같을 수 있습니다:
  • Fill rate:
    • 클락 당 8 픽셀 = 4.8 GPix/s.
    • 완전한 1080p 에 대해 초 당 2314 프레임.
  • Texture rate:
    • 클락 당 8 개의 바이리니어 텍셀 = 4.8 GPix/s.
    • 1080p @ 60 FPS 를 위해, 픽셀 당 38 바이리니어 텍스쳐 룩업( lookup ).
  • Arithmetic reate:
    • 코어 당 파이프 당 17 FLOPS = 163 FP32 GFLOPS.
    • 1080p @ 60 FPS 를 위해, 픽셀 당 1311 FPLOPS.
  • Bandwidth:
    • 클락 당 256 비트 메모리 접근 = 19.2 GB/s 일기 및 쓰기 대역폭.
    • 1080p @ 60 FPS 를 위해, 픽셀당 154 바이트.

OpenCL and Compute

주의깊은 독자들은 필자가 버텍스와 프래그먼트에 대해서 많이 이야기하고 있음을 눈치챘을 겁니다 - the staple of graphics work - 하지만 OpenCL 과 RenderScript 컴퓨트 스레드가 코어로 들어 오고 있는 과정에 대해서는 거의 언급하지 않았습니다. 이 두 가지 유형의 작업은 버텍스 스레드와 거의 유사하게 동작합니다 - 버텍스 셰이더를 실행하는 것을 1 차원 컴퓨트 문제로서의 버텍스 배열로 볼 수 있습니다. 그러므로 버텍스 스레드 생성자( creator )는 컴퓨트 스레드를 생성( spawn )하기도 합니다. 더 정확하게 이야기하자면, 컴퓨트 스레드 생성자가 버텍스도 생성하고 있는 것입니다.

Performance Counters

문서는 Midgard 패밀리 성능 카운터에 대해 설명하고 있습니다. 이 블로그에서 설명한 블록 아케텍쳐에 매핑되어 있으며, Midgard 패밀리에 대한 필자의 블로그에서 찾아 보실 수 있습니다.

Next Time ...

이 블로그는 시리즈의 첫 번째 챕터를 마무리하며, 추상 머진을 개발했습니다. 추상 머신은 기본 동작을 정의하는데, 애플리케이션 개발자가 Midgard 패밀리인 Mali GPU 를 이해할 수 있도록 합니다. 이 시리즈의 후반에서, 필자는 이 새로운 지식이 작동하도록 하기 시작할 것입니다. 그리고 일반적인 애플리케이션 개발 함정과 유용한 최적화 기술들에 대해서 조사할 것입니다. 이런 것들은 Arm DS-5 Streamline 프로우파일링 툴의 Mali integration 을 사용해 식별되고 디버깅될 수 있습니다.

댓글과 의견을 항상 환영합니다.

Pete

원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/the-mali-gpu-an-abstract-machine-part-2---tile-based-rendering

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

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


이전 블로그에서 추상 머신을 정의하기 시작했으며, 그것은 Mali GPU 와 드라이버의 애플리케이션 단에서 볼 수 있는 동작들에 대해서 기술하는 데 사용될 수 있습니다. 이 머신의 목적은 개발자들에게 OpenGL ES API 의 기저에 깔린 흥미로운 동작들에 대한 멘탈 모델을 제공하는 것입니다. 이 모델은 애플리케이션의 성능에 영향을 주는 이슈들을 설명하는 데도 사용될 수 있습니다. 필자는 이 시리즈의 뒤쪽 블로그에서 이 모델을 사용해 개발자가 그래픽스 애플리케이션을 개발하는 동안 만날 수 있는 일반적인 성능 함정( pot-hole )들에 대해서 살펴 볼 것입니다.

이 블로그는 계속해서 추상 머신의 개발에 대해서 이야기하며, Mali GPU 패밀리의 타일 기반 렌더링에 대해 살펴 볼 것입니다. 여러분이 파이프라인에 관한 첫 번째 블로그를 읽어다고 가정합니다. 만약 읽지 않았다면 먼저 읽고 오시기 바랍니다.

The "Traditional" Approach

메인전원에 의해 전력을 공급받는 전통적인 데스크탑 GPU 아키텍쳐 -- 일반적으로 즉시 모드 아키텍쳐( immediate mode architecture )라 불립니다 -- 에서는 프래그먼트 셰이더( fragment shader )들이 각 드로 콜( draw call )에서 순차적으로 각 프리미티브( primitive ) 상에서 수행됩니다. 각 프리미티브들은 다음 프리미티브를 시작하기 전에 완료되기 위해 렌더링되는데, 대충 다음과 같은 알고리즘을 사용합니다:

스트림 내의 어떤 삼각형은 화면 내의 일부 영역을 덮고 있기 때문에, 이들 렌더러에 의해서 유지되는 데이터 워킹 셋( working set of data )은 큽니다; 일반적으로는 적어도 전체 화면 크기의 컬러 버퍼( color buffer ), 뎁스( depth ) 버퍼, 그리고 스텐실( stencil ) 버퍼. 최신 장치들을 위한 일반적인 워킹 셋은 픽셀당 32 비트 컬러 버퍼, 그리고 32 비트로 패킹된( packed ) 뎁스/스텐실 버퍼입니다. 그러므로 1080p 디스플레이는 16 MB 의 워킹 셋을 가지며, 4k2k TV 는 64 MB 의 워킹 셋을 가집니다. 그것들의 크기 때문에, 이 워킹 버퍼들은 반드시 칩 바깥에 있는 DRAM 에 저장되어야 합니다.

모든 블렌딩( blending ), 뎁스 테스트, 스텐실 테스트 연산은 현재 프래그먼트의 픽셀 좌표를 위한 현재 데이터 값을 요구합니다. 이 값들은 이 워킹 셋으로부터 획득( fetch ) 니다. 셰이딩된 모든 프래그먼트들은 일반적으로 이 워킹 셋을 손대게 되며, 그래서 고해상도에서는 프래그먼트에 대해 읽기-수정-쓰기 연산을 하게 되면, 캐싱이 다소 완화해 준다고 하더라도, 이 메모리 상에서 예외적으로 높은 대역폭을 소비할 수 있습니다. 또한 높은 대역폭 접근에 대한 요구는 매우 많은 핀과 특별한 고주파수 메모리를 가진 넓은 메모리 인터페이스를 요구합니다. 둘 다 외부 메모리 접근을 필요로 하며, 이는 특히 에너지를 많이 사용하는 작업입니다.

The Mali Approach

Mali GPU 패밀리는 매우 다른 접근 방법을 사용하는데, 일반적으로 타일 기반 렌더링이라 불리며, 렌더링 동안 필요한 외부 메모리 접근과 전력( power ) 소비량을 최소화하기 위해서 설계되었습니다. 이 시리즈의 첫 번째 블로그에서 설명했듯이, Mali 는 각 렌더 타깃에 대해 구별되는 두 개의 패스 렌더링 알고리즘을 사용합니다. 그것은 먼저 지오메트리 처리를 모두 실행한 다음에 프래그먼트 처리를 실행합니다. 지오메트리 처리 스테이지 동안, Mali GPU 는 화면을 작은 16x16 픽셀 타일들로 쪼개고, 렌더링 중인 프리미티브들이 어떤 타일에 제출되고 있는지에 대한 리스트를 만듭니다. GPU 프래그먼트 셰이딩 단계가 실행될 때, 각 셰이더 코어는 한 번에 하나의 16x16 픽셀 타일을 처리합니다. 다음 타일을 렌더링하기 전에 렌더링이 끝납니다. 타일 기반 아키텍쳐에서 알고리즘은 다음과 같은 식입니다:

16x16 픽셀 타일은 전체 화면 영역에 비해 작은 조각이므로, GPU 셰이더 코어와 강하게 결합되어 있는 빠른 RAM 에 전체 타일을 위한 전체 워킹 셋( 컬러, 뎁스, 스텐실 )을 유지하는 것이 가능합니다.

이 타일 기반 접근 방법은 여러 개의 이점을 가지고 있습니다. 그것들은 거의 개발자들에게 있어 투명하지만( transparent ) 알아 두면 가치가 있습니다. 특히 여러분의 칸텐트가 가지는 대역폭 비용을 이해하려고 할 때 그렇습니다:

  • 워킹 셋에 대한 모든 접근들은 지역적( local ) 접근입니다. 그것은 빠르면서 전력을 덜 소비합니다. 외부 DRAM 에 대한 읽기나 쓰기에 필요한 전력은 시스템 설계에 따라 다양해지지만, 1 Gbyte/s 의 대역폭을 위해 120 mW 정도는 쉽게 소비됩니다. 내부 메모리 접근들은 대략 이 보다는 자리수가 다르게 낮습니다. 그러므로 이것이 매우 중요하다는 것을 이해할 수 있을 겁니다.
  • 블렌딩은 빠르고 전력 효율이 높습니다. 왜냐하면 블렌드 연산들을 위해 요구되는 대상 컬러 데이터를 손쉽게 이용할 수 있기 때문입니다.
  • 타일은 4x, 8x, 16x 멀티샘플 안티에일리어싱( multisample antialiasing ) 을 허용하기 위해 타일 메모리에 충분한 샘플들을 저장할 수 있을만큼 충분합니다. 이는 고품질이며 부하가 적은 안티 에일리어싱을 제공합니다. 하지만 관련 작업 셋의 크기 때문에 ( 일반적인 단일 샘플링 렌더 타깃의 4, 8, 16 배; 4k2k 디스플레이 패널의 16x MSAA 에는 1 GB 의 워킹 셋 데이터가 필요합니다 ), 개발자에게 MSAA 를 기능으로 제공하는 즉시 모드 렌더러는 드뭅니다. 왜냐하면 일반적으로 외부 메모리 공간과 대역폭이 너무 비싸기 때문입니다.
  • Mali 는 타일 작업의 끝에서 메모리에다가 단일 타일을 위한 컬러 버퍼만을 써야만 합니다. 이 시점에 우리는 그것의 최종 상태를 알게 됩니다. CRC 체크를 통해 메인 메모리에 있는 현재 데이터와 블록의 컬러를 비교할 수 있습니다 -- Transaction Elimination 이라 불리는 처리입니다 -- 이때 타일 칸텐츠가 동일하다면 쓰기를 취소하며 SoC 의 전력을 절약하게 됩니다. 필자의 동료인 tomolson 은 이 기술에 대한 훌륭한 블로그를 작성했습니다. 그는 Transaction Elimination( Angry Birds 라 불리는 어떤 게임에서 이것에 대해 들어 봤을 겁니다 )에 실제 예제를 완성했습니다. 필자는 Tom 의 블로그에서 이 기술을 더욱 자세하게 설명할 것입니다만, 여기에서는 그 기술에 대해서 살짝 훔쳐 보기만 하겠습니다( "extra pink" 타일들만이 GPU 에 의해 쓰여진 것입니다 - 다른 것들은 성공적으로 취소된 것입니다 ).
  • 빠르고, 손실없는 압축 기법을 사용해서 Transaction Elimination 을 실행하는 타일을 위해 컬러 데이터를 압축할 수 있습니다 -- ARM Frame Buffer Compression( AFBC ) -- 이는 대역폭과 전력을 더 줄여 줍니다. 이 압축은 오프스크린 FBO 렌더 타깃들에 대해서 수행될 수 있으며, 그것은 GPU 에 의해 일련의 렌더링 패스들에 텍스쳐로서 읽어들여질 수 있습니다. 또한 거기에 공급되는 메인 윈도우 서피스는 시스템의 Mali-DP500 과 같은 AFBC 호환 디스플레이 컨트롤러입니다.
  • 대부분의 칸텐트는 뎁스 버퍼와 스텐실 버퍼를 가지고 있습니다. 하지만 그것들의 칸텐츠는 일단 프레임 렌더링이 끝낙 나면 유지될 필요가 없습니다. 만약 개발자가 Mali 드라이버에 뎁스 버퍼와 스텐실 버퍼를 유지할 필요가 없다고 알리면 -- 이상적으로는 glDiscardFramebufferEXT ( OpenGL ES 2.0 ) 이나 glInvalidateFramebuffer ( OpenGL ES 3.0 ) 에 대한 호출을 통해, 그리고 어떤 경우에는 드라이버에 의해서 추론될 수도 있지만 -- 타일의 뎁스 칸텐트와 스텐실 칸텐트는 결코 메인 메모리에 써지지 않을 것입니다. 대역폭과 전력을 크게 절약하는 또다른 방법입니다.

위의 리스트들을 통해 명확해 진 것은 타일 기반 렌더링이 여러 가지 이점을 가지고 있다는 것입니다. 특히 프레임버퍼 데이터와 관련한 대역폭과 전력에 있어서 매우 많이 절약해 주며, 저비용 안티 에일리어싱을 제공할 수 있습니다. 단점은 무엇일까요?

모든 타일 기반 구조의 필수적인 부하는 버텍스 셰이더에서 프래그먼트 셰이더로 넘어 가는 시점에 발생합니다. 지오메트리 처리 스테이지의 출력은 버텍스와 타일러 즉시 상태( tiler immediate state ), 반드시 메인 메모리에 쓰여진 후에 프래그먼트 처리 스테이지에서 다시 읽어들여져야 합니다. 그러므로 다양한 데이터와 타일러 상태를 위한 추가 대역폭을 소비하는 것과 프레임 버퍼 데이터를 위한 대역폭을 절약하는 것 사이에서 절충을 하기 위해 균형이 잡혀 있어야 합니다.

오늘날 현대의 소비자 가전제품은 높은 해상도 디스플레이로 이동하고 있습니다; 1080p 는 이제 스마트 폰에서 일반적이며, Google Nexus 10 의 Mali-T604 와 같은 태블릿은 WQXGA( 2560x1600 )에서 동작합니다. 4k2k 는 텔레비전에서는 새로운 "필수품( must have )"이 되고 있습니다. 화면 해상도와 프레임 버퍼 대역폭은 빠르게 커지고 있습니다. 이 영역에서 Mali 가 활약하고 있으며, 대부분 애플리케이션 개발자들에게 있어서는 투명하게 동작합니다 - 여러분은 이런 기능들을 애플리케이션을 변경하지 않고도 그냥 사용할 수 있습니다.

지오메트리 측면에서, Mali 는 복잡도에 잘 대응하고 있습니다. 많은 고사양 벤치마크들이 프레임 당 백만 개의 삼각형들을 사용하며, 이는 Android 앱 스토어에 있는 인기있는 게이밍 애플리케이션들과 비교하면 한 두 자리수 만큼 더 복잡도를 가지고 있습니다. 하지만 즉시 지오메트리 데이터는 메인 메모리에 접근하게 되므로, GPU 성능을 튜닝하고 시스템이 최선을 다하게 하는 데 적용될 수 있는 몇 가지 유용한 팁들과 트릭들이 있습니다. 그것들은 각각 블로그를 써도 될 만큼 가치가 있습니다. 그래서 우리는 이 시리즈의 뒷 부분에서 그것들에 대해 다룰 것입니다.

Summary

이 블로그에서 필자는 데스크탑 스타일 즉시 모드 렌더러와 Mali 에서 사용하는 타일 기반 접근 방법을, 특히 메모리 대역폭을 살펴 봄으로써, 비교하고 그것들의 차이점을 보여 주었습니다.

다음 시간에 튜닝을 하고 Mali 셰이더 코어 자체에 대한 간단한 블록 모델을 살펴 보면서 추상 머신에 대한 정의를 마칠 것입니다. Once we have that out of the way we can get on with the useful part of the series: putting this model to work and earning a living optimizing your applications running on Mali.

항상 댓글과 의견을 환영합니다.

Pete

원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/the-mali-gpu-an-abstract-machine-part-1---frame-pipelining

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

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


그래픽스 워크로드( workload )를 최적화하는 것은 보통 많은 현대 모바일 애플리케이션에서 필수입니다. 거의 대부분의 렌더링은 이제 렌더링 백엔드( back-end ) 에 기반한 OpenGL ES 에 의해 직접 혹은 간접적으로 다뤄집니다. 내 동료 중의 하나인 Michael McGeagh 는 최근에 Mali-T604 GPU 를 사용하는 그래피컬 애플리케이션들을 프로우파일링하고 최적화하기 위한 목적으로 Google Nexus 10 에서 동작하는 프로우파일링 툴인 Arm DS-5 Streamline 에 대한 작업 가이드를 포스팅했습니다. Streamline 은 강력한 도구이며, 전체 시스템의 동작에 대한 고해상도 가시화를 제공합니다. 하지만 그것은 엔지니어가 데이터를 해석( interpret )하고 문제 영역을 식별하고 수정할 점을 제안하는 데 시간을 쏟게 만듭니다.

그래픽스 최적화에 익숙하지 않은 개발자들에게는, 처음 시작할 때 공부할 것이 좀 있다고 할 수 있습니다. 그러므로 이 새로운 블로그 시리즈는 칸텐트 개발자들에게 필수적인 지식들을 제공해서 Mali GPUs 에서 성공적으로 최적화를 할 수 있도록 하기 위해 작성되었습니다. 이 시리즈의 코스 전반에서, 필자는 기본적인 macro-scale architectural structure 들과 동작들에 대해서 살펴 볼 것이므로, 개발자들은 이것을 어떻게 칸텐트에서 발생할 수 있는 가능한 문제로 해석할 것인지 그리고 Streamline 에서 그것들을 어떻게 확인할 것인지에 대해 걱정할 필요는 없습니다.

Abstract Rendering Machine

애플리케이션의 그래픽스 성능을 성공적으로 분석하기 위해서 필요한 가장 필수적인 조각은 OpenGL ES API 함수들을 밑바탕에 있는 시스템들에 대한 멘탈 모델( 역주 : 한글 링크 )입니다. 이는 엔지니어가 그들이 관찰하고 있는 동작들에 대해 추론할 수 있도록 합니다.

개발자들이 제어할 수 없고 결국에는 제한된 가치인 드라이버 소프트웨어와 하드웨어 서브시스템의 구현 세부사항 때문에 개발자들이 압도되지 않도록 하기 위해서는, 단순된 추상 머신을 정의하는 것이 유용합니다. 이는 관찰되는 동작들에 대해 설명하기 위한 기초로 사용될 수 있습니다. 이 머신에는 세 가지 주요 파트가 있으며, 그것들은 거의 직교적( orthogonal, 역주 : 서로 간섭하지 않고 독립적이어서 따로 따로 이해할 수 있다는 의미인듯 )입니다. 그래서 필자는 각각을 시리즈의 처음 몇 개의 블로그를 통해 순서대로 다루려고 합니다. 하지만 이 모델의 세가지 파트들에 대해 미리 살펴 보면 다음과 같습니다 :

  1. The CPU-GPU rendering pipeline.
  2. Tile-based rendering.
  3. Shader core architecture.

이 블로그에서, 우리는 이들 중 첫 번째인 CPU-GPU rendering pipeline 에 대해서 살펴 보겠습니다.

Synchronous API, Asynchronous Execution

이를 이해하는 데 있어서 중요한 지식의 가장 근본적인 조각은 OpenGL ES API 에서 애플리케이션의 함수 호출과 그 API 호출들이 요구하는 렌더링 연산( operations )들의 실행 사이의 임시적 관계( temporal relationship )입니다. OpenGL ES API 는 애플리케이션의 관점에서는 동기적 API 로서 설계되었습니다( specified ). 애플리케이션은 일련의 함수 호출들을 만들어서 그것의 다음 드로잉 태스크에 필요한 상태( state )를 설정합니다. 그리고 나서 glDraw 함수를 호출해서 -- 보통 드로 콜이라 불립니다 - 실제 드로잉 연산을 발생시킵니다. API 는 동기적이므로, 드로 콜이 만들어진 후의 모든 순차적 API 동작은 렌더링 연산이 이미 실행된 것처럼 행동하도록 설계되어 있습니다. 하지만 거의 대부분의 하드웨어 가속 OpenGL ES 구현들에서는, 이는 드라이버 스택에 의해서 유지되는 정교한 환상입니다.

드로 콜에서도 이와 유사한 것이 있습니다. 드라이버에 의해서 유지되는 두 번째 환상은 프레임 버퍼 교체 완료( end-of-framebuffer flip )입니다. OpenGL ES 애플리케이션을 처음 작성하는 대부분의 개발자들은 eglSwapBuffers 호출이 프런트 버퍼와 백 버퍼를 교환( swap )한다고 말할 겁니다. 이것이 논리적으로는 사실이기는 하지만, 드라이버는 또 다시 동기화에 대한 환상을 유지하고 있는 것입니다; 거의 대부분의 플랫폼에서 물리 버퍼 교환은 한참 후에 발생합니다.

Pipelining

이러한 환상이 필요한 이유는, 여러분이 원하는 성능때문입니다. 만약 렌더링 연산을 실제로 동기적으로 수행하도록 강제한다면, CPU 가 다음 드로 연산을 위한 상태를 생성하느라 바쁠 때 GPU 가 놀게( idle ) 될 것입니다. 그리고 GPU 가 렌더링하고 있는 동안 CPU 가 놀 것입니다. 성능의 관점에서는 이런 유휴 시간( idle time )들은 허용될 수 없습니다.

이 유휴 시간을 제거하기 위해서, 우리는 OpenGL ES 드라이버를 사용해 동기적으로 렌더링되는 것처럼 환상을 유지했습니다. 하지만 실제로는 렌더링과 프레임 교환을 비동기적으로 처리하고 있습니다. 비동기로 실행하면 작은 백로그( backlog ) 작업을 빌드할 수 있으므로, GPU 가 파아프라인의 한쪽 끝에서 오래된 워크로드를 처리하는 곳에서 파이프라인을 만들 수 있고 CPU 는 새로운 작업을 다른 쪽에서 밀어 넣을 수 있습니다. 이 접근 방법의 장점은 파이프라인을 가득 채우면 GPU 에서 실행할 수 있는 작업이 항상 최고의 성능을 제공한다는 것입니다.

Mali GPU 파이프라인에서 작업 유닛( unit )들은 렌더 타깃( render target ) 기반( basis )으로 스케줄링됩니다. 여기에서 렌더 타깃은 윈도우 서피스( surface )이거나 오프스크린 렌더 버퍼일 수 있습니다. 단일 렌더 타깃은 두 단계의 절차로 처리됩니다. 먼저 GPU 는 렌더 타깃 내의 모든 드로 콜들에 대한 버텍스 셰이딩( vertex shading )을 처리합니다. 다음으로 전체 렌더 타깃을 위한 프래그먼트( fragment ) 셰이딩이 처리됩니다. 그러므로 Mali 를 위한 논리 렌더링 파이프라인은 세 가지 스테이지의 파이프라인으로 구별됩니다: CPU 프로세싱 스테이지, 지오메트리( geometry ) 프로세싱 스테이지, 프래그먼트 프로세싱 스테이지.

Pipeline Throttling

관찰력있는 독자들은 위의 그림에 있는 프래그먼트 작업이 CPU 및 지오메트리 처리 스테이지들보다 점점 더 느려지고 세 가지 연산 중에 가장 느리다는 것을 눈치챘을 것입니다. 이런 상황이 비정상적인 것은 아닙니다; 대부분의 칸텐트들은 셰이딩되는 버텍스보다는 프래그먼트를 더욱 많이 가지고 있으므로, 프래그먼트 셰이딩이 보통 주요 처리 연산입니다.

사실은 CPU 작업을 완료하는 것부터 프레임이 렌더링되는 것까지의 지연양( amount of latency )을 최소화하는 것이 바람직합니다 -- 최종 사용자 입장에서는 자신들이 터치 이벤트를 입력하고 있는 터치 스크린 디바이스와 상호작용( interacting )이 스크린의 데이터와 몇 백 밀리세컨드 정도 차이가 나는 상황이 가장 혼란스럽습니다 -- 그래서 우리는 프래그먼트 처리 스테이지를 위해 대기하는 밀린 작업이 너무 많아지는 것을 원하지 않습니다. 간단히 말해, CPU 스레드를 주기적으로 느리게 만드는 메커니즘( mechanism )이 필요합니다. 파이프라인이 이미 성능을 유지하기에 충분할 때 작업 대기열( queuing up )을 중단합니다.

이 조절( throttling ) 메커니즘은 보통 그래픽스 드라이버가 아니라 호스트 윈도우 시스템에 의해 제공됩니다. 예를 들어 안드로이드에서는 버퍼의 방향( orientation )을 알기 전까지는 프레임에서 어떠한 드로 연산도 처리할 수 없습니다. 왜냐하면 유저가 장치를 회전시켜서 프레임 크기를 변화시킬 수 있기 때문입니다. SurfaceFlinger -- 안드로이드 윈도우 서피스 관리자 -- 는 렌더링을 위해서 N 개의 이상의 버퍼가 이미 존재할 때 애플리케이션의 그래픽스 스택에 버퍼를 반환하는 것을 거절함으로써 파이프라인 깊이를 단순하게 제어합니다.

만약 이런 상황이 발생한다면, "N" 에 도달하자 마자 CPU 가 프레임당 한 번씩 유휴상태가 되어, EGL 이나 OpenGL ES API 함수들 내부에서 블락킹( blocking )이 발생하는데, 이는 디스플레이가 보류중인( pending ) 버퍼를 소비하고 새로운 렌더링 연산을 위해서 하나를 비워줄( freeing ) 때까지 유지됩니다.

그래픽스 스택이 디스플레이 리프레시 비율( rate )보다 더 빠르게 실행되고 있을 때 이와 동일한 방식으로 파이프라인 버퍼링에 제약이 가해집니다; 이 시나리오에서는 칸텐트는 디스플레이 컨트롤러에 다음 프론트 버퍼를 교체할 수 있다고 알려주는 수직 공백( vertical blank, vsync )를 기다리면서 "vsync limited" 됩니다. 만약 GPU 가 디스플레이가 그것을 보여줄 수 있는 것보다 빠르게 프레임을 생산한다면, SurfaceFlinger 는 렌더링이 완료되었지만 여전히 화면에 출력되어야 할 버퍼들의 개수를 누적할 것입니다; 심지어 이 버퍼들은 더 이상 Mali 파이프라인의 일부가 아님에도 불구하고, 그것들은 애플리케이션 처리를 위한 N 프레임 제약을 위해 카운트됩니다.

위의 파이프라인 다이어그램에서 보이듯이, 만약 칸텐트가 vsync 제한에 걸리면, CPU 와 GPU 가 완전히 유휴상태가 되는 주기를 가지는 것이 일반적입니다. 플랫폼 동적 전압( Platform dynamic voltage )과 주파수 스케일링( frequency scaling, DVFS )은 일반적으로 이런 시나리오에서 현재의 연산 주파수를 줄이려고 시도합니다. 이를 통해 전압과 에너지 소비가 줄어들지만, DVFS 주파수 선택은 보통 상대적으로 거칠기 때문에 일부 유휴 시간이 예상됩니다.

Summary

이 블로그에서 우리는 OpenGL ES API 에 의해 제공되는 동기화 환상과 API 뒤에서 비동기 렌더링 파이프라인이 실제로 실행되는 이유에 대해 살펴 보았습니다. 다음 시간에 필자는 계속해서 추상 머신 개발에 대해서 이야기할 것입니다. Mali GPU 의 타일 기반 렌더링 접근법에 대해서 살펴 보도록 하겠습니다.

댓글과 질문을 환영합니다.

Pete

+ Recent posts