0

GPU で単純な幾何学的ブラウン運動を実装しています。私のコードはうまく機能します。つまり、正しい値が得られます。私の懸念は、私が得ているスピードアップに関してです。私はもう少し期待していました。これまでのところ、グローバル メモリのみにアクセスする 2 つの実装があり、約 3 倍の速度が向上し、2 つ目は共有メモリを使用して約 2.3 倍の速度が向上します。

私の質問は、Nvidia Visual Profiler でアプリケーションをプロファイリングした後に来ました。それによると、ロード/ストアの効率は 100% ですが、DRAM の使用率は非常に低く (約 10%)、アクセスが結合されていないため、グローバル メモリのリプレイはほぼ 50% です。

共有メモリを使用して常にグローバル メモリ アクセスを回避しようとしていることがわかりましたが、DRAM が低下し (4.5%)、グローバル メモリ リプレイが 46.3% になったことに驚きました。

ブロックごとに使用可能なすべての共有メモリをほとんど使用しているため、カーネル起動時の占有率が低いことに気付きましたが、これが 2 番目のアプローチのパフォーマンスの低下を説明できるかどうかはわかりません。

パフォーマンスに関して何が起こっているのか、そしてそれを改善するためにどこで/何を探すことができるかについて、アドバイスをいただけますか?

CUDA_IMPLEMENTATION.CU

#define BLOCK_SIZE  64

#define SHMEM_ROWS  7       //The same as c_numTimeSteps = numTimeSteps
#define SHMEM_COLS  BLOCK_SIZE

__constant__ double c_c1;
__constant__ double c_c2;
__constant__ int c_numTimeSteps;
__constant__ int c_numPaths;
__constant__ double c_timeNodes[2000];

__global__
void kernelSharedMem(double *rv, double *pb)
{
    __shared__ double sh_rv[SHMEM_ROWS*SHMEM_COLS];
    __shared__ double sh_pb[(SHMEM_ROWS+1)*SHMEM_COLS];

    int p = blockDim.x * blockIdx.x + threadIdx.x;

    //The idea of this outter loop is to have tiles along the rows
    for(int tb = 0; tb < c_numTimeSteps; tb += SHMEM_ROWS)
    {
        //Copy values into shared memory
        for(int is = tb, isSh = 0;
            is < tb+SHMEM_ROWS && is < c_numTimeSteps;
            is++, isSh++)
        {
            sh_rv[isSh*SHMEM_COLS+threadIdx.x] = 
                rv[is*c_numPaths+p];
        }

        sh_pb[threadIdx.x] = pb[tb*numPaths+p];

        __syncthreads();

        //Main computation in SHARED MEMORY
        for(int isSh = 0; isSh < SHMEM_ROWS; isSh++)
        {
            double dt = c_timeNodes[isSh];
            double sdt = sqrt(dt) * c_c1;
            double mdt = c_c2 * dt;

            sh_pb[(isSh+1)*SHMEM_COLS+threadIdx.x] =
                sh_pb[isSh*SHMEM_COLS+threadIdx.x] *
                exp(mdt + sdt * rv[isSh*SHMEM_COLS+threadIdx.x]);

        }

        __syncthreads();

        for(int is = tb, isSh = 0;
            is < tb+SHMEM_ROWS && is < c_numTimeSteps;
            is++, isSh++)
        {
            pb[(is+1)*c_numPaths+p] = 
                sh_pb[(isSh+1)*SHMEM_COLS+threadIdx.x];
        }

    }

}

__global__
void kernelGlobalMem(double *rv, double *pb)
{
    int p = blockDim.x * blockIdx.x + threadIdx.x;

    for(int i = 0; i < c_numTimeSteps; i++)
    {
        double dt = c_timeNodes[i];
        double sdt = sqrt(dt) * c_c1;
        double mdt = c_c2 * dt;

        pb[(i+1)*c_numPaths+p] = 
            pb[i*c_numPaths+p] *
            exp(mdt + sdt * rv[i*c_numPaths+p]);

    }

}

extern "C" void computePathGpu(vector<vector<double>>* rv,
                                vector<vector<double>>* pb,
                                int numTimeSteps, int numPaths,
                                vector<double> timeNodes,
                                double c1, double c2)
{

    cudaMemcpyToSymbol(c_c1, &c1, sizeof(double));
    cudaMemcpyToSymbol(c_c2, &c2, sizeof(double));
    cudaMemcpyToSymbol(c_numTimeSteps, &numTimeSteps, sizeof(int));
    cudaMemcpyToSymbol(c_numPaths, &numPaths, sizeof(int));
    cudaMemcpyToSymbol(c_timeNodes, &(timeNodes[0]), sizeof(double)*numTimeSteps);

    double *d_rv;
    double *d_pb;

    cudaMalloc((void**)&d_rv, sizeof(double)*numTimeSteps*numPaths);
    cudaMalloc((void**)&d_pb, sizeof(double)*(numTimeSteps+1)*numPaths);

    vector<vector<double>>::iterator itRV;
    vector<vector<double>>::iterator itPB;

    double *dst = d_rv;
    for(itRV = rv->begin(); itRV != rv->end(); ++itRV)
    {
        double *src = &((*itRV)[0]);
        size_t s = itRV->size();
        cudaMemcpy(dst, src, sizeof(double)*s, cudaMemcpyHostToDevice);
        dst += s;
    }

    cudaMemcpy(d_pb, &((*(pb->begin()))[0]),
        sizeof(double)*(pb->begin())->size(), cudaMemcpyHostToDevice);

    dim3 block(BLOCK_SIZE);
    dim3  grid((numPaths+BLOCK_SIZE-1)/BLOCK_SIZE);

    kernelGlobalMem<<<grid, block>>>(d_rv, d_pb);
    //kernelSharedMem<<<grid, block>>>(d_rv, d_pb);
    cudaDeviceSynchronize();

    dst = d_pb;
    for(itPB = ++(pb->begin()); itPB != pb->end(); ++itPB)
    {
        double *src = &((*itPB)[0]);
        size_t s = itPB->size();
        dst += s;
        cudaMemcpy(src, dst, sizeof(double)*s, cudaMemcpyDeviceToHost);
    }

    cudaFree(d_pb);
    cudaFree(d_rv);

}

メイン.CPP

extern "C" void computeOnGPU(vector<vector<double>>* rv,
                                vector<vector<double>>* pb,
                                int numTimeSteps, int numPaths,
                                vector<double> timeNodes,
                                double c1, double c2);

int main(){

    int numTimeSteps = 7;
    int numPaths = 2000000;

    vector<vector<double>> rv(numTimeSteps, vector<double>(numPaths));
    //Fill rv

    vector<double> timeNodes(numTimeSteps);
    //Fill timeNodes

    vector<vector<double>> pb(numTimeSteps, vector<double>(numPaths, 0));

    computeOnGPU(&rv, &pb, numTimeSteps, numPaths, timeNodes, 0.2, 0.123);

}
4

3 に答える 3

3

他の人が指摘しているように、共有メモリ バージョンはグローバル メモリ アクセス パターンをまったく変更せず、スレッド間でカーネル内のデータを事実上再利用しません。したがって、合体の問題は解決されず、効果的に行っているのは、オーバーヘッドとして共有メモリ アクセスといくつかの同期ポイントを追加することだけです。

しかし、カーネルが実際に何をしているのかを少し見てみましょう。カーネルは倍精度で動作しており、コンシューマー カードでは遅く、計算ループ内の操作数がかなり妥当であり、これは良いことです。コンパイラにアクセスできなければ、合計時間の約半分が浮動小数点演算expの呼び出しであり、呼び出しの半分であると推測できsqrtます。これはおそらく、コンシューマ GPU 上のメモリ バウンド カーネルであってはなりません。しかし、倍精度演算の約半分は、各スレッドが同じ sqrt(dt)値を計算しているだけです。それはサイクルの膨大な浪費です。代わりに、「次元化されていない」sqrt(dt)ドメインでカーネルを反復させないでください。これは、(最大で) 2000 を事前計算することを意味します。sqrt(dt)値をホストに保存し、定数メモリに保存します。カーネル ループは次のように記述できます。

double pb0 = pb[p];
for(int i = 0; i < c_numTimeSteps; i++)
{
    double sdt = c_stimeNodes[i]; // sqrt(dt)
    double mdt = c_c2 * sdt * sdt;
    sdt *= c_c1;

    double pb1 = pb0 *  exp(mdt + sdt * rv[p]);

    p += c_numPaths;
    pb[p] = pb1;
    pb0 = pb1;
}

[ 免責事項: 午前 5 時にラップランドの真ん中で iPad に書かれています。自己責任で使用してください]

これを行うと、sqrt が乗算に置き換えられ、演算が大幅に削減されます。また、ループごとに 1 つの整数の加算にまでインデックス計算を単純化する自由も取ったことに注意してください。コンパイラは非常にスマートですが、その仕事を好きなだけ簡単にすることも難しくすることもできます。上記のようなループは、現在のものよりも大幅に高速になると思います。

于 2013-07-23T03:20:31.520 に答える
2

私のTesla M2090でコードをプロファイリングした後、これらの回答によって提供されるこれらすべての提案を並べ替える必要があると思います.

  1. memcopy 時間を短縮してみてください。H2D と D2H を含む memcopy に 97% の時間が費やされました。pageable memcpy を使用しているため、速度は 2.5G/s~3G/s です。pinned mem cpyを使用すると、速度を 2 倍にすることができます。ゼロコピーおよびその他のMem 最適化手法を適用して、memcopy の速度をさらに向上させることができます。

  2. sqrt() をカーネルの外に移動します。GPU で 7 x 2,000,000 回実行する代わりに、CPU で sqrt() を 7 回実行できます。ただし、カーネルが小さいため ( の合計時間の 3% computePathGpu())、これはあまり効果がありません。

  3. グローバル メモリ アクセスを減らします。あなたのコードでは、rv一度読んで、一度読んで、pb一度書くpbだけです。ただしpb、犬小屋を呼び出す前に、有用なデータが含まれているのは最初の行だけです。したがって、pbレジスタを使用することで、全体の読み取りをなくすことができます。解決策はコードで提供されます。

  4. 結合されていないメモリ アクセスについては、ここで議論を見つけることができます。あなたのケースは「A Sequential but Misaligned Access Pattern」に属しています。cudaMallocPitch()を使用したソリューションを以下で説明し、次のコードで提供します。

注: DRAM 使用率が低い (約 10%) とのことでしたが、私のデバイスでのプロファイリングは問題ありません (55.8%)。少し古いのは私のデバイスかもしれません(M2090 CC2.0)

プロファイリング結果

#include <vector>

using namespace std;

#define BLOCK_SIZE  64
#define BLOCK_SIZE_OPT  256

__constant__ double c_c1;
__constant__ double c_c2;
__constant__ int c_numTimeSteps;
__constant__ int c_numPaths;
__constant__ double c_timeNodes[2000];

__global__ void kernelGlobalMem(double *rv, double *pb)
{
    int p = blockDim.x * blockIdx.x + threadIdx.x;

    for (int i = 0; i < c_numTimeSteps; i++)
    {
        double dt = c_timeNodes[i];
        double sdt = sqrt(dt) * c_c1;
        double mdt = c_c2 * dt;

        pb[(i + 1) * c_numPaths + p] =
                pb[i * c_numPaths + p] *
                        exp(mdt + sdt * rv[i * c_numPaths + p]);

    }

}

__global__ void kernelGlobalMemOpt(double *rv, double *pb, const size_t ld_rv, const size_t ld_pb)
{
    int p = blockDim.x * blockIdx.x + threadIdx.x;

    double pb0 = pb[p];
    for (int i = 0; i < c_numTimeSteps; i++)
    {
        double dt = c_timeNodes[i];
        double sdt = dt * c_c1;
        double mdt = c_c2 * dt * dt;

        pb0 *= exp(mdt + sdt * rv[i * ld_rv + p]);
        pb[(i + 1) * ld_pb + p] = pb0;
    }
}

void computePathGpu(vector<vector<double> >* rv,
        vector<vector<double> >* pb,
        int numTimeSteps, int numPaths,
        vector<double> timeNodes,
        double c1, double c2)
{

    cudaMemcpyToSymbol(c_c1, &c1, sizeof(double));
    cudaMemcpyToSymbol(c_c2, &c2, sizeof(double));
    cudaMemcpyToSymbol(c_numTimeSteps, &numTimeSteps, sizeof(int));
    cudaMemcpyToSymbol(c_numPaths, &numPaths, sizeof(int));
    cudaMemcpyToSymbol(c_timeNodes, &(timeNodes[0]), sizeof(double) * numTimeSteps);

    double *d_rv;
    double *d_pb;

    cudaMalloc((void**) &d_rv, sizeof(double) * numTimeSteps * numPaths);
    cudaMalloc((void**) &d_pb, sizeof(double) * (numTimeSteps + 1) * numPaths);

    vector<vector<double> >::iterator itRV;
    vector<vector<double> >::iterator itPB;

    double *dst = d_rv;
    for (itRV = rv->begin(); itRV != rv->end(); ++itRV)
    {
        double *src = &((*itRV)[0]);
        size_t s = itRV->size();
        cudaMemcpy(dst, src, sizeof(double) * s, cudaMemcpyHostToDevice);
        dst += s;
    }

    cudaMemcpy(d_pb, &((*(pb->begin()))[0]),
            sizeof(double) * (pb->begin())->size(), cudaMemcpyHostToDevice);

    dim3 block(BLOCK_SIZE);
    dim3 grid((numPaths + BLOCK_SIZE - 1) / BLOCK_SIZE);

    kernelGlobalMem<<<grid, block>>>(d_rv, d_pb);
    //kernelSharedMem<<<grid, block>>>(d_rv, d_pb);
    cudaDeviceSynchronize();

    dst = d_pb;
    for (itPB = ++(pb->begin()); itPB != pb->end(); ++itPB)
    {
        double *src = &((*itPB)[0]);
        size_t s = itPB->size();
        dst += s;
        cudaMemcpy(src, dst, sizeof(double) * s, cudaMemcpyDeviceToHost);
    }

    cudaFree(d_pb);
    cudaFree(d_rv);

}

void computePathGpuOpt(vector<vector<double> >* rv,
        vector<vector<double> >* pb,
        int numTimeSteps, int numPaths,
        vector<double> timeNodes,
        double c1, double c2)
{
    for(int i=0;i<timeNodes.size();i++)
    {
        timeNodes[i]=sqrt(timeNodes[i]);
    }

    cudaMemcpyToSymbol(c_c1, &c1, sizeof(double));
    cudaMemcpyToSymbol(c_c2, &c2, sizeof(double));
    cudaMemcpyToSymbol(c_numTimeSteps, &numTimeSteps, sizeof(int));
    cudaMemcpyToSymbol(c_numPaths, &numPaths, sizeof(int));
    cudaMemcpyToSymbol(c_timeNodes, &(timeNodes[0]), sizeof(double) * numTimeSteps);

    double *d_rv;
    double *d_pb;
    size_t ld_rv, ld_pb;

    cudaMallocPitch((void **) &d_rv, &ld_rv, sizeof(double) * numPaths, numTimeSteps);
    cudaMallocPitch((void **) &d_pb, &ld_pb, sizeof(double) * numPaths, numTimeSteps + 1);
    ld_rv /= sizeof(double);
    ld_pb /= sizeof(double);

//  cudaMalloc((void**) &d_rv, sizeof(double) * numTimeSteps * numPaths);
//  cudaMalloc((void**) &d_pb, sizeof(double) * (numTimeSteps + 1) * numPaths);

    vector<vector<double> >::iterator itRV;
    vector<vector<double> >::iterator itPB;

    double *dst = d_rv;
    for (itRV = rv->begin(); itRV != rv->end(); ++itRV)
    {
        double *src = &((*itRV)[0]);
        size_t s = itRV->size();
        cudaMemcpy(dst, src, sizeof(double) * s, cudaMemcpyHostToDevice);
        dst += ld_rv;
    }

    cudaMemcpy(d_pb, &((*(pb->begin()))[0]),
            sizeof(double) * (pb->begin())->size(), cudaMemcpyHostToDevice);

    dim3 block(BLOCK_SIZE_OPT);
    dim3 grid((numPaths + BLOCK_SIZE_OPT - 1) / BLOCK_SIZE_OPT);

    kernelGlobalMemOpt<<<grid, block>>>(d_rv, d_pb, ld_rv, ld_pb);
    //kernelSharedMem<<<grid, block>>>(d_rv, d_pb);
    cudaDeviceSynchronize();

    dst = d_pb;
    for (itPB = ++(pb->begin()); itPB != pb->end(); ++itPB)
    {
        double *src = &((*itPB)[0]);
        size_t s = itPB->size();
        dst += ld_pb;
        cudaMemcpy(src, dst, sizeof(double) * s, cudaMemcpyDeviceToHost);
    }

    cudaFree(d_pb);
    cudaFree(d_rv);

}

int main()
{

    int numTimeSteps = 7;
    int numPaths = 2000000;

    vector<vector<double> > rv(numTimeSteps, vector<double>(numPaths));
    vector<double> timeNodes(numTimeSteps);
    vector<vector<double> > pb(numTimeSteps, vector<double>(numPaths, 0));
    vector<vector<double> > pbOpt(numTimeSteps, vector<double>(numPaths, 0));
    computePathGpu(&rv, &pb, numTimeSteps, numPaths, timeNodes, 0.2, 0.123);
    computePathGpuOpt(&rv, &pbOpt, numTimeSteps, numPaths, timeNodes, 0.2, 0.123);
}

各 cuda スレッドは、すべての時間ステップに対して 1 つのパスを計算します。GlobalMem コードによると、パス間でデータを共有していません。したがって、共有メモリは必要ありません。

nvprof によって検出された結合されていないアクセスの問題については、データ pb と rv が適切に配置されていないことが原因です。pb と rv は、[タイム ステップ x #パス] のサイズの行列として見ることができます。#path はキャッシュ ラインの倍数ではないため、2 行目、つまり時間ステップから始まるため、すべてのグローバル メモリ アクセスは結合されていません。CUDA デバイスが古い場合、50% のメモリ リプレイが発生します。新しいデバイスは、この種の結合されていないアクセスの影響を受けません。

解決策は簡単です。行の両端にパディング バイトを追加するだけで、すべての行が結合された DRAM アドレスから開始できるようになります。これはcudaMallocPitch()によって自動的に行うことができます

別の問題があります。コードでは、rv を 1 回読み取り、pb を 1 回読み取り、pb を 1 回書き込むだけです。ただし、犬小屋を呼び出す前に、pb には有用なデータが含まれていません。したがって、レジスタを使用することで pb の読み取りをなくすことができ、結合されていないアクセスの問題を解決するだけでなく、速度をさらに 50% 向上させることができます。

于 2013-07-23T01:47:49.003 に答える