2013-10-25 115 views
2

這是我的理解(請參閱,例如How can I enforce CUDA global memory coherence without declaring pointer as volatile?,CUDA block synchronization differences between GTS 250 and Fermi devicesthis post in the nvidia Developer Zone__threadfence()保證在線程繼續之前全局寫入對其他線程可見。但是,在__threadfence()已返回之後,另一個線程仍然可以從L1高速緩存讀取陳舊值,即使也是如此。__threadfence()和L1高速緩存一致性

即:

線程A的一些數據寫入到全局存儲器,然後調用__threadfence()。然後,在之後__threadfence()已經返回,並且寫入可見到全部其他線程,線程B被要求從這個內存位置讀取。它發現它具有L1中的數據,因此會加載該數據。不幸的是,對於開發者來說,線程B的L1中的數據是陳舊的(即和線程A更新這個數據一樣)。

首先:這是正確的嗎?

假如是的話,它似乎我__threadfence()只有有用的,如果任何一個可以是某些該數據不會在L1(有點不太可能?),或者如果例如讀取總是繞過L1(例如易失性或原子性)。它是否正確?


我問,因爲我有一個比較簡單的用例 - 傳播的數據了一個二叉樹 - 用原子級組標誌和__threadfence():第一個線程到達某個節點退出,而第二寫入數據到它基於其兩個孩子(例如他們的最小數據)。這適用於大多數節點,但通常至少失敗一次。聲明數據volatile可獲得始終如一的正確結果,但會導致99%以上的未從L1中獲取陳舊值的情況下的性能下降。我想確定這是該算法的唯一解決方案。下面給出了一個簡化的例子。請注意,節點數組的排列寬度優先,葉子從索引start開始,並已填充數據。

__global__ void propagate_data(volatile Node *nodes, 
           const unsigned int n_nodes, 
           const unsigned int start, 
           unsigned int* flags) 
{ 
    int tid, index, left, right; 
    float data; 
    bool first_arrival; 

    tid = start + threadIdx.x + blockIdx.x*blockDim.x; 

    while (tid < n_nodes) 
    { 
     // We start at a node with a full data section; modify its flag 
     // accordingly. 
     flags[tid] = 2; 

     // Immediately move up the tree. 
     index = nodes[tid].parent; 
     first_arrival = (atomicAdd(&flags[index], 1) == 0); 

     // If we are the second thread to reach this node then process it. 
     while (!first_arrival) 
     { 
      left = nodes[index].left; 
      right = nodes[index].right; 

      // If Node* nodes is not declared volatile, this occasionally 
      // reads a stale value from L1. 
      data = min(nodes[left].data, nodes[right].data); 

      nodes[index].data = data; 

      if (index == 0) { 
       // Root node processed, so all nodes processed. 
       return; 
      } 

      // Ensure above global write is visible to all device threads 
      // before setting flag for the parent. 
      __threadfence(); 

      index = nodes[index].parent; 
      first_arrival = (atomicAdd(&flags[index], 1) == 0); 
     } 
     tid += blockDim.x*gridDim.x; 
    } 
    return; 
} 
+0

在你的推理中,'volatile'關鍵字的作用基本上是禁止使用緩存。現在,你的問題是L1緩存不一致。但L2是連貫的。那麼禁用L2緩存而不是使用'volatile'呢? – JackOLantern

+0

禁用L2緩存?你是怎樣做的? –

+0

@RobertCrovella對不起,羅伯特,這是一個誤印。我的意思是禁用L1(不是L2),這是由'-Xptxas -dlcm = cg'完成的。我的理解,也似乎從你的回答中推斷出來的,就是使用'volatile',並結合'__threadfence()',繞過L1。所以,我想知道禁用L1緩存與使用'volatile'結合使用'__threadfence()'會有什麼效果。缺點是'-Xptxas -dlcm = cg'會在整個執行過程中禁用L1緩存,而'volatile'則是「選擇性的」。 – JackOLantern

回答

4

首先:這是正確的?

是的,__threadfence()將數據推入L2並輸出到全局內存。它對其他 SM中的L1緩存沒有影響。

這是正確的嗎?

是的,如果你有volatile結合__threadfence()對於全局存儲器訪問,你應該有信心,價值最終會以其他threadblocks可見。但請注意,線程塊之間的同步在CUDA中並不是一個明確的概念。沒有明確的機制可以這樣做,也不能保證線程塊的執行順序,所以僅僅因爲你的代碼有一個__threadfence()某處在volatile項上運行,但仍然不能保證另一個線程塊可能獲得哪些數據。這也取決於執行的順序。

如果您使用volatile,應該繞過L1(如果啓用 - current Kepler devices don't really have L1 enabled for general global access)。如果您不使用volatile,那麼當前正在執行__threadfence()操作的SM的L1應該在完成__threadfence()操作時與L2(和全局)一致/一致。

請注意,L2緩存在設備中是統一的,因此始終是「一致的」。至少從設備代碼的角度來看,對於您的用例來說,無論您在使用哪個SM,L2和全局內存之間沒有區別。

而且,如您所示,(全局)原子總是在L2 /全局內存上運行。

+0

啊哈!這也解釋了爲什麼我在費米硬件上看到這個問題,而不是在開普勒。 – Sam

+0

雖然,當*使用__threadfence()時,我仍然被*弄糊塗了。例如,以[內存圍欄功能](http://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#memory-fence-functions)中的CUDA編程指南中的示例爲例。 最後一個塊中的線程在執行總和時是否可以從L1中讀取結果的陳舊值?例如。他們可能將'result [blockIdx.x-1]'讀爲0? – Sam

+1

'__threadfence()'*不會繞過L1('volatile' * does *)。先前在L1中寫入(因此緩存線加載)在這個例子中都被後綴爲'__threadfence()'操作。這因此保證了被加載以服務先前寫入'result [blockIdx.x]'的L1緩存線都與L2 /全局或無效一致。無論哪種方式,都沒有陳舊的數據。我只是在這裏重申我在回答中已經說過的內容(以及該例中的評論中提及的內容),所以我可能不會理解您的困惑。 –