How to Avoid Branching on the GPU 如何在GPU避免分支

在GPU避免分支的方法

Authored by Brandon Fogerty(XR Graphics Engineer at Unity Technologies.) with Additional Organized by JP.Lee(李正彪)[email protected]
在這裏插入圖片描述

概要

這邊來稿文章中,希望瞭解如何編寫GPU着色器友好型代碼,以避免和分支相關的性能費用。

"分支"有着什麼意義?
使用明確的 if / then隨時都可以產生分支。編譯器遇到條件會作出命令。GPU可去的地方有兩處。
因此需要決定要使用哪種代碼路徑。以下示例展示了在彙編GPU裏面運行的示例。
運算變量設置爲幻數7,則添加該數字。否則的話要減掉。

在這裏插入圖片描述

爲什麼要避免分支?

爲了增加CPU使用率,大多數計算機都嘗試在管線上執行儘可能多的任務。彙編指令按順序執行,CPU會嘗試在儘可能多的CPU內核上執行儘可能多的指令。舉個簡單的例子。
想象一下,在Twilight Zone內,我們就是世界上最高效的程序員!
用兩行代碼寫了電子遊戲!
CPU會盡可能在每一個CPU內核上各執行一個,共計兩個光榮的指令。
這是計算機能夠更有效的運行代碼。
但是在分支時,計算機可能會花時間準備未運行的代碼結果。從結論上看處理器時間被浪費,影響了遊戲或應用程序的應答能力。

CPU 側分枝誤診通常會導致週期遺漏超過40次。– Charles Sanglimsuwan (集成開發人員相關工程師)

運氣不錯,最新的CPU處理器速度驚人,實際分支預測出色,因此分支錯失幾乎不會有問題。

但是GPU仍然存在性能問題。

GPU嘗試並行解決大量計算,因此大部分GPU不支持分支預測

GPU爲什麼會發生性能問題?

GPU爲生成美麗的圖像,喜歡並行進行很多工作!
GPU爲進行多種固有的結果計算,進行了精心設計,解決了通過單一濾鏡(例如:着色器程序)實施的多種輸入和相關的問題。
這就是渲染經常使用GPU(Graphics processor units)的原因。通過幾個Shader程序運行位置,法線,貼圖座標等具有不同屬性的固有頂點,該Shader程序輸出在畫面上表現的大量固有像素顏色。
舉個例子,顯示器是1080 × 960這樣的一般HD像素的情況下,GPU在一組輸入時,將計算1080 × 960 = 1,036,800個的固有像素顏色值!
那是很多的計算結果!
想象一下在1080x960的高清遊戲體驗。遊戲嘗試每秒渲染60個固有的圖像或幀。因此GPU在1秒之內需要計算1080x960x60以上的固有像素顏色值!
結論上看一秒之內可以計算62,208,000個固有像素值!

撰寫本文的節點,NVDIA最現金的GPU是GeForce RTX 2080 Ti。它有着比英特爾最先進的CPU處理器i9-7980XE更多4,352個Cuda內核,18個內核。
非常大的差異吧?
GPU有大量的處理內核,但是與最新的CPU相比,其核心處理數量仍不足百萬。
至少目前還沒有!顯卡在物理上太大了,可能會很昂貴。
因此,GPU將嘗試對要解決的問題類型進行特定假設。GPU充分利用SIMD。SIMD表示單個命令的多種數據。SIMD允許以並行方式爲多個輸入運行計算。SIMD通常希望輸入和輸出位於相鄰的內存塊中。因此SIMD操作不必加載每個輸入並單獨保存每個結果,而是加載輸入並將結果保存爲單個加載/保存操作,從而減少昂貴的內存加載和保存。SIMD的使用要求內存佈局嚴格,並在應用程序設計中維持更好地內存緩存一致性上有積極作用。SIMD在CPU和GPU均可使用.
在CPU使用SIMD的示例可參考此處。與CPU程序不同,GPU程序幾乎始終使用SIMD。爲了充分利用SIMD,GPU通常每個內核都有很多ALU。ALU是算術邏輯設備的縮寫。ALU執行數學命令。舉個例子,使用“add 4, 3”命令計算結果7。因此,單一內核上有8個ALU的話,就可以同事並行運行8個計算。即,單一GPU內核有8個ALU,則單個GPU內核則可以同事計算8個像素值!但是,着色器階段使用動態分支會發生什麼呢?核心是必須執行所有代碼路徑,並最終丟棄不滿足條件的代碼路徑。這意味着ALU會浪費時間執行任何未使用的操作。也就是說渲染圖像需要更長的時間。

在這裏插入圖片描述

使用示例:

在Unity裏,使用立體shader以保證雙眼能夠準確渲染。左眼以綠色表示渲染,右眼渲染的顯示爲紅色。下面是使用立體shader渲染雙眼鎖渲染的單個球體。

針對此的純接近方式是基於unity_StereoEyeIndex變數的條件方法。

Shader "XR/StereoEyeIndexColor"
{
    Properties
    {
        _LeftEyeColor("Left Eye Color", COLOR) = (0,1,0,1)
        _RightEyeColor("Right Eye Color", COLOR) = (1,0,0,1)
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            float4 _LeftEyeColor;
            float4 _RightEyeColor;

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2f vert (appdata v)
            {
                v2f o;

                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_OUTPUT(v2f, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                o.vertex = UnityObjectToClipPos(v.vertex);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
                if(unity_StereoEyeIndex == 0)
                {
                	return _LeftEyeColor;
                }

                return _RightEyeColor;
            }
            ENDCG
        }
    }
}

分段shader的集合輸出如下:

Shader hash 084f3be2-e9e6b34f-66f52d0a-1b95fab0

ps_4_0
      dcl_constantbuffer cb0[2], immediateIndexed
      dcl_input_ps_siv v1.x, rendertarget_array_index
      dcl_output o0.xyzw
   0: if_z v1.x
   1:   mov o0.xyzw, cb0[0].xyzw
   2:   ret
   3: endif
   4: mov o0.xyzw, cb0[1].xyzw
   5: ret

注意從0行開始的分支。分段shader stage上指定的內核將強制所有ALU執行兩個條件結果。
但是不滿足條件的結果將被丟棄。這種情況,可能是一部分ALU執行了被浪費的操作。我們剛剛讓幾種ALU很傷心,現在他們提出了他們爲什麼需要存在。
不要讓可憐的小ALU做很忙的工作了。(✖╭╮✖)
你肯定不喜歡浪費時間吧?用ALU做一些游泳的事情吧。
如果能夠去掉條件部,可以減少浪費,讓ALU更有效率,更加幸福!
以下示例演示了分段着色器的實現方式:

Shader "XR/StereoEyeIndexColor"
{
    Properties
    {
        _LeftEyeColor("Left Eye Color", COLOR) = (0,1,0,1)
        _RightEyeColor("Right Eye Color", COLOR) = (1,0,0,1)
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            float4 _LeftEyeColor;
            float4 _RightEyeColor;

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2f vert (appdata v)
            {
                v2f o;

                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_OUTPUT(v2f, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                o.vertex = UnityObjectToClipPos(v.vertex);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
                return lerp(_LeftEyeColor, _RightEyeColor, unity_StereoEyeIndex);
            }
            ENDCG
        }
    }
}

沒有明確的條件,仔細看下HLSL組件輸出。

Shader hash 7f0a4d98-21be8f11-77007603-2899b3a0

ps_4_0
      dcl_constantbuffer cb0[2], immediateIndexed
      dcl_input_ps_siv v1.x, rendertarget_array_index
      dcl_output o0.xyzw
      dcl_temps 2
   0: utof r0.x, v1.x
   1: add r1.xyzw, -cb0[0].xyzw, cb0[1].xyzw
   2: mad o0.xyzw, r0.xxxx, r1.xyzw, cb0[0].xyzw
   3: ret

再一次確認也是!沒有分支!
我們現在使用的不是明確的條件部,而是Lerp。
lerp在兩個值之間執行線型插值。
Lerp本質上是:

float lerp( float a, float b, float t)
{
            return (1.0f-t)*a + t*b;
}

如組件輸出所示,lerp會轉換一些比乘法和分支更好地附加輸出。基本上會向lerp函數提供兩個任意值,傳遞第三個參數"t value"。t值一般是易於操作的空間數0.0和1.0之間的數字。
t值爲0.0,結果爲a。如果t值爲1,則結果爲b。
t值介於0.0和1.0之間的話,結果將在a和b之間線性顯示。
對於我們來說,我們知道unity_StereoEyeIndex是左眼或者右眼分別是0或者1。
因此,這可以很好的操作,並可以避免明確的分支。

避免分支的其他方法是什麼?

默認情況下,我們希望得到的是將條件轉換爲數學方程式。爲了幫助實現這個目標,我們繼續介紹HLSL硬件加速功能。
step(y,x)
x 參數比y參數大或者一樣的話,1;反之則是0。
lerp(x,y,t)
線性插值結果。
min(x,y)
x或者y參數中最小的值。
max(x,y)
x 或者y參數中最大的值。
Lerp嘗試在某些條件下返回兩個不同的任意值時最方便。
這一步驟對於Boolean運算非常有用。如果這是真的,你可能還會想要返回一些東西,例如假設你有以下功能:

// The enable parameter should be either 0 or 1
// If enable is 1, then the color blue is returned.  Otherwise the color black is returned.
// If you add the result of AddBlueTint to your final color and black is returned,
// your original color will remain unchanged.
float3 AddBlueTint( float enable )
{
            return step(0.5, enable) * float3(0,0,1);
}

float3 finalColor = someColor + AddBlueTint(1.0);

也可以修改立體聲shader並使用step代替lerp。

fixed4 frag (v2f i) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
    return step(unity_StereoEyeIndex, 0.5) *_LeftEyeColor + step(0.5, unity_StereoEyeIndex) * _RightEyeColor;
}

那樣的話,HLSL彙編如下所示:

Shader hash e56cb1bf-ce5441de-72c92b9d-336cc956

ps_4_0
      dcl_constantbuffer cb0[2], immediateIndexed
      dcl_input_ps_siv v1.x, rendertarget_array_index
      dcl_output o0.xyzw
      dcl_temps 2
   0: utof r0.x, v1.x
   1: ge r0.y, r0.x, l(0.500000)
   2: ge r0.x, l(0.500000), r0.x
   3: and r0.xy, r0.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)
   4: mul r1.xyzw, r0.yyyy, cb0[1].xyzw
   5: mad o0.xyzw, r0.xxxx, cb0[0].xyzw, r1.xyzw
   6: ret

再一次…
沒有分支!SIMD shader彙編命令"ge"不是分支。基本條件下,HLSL階段命令在shader彙編中進行轉換。ge是計算輸入值是否大於或等於其他輸入值的命令,並根據結果返回值1或者0。使用多階段函數,可以根據輸入值是都在特定範圍內兒返回結果。這一般稱爲"脈衝功能"(“pulse function” : The pulse function may also be expressed as a limit of a rational function)
例如可以考慮以下條件:

if(value >= 0.5 && value <= 1.0)
{
	return float3(0,0,1);
}

使用2 階段功能構成的脈衝功能。

float pulse(float value, float minValue, float maxValue)
{
	return step(minValue,value) - step(maxValue, value);
}

float3 finalColor = float3(0,0,1) * pulse(0.4, 0.1, 0.5);

可以使用最小值/最大值去選擇兩個值中大或較小的值。這樣的內置功能都轉換爲硬件加速功能或者與lerp相同的硬件加速功能。即,這些方法通過顯卡的硅膠中烤制的邏輯迴路進行轉換或被轉換。
刪除條件的更多方法請關注Orange Duck的以下文章。GPU內核即工作方式的詳細內容請參考一下UC Davis的演示。

附錄. 1

GPU 運算方式

  1. SIMD, MIMD, SIMT

(a) SIMD(Single Instruction, Multiple Data): 用一個命令加工多個數據的計算。

(b) MIMD(Multiple Instruction, Multiple Data): 幾個處理器內核運行各自獨立的程序(命令 列),並各自處理數據。

© SIMT(Single Instruction, Multiple Thread): 用一個命令(NVIDIA GPU時)並行運行32個線程
*P(Predicate): 有一個寄存器用於記住上述的比較命令條件代碼,使用各個命令的Fredicate比特和條件代碼的匹配/不匹配控制是否忽略在運算其中執行命令。
在這裏插入圖片描述

SIMD

  • 因爲涉及到4各元素的向量,所以用相同的指令將4個運算器結合在一起的方法很有效。
  • 如果只輸入一個元素,運算器4個裏面只會運行1個,剩餘3個空閒,效率低。
  • 另外,必須讓按照一般順序運行的程序執行不同的向量運算。
  • 用CUDA或OpenCL編寫程序時,SIMD用矢量化或詳細的指令執行順序等是編譯器負責的,因此不需要程序員考慮。編譯器無法保證可以生成所有的SIMD代碼,可能會使性能降低。
  • 命令供應是一個整體,各運算器都不需要命令供應,因此所需的晶體管數量很少,對比MIMD,面積要更小。
    需要的硬件數量: MIMD > SIMT > SIMD
    運行自由度: MIMD > SIMT > SIMD

GPU有從運行SIMD轉變爲運行SIMT的趨勢。

  1. 一般情況下,CPU和GPU都有各自的內存。
  • 通過DMA(Direct Memory Access)引擎,在兩個內存之間傳送數據。
  • 這樣的情況下,每次數據傳送都需要在內存之間複製數據,但也會發生與‘深層複製’的情況一樣的問題。
  • 因此製作共同內存的空間成爲了GPU製造商的重要研發目標。

CPU => 需要大容量內存(CPU數十GB / GPU數GB)
GPU => 需要高帶寬內存以支持高計算性能
在這裏插入圖片描述
4. GPU的3D圖像處理需要大量計算,因此GPU需要搭載比通用CPU更多的浮點運算器
因此產生了將這樣的運算能力運用到科技計算的需求,GPU的適用範圍更廣了。

e.g.) 利用生物分子系統功能控制,建立個新的制約基礎時

  • 生物體組織等按照分子原子的水平模型化
  • 模擬與藥物的互動產生怎樣的結果
  • 原子層面上,爲計算移動的力量,因爲原子數量大,需要大量的計算

諸如科技計算,圖像識別,語音識別等研究趨勢將數據分成更小的單位進行分析,精密度準確度也越來越高。
因此需要更多的計算,使用GPU的並行計算已經成爲處理的重心。
在這裏插入圖片描述

附錄 2.

Warp Execution

Warp是執行GPU Instruction的最基本單位。Warp的所有Thread只執行一個相同的Instruction。但是Warp的各Thread都可以讀取不同的數據值。因此可以認爲Warp的每個Thread使用相同的Instruction不同的數據值來執行運算。這種運算方法被稱爲SIMD (Single Instruction Multiple Data) 或者SIMT (Single Instruction Multiple Thread)。

SIMD和SIMT有一點差異。SIMD可以看做是執行Vector運算。要執行SIMD運算,爲了執行Vector運算需要Instruction。而SIMT則是在編寫程序時,根據一個Thread來編寫。而且硬件是將幾個Thread組合在一起來執行運算。
從Volta Architecture開始,Warp的Thread可以執行相互獨立的運算。沒有詳細的閱讀Volta Architecture,所以操作方法不是很清楚下次打算整理一下Volta或Turing Architecture(出處1, 2)
在這裏插入圖片描述
圖1顯示了使用SIMT運算方法執行ADD運算。32個Thread有獨立的存儲空間(Register)。爲了執行ADD運算,每個Thread從兩個Source Register加載Data值。執行ADD運算之後得到的結果存儲在其他32個Destination Register。結果上看一個Warp將訪問64個Source Register,並將運算結果值存儲在32個Register裏面。Warp的所有Thread都爲了執行同樣的運算,之家在一個Instruction。準確的說,在被調度時要執行相同的運算的Instruction執行了Broadcasting,同時所有的Thread執行被Broadcasting的一個Instruction。
通常一個SM (Streaming Multiprocessor)有48個64個的Warp。每個SM一般有數十數百個的運算器(ALU),可同時進行2~4個Warp Instruction。每個SM的Warp總數,ALU個數等因GPU Architecture的不同而略有不同。GPU與CPU一樣使用Pipeline執行Instruction。通常GPU由5個Pipeline Stage組成,分別是Instruction Fetch, Decode, Issue, ALU&Memory, Writeback格式。(出處 3)
從Turing GPU Architecture開始 Integer, Floating運算器已經分離了。以前Architecture的情況下,Integer, Floating Point運算都在一個ALU裏面執行了。
GPGPU-Sim是對NVIDIA GPU進行建模的計算器Architecture模擬器。用C, C++寫的。雖然模擬器陳舊,但是因爲沒有比這個更好地選擇,所以很多研究室都是用相應模擬器編寫論文。
在這裏插入圖片描述
圖2展示了5個Pipeline Stage。以下是對各Stage的說明。
Fetch: Fetch階段使用PC (Program Counter)的值加載Instruction。最近的GPU,與CPU一樣使用64-Bit Instruction。Fetch Instruction的順序是Round-Robin方式。用Warp 0, 1, 2, ….的順序Fetch Instruction。
Decode: Decode階段是爲執行在內存裏面加載的Instruction執行ALU/Memory運算而查找運算Type及需要的Register的階段。 Decode Instruction存儲在Instruction Buffer中。GPU由不同的邏輯計算,如Integer, Floating Point, Special Function, Memory。
Issue: 是用運算器發送Decode Instruction的階段。在此階段中,運算所需的數據值在Source Register讀取。在Source Register讀取的值發送到ALU,ALU將執行運算。一般情況下,Instruction Fetch/Decode的速度(?)因爲比Instruction Issue的速度快,所以到了這個階段Decode的(可能的Issue)Warp個數多的概率會很高。所以Warp Scheduler選擇Issue的Warp。衆所周知Warp Scheduling技術有Round-Robin (RR)和Greedy-Then-Oldest (GTO)方式。SM平均有2~4個Warp Scheduler,所以每個週期會進行2~8個Warp Instruction Issue。Issue Instruction可在ALU/LSU執行運算。根據運算類型,Instruction會在數十個週期內需要數百週期的運算時間。
RR Warp Scheduling是從幾個Warp中用ALU組成的Warp的順序,依次選擇的方法。例如,假設SM中有24個Warp,並且所有Warp都已經準備好了運行Instruction。Warp 0, Warp 1, …, Warp 23在ALU裏面依次出現一個Instruction,是Issue的形態。
GTO Warp Scheduling方法已經執行了在Warp 0號可執行Instruction。如果沒有準備好的Warp 0的可執行Instruction(Stall發生的情況),則選擇下一個準備好的Warp來進行Instruction Issue。下一個選擇的Warp選擇現在Stall Warp的下一個ID的 Warp。例如,Warp 3號沒有可執行的 Instruction,則4號Warp Instruction將成爲Issue。
ALU&Memory: 使用從Source Register讀取的值執行運算。根據Instruction類型,使用Load/Store Unit (LSU), ALU, Special Function Unit (SFU)運算器。ALU執行Integer, Floating Point運算。SFU執行一些有些複雜的指令,如Sin, Cos等。LSU執行內存運算。…
Writeback: 是將在ALU, LSU, SFU運算的值存儲在Destination Register的階段。
GPU執行上述5個階段的Warp Instruction。一般Computer Architecture書裏面介紹的5階段Pipeline Stage和它幾乎相同。
出處
https://images.nvidia.com/content/volta-architecture/pdf/volta-architecture-whitepaper.pdf
https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/technologies/turing-architecture/NVIDIA-Turing-Architecture-Whitepaper.pdf
http://www.gpgpu-sim.org/

附錄3.

GPU Branch Divergence

GPU將Thread綁定爲32或64個,執行一個相同的Instruction。Branch Divergence發生在屬於Warp的Thread必須執行不同的操作時。例如,屬於Warp的Thread偶數ID必須執行與IF語句相對應的代碼,而爲奇數ID的Thread必須執行ELSE相對應的代碼,就會發生Branch Divergence。以下代碼是檢查Thread ID是否爲奇數,並運行不同代碼的示例(未驗證)。

__global__ void mkTest(){
	int threadID = threadIdx.x
	if((threadIdx.x % 2) == 0){
		Even number Thread code...
	}
	else{
		odd number Thread code...
	}
}

圖1表示了發生Branch Divergence的情況下,Warp執行的順序。簡而言之,Warp依次執行IF和ELSE等代碼。執行與IF相應的代碼運算時,不需要IF運算的Thread運算除外。相反,執行ELSE相應的運算時,不需要ELSE運算的Thread將從運算中除外。不知道運算中去除的正確方法。也有不完全執行運算的方法,相反也有執行運算但是不在Register使用結果值的方法。有效的方法是不執行計算本身。
在這裏插入圖片描述

圖 1: Branch Divergence發生的情況下,執行Warp的順序

因此當發生Branch Divergence時,GPU將按序執行IF/ELSE相應的代碼。因爲Warp的所有Thread不會同時執行運算,所以GPU的Utilization下降。因此在使用GPU編程時,最好減少Branch Divergence。有事是在將IF/ELSE除以Thread ID/Block ID/Grid ID時,最好添加一到兩個不同的運算來刪除Branch Divergence。GPGPU早期(2010年左右),進行了很多從HW中刪除Branch Divergence的研究。例如,當發生Branch Divergence,將Warp的Thread變成其他的Warp Thread。在研究生時期讀論文的時候,我覺得這是個很酷的想法。但是,現在回想起來在性能上應該不會有很大的益處。最大的原因是Register值讀取的速度急速變慢,所以可能實際性能並不會像論文中說的那麼好。此外也有很多在編譯階段解決Branch Divergence問題的研究發佈。很多論文發表意味着Branch Divergence對GPU的性能有很大的影響。
根據GPGPU-Sim的說法,使用SIMT Stack硬件邏輯Handling Branch Divergence(出處 1)。每個Warp都有一個名爲SIMT Stack的存儲空間。SIMT Stack由幾個Entry構成。根據論文內容,SIMT Stack由4個Entry構成(出處2)。每個Entry由PC (Program Counter) + 32Bit存儲空間構成。如果PC是64 Bit,每個Entry由92 Bit構成。PC後面的32 Bit用於存儲True/False值,即屬於Warp的Thread是執行還是不執行與PC相對應的Instruction。對於NVIDIA GPU,由於Warp由32個Thread構成,因此使用32 Bit檢查Thread的Instruction是否執行。但是如果Warp由64個Thread構成,則使用64 Bit檢查是否執行Thread Instruction。
在這裏插入圖片描述
想了一下如何解釋運行原理,似乎使用示例進行解釋是最有效的。圖2展示了Branch Divergence發生的情況下,使用SIMT Stack執行Instruction的方法。爲方便起見,我們假設Warp是由8個Thread構成的。如前面示例代碼所說,假設偶數ID Thread執行IF語句,奇數ID Thread執行ELSE語句。
Step 1: 在IF/ELSE之前,SIMT Stack有一個Entry。該Entry存儲了PC值 + 11111111 值。PC值存儲了Warp必須執行的下一個Instruction位置值。隨後存儲的 “11111111”意味着所有現存的Thread都必須執行與PC相對應的Instruction運算。
Step 2: 如果遇到IF/ELSE語句,總共會生成3個Entry。最上面存儲IF語句相應的IF Instruction PC值 + “10101010”值,表示需要進行偶數Thread運算(因爲是從0開始,第一個Thread標記爲1)。以下Entry存儲與ELSE語句Instruction相對應的PC值 + “01010101”值。最後存儲完成了與IF/ELSE相對應的Instruction運算的PC 值 (RPC) + “111111111”。IF/ELSE語句完成後,Warp的所有Thread爲了再次執行相同的Instruction而合併,稱爲 “Reconverge”。
Step 3: Stack這個名稱可以得知,使用POP運算在SIMT Stack存儲的Entry值。首先在SIMT Stack執行POP運算會得出與IF語句相對應的Entry。判斷出IF語句相對應的PC值和Warp的Instruction運算是否需要後執行運算。標記爲“1”的Thread執行其Instruction運算。反之,標記爲“0”的Thread不執行。
Step 4: 與Step 3一樣執行以下Entry運算。以下Entry是對應ELSE的Instruction的運算。
如果IF/ELSE的Instruction個數多於1,則更新PC值並將可執行的Thread信息再次保存至SIMT Stack。Stack遵循Last-In-First-Out (LIFO)順序,因此執行所有相應於IF運算的Instruction運算,並按序執行ELSE相應的Instruction運算。
Step 5: IF/ELSE相應的Instruction全部執行後,SIMT Stack將只剩下具有Reconverge PC (RPC)值的Entry。該Entry被POP,Warp的所有Thread都執行相同的運算。
如上所述,Branch Divergence發生的話,使用SIMT Stack按序執行Instruction運算。發生Branch Divergence時,由於屬於Warp的Thread無法執行Instruction,導致GPU的Utilization下降。此外,由於執行了IF/ELSE等相應的所有Instruction,GPU必須要執行的Instruction的個數增加了。總之,最重要的是要確保在編程過成功不發生Branch Divergence。
出處
http://www.gpgpu-sim.org/
Stack-less SIMT Reconvergence at Low Cost

About JP

鏈接: Website.
在這裏插入圖片描述
在這裏插入圖片描述
出生在韓國的TA。
1997年開始從事電腦圖形視覺化工作後,在這個行業已經有21年經驗了。
在多個網絡遊戲公司引領過美術團隊,之前在allegorithmic擔任TA負責人,在中國網易盤古工作室擔任TA總監,現在是巨人網絡TA部門的總負責人。
懶惰的人才有創意”是他堅信並執行的哲學道理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章