23
CUDA画像処理入門 エヌビディアジャパン CUDAエンジニア 森野慎也 GTC Japan 2014

CUDA画像処理入門 - NVIDIA...CUDAにおける画像処理の基礎 2次元メモリ確保API —Pitchを考慮 —cudaMallocPitch()、cudaMemcpy2D() 並列化 —CUDAの並列度:

  • Upload
    others

  • View
    13

  • Download
    0

Embed Size (px)

Citation preview

  • CUDA画像処理入門

    エヌビディアジャパン CUDAエンジニア 森野慎也

    GTC Japan 2014

  • CUDAを用いた画像処理

    画像処理をCUDAで並列化

    — 基本的な並列化の考え方

    — 目標 : 妥当なNaïveコードが書ける

    最適化の初歩がわかる

    — ブロックサイズ

    — メモリアクセスパターン

  • RGB → Y(輝度) 変換

    カラー画像から、グレイスケールへの変換

    — Y = 0.299 × R + 0.587 × G + 0.114 × B

  • CUDAにおける画像処理の基礎

    2次元メモリ確保API

    — Pitchを考慮

    — cudaMallocPitch()、cudaMemcpy2D()

    並列化

    — CUDAの並列度 : 数万以上欲しい…

    Keplerでの目安 : CUDA Core数 x 10 程度 (最低限)

  • PITCHを考慮したメモリレイアウト

    RGBA(8 bit, uchar4)の配列

    index = x + y * pitchInPixel

    width

    pitchInPixel = pitchInByte / sizeof(uchar4)

    (x, y)

  • 2次元メモリ 確保・転送

    cudaError_t cudaMallocPitch ( void** devPtr, size_t* pitch, size_t width, size_t height )

    — widthバイトのメモリを、height行分、取得する。

    — 行は、pitchバイトで整列する。

    cudaError_t cudaMemcpy2D ( void* dst, size_t dpitch, const void* src, size_t spitch, size_t width, size_t height,cudaMemcpyKind kind )

    — dstで示されるメモリ (dpitchバイトで整列)に、srcで示されるメモリ (spitchバイトで整列) を、width (バイト) x height (行)、コピーする。

  • サンプルコード

    uchar4 *src, *dImage;

    size_t spitch, dPitch, dPitchInPixel;

    // ピッチつきで、メモリをアロケート

    cudaMallocPitch(&dImage, *dPitch, width * sizeof(uchar4), height);

    dPitchInPixel = dPitch / sizeof(uchar4);

    // ピッチを変換しつつ、ホスト→デバイスへと、メモリ転送

    cudaMemcpy2D(dImage, dPitch, src, sPitch, width * sizeof(uchar4), height,

    cudaMemcpyHostToDevice);

  • 画像処理における並列化の基本

    基本 : 1 ピクセルに対して、 1スレッドを対応させる

    — ピクセル数分、スレッドが走る。例 : 262,144 (= 512 x 512) スレッド

    スレッドは、処理対象のピクセルを持つ。

    — 自分の位置 (x, y) を知ることが必要

  • 2DでのBLOCK・THREADの割り当て

    Thread : 「2次元」でピクセルに対応。

    Block : 「2次元」で定義。一定のサイズのタイル。

    Grid : 必要数のBlockを「2次元」に敷き詰める。

    1 Block

    1 Pixel = 1 Thread

    (x, y) =

    (Global ID X, Global ID Y)

  • 2DでのBLOCK・THREADの割り当て

    GlobalID は、(x, y, z)方向に計算できる

    — GlobalID(x) = blockDim.x * blockIdx.x + threadIdx.x

    — GlobalID(y) = blockDim.y * blockIdx.y + threadIdx.y

    — GlobalID(z) = blockDim.z * blockIdx.z + threadIdx.z

    blockDim.x * blockIdx.xthreadIdx.x

    blockDim.y * blockIdx.y

    threadIdx.y

  • RGB → Y 変換 カーネル

    __global__

    void RGBToYKernel(uchar4 *dDst, const uchar4 *dSrc, int width, int height, int pitch){

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

    int gidy = blockDim.y * blockIdx.y + threadIdx.y;

    if ((gidx < width) && (gidy < height)) {

    int pos = gidx + gidy * pitch;

    // Y = 0.299 R + 0.587 G + 0.114 B

    uchar4 value = src[pos];

    float Y = 0.299f * value.x + 0.587f * value.y + 0.114f * value.z;

    unsigned char y = (unsigned char)min(255, (int)Y);

    dDst[pos ] = pixel;

    }

    }

  • カーネル呼び出し (GRIDサイズ指定)

    /* value、radixで割って、切り上げる */int divRoundUp(int value, int radix) {

    return (value + radix – 1) / radix;

    }

    /* gridDim, blockDimを、2次元(x, y方向)に初期化 */dim3 blockDim(64, 2);

    /* divRoundUp()は、切り上げの割り算 */dim3 gridDim(divRoundUp(width, blockDim.x), divRoundUp(height, blockDim.y));

    RGBToYKernel(dDst, dSrc, …);

  • 悪い並列化の例

    GPUの並列化としては、NG。非常に低速。

    — 並列度が低い

    — メモリアクセスパターンが悪い

    ただし… CPU的発想としてはふつう。

    Thread 0

    Thread 1

    Thread 2

    Thread 3

  • ここはポイント!コアレス(COALESCED)アクセス

    連続するスレッドが、連続するメモリにアクセスする。— threadIdx.xに対して、連続。

    Memory :

    0 1 2 3 4 5 6 7 8 …

    threadIdx.x

    Thread :

  • 再掲 : 2DでのBLOCK・THREADの割り当て

    GlobalID は、(x, y, z)方向に計算できる

    — GlobalID(x) = blockDim.x * blockIdx.x + threadIdx.x

    — GlobalID(y) = blockDim.y * blockIdx.y + threadIdx.y

    — GlobalID(z) = blockDim.z * blockIdx.z + threadIdx.z

    blockDim.x * blockIdx.xthreadIdx.x

    blockDim.y * blockIdx.y

    threadIdx.y

  • 動かしてみる

  • FAQ : BLOCKDIMの決め方

    1. Occupancy (占有率) を 100 %にする

    2. Blockあたりのスレッド数は、なるべく小さく。

    3. 横方向は、コアレスアクセス。なるべく、長くする。

  • BLOCKDIMの決め方 (OCCUPANCY から)

    SMXあたり、2048 Thread走らせたい。

    — Occupancy (占有率) = 100 %

    Occupancy = 100 % を満たす、Blockあたりのスレッド数は、

    2048 Thread / 16 Block = 128 Thread / Block2048 Thread / 8 Block = 256 Thread / Block2048 Thread / 4 Block = 512 Thread / Block2048 Thread / 2 Block = 1024 Thread / Block

    項目 値

    最大のBlock数 / SMX 16

    最大のThread数 / SMX 2048

    最大のThread数 / Block 1024

  • BLOCKDIMの決め方(BLOCKの粒度から)

    Grid = 4096 Thread の実行例を考えてみる

    — Block : 256 Thread、1024 Threadで比較

    — 3 SMX / GPU、1 SMXあたり 1 Blockが実行可能とする

    1024 Thread / Block

    Block

    Block

    Block

    BlockSMX 0

    SMX 1

    SMX 2

    256 Thread / Block

    SMX 0 Block

    Block

    Block

    Block

    Block

    Block

    Block

    Block

    Block

    Block

    Block

    Block

    Block

    Block

    Block

    SMX 1

    SMX 2

    Block

    t

    t

    Blockサイズは小さいほうが得 → 128 Threads / Block

  • BLOCKDIMの決め方 (SMXの構造から)

    Warp Scheduler x 4 :

    — 1 clockあたり、4 Warpに対する命令発行

    — Blockのサイズは、 128 Thread の倍数が望ましい。(128 Thread = 32 Thread/Warp x 4 Warp)

  • タイルは横長がよい

    タイルの横幅は、32(Warpの幅)の倍数がよい。

    32より小さい場合、16、もしくは、8 を使う。

    Memory :

    threadIdx.x

    0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31Thread :

  • RGB→Y変換時のバンド幅 : TESLA K20CblockDim.x

    1 2 4 8 16 32 64 128 256 512 1024blo

    ckD

    im.y

    1 1.4 2.8 5.6 11.2 22.1 43.9 78.5 119.8 119.3 115.4 87.7

    2 2.6 5.2 10.4 20.6 40.7 77.9 119.8 119.4 115.3 87.4 -

    4 4.8 9.6 19.2 37.8 74.0 119.4 118.2 114.2 87.3 - -

    8 8.4 16.7 33.3 69.6 115.0 117.9 111.9 87.1 - - -

    16 13.4 26.3 60.6 106.7 115.0 114.3 87.2 - - - -

    32 17.7 40.4 81.1 103.9 110.9 86.9 - - - - -

    64 20.7 41.7 79.8 99.0 83.5 - - - - - -

    128 20.7 41.6 75.6 75.3 - - - - - - -

    256 20.7 41.0 60.3 - - - - - - - -

    512 20.5 37.6 - - - - - - - - -

    1024 19.1 - - - - - - - - - -

    値: バンド幅 (GB/sec)Tesla K20c (ECC off)

    Occupancy < 100 %

    blockDim.x < 8

  • まとめ

    画像処理におけるCUDA

    — Pitchを考慮したメモリレイアウト

    — 2次元のGridの呼び出し

    正しいNaïveコード (カーネル) の書き方

    — コアレスアクセス

    — ピクセルごとに、スレッドを割り当てる

    並列度は、数万以上。

    — Blockサイズは、128が適当 (単純なカーネルの場合)