ドキュメンテーション

最新のリリースでは、このページがまだ翻訳されていません。 このページの最新版は英語でご覧になれます。

MEX を使用した高度な CUDA 機能へのアクセス

この例では、MEX ファイルを使用して GPU の高度な機能にアクセスする方法を示します。GPU でのステンシル演算の例を基にして説明します。この前出の例では Conway の "ライフ ゲーム" を使用して、GPU で実行される MATLAB® コードでステンシル演算を実行する方法を示しました。ここでの例では、GPU の 2 つの高度な機能を使用してステンシル演算のパフォーマンスをさらに高める方法を示します。その機能とは、共有メモリとテクスチャ メモリです。これを実行するには、独自の CUDA コードを MEX ファイルに作成し、その MEX ファイルを MATLAB から呼び出します。MEX ファイルにおける GPU の使用の紹介は、ドキュメンテーションに記載されています。

前の例で定義されたように、"ステンシル演算" では出力配列の各要素が入力配列の小さな領域によって決まります。例としては、有限差分、畳み込み、メディアン フィルター処理、有限要素法などが挙げられます。ステンシル演算がワークフローの主要部分であると見なすのであれば、GPU を最大限に活用できるように、この演算を手書きで CUDA カーネルに変換するだけの手間をかけることができます。この例では Conway の "ライフ ゲーム" をステンシル演算として使用し、計算を MEX ファイルへと移します。

"ライフ ゲーム" ではいくつかの簡単なルールに従います。

  • セルは 2 次元グリッドに並べられる

  • 各ステップで、それぞれのセルの運命は隣接する 8 つのセルの生命力によって決定される

  • あるセルが、生命をもつ 3 つのセルと隣接していると次のステップで生命を得る

  • 生命をもつセルが、生命をもつ 2 つのセルと隣接していると、次のステップで生命が維持される

  • 他のセルはすべて (生命をもつ隣接セルが 3 つを超える場合を含め) 次のステップで生命を失うか、空のままとなる

したがって、この場合の "ステンシル" は、各要素を囲む 3 行 3 列の領域となります。詳細については、paralleldemo_gpu_stencil のコードを参照してください。

この例は、サブ関数を使用できる関数です。

function paralleldemo_gpu_mexstencil()

ランダムな初期集団の生成

セルの初期集団は、およそ 25% の位置が生命をもつように 2 次元グリッド上に作成されます。

    gridSize = 500;
    numGenerations = 100;
    initialGrid = (rand(gridSize,gridSize) > .75);

    hold off
    imagesc(initialGrid);
    colormap([1 1 1;0 0.5 0]);
    title('Initial Grid');

MATLAB でのベースライン GPU バージョンの作成

パフォーマンスのベースラインを得るため、Experiments with MATLAB で説明されている初期実装から開始します。このバージョンは、gpuArray を使用して初期集団を必ず GPU に配置することにより、GPU で実行できます。

function X = updateGrid(X, N)
    p = [1 1:N-1];
    q = [2:N N];
    % Count how many of the eight neighbors are alive.
    neighbors = X(:,p) + X(:,q) + X(p,:) + X(q,:) + ...
        X(p,p) + X(q,q) + X(p,q) + X(q,p);
    % A live cell with two live neighbors, or any cell with
    % three live neighbors, is alive at the next step.
    X = (X & (neighbors == 2)) | (neighbors == 3);
end

currentGrid = gpuArray(initialGrid);
% Loop through each generation updating the grid and displaying it
for generation = 1:numGenerations
    currentGrid = updateGrid(currentGrid, gridSize);

    imagesc(currentGrid);
    title(num2str(generation));
    drawnow;
end

次に、ゲームを再度実行し、各世代にどのくらい時間を要するかを測定します。

% This function defines the outer loop that calls each generation, without
% doing the display.
function grid=callUpdateGrid(grid, gridSize, N)
    for gen = 1:N
        grid = updateGrid(grid, gridSize);
    end
end

gpuInitialGrid = gpuArray(initialGrid);

% Retain this result to verify the correctness of each version below.
expectedResult = callUpdateGrid(gpuInitialGrid, gridSize, numGenerations);

gpuBuiltinsTime = gputimeit(@() callUpdateGrid(gpuInitialGrid, ...
                                               gridSize, numGenerations));

fprintf('Average time on the GPU: %2.3fms per generation \n', ...
        1000*gpuBuiltinsTime/numGenerations);
Average time on the GPU: 1.528ms per generation 

共有メモリを使用する MEX バージョンの作成

ステンシル演算の CUDA カーネル バージョンを作成する際は、入力データをブロックに分割し、それに対し各スレッド ブロックが演算を行えるようにしなければなりません。ブロックの各スレッドは、ブロックの他のスレッドでも必要とされるデータを読み取ります。読み取り操作の数を最小化する 1 つの方法は、必要な入力データを処理前に共有メモリにコピーすることです。ブロックの境目の計算を正しく行うために、このコピーには一部の隣接する要素も含めなければなりません。ライフ ゲームで、ステンシルが 3 行 3 列からなる要素の正方形であるとすると、1 要素分の境界が必要になります。たとえば、9 行 9 列のグリッドを 3 行 3 列のブロックを使用して処理する場合では、5 番目のブロックは強調表示された範囲で処理され、黄色の要素が読み取りが必要な "黄色い枠" となります。

このアプローチを表す CUDA コードは、ファイル pctdemo_life_cuda_shmem.cu に収められています。このファイルの CUDA デバイス関数は次のように動作します。

  1. すべてのスレッドは入力グリッドの関連部分を、黄色い枠の部分も含めて共有メモリにコピーする。

  2. スレッドが互いに同期して、共有メモリの準備が完了していることを確認する。

  3. 出力グリッドに適合するスレッドは、ライフ ゲームの計算を実行する。

このファイルのホスト コードは、CUDA ランタイム API を使用して、CUDA デバイス関数を世代ごとに 1 回呼び出します。このコードでは、入力用と出力用に 2 つの異なる書き込み可能なバッファーが使用されます。すべての反復に際して MEX ファイルは入力と出力のポインターを入れ替えるため、コピーは必要ありません。

この関数を MATLAB から呼び出すには MEX ゲートウェイが必要です。このゲートウェイは、入力配列を MATLAB からアンラップし、GPU にワークスペースを作成し、出力を返します。MEX ゲートウェイ関数は、ファイル pctdemo_life_mex_shmem.cpp にあります。

MEX ファイルを呼び出すには、mexcuda を使用して MEX ファイルをコンパイルしなければならず、そのためには nvcc コンパイラをインストールする必要があります。次のようなコマンドを使用して、この 2 つのファイルを単一の MEX 関数にコンパイルできます。

  mexcuda -output pctdemo_life_mex_shmem ...
         pctdemo_life_cuda_shmem.cu pctdemo_life_mex_shmem.cpp

このコマンドは pctdemo_life_mex_shmem という名前の MEX ファイルを生成します。

% Calculate the output value using the MEX file with shared memory. The
% initial input value is copied to the GPU inside the MEX file.
grid = pctdemo_life_mex_shmem(initialGrid, numGenerations);
gpuMexTime = gputimeit(@()pctdemo_life_mex_shmem(initialGrid, ...
                                                 numGenerations));
% Print out the average computation time and check the result is unchanged.
fprintf('Average time of %2.3fms per generation (%1.1fx faster).\n', ...
        1000*gpuMexTime/numGenerations, gpuBuiltinsTime/gpuMexTime);
assert(isequal(grid, expectedResult));
Average time of 0.055ms per generation (27.7x faster).

テクスチャ メモリを使用する MEX バージョンの作成

繰り返される読み取り操作の問題に対処するための 2 番目の方法は、GPU のテクスチャ メモリを使用することです。テクスチャ アクセスは、いくつかのスレッドが 2 次元データにオーバーラップしてアクセスする場合に優れたパフォーマンスが提供されるような方法でキャッシュされます。これは、ステンシル演算でのアクセス パターンそのものです。

テクスチャ メモリの読み取りに使用できる CUDA API は 2 つあります。ここでは、すべての CUDA デバイスでサポートされる CUDA テクスチャ参照 API を使用します。テクスチャにバインドされた配列に格納できる要素の最大数は であり、入力の要素数がこれより多い場合、テクスチャ アプローチは機能しません。

このアプローチを表す CUDA コードは、pctdemo_life_cuda_texture.cu にあります。前述のバージョンと同様、このファイルにはホスト コードとデバイス コードの両方が含まれています。このファイルの 3 つの機能によって、デバイス関数でテクスチャ メモリが使用できるようになります。

  1. テクスチャ変数が MEX ファイルの冒頭で宣言される。

  2. CUDA デバイス関数がテクスチャ参照から入力を取得する。

  3. MEX ファイルがテクスチャ参照を入力バッファーにバインドする。

このファイルでは、CUDA デバイス関数は前の例よりも単純です。必要なのは、ライフ ゲームの計算を実行することだけです。共有メモリへのコピーやスレッドの同期を行う必要はありません。

共有メモリ バージョンと同様、ホスト コードは CUDA ランタイム API を使用して、CUDA デバイス関数を世代ごとに 1 回呼び出します。やはり同様に、入力と出力に対して 2 つの書き込み可能なバッファーを使用し、反復ごとにそのポインターを入れ替えます。デバイス関数のそれぞれの呼び出しの前に、テクスチャ参照を適切なバッファーにバインドします。デバイス関数が実行された後に、テクスチャ参照のバインドを解除します。

このバージョン用の MEX ゲートウェイ ファイル pctdemo_life_mex_texture.cpp があり、入力と出力の配列と、ワークスペースの割り当てを処理します。これらのファイルは、次のようなコマンドを使用して単一の MEX ファイルに組み込むことができます。

  mexcuda -output pctdemo_life_mex_texture ...
         pctdemo_life_cuda_texture.cu pctdemo_life_mex_texture.cpp
% Calculate the output value using the MEX file with textures.
grid = pctdemo_life_mex_texture(initialGrid, numGenerations);
gpuTexMexTime = gputimeit(@()pctdemo_life_mex_texture(initialGrid, ...
                                                  numGenerations));
% Print out the average computation time and check the result is unchanged.
fprintf('Average time of %2.3fms per generation (%1.1fx faster).\n', ...
        1000*gpuTexMexTime/numGenerations, gpuBuiltinsTime/gpuTexMexTime);
assert(isequal(grid, expectedResult));
Average time of 0.025ms per generation (61.5x faster).

まとめ

この例では、ステンシル演算の入力をコピーする 2 つの異なる方法について説明しました。ブロックを共有メモリに明示的にコピーする方法と、GPU のテクスチャ キャッシュを利用する方法です。最良の方法は、ステンシルのサイズ、オーバーラップする領域のサイズ、GPU ハードウェアの世代によって異なります。大切なのは、これらの各アプローチを MATLAB コードと共に使用して、アプリケーションを最適化できるという点です。

fprintf('First version using gpuArray:  %2.3fms per generation.\n', ...
        1000*gpuBuiltinsTime/numGenerations);
fprintf(['MEX with shared memory: %2.3fms per generation ',...
         '(%1.1fx faster).\n'], 1000*gpuMexTime/numGenerations, ...
        gpuBuiltinsTime/gpuMexTime);
fprintf(['MEX with texture memory: %2.3fms per generation '...
         '(%1.1fx faster).\n']', 1000*gpuTexMexTime/numGenerations, ...
        gpuBuiltinsTime/gpuTexMexTime);
First version using gpuArray:  1.528ms per generation.
MEX with shared memory: 0.055ms per generation (27.7x faster).
MEX with texture memory: 0.025ms per generation (61.5x faster).
end