4

OpenCL でより効率的に実行するために、2D 配列の多くの重複しているがオフセットされたブロックに対する操作をどのように構造化できますか?

たとえば、次の OpenCL カーネルがあります。

__kernel void test_kernel(
    read_only image2d_t src,
    write_only image2d_t dest,
    const int width,
    const int height
)
{
    const sampler_t sampler =  CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;
    int2 pos = (int2)(get_global_id(0), get_global_id(1));
    int2 pos0 = (int2)(pos.x - pos.x % 16, pos.y - pos.y % 16);

    uint4 diff = (uint4)(0, 0, 0, 0);

    for (int i=0; i<16; i++)
    {
        for (int j=0; j<16; j++)
        {
            diff += read_imageui(src, sampler, (int2)(pos0.x + i, pos0.y + j)) -
                read_imageui(src, sampler, (int2)(pos.x + i, pos.y + j));
        }
    }
    write_imageui(dest, pos, diff);
}

正しい結果が得られますが、遅いです... 1k x 1k 入力の NVS4200M でわずか 25 GFLOPS です。(ハードウェア仕様は 155 GFLOPS です)。これは、メモリアクセスパターンに関係していると思います。各作業項目は、16x16 領域内のすべての隣接ブロックと同じ 16x16 データ ブロックを 1 つ読み取ります。また、データの別のオフセット ブロックは、ほとんどの場合、すぐ隣のブロックと重複します。すべての読み取りはサンプラーを介して行われます。ホスト プログラムは PyOpenCL (実際には何も変わらないと思います) で、ワーク グループのサイズは 16x16 です。

編集:以下の提案に従ってカーネルの新しいバージョン、作業領域をローカル変数にコピーします:

__kernel __attribute__((reqd_work_group_size(16, 16, 1)))
void test_kernel(
    read_only image2d_t src,
    write_only image2d_t dest,
    const int width,
    const int height
)
{
    const sampler_t sampler =  CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;
    int2 pos = (int2)(get_global_id(0), get_global_id(1));

    int dx = pos.x % 16;
    int dy = pos.y % 16;

    __local uint4 local_src[16*16];
    __local uint4 local_src2[32*32];

    local_src[(pos.y % 16) * 16 + (pos.x % 16)] = read_imageui(src, sampler, pos);
    local_src2[(pos.y % 16) * 32 + (pos.x % 16)] = read_imageui(src, sampler, pos);
    local_src2[(pos.y % 16) * 32 + (pos.x % 16) + 16] = read_imageui(src, sampler, (int2)(pos.x + 16, pos.y));
    local_src2[(pos.y % 16 + 16) * 32 + (pos.x % 16)] = read_imageui(src, sampler, (int2)(pos.x, pos.y + 16));
    local_src2[(pos.y % 16 + 16) * 32 + (pos.x % 16) + 16] = read_imageui(src, sampler, (int2)(pos.x + 16, pos.y + 16));
    barrier(CLK_LOCAL_MEM_FENCE);


    uint4 diff = (uint4)(0, 0, 0, 0);

    for (int i=0; i<16; i++)
    {
        for (int j=0; j<16; j++)
        {
            diff += local_src[ j*16 + i ] - local_src2[ (j+dy)*32 + i+dx ];
        }
    }
    write_imageui(dest, pos, diff);
}

結果: 出力は正しく、実行時間は56% 遅くなります。(local_src2 ではなく) local_src のみを使用すると、結果は最大 10% 高速になります。

編集: はるかに強力なハードウェアでベンチマークされた、AMD Radeon HD 7850 は 420GFLOPS を取得し、仕様は 1751GFLOPS です。公平を期すために、仕様は乗加算用であり、ここには乗算がないため、予想される値は ~875GFLOPS ですが、理論上のパフォーマンスと比較すると、これはまだかなりずれています。

編集:これを試してみたい人のためにテストを簡単に実行できるように、以下の PyOpenCL のホスト側プログラム:

import pyopencl as cl
import numpy
import numpy.random
from time import time

CL_SOURCE = ''' 
// kernel goes here
'''

ctx = cl.create_some_context()
queue = cl.CommandQueue(ctx, properties=cl.command_queue_properties.PROFILING_ENABLE)
prg = cl.Program(ctx, CL_SOURCE).build()

h, w = 1024, 1024
src = numpy.zeros((h, w, 4), dtype=numpy.uint8)
src[:,:,:] = numpy.random.rand(h, w, 4) * 255

mf = cl.mem_flags
src_buf = cl.image_from_array(ctx, src, 4)
fmt = cl.ImageFormat(cl.channel_order.RGBA, cl.channel_type.UNSIGNED_INT8)
dest_buf = cl.Image(ctx, mf.WRITE_ONLY, fmt, shape=(w, h))

# warmup
for n in range(10):
    event = prg.test_kernel(queue, (w, h), (16,16), src_buf, dest_buf, numpy.int32(w), numpy.int32(h))
event.wait()

# benchmark
t1 = time()
for n in range(100):
    event = prg.test_kernel(queue, (w, h), (16,16), src_buf, dest_buf, numpy.int32(w), numpy.int32(h))
event.wait()
t2 = time()
print "Duration (host): ", (t2-t1)/100
print "Duration (event): ", (event.profile.end-event.profile.start)*1e-9

EDIT : メモリ アクセス パターンを考えると、元の素朴なバージョンはかなり良いかもしれません。ワークグループ内のすべてのワークアイテムを呼び出すとread_imageui(src, sampler, (int2)(pos0.x + i, pos0.y + j))、同じ場所が読み取られます (つまり、これは 1 つの読み取りに過ぎませんか??)。また、呼び出すとread_imageui(src, sampler, (int2)(pos.x + i, pos.y + j))、それらは連続した場所を読み取っています (したがって、読み取りを完全に結合できますか??)。

4

2 に答える 2

6

これは間違いなくメモリ アクセスの問題です。隣接するワークアイテムのピクセルは 15x16 ほどオーバーラップする可能性があり、さらに悪いことに、各ワークアイテムは少なくとも 225 の他のワークアイテムとオーバーラップします。

ローカル メモリを使用し、ワーク グループに多数の 16x16 ブロックを協調的に処理させます。私は、各ワーク グループに大きな正方形のブロックを使用するのが好きです。長方形のブロックはもう少し複雑ですが、メモリの使用率を向上させることができます。

ソース画像から n x n ピクセルのブロックを読み取ると、ボーダーは nx15 (または 15xn) だけ重なります。使用可能なローカル メモリ サイズ (LDS) に基づいて、n の可能な最大値を計算する必要があります。opencl 1.1 以降を使用している場合、LDS は少なくとも 32kb です。opencl 1.0 は、ワーク グループごとに 16kb を約束します。

n <= sqrt(32kb / sizeof(uint4))
n <= sqrt(32768 / 16)
n ~ 45

n=45 を使用すると、LDS の 32768 バイトのうち 32400 が使用され、グループごとに 900 個の作業項目を使用できます (45-15)^2 = 900. 注: ここでは、長方形のブロックが役立ちます。たとえば、64x32 はすべての LDS を使用しますが、グループ サイズ = (64-15)*(32-15) = 833 です。

カーネルに LDS を使用する手順:

  1. 画像のキャッシュされたブロックに 1D または 2D のローカル配列を割り当てます。#define 定数を使用していますが、変更する必要はほとんどありません。
  2. 画像から uint 値を読み取り、ローカルに保存します。
  3. ローカルメモリに関連するように各ワークアイテムの「pos」を調整します
  4. 同じ i,j ループを実行しますが、ローカル メモリを使用して値を読み取ります。i ループと j ループは、n の 15 手前で停止することに注意してください。

実装方法がわからない場合は、各ステップをオンラインで検索できます。また、必要な場合は私に尋ねることもできます。

デバイスの LDS がテクスチャの読み取り速度を上回る可能性は十分にあります。これは直感に反しますが、一度に読み取るデータの量が少ないため、GPU がピクセルを効果的にキャッシュできない可能性があることに注意してください。LDS を使用すると、ピクセルが利用できることが保証されます。各ピクセルが読み取られる回数を考えると、これが大きな違いを生むと期待しています。

どのような結果が得られたか教えてください。

更新:これが私の解決策をよりよく説明するための私の試みです。私は画像操作ソフトウェアがあまり得意ではないので、方眼紙を使って絵を描きました。

'src' からの値

上記は、最初のコード スニペットで src から値を読み取る方法のスケッチです。大きな問題は、pos0 の長方形 (16x16 の uint4 値) が、グループ内の各作業項目 (256 個) ごとに完全に読み取られることです。私の解決策は、広い領域を読み取り、256 のワーク グループすべてのデータを共有することです。

ここに画像の説明を入力

画像の 31x31 領域をローカル メモリに保存すると、256 個の作業項目のデータすべてが利用可能になります。

手順:

  • ワーク グループ ディメンションを使用: (16,16)
  • src の値を大きなローカル バッファに読み込みます。つまり、uint4 buff[31][31]; 「pos0」が buff[0][0] になるようにバッファを変換する必要があります。
  • メモリ コピー操作を待機するバリア(CLK_LOCAL_MEM_FENCE)
  • pos と pos0 の値を除外することを除いて、元のループに対して同じ i,j を実行します。場所には i と j のみを使用します。最初に行っていたのと同じ方法で「差分」を累積します。
  • 解決策を「dest」に書き込みます

これは、あなたの質問に対する最初の回答と同じですが、n=16 を使用しています。この値は、ローカル メモリを完全には使用しませんが、おそらくほとんどのプラットフォームでうまく機能します。256 が一般的な最大ワーク グループ サイズになる傾向があります。

これで問題が解決することを願っています。

于 2013-01-28T15:22:22.550 に答える
1

いくつかの提案:

  • 各ワークアイテムで複数の出力ピクセルを計算します。これにより、データの再利用が増加します。
  • テクスチャ キャッシュの使用率を最大化するために、さまざまなワーク グループ サイズをベンチマークします。
  • カーネルを 2 つのパス (水平と垂直) に分ける方法があるかもしれません。

更新:その他の提案

すべてをローカル メモリにロードする代わりに、local_src 値のみをロードして、もう一方の値には read_image を使用してみてください。

ほとんど計算を行わないため、読み取り速度を GB/秒で測定し、ピーク時のメモリ速度と比較する必要があります。

于 2013-01-30T03:32:15.923 に答える