2

初めて OpenCL を使い始めたので、リダクション カーネルを最適化しようとしています。カーネルは、幅×長さピクセルのサイズの浮動小数点数 (データはグレースケール イメージの輝度値を表す) の正方グリッドを使用します。カーネルはすべての列に沿って合計し、各列の合計を出力配列に返します。

/* 
input  -- "2D" array of floats with width * height number of elements
output -- 1D array containing summation of column values with width number of elements
width  -- number of elements horizontally
height -- number of elements vertically

Both width and height must be a multiple of 64.
*/
kernel void sum_columns(global float* input, global float* output,  int width, int height)
{        
    size_t j = get_global_id(0);
    float sum = 0.0;
    int i;
    for(i=0; i<height; i++) {
        sum += input[i + width*j];
    }
    output[j] = sum;
}

グローバル次元をデータの列数に設定したため、OpenCLはすべての列の合計を同時に実行する必要があります。私は MacOS で Instruments.app を使用し、CPU と GPU で実行するときにカーネルの 1000 回の反復にかかる時間を計りました。これは、デバイスを または のいずれかに指定することで実行できCL_DEVICE_TYPE_CPUますCL_DEVICE_TYPE_GPU

性能がイマイチ!実際、CPU は一貫して GPU より高速ですが、これは非常に奇妙に思えます。カーネルに何か問題がありますか?せいぜい約 8 スレッドしか同時に実行できない場合、CPU を高速化するにはどうすればよいでしょうか?

このプロジェクトのコードはこちら (Xcode プロジェクト)、https://github.com/danieljfarrell/Xcode-4-OpenCL-Exampleです。

データのサイズを増やしたときのタイミング結果は次のとおりです。

画像サイズの関数としての実行時間。

アップデート

CPU と GPU のタイミングは、Instruments.app を使用してカーネルを実行するのにかかった時間を見ることで簡単にわかります。 インターフェイスを使用した Instruments.app のスクリーンショット。カーネルの実行時間が強調表示されています。

4

4 に答える 4

4

試してみる簡単な改善の 1 つは、グローバルではなく入力定数メモリを作成することです。バッファを作成するときに CL_MEM_READ_ONLY で設定する必要があります。私が使用しているプロファイラーは、そのパラメーターのみを __constant に変更すると、カーネルがそのまま気に入っているようです。

別のオプションは、入力行列を転置することです。これにより、メモリの列を読み取ろうとしなくなります。ワーク グループ全体を使用してデータ行を合計し、出力に 1 つのエントリを生成するカーネルを作成しました。__constant パラメーターは、このカーネルを大いに助け、私が実行した 4 つの試行のうち、グローバル フェッチ バウンドではなく、ALU バウンドになる唯一の実行になりました。

高さパラメーターをループしませんでしたが、それを設定するか、出力データ用に十分な作業グループ (要素ごとに 1 つ) を作成することができます。

kernel void sum_rows(__constant  float* input, global float* output,  int width, int height)
{        
    int gid = get_local_id(0);
    int gsize = get_local_size(0);
    local float sum[64]; //assumes work group size of 64
    sum[gid] = 0;
    int i;
    int rowStart = width * get_group_id(0);
    for(i=gid; i<width; i+=gsize) {
        sum[gid] += input[rowStart + i];
    }
    barrier(CLK_LOCAL_MEM_FENCE);
    if(gid == 0){
        for(i = 0;i<64;i++){
            sum[0] += sum[i];
        }
        output[get_group_id(0)] = sum[0];
    }
}

さらに、ホストレベルの最適化を検討することをお勧めします。十分な大きさのデータ セットがあれば、gpu がリダクション カーネルの cpu よりも優れたパフォーマンスを発揮する問題はありません。

于 2013-03-27T02:45:00.597 に答える
3

問題を複数のカーネル実行に分割する必要があります。大きなループ (この場合、「高さ」が大きくなる可能性があります) を持つことは、OpenCL では好ましくなく、ループを展開する必要もあります。これは、「#pragma unroll X」を使用して自動的に行うことができます。ここで、「X」はループが実行される回数です。

複数のカーネル実行に分割する必要があるため、問題は少し難しくなります。手順は次のとおりです。

  1. アンロールできる最大数を決定し、各スレッドがその数の要素に対してのみ動作するようにします。
  2. 説明したループ展開を使用する
  3. これにより、各列の部分合計が生成されます。
  4. 合計が少量になるまで、カーネルを何度も実行します (試行錯誤によってこの数を決定します)。
  5. CPU で最終的な部分合計を合計します。注: 最後の CPU 加算ステップまで、部分合計を CPU にコピーしないでください。

他にも戦略はありますが、これは実際にこの計算で GPU が勝つための最初のステップになります。

于 2013-03-26T16:24:01.553 に答える
3

実際、OPenCL の使用にはトレードオフがあり、特定のタスクに OpenCL を使用すると実際にはパフォーマンスが低下する可能性があります。

おそらく、GPU で実際に実行される作業を増やして、コンテキストをセットアップするコストを超えるようにすることもできます。

すべてのジョブが OpenCL の恩恵を受けるわけではないことに注意してください。ほとんどの場合、アプリケーションが OpenCL の恩恵を受けるかどうかを判断するためにテストを行う必要があります。
参照: OpenCL はどのような作業に適しているか

  • コンテキストのセットアップとデータ転送のオーバーヘッド

コンテキストのセットアップと PCI バスを介したデータ転送のオーバーヘッドのため、OpenCL を使用するメリットを得るには、かなり大きなデータ セットを処理する必要があります。利点が見られる正確なポイントは、OpenCL の実装と使用されているハードウェアによって異なるため、アルゴリズムをどれだけ速く実行できるかを実験する必要があります。一般に、データ アクセスに対する計算の割合が高く、多くの数学的計算が OpenCL プログラムに適しています。

出典: MAC 用 OpenCL プログラミング ガイド

  • 小さなジョブ / 細分化されすぎたジョブは、CPU でのパフォーマンスが向上します

たとえば、OpenCL を使用して実行するジョブが小さすぎたり、細分化されていたりすると、OpenCL を使用して実際のジョブを実行するよりも、すべてをセットアップする時間が失われます。

追加するセグメンテーション コードが増えるほど、OpenCL コードは遅くなります。
[…]
3 つのことがあなたを殺します。
OpenCL 呼び出しのレイテンシ。つまり、OpenCL 関数を呼び出すには、「実際の Java/C# 関数」を呼び出すよりも時間がかかります。
第 2 に、GPU がメイン コンピューター メモリにアクセスしてデータをコピーするのにかなりの時間がかかります。[...]

出典: OpenCL の現在の問題 (2010)

  • ベンチマーク担当者が次のように述べているこのページもあります。

GPU が提供する複数のスレッドをプログラムが使用できるため、サイズの値が大きい場合、GPU が CPU よりも優れていることが明らかにわかります。サイズの値が小さいと、GPU に関連するかなりのアクセス時間が発生するため、CPU のパフォーマンスが向上します。

出典: OpenCL による CPU と GPU のパフォーマンス比較(2011 年 10 月)

于 2013-03-24T14:07:37.327 に答える
1

First of all I agree with mfa it would be better if you have your data transposed. This way you will read data from global memory sequentially (search for bank conflicts). But this is only one thing.

Other thing is to change your algoritm. Current drawbacks of your approach 1) you have small number of work items - equal to height of your image. 2) long cycle.

I'd suggest you to rewrite your algo to actually make it parallel next way: E.g. you have to sum 512 items. Then you run workgroup that has 256 work items. each work item adds up 2 values. E.g. 1st will add v[1]=v[0]+v[1], 2nd v[3] = v[2]+v[3] and so on. So, after first operation you'll have pair sums in odd indices. Next cycle you make similar procedure, but only 128 work items do the job, because you already have only 256 elements to process. The only difference that now 1st work item will do v[3] = v[1]+v[3], 2nd v[7] = v[5] + v[7] and so on. This way you have 1) O(logN) complexity instead of O(N) 2) you spawn more items which do less work. -> benefit from parallelization.

Of course you will need to call barrier(...) instruction after every write to sync calculations between work items in work group.

To speed up even more, first each work group copies its values from global to local memory and performs calculations using local memory.

One question, that you might want to ask: "What if i need to sum too many values (say 100000) and i cant create work group with such big number of work items". In this case you do partial sums and then run your kernel second time to sum those partial sums. Because you can't synchronize between work groups while executing kernel.

To be more clear here is the code. It sums 2 x blockSize values. Hope I did not make any mistakes (did not actually compile this one)

// run Work group with local size = (BLK_SIZ, 1), global size = ( width, height )
__kernel void calc_sum(__global float* d_in, __global float* d_sums, const int rowLen)
{

    int our_row = get_global_id(1);
    int lx = get_local_id(0);
    int gr = get_group_id(0);

    __local float our_mem[(2*BLK_SIZ)];

    // copy glob -> loc mem
    our_mem[2*lx + 0] = d_in[gr*2*BLK_SIZ + 2*lx + 0];
    if(gr*2*BLK_SIZ + 2*lx + 0 >= rowLen)
        our_mem[2*lx + 0] = 0;

    our_mem[2*lx + 1] = d_in[gr*2*BLK_SIZ + 2*lx + 1];
    if(gr*2*BLK_SIZ + 2*lx + 1 >= rowLen)
        our_mem[2*lx + 1] = 0;

    // do the calculations

    int width = 2;
    int num_el = 2*BLK_SIZ / width;
    int wby2 = width>>1;
    for(int i = 0;i<7;++i) 
    {
        barrier(CLK_LOCAL_MEM_FENCE);
        if(lx < num_el)
        {
            int idx = width*(lx + 1) - 1;
            our_mem[idx] = min(our_mem[idx], our_mem[idx-wby2]);
        }
        width<<=1;
        wby2 = width>>1;
        num_el>>=1;
    }
    barrier(CLK_LOCAL_MEM_FENCE);
    // store res
    if(lx == 0) // choose some element from work group to actualy write the sum
    {
        d_sums[our_row] = our_mem[2*lx-1]; // sum is in last element
    }
}

Also search in the internet for blelloch / hillis steele parallel prefix sum algoritms. nVidia also has nice example with good documentation for parallel prefix sum algorithm. It does more than you need, but has same approach that I've described.

Hope this helps.

于 2013-04-04T21:44:04.357 に答える