ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [번역] INTRO TO GPU SCALARIZATION – PART 1
    Graphics/번역 2020. 7. 20. 09:30

    개인 공부용으로 번역한 거라 잘못 번역된 내용이 있을 수 있습니다.

    또한 원작자의 동의 없이 올려서 언제든 글이 내려갈 수 있습니다.

    출처 : https://flashypixels.wordpress.com/2018/11/10/intro-to-gpu-scalarization-part-1/

     

     

    주의: 아래의 포스트들은 GCN에 특화될 것입니다, 나의 첫번째 확장으로 충분히 깔끔하게 하지 못했습니다, 그렇기 때문에 PSA가 글이 맨위에 있습니다! 만약 당신이 그것을 읽었고 그게 혼란스럽게 했다면 미안합니다.

    직장에서, 나는 최근 라이트 반복문을 스칼라화 하는것을 찾았습니다. 그리고 그것은 완전 재미있었습니다, 그래서 이번주에 나는 주말에 내 친구와 새 작업이 어떻게 되어가는지 이야기 하였습니다, 나는 그것을 언급하였습니다. 이것을 하면서 깨닳은 것은 적어도 내가 아는선에서는, 신입에게 이 과정의 이해를 도와주는 "초보를 위한" 문서가 충분하지 않다는 것 입니다.

    그래서, 나는 이 틈을 매우기 위해서 뭔가를 해보려합니다. 이 글이 고급레벨의 글이 될거란 의미는 아닙니다, 그러나 내 목표는 인터넷에서 짧지만 이해하기 쉬운 프로세스를 누군가가 직접해볼 수 있는 것 입니다.  나는 이것을 2개의 조각으로 나눌 것입니다:

    Part 1 – Introduction to concepts and simple example.
    Part 2 – Scalarizing a forward+ light loop.

    만약 gif들을 당신이 원하는 속도로 단계별로 따라가보고 싶다면: 내가 사용한 pptx 입니다.

     


    WAVEFRONTS

    만약 당신이 이 주제에 관심이 있다면, 나는 당신이 GPU가 하는 일에 대해서 알고있을 거라고 가정합니다, 그러나 이 글의 남은 부분의 이해에 필요한 몇가지 기본적인 것들을 빠르게 알아볼 것입니다. 만약 이미 당신이 Wave가 무엇인지와 스레드 하나가 어떻게 작업하는지 안다면 이 섹션을 넘기세요.

    GPU 실행 모델에 대한 더욱더 철저한 소개를 위해서 나는 당신이 Matthäus Chajdas이 훌륭한 시리즈 를 참조하였으면 합니다.

    GPU는 한번에 믿을 수 없는 양의 데이터를 처리하는 대규모 병렬 프로세서들 입니다. 중요한 면은, 대부분의 시간, 우리는 같은 쉐이더를 실행하는 많은 스레드를(혹은 당신이 픽셀 쉐이더에 대해서 생각하는 것이 편하면 픽셀로) 가집니다. 이런 분명한 상황을 다루기 위해서, 스레드들은 Wavefronts나 Waves로 불리는(Nvidia lingo에서는 warps) 그룹(Group)으로 배치(batch) 되어집니다. 이름처럼, 한개의 Wave의 스레드의 수는 아키텍쳐에 의존합니다, NVIDA GPU는 32, AMD의 GCN에서는 64 그리고 Intel 카드에서는 다양한 숫자 입니다.

     

     

     

     

     

     

     

    알아둘 점은 Wave의 각 스레드는 Lane으로도 불릴 수 있습니다.

     

    Wave에 있는 모든 스레드들 같은 쉐이더를 lockstep으로 실행합니다. (역주 : lockstep으로 처리된다는 것은, Wave의 모든 스레드가 쉐이더를 실행할 때, 모든 스레드가 1번줄을 동시에 다 실행을 마치고, 기다렸다가 2번줄... 3번줄 순서로 실행된다는 의미 입니다.) 이것이 만약 당신이 Wave 범위내에서 분기되어질 분기문을 가지면, 2가지 경우(역주: 분기에서 if와 else 경우 모두) 모두 비용을 지불해야 하는 이유입니다.

     

     

     

     

     

    gif에서 당신은 exe mask라 불리는 것을 볼 수 있습니다. 이것은 Wave에서 스레드당 한 비트인 bitmask 입니다. 만약 특정 스레드에 비트를 1로 설정하면 스레드(혹은 lane)이 활성화되었단 의미입니다, 만약 0으로 설정되었다면, 그것은 비활성화된 lane이라고 간주됩니다 그러므로 어떤 것이 실행되던 이 lane은 무시될 것입니다. 이것이 두개의 분기가 모두 실행되지만 각 스레드가 옳바른 결과를 가질 수 있는 이유입니다.

     

    SCALAR VS VECTOR

    Wave가 실행 될때, 각 스레드는 당연히 레지스터들이 필요합니다. 2가지 타입의 레지스터가 있습니다:

     

    Vector registers (VGPR): Wave의 스레드들간의 분기되는 모든 값을 위한 레지스터. 대부분의 당신의 로컬 변수는 아마도 VGPR일 것입니다.

     

    Scalar registers (SGPR): Wave의 모든 스레드들에 대해서 같은 값을 가지도록 보장되어진 모든 것들이 이 레지스터에 들어갑니다. 간단한 예제는 constant buffer로 부터 오는 값들 입니다.

     

    어떤 것이 SGPR 그리고 VGPR로 들어가게 되는지에 대한 몇가지 예제입니다. (글의 아래에 있는 [0]을 확인해보세요)

    cbuffer MyValues
    {
        float aValue;
    };
     
    Texture2D aTexture;
    StructuredBuffer aStructuredBuffer;
     
    float4 main(uint2 pixelCoord) : SV_Target
    {
        // This will be in a SGPR
        float s_value = aValue;
     
        // This will be put in VGPRs via a VMEM load as pixelCoord is in VGPRs
        float4 v_textureSample = aTexture.Load(pixelCoord);
     
        // This will be put in SGPRs via a SMEM load as 0 is constant.
        SomeData s_someData = aStructuredBuffer.Load(0);
     
        // This should be an SALU op (output in SGPR) since both operands are in SGPRs
        // (Note, check note [0])
        float s_someModifier = s_value + s_someData.someField;
     
        // This will be a VALU (output in VGPR) since one operand is VGPR.
        float4 v_finalResult = s_someModifier * v_textureSample;
     
        return v_finalResult;
    }

    내가 코드에 주석을 달았듯이, 어떤 레지스터의 오퍼랜드에 의존되느냐에 따라서, 산술 명령어는 다른 유닛에서 실행되어 집니다: SALU 혹은 VALU. 비슷하게, 주소가 SGPR 혹은 VGPR에 있느냐에 따라서 벡터 메모리 연산 그리고 스칼라 메모리 연산이 있습니다(몇몇 예외)

    이제, 왜 이것이 문제가 되는지 당신이 물어볼 것인가요? 아주 중요한 몇가지 이유가 있습니다:

    • VGPR은 종종 점유를 제한하는 자원입니다; 우리가 SGPR를 더 사용하는 것이 VGPR의 압박을 더 줄여서, 점유가 높아지는 결과를 만듭니다. (점유에 대한 것이 혼란스럽다면, 더 자세한 것은 [1]을 확인해주세요)
    • 스칼라 로드(load)와 벡터 로드는 서로다른 캐시를 가집니다(스칼라가 더 낮은 지연), SMEM과 VMEM은 서로 다른 경로입니다. 그래서 오퍼랜드가 더 많이 기다리는 것을 피하기 위해서 VMEM에 모든 로드를 쌓아두는 것인 좋지 않습니다.
    • 스레드 사이의 몇몇 분기 경로를 일관되게 만드는 것은 이득일 수 있습니다. 예를들어 이전 섹션의 그림에서 비싼 그리고 싼 분기가 모든 스레드들에 의해 실행되어지에 주목해주세요.

    그래서, 이런 전체 스칼라의 거래는 괜찮게 들리죠? 실제로 그렇습니다! 그리고 우리는 스칼라 유닛과 레지스터를 가능한 더 많이 활용해야합니다, 그리고 때때로 컴파일러가 그럴 수 있게 도와줘야 합니다.

    스칼라화에 들어갑니다...

     

    GETTING WAVE INVARIANT DATA

    어떻게 우리가 스칼라 유닛 활용을 강제할까요? 물론 우리는 wave에서 변하지 않는 데이터의 연산이 필요합니다. 이제 때때로 스칼라가 될 수 있다고 확신되는 데이터에서의 연산으로 인해 우리는 이 것을 얻을 수 있습니다(즉, wave 크기의 배수인 group인 SV_GroupID). 그렇지만 가끔은 우리가 wave에서 불변의 데이터로 연산중이라는 것을 보장할 필요가 있습니다.

     

    그렇게 하기 위해서, 우리는 wave intrinsics을 활용할 수 있습니다! Wave intrinsics은 정보 질의와 wave 레벨에서 연산 수행을 가능하게 해줍니다. 내가 무슨말을 하는거냐고 물었나요? 몇가지 예제를 보여주겠습니다, 이것이 그것을 더 명확하게 해줄 것입니다. (더 많은 방법이 있습니다):

     

    IntrinsicDescription
    uint WaveGetLaneIndex()현재 wave 범위에서의 lane의 인덱스를 리턴(물론 VGPR에서)
    uint4 WaveActiveBallot(bool)모든 활성 lane에 대해 전달된 bool 변수의 상태를 64비트 마스크에 포함하여 리턴합니다. 이 마스크는 SGPR에 있을 것입니다.
    bool WaveActiveAnyTrue(bool)아마도 ballot을 사용하여, 전달된 bool 변수 값이 true인 활성화된 lane이 하나라도 있는지 여부를 리턴하빈다. 결과는 SGPR에 있습니다.
    bool WaveActiveAllTrue(bool)아마도 ballot을 사용하여, 모든 활성화된 lane이 전달된 bool 변수 값이 true인지 여부를 리턴합니다. 결과는 SGPR에 있습니다.
    <type> WaveReadLaneFirst(<type>)Wave에서 첫번째 활성화된 lane에 대해서 전달된 표현식(역주 : 표현식은 변수로 보면 됨)의 값을 리턴합니다. 결과는 SGPR에 있습니다.
    <type> WaveActiveMin(<type>)Wave의 모든 활성화된 lane 들 중 전달된 표현식의 최소값을 리턴합니다. 결과는 SGPR에 있습니다.

    현재 세대의 콘솔은 이러한 intrinsics를 노출하고, Shader Model 6.0도 그렇습니다. 만약 당신이 콘솔에서 일한다면, SM6 intrinsics 의 모든 것이 노출되지는 않았을 것입니다. 그러나 당신은 사용가능한 것들로 그것을 할 수 있습니다.

     

    이전에 우리가 가진 예제를 향상시키기 위해서 우리가 어떻게 wave intrinsics을 사용할 수 있는지 봅시다:

     

     

    주의할 점은 이 최적화는 오직 fast와 slow 경로가 스레드에 허용할 수 있는 결과를 생성하는 경우만 가능합니다.

     

     

    모든 스레드에 어떻게 더 비싼 경로가 실행되는지에 주목하세요, 우리는 분기 경로로 실제로 우리가 명령어를 덜 실행합니다. 이것만으로도 좋을 것 입니다!

    우리는 우리의 첫 스칼라화를 했습니다! 이제 더 실용적이고 좀더 복잡한 것을 볼 시간입니다. 이 시리즈의 두번째 파트를 보세요, 거기서 우리는 forward+ 라이트 반복문의 스칼라화를 할 것입니다.

    다음 까지!

    – Francesco (@FCifaCiar)

     


    NOTES

    [0] 주의점, 실제코드는 다양한 이유로 주어진 문맥에서 내가 마킹해둔 것처럼 동작하지 않을 수 있습니다. 특히, SALU라고 마크 해둔 것은 SALU에 대해 ISA 부동소수점이 없기 때문에 VALU가 될 것이라 믿습니다. (코맨트 섹션의 Andy Robbins 에 감사합니다) 코맨트와 그것이 일치할때 별도의 명령어를 고려하세요.

     

    [1] GPU는 엄청난 양의 대역폭 가집니다, 그러나 메모리 연산의 지연 또한 역시 높습니다! 이것을 보상하기 위해서, GPU는 메모리로 부터 데이터를 기다리고 있는 wave의 실행을 중단할 수 있습니다. 그리고 그동안 실행 가능한 또 다른 wave로 전환이 가능합니다. 이것이 바로 "지연 숨김(latency hiding)" 입니다.

     

     

     

    An example with an occupancy of 2

     

     

     

    위의 예에서 5 유닛을 기다리는 대신 다른 wave로 전환하여 우리가 2 유닛만 기다리도록 하는 것에 주목하세요.

     

    우리가 서로서로 wave를 전환할 수 있는 수는 제한되어있습니다. 이 수는 "점유(occupancy)"라고 부르는 것이며, 최대 10개 입니다. 우리가 점유를 결정하는 몇가지 제한 요소가 있습니다, 대 부분의 경우 쉐이더에서 사용되어지는 VGPR와 LDS의 수입니다. 그래서, 스칼라화는 종종 VGPR의 사용을 줄이는 것을 의미하기 때문에, 이것은 더 나은 점유로 해석됩니다.

     

    나는 많은 중요한 것들을 건너 뛰었습니다, 이 주제를 더 자세하게 그리고 더 분석하는 것은 Sebastian Aaltonen의 great GPUOpen’s post 에서 볼 수 있습니다.

     

    댓글

Designed by Tistory & scahp.