簡體   English   中英

CUDA的nvvp報告非理想的內存訪問模式,但帶寬幾乎達到峰值

[英]CUDA's nvvp reports non-ideal memory access pattern, but bandwidth is almost peaking

編輯:一個新的最小工作示例,以說明問題並更好地解釋nvvp的結果(遵循評論中給出的建議)。

因此,我制作了一個“最小”的工作示例,如下所示:

#include <cuComplex.h>
#include <iostream>

int const n = 512 * 100;

typedef float real;

template < class T >
struct my_complex {
   T x;
   T y;
};

__global__ void set( my_complex< real > * a )
{
   my_complex< real > & d = a[ blockIdx.x * 1024 + threadIdx.x ];
   d = { 1.0f, 0.0f };
}

__global__ void duplicate_whole( my_complex< real > * a )
{
   my_complex< real > & d = a[ blockIdx.x * 1024 + threadIdx.x ];
   d = { 2.0f * d.x, 2.0f * d.y };
}

__global__ void duplicate_half( real * a )
{
   real & d = a[ blockIdx.x * 1024 + threadIdx.x ];
   d *= 2.0f;
}

int main()
{
   my_complex< real > * a;
   cudaMalloc( ( void * * ) & a, sizeof( my_complex< real > ) * n * 1024 );

   set<<< n, 1024 >>>( a );
   cudaDeviceSynchronize();
   duplicate_whole<<< n, 1024 >>>( a );
   cudaDeviceSynchronize();
   duplicate_half<<< 2 * n, 1024 >>>( reinterpret_cast< real * >( a ) );
   cudaDeviceSynchronize();

   my_complex< real > * a_h = new my_complex< real >[ n * 1024 ];
   cudaMemcpy( a_h, a, sizeof( my_complex< real > ) * n * 1024, cudaMemcpyDeviceToHost );

   std::cout << "( " << a_h[ 0 ].x << ", " << a_h[ 0 ].y << " )" << '\t' << "( " << a_h[ n * 1024 - 1 ].x << ", " << a_h[ n * 1024 - 1 ].y << " )"  << std::endl;

   return 0;
}

當我編譯並運行上述代碼時,內核duplicate_wholeduplicate_half大約需要相同的時間才能運行。

但是,當我使用nvvp分析內核時,在以下意義上,我對每個內核都有不同的報告。 對於內核duplicate_whole ,nvvp警告我,在線路23( d = { 2.0f * dx, 2.0f * dy };內核是執行

Global Load L2 Transaction/Access = 8, Ideal Transaction/Access = 4

我同意我正在加載8個字節的單詞。 我不明白的是為什么4個字節是理想的字長。 特別是,內核之間沒有性能差異。

我認為在某些情況下,這種全局存儲訪問模式可能會導致性能下降。 這些是什么?

為什么我沒有表現出色?

我希望此編輯澄清了一些不清楚的地方。

++++++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++++++++

我將開始一些內核代碼來舉例說明我的問題,下面將對此進行介紹

template < class data_t >
__global__ void chirp_factors_multiply( std::complex< data_t > const * chirp_factors,
                                        std::complex< data_t > * data,
                                        int M,
                                        int row_length,
                                        int b,
                                        int i_0
                                        )
{
#ifndef CUGALE_MUL_SHUFFLE
    // Output array length:
    int plane_area = row_length * M;
    // Process element:
    int i = blockIdx.x * row_length + threadIdx.x + i_0;
    my_complex< data_t > const chirp_factor = ref_complex( chirp_factors[ i ] );
    my_complex< data_t > datum;
    my_complex< data_t > datum_new;

    for ( int i_b = 0; i_b < b; ++ i_b )
    {
        my_complex< data_t > & ref_datum = ref_complex( data[ i_b * plane_area + i ] );
        datum = ref_datum;
        datum_new.x = datum.x * chirp_factor.x - datum.y * chirp_factor.y;
        datum_new.y = datum.x * chirp_factor.y + datum.y * chirp_factor.x;
        ref_datum = datum_new;
    }
#else
    // Output array length:
    int plane_area = row_length * M;
    // Element to process:
    int i = blockIdx.x * row_length + ( threadIdx.x + i_0 ) / 2;
    my_complex< data_t > const chirp_factor = ref_complex( chirp_factors[ i ] );

    // Real and imaginary part of datum (not respectively for odd threads):
    data_t datum_a;
    data_t datum_b;

    // Even TIDs will read data in regular order, odd TIDs will read data in inverted order:
    int parity = ( threadIdx.x % 2 );
    int shuffle_dir = 1 - 2 * parity;
    int inwarp_tid = threadIdx.x % warpSize;

    for ( int i_b = 0; i_b < b; ++ i_b )
    {
        int data_idx = i_b * plane_area + i;
        datum_a = reinterpret_cast< data_t * >( data + data_idx )[ parity ];
        datum_b = __shfl_sync( 0xFFFFFFFF, datum_a, inwarp_tid + shuffle_dir, warpSize );

        // Even TIDs compute real part, odd TIDs compute imaginary part:
        reinterpret_cast< data_t * >( data + data_idx )[ parity ] = datum_a * chirp_factor.x - shuffle_dir * datum_b * chirp_factor.y;
    }
#endif // #ifndef CUGALE_MUL_SHUFFLE
}

讓我們考慮data_t為float的情況,這是受內存帶寬限制的。 從上面可以看出,內核有兩個版本,一個版本的每個線程讀/寫8個字節(整個復數),另一個版本的每個線程讀/寫4個字節,然后對結果進行混洗,因此復雜乘積為計算正確。

之所以使用shuffle編寫版本,是因為nvvp堅持認為每個線程讀取8個字節並不是最好的主意,因為這種內存訪問模式效率不高。 即使在兩個測試的系統(GTX 1050和GTX Titan Xp)中,內存帶寬都非常接近理論最大值。

當然,我知道不可能進行任何改進,的確如此:兩個內核幾乎都在同一時間運行。 因此,我的問題如下:

為什么nvvp報告讀取8個字節的效率比讀取每個線程4個字節的效率低? 在什么情況下會是這種情況?

附帶說明一下,單精度對我來說更重要,但在某些情況下雙精度也很有用。 有趣的是,在data_t為double的情況下,兩個內核版本之間也沒有執行時間差,即使在這種情況下,內核是受計算限制的,並且shuffle版本比原始版本執行更多的翻轉。

注意:將內核應用於row_length * M * b數據集( with row_length列和M行的b圖像),並且chirp_factor數組為row_length * M 兩個內核都運行良好(如果您對此有疑問,可以編輯問題以向您顯示對兩個版本的調用)。

這里的問題與編譯器如何處理您的代碼有關。 nvvp只是盡職盡責地報告運行代碼時發生的情況。

如果您使用cuobjdump -sass上的可執行文件的工具,你會發現, duplicate_whole程序做兩個4字節的負載和兩個4字節存儲。 這不是最佳選擇,部分原因是每次加載和存儲都需要大步前進(每個加載和存儲觸摸會在內存中交替出現)。

這樣做的原因是編譯器不知道my_complex結構的對齊方式。 您的結構在防止編譯器生成(合法)8字節負載的情況下使用是合法的。 如此處所討論的我們可以通過通知編譯器我們只打算在CUDA 8字節加載合法(即“自然對齊”)的對齊方案中使用該結構來解決此問題。 對結構的修改如下所示:

template < class T >
struct  __align__(8) my_complex {
   T x;
   T y;
};

更改了代碼后,編譯器將為duplicate_whole內核生成8字節的加載,並且您應該會從分析器看到不同的報告。 僅在了解含義並願意與編譯器簽訂合同以確保確實如此時,才應使用這種修飾。 如果您執行不尋常的操作(例如異常的指針轉換),則可能會違反協議,並會導致機器故障。

您看不到太多性能差異的原因幾乎可以肯定與CUDA加載/存儲行為以及GPU 緩存有關

當您跨步加載時,GPU仍然會加載整個緩存行,即使(在這種情況下)您只需要為特定的加載操作使用一半的元素(實際元素)。 但是,無論如何,您都需要元素的另一半(虛構的元素)。 它們將被加載到下一條指令上,由於先前的加載,該指令很可能會命中高速緩存。

在這種情況下,在跨步存儲中,將跨步元素寫入一條指令而將替代元素寫入下一指令將最終使用其中一個緩存作為“合並緩沖區”。 在CUDA術語中,這並不是典型意義上的合並。 這種合並僅適用於單個指令。 但是,高速緩存的“ coalescing緩沖區”行為允許它在該行被寫出或逐出之前“積累”對已駐留行的多次寫入。 這大約等效於“寫回”緩存行為。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM