6

BS_x * BS_Yスレッドがコンテンツを共有メモリに移動することで(BS_X + 1)*(BS_Y + 1)グローバルメモリの場所を読み取りたいので、次のコードを開発しました。

int i       = threadIdx.x;
int j       = threadIdx.y;
int idx     = blockIdx.x*BLOCK_SIZE_X + threadIdx.x;
int idy     = blockIdx.y*BLOCK_SIZE_Y + threadIdx.y;

int index1  = j*BLOCK_SIZE_Y+i;

int i1      = (index1)%(BLOCK_SIZE_X+1);
int j1      = (index1)/(BLOCK_SIZE_Y+1);

int i2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)%(BLOCK_SIZE_X+1);
int j2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)/(BLOCK_SIZE_Y+1);

__shared__ double Ezx_h_shared_ext[BLOCK_SIZE_X+1][BLOCK_SIZE_Y+1];     

Ezx_h_shared_ext[i1][j1]=Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1)];

if ((i2<(BLOCK_SIZE_X+1))&&(j2<(BLOCK_SIZE_Y+1))) 
Ezx_h_shared_ext[i2][j2]=Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j2)*xdim+(blockIdx.x*BLOCK_SIZE_X+i2)];

私の理解では、合体は、順次処理の連続メモリー読み取りと並列に相当します。グローバルメモリアクセスが合体しているかどうかを今すぐ検出するにはどうすればよいですか?(i1、j1)から(i2、j2)へのインデックスジャンプがあることに注意してください。前もって感謝します。

4

3 に答える 3

5

手書きの合体アナライザーを使用して、コードのメモリアクセスを評価しました。評価は、コードが合体をあまり利用していないことを示しています。役立つと思われる合体アナライザーは次のとおりです。

#include <stdio.h>
#include <malloc.h>

typedef struct dim3_t{
    int x;
    int y;
} dim3;


// KERNEL LAUNCH PARAMETERS
#define GRIDDIMX 4
#define GRIDDIMY 4
#define BLOCKDIMX 16
#define BLOCKDIMY 16


// ARCHITECTURE DEPENDENT
// number of threads aggregated for coalescing
#define COALESCINGWIDTH 32
// number of bytes in one coalesced transaction
#define CACHEBLOCKSIZE 128
#define CACHE_BLOCK_ADDR(addr,size)  (addr*size)&(~(CACHEBLOCKSIZE-1))


int main(){
    // fixed dim3 variables
    // grid and block size
    dim3 blockDim,gridDim;
    blockDim.x=BLOCKDIMX;
    blockDim.y=BLOCKDIMY;
    gridDim.x=GRIDDIMX;
    gridDim.y=GRIDDIMY;

    // counters
    int unq_accesses=0;
    int *unq_addr=(int*)malloc(sizeof(int)*COALESCINGWIDTH);
    int total_unq_accesses=0;

    // iter over total number of threads
    // and count the number of memory requests (the coalesced requests)
    int I, II, III;
    for(I=0; I<GRIDDIMX*GRIDDIMY; I++){
        dim3 blockIdx;
        blockIdx.x = I%GRIDDIMX;
        blockIdx.y = I/GRIDDIMX;
        for(II=0; II<BLOCKDIMX*BLOCKDIMY; II++){
            if(II%COALESCINGWIDTH==0){
                // new coalescing bunch
                total_unq_accesses+=unq_accesses;
                unq_accesses=0;
            }
            dim3 threadIdx;
            threadIdx.x=II%BLOCKDIMX;
            threadIdx.y=II/BLOCKDIMX;

            ////////////////////////////////////////////////////////
            // Change this section to evaluate different accesses //
            ////////////////////////////////////////////////////////
            // do your indexing here
            #define BLOCK_SIZE_X BLOCKDIMX
            #define BLOCK_SIZE_Y BLOCKDIMY
            #define xdim 32
            int i       = threadIdx.x;
            int j       = threadIdx.y;
            int idx     = blockIdx.x*BLOCK_SIZE_X + threadIdx.x;
            int idy     = blockIdx.y*BLOCK_SIZE_Y + threadIdx.y;

            int index1  = j*BLOCK_SIZE_Y+i;

            int i1      = (index1)%(BLOCK_SIZE_X+1);
            int j1      = (index1)/(BLOCK_SIZE_Y+1);

            int i2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)%(BLOCK_SIZE_X+1);
            int j2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)/(BLOCK_SIZE_Y+1);
            // calculate the accessed location and offset here
            // change the line "Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1)];" to
            int addr = (blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1);
            int size = sizeof(double);
            //////////////////////////
            // End of modifications //
            //////////////////////////

            printf("tid (%d,%d) from blockid (%d,%d) accessing to block %d\n",threadIdx.x,threadIdx.y,blockIdx.x,blockIdx.y,CACHE_BLOCK_ADDR(addr,size));
            // check whether it can be merged with existing requests or not
            short merged=0;
            for(III=0; III<unq_accesses; III++){
                if(CACHE_BLOCK_ADDR(addr,size)==CACHE_BLOCK_ADDR(unq_addr[III],size)){
                    merged=1;
                    break;
                }
            }
            if(!merged){
                // new cache block accessed over this coalescing width
                unq_addr[unq_accesses]=CACHE_BLOCK_ADDR(addr,size);
                unq_accesses++;
            }
        }
    }
    printf("%d threads make %d memory transactions\n",GRIDDIMX*GRIDDIMY*BLOCKDIMX*BLOCKDIMY, total_unq_accesses);
}

コードはグリッドのすべてのスレッドに対して実行され、マージされたリクエストの数、メモリアクセスの合体のメトリックを計算します。

アナライザーを使用するには、コードのインデックス計算部分を指定された領域に貼り付け、メモリアクセス(配列)を「アドレス」と「サイズ」に分解します。インデックスが作成されているコードに対して、これはすでに実行されています。

int i       = threadIdx.x;
int j       = threadIdx.y;
int idx     = blockIdx.x*BLOCK_SIZE_X + threadIdx.x;
int idy     = blockIdx.y*BLOCK_SIZE_Y + threadIdx.y;

int index1  = j*BLOCK_SIZE_Y+i;

int i1      = (index1)%(BLOCK_SIZE_X+1);
int j1      = (index1)/(BLOCK_SIZE_Y+1);

int i2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)%(BLOCK_SIZE_X+1);
int j2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)/(BLOCK_SIZE_Y+1);

そして、メモリアクセスは次のとおりです。

Ezx_h_shared_ext[i1][j1]=Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1)];

アナライザーは、4096個のスレッドが4064個のキャッシュブロックにアクセスしていることを報告します。実際のグリッドとブロックサイズのコードを実行し、合体動作を分析します。

于 2012-12-07T23:08:06.893 に答える
3

GPU が進化するにつれて、結合されたアクセスを取得するための要件の制限が緩和されました。合体アクセスの説明は、最近の GPU アーキテクチャよりも以前の GPU アーキテクチャの方が正確です。特に、Fermi (計算能力 2.0) では要件が大幅に緩和されました。Fermi 以降では、メモリ位置に連続してアクセスすることは重要ではありません。代わりに、可能な限り少ないメモリ トランザクションでメモリにアクセスすることに焦点が移りました。Fermi では、グローバル メモリ トランザクションは 128 バイト幅です。そのため、ワープ内の 32 のスレッドがロードまたはストアを実行する命令にヒットすると、ワープ内のすべてのスレッドを処理するために 128 バイトのトランザクションがスケジュールされます。パフォーマンスは、必要なトランザクションの数によって異なります。すべてのスレッドが、128 バイトにアラインされた 128 バイト領域内の値にアクセスする場合、単一のトランザクションが必要です。すべてのスレッドが異なる 128 バイト領域の値にアクセスする場合、32 トランザクションが必要になります。これは、ワープ内の単一の命令に対する要求を処理するための最悪のシナリオです。

CUDA プロファイラーの 1 つを使用して、リクエストの処理に必要なトランザクション数の平均を決定します。数値はできるだけ 1 に近づける必要があります。数値が大きいほど、カーネルでメモリ アクセスを最適化する機会があるかどうかを確認する必要があります。

于 2012-12-07T22:49:09.463 に答える
1

ビジュアル プロファイラーは、作業を確認するための優れたツールです。機能的に正しいコードを作成したら、ビジュアル プロファイラー内から実行します。たとえば Linux では、X セッションがあると仮定すると、ターミナル ウィンドウから nvvp を実行するだけです。ウィザードが表示され、コマンド ライン パラメータとともにアプリケーションをプロファイリングするよう求められます。

次に、プロファイラーはアプリの基本的な実行を行い、統計を収集します。より高度な統計収集 (追加の実行が必要) を選択することもできます。これらの 1 つがメモリ使用率の統計です。メモリ使用率をピーク時のパーセンテージとして報告し、使用率が低いと見なされて注意が必要な場合は警告を発します。

使用率が 50% を超えている場合、アプリはおそらく期待どおりに動作しています。数値が低い場合は、合体の詳細を見逃している可能性があります。メモリ読み取りとメモリ書き込みの統計を個別にレポートします。100% またはそれに近い値を得るには、warp からの結合された読み取りと書き込みが 128 バイト境界で整列されていることを確認する必要もあります。

このような状況でよくある間違いは、threadIdx.y ベースの変数を最も急速に変化するインデックスとして使用することです。あなたがその間違いを犯したようには思えません。shared[threadIdx.x][threadIdx.y]たとえば、これは C で考える方法であることが多いため、よくある間違いです。しかし、スレッドは x 軸で最初にグループ化されるためshared[threadIdx.y][threadIdx.x]、または類似のものを使用したいと考えています。この間違いを犯した場合でも、コードは機能的には正しい可能性がありますが、プロファイラーで得られる使用率の数値は、約 12% または 3% のように低くなります。

既に述べたように、50% を超えて 100% に近づけるには、すべてのスレッド リクエストが隣接しているだけでなく、128B 境界に配置されていることを確認する必要があります。L1/L2 キャッシュのため、これらは厳格なルールではなく、ガイドラインです。キャッシュは、いくつかの間違いをある程度軽減する可能性があります。

于 2012-12-07T22:46:16.073 に答える