ドキュメンテーション センター

  • 評価版
  • 製品アップデート

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

GPU パフォーマンスの測定と向上

パフォーマンス向上のための基本的なワークフロー

MATLAB の GPU 計算の目的は、アプリケーションの速度向上です。このトピックでは、GPU ハードウェアの構成やコード内でのベスト プラクティスなど、GPU のパフォーマンスを向上させるための基本的な概念や方針について説明します。ここでは、難易度の高い実装とパフォーマンスのトレードオフについて説明します。また、gpuArray 関数、arrayfun、MEX ファイル、CUDA カーネルのどれを使用するかを選択するための基準についても説明します。最後に、GPU でのパフォーマンスを正確に測定する方法を説明します。

MATLAB コードを変換して GPU で実行できるようにする場合、最初は既に高いパフォーマンスを実現している MATLAB コードを使用するのが最善です。GPU と CPU のパフォーマンス特性は異なりますが、適切な MATLAB コードを作成するための一般的な指針は GPU 用の適切な MATLAB コードを作成する場合にも役立ちます。ほとんどの場合、最初の手順は CPU コードのプロファイリングです。プロファイラーで CPU での処理時間が最も長いと表示されるコードの行については、GPU 用のコード作成時に注意しなければならないものと考えられます。

コードの変換時には、gpuArray データをサポートする MATLAB 組み込み関数を使用するのが最も簡単です。これらの関数は gpuArray 入力を使用し、GPU で計算を実行して gpuArray 出力を返します。gpuArray データをサポートする MATLAB 関数の一覧については、「GPU での組み込み関数の実行」を参照してください。一般的に、これらの関数は CPU で計算される標準の MATLAB 関数と同じ引数とデータ型をサポートしています。これらの gpuArray 用のオーバーロードされた関数の制限については、コマンド ライン ヘルプ (help gpuArray/qr など) で説明します。

使用するすべての関数が GPU でサポートされている場合、gpuArray を呼び出して入力データを GPU に転送し、完了後に gather を呼び出して GPU から出力データを取得することにより、GPU でのコードの実行を簡単に行うことができる場合があります。多くの場合はコードをベクトル化し、ループ化されたスカラー演算の代わりに MATLAB 行列およびベクトル操作を使用します。ベクトル化は CPU では一般的によい結果が得られますが、GPU では通常、高いパフォーマンスを実現するために重要となります。詳細は、「GPU のパフォーマンス向上のためのベクトル化」を参照してください。

パフォーマンス向上のための高度なツール

入力を gpuArray に変換してコードをベクトル化した後でも、アルゴリズム内に組み込み関数でない処理や速度がアプリケーションの要件を満たさない処理が存在する場合があります。このような場合の対処としては 3 通りあり、arrayfun を使用してアプリケーションの要素単位の部分をプリコンパイルするか、GPU ライブラリ関数を使用するか、カスタム CUDA カーネルを作成します。

純粋に要素単位の関数を使用している場合は、その関数を arrayfun で呼び出すとパフォーマンスを向上させることができます。GPU の関数 arrayfun を使って要素単位の MATLAB 関数をカスタム CUDA カーネルに変換すると、処理実行のオーバーヘッドを削減できます。多くの場合、アプリケーション全体を arrayfun とあわせて使用することはできなくても、アプリケーションのサブセットでは可能となっています。「arrayfun を使用した、要素単位の MATLAB 関数の GPU におけるパフォーマンス改善」の例では、この方法の基本的な概念を説明します。また、「モンテカルロ シミュレーションでの GPU arrayfun の使用」の例では、財務アプリケーションのシミュレーションでこの操作を実行する方法を説明します。

MATLAB では、Parallel Computing Toolbox™、Image Processing Toolbox™、Signal Processing Toolbox™ などの製品で GPU 対応関数の拡張ライブラリを使用できます。しかし、MATLAB の GPU サポートでは直接組み込みアナログがない追加関数のライブラリが多数あります。たとえば、MATLAB 同梱の CUDA ツールキットに含まれている NVIDIA Performance Primitives ライブラリや CURAND ライブラリなどがこれに該当します。これらのライブラリの関数を呼び出す必要がある場合は、GPU MEX インターフェイスを使用します。このインターフェイスを使用すると、MATLAB gpuArray からポインターをデバイスのデータへ抽出して GPU 関数に渡すことができます。戻り値は gpuArray に変換して MATLAB に返すことができます。詳細は、「CUDA コードを含む MEX 関数の実行」を参照してください。

最後に、必要な処理のためのカスタム CUDA カーネルを作成するという方法もあります。これらのカーネルは CUDAKernel オブジェクトを使用して MATLAB に直接統合することができます。

GPU コンピューティングへの 3 つのアプローチの説明: マンデルブロ集合」の例では、この節で説明している 3 通りの方法による簡単な計算の実施方法を説明します。この例では、まず最初に GPU で実行するために簡単に変換できる MATLAB コードを作成し、要素単位の処理で arrayfun を使用するためにコードを変更してから同じ処理でカスタム CUDA カーネルを統合する方法を説明します。

この他に、CUDA カーネルを MEX ファイルの一部として作成し、MEX ファイル内の CUDA Runtime API を使用して呼び出すこともできます。いずれの方法でも、MATLAB コードで直接利用できない共有メモリやテクスチャ メモリなどの GPU の低水準の機能を使用できるようになります。詳細については、「MEX を使用した高度な CUDA 機能へのアクセス」の例を参照してください。

パフォーマンス向上のためのベスト プラクティス

ハードウェア構成

一般的に、GPU が計算のみに使用されている場合にパフォーマンスは最高となります。問題処理のために相当量のメモリが使用され、さらにグラフィックス用のシステムによりデバイスが恒常的に使用されるため、計算とグラフィックスの両方に同じ GPU デバイスを使用することは推奨されません。可能であれば、グラフィックス用には別のデバイスを使用してください。計算またはグラフィックス用のデバイス構成についての詳細は、オペレーティング システムやドライバーのバージョンによって異なります。

Windows® システムでは、GPU デバイスは次の 2 種類のうち 1 つのモードで使用できます。WDDM (Windows Display Driver Model) モードまたは TCC (Tesla Compute Cluster) モードです。最高のパフォーマンスを実現するには、計算に使用するデバイスを TCC モードにする必要があります。詳細は、NVIDIA のドキュメンテーションを参照してください。

NVIDIA の最高パフォーマンス計算デバイスである Tesla シリーズでは、GPU メモリの読み取りと書き込みで ECC (エラー修正コード) がサポートされています。ECC の目的は、まれに発生するビット エラーを修正することです。このエラーは主に動的メモリの読み取りや書き込みの際に発生します。パフォーマンスを向上させる方法のひとつとして、ECC を無効にし、達成可能なメモリの帯域幅を増やすことがあげられます。この方法でハードウェアを構成することは可能ですが、MathWorks ではこの方法を推奨していません。パフォーマンスの向上より、見えないエラーのために精度が低下する方が影響が大きくなる場合があります。

MATLAB でのコーディング方法

このトピックでは、GPU のパフォーマンスを向上させるための一般的な技法について説明します。ここで紹介するヒントの一部は、CPU 用の MATLAB コードの作成にも適用できます。

MATLAB 配列のデータは列優先の順序で保存されます。そのため、配列の最初の列の次元で操作を行うと高い効果が得られます。データの次元のひとつが他の次元より大幅に長い場合は、その次元を最初の次元にするとパフォーマンスが高くなることがあります。同様に、ある特定の次元で頻繁に操作を行う場合は、その次元を最初の次元にすると最高のパフォーマンスを得られることが多くなります。連続する操作が配列の複数の次元を対象としている場合、各操作の間で配列の転置や並べ替えを行うとパフォーマンスが高まることがあります。

GPU では、複数の結果を並列に計算するとパフォーマンスが高まります。そのため、通常は行列および高次元配列操作の方がベクトルやスカラーでの操作に比べてパフォーマンスが大幅に高まります。高次元操作を使用するようにループを書き換えると、パフォーマンスが高まります。ループベースでスカラー指向のコードを、MATLAB 行列およびベクトル操作を使用するように変更するプロセスはベクトル化と呼ばれます。詳細は、「ベクトル化の使用」を参照してください。

既定では、MATLAB でのすべての操作は倍精度浮動小数点演算で実行されます。実際には、ほとんどの操作で整数や単精度浮動小数点など多数のデータ型がサポートされています。最新の GPU や CPU では通常、単精度操作の方がスループットがはるかに高くなります。また、単精度浮動小数点データの方がメモリの使用量が少なくなります。アプリケーションの精度に関する要件で単精度浮動小数点を使用できる場合、MATLAB コードのパフォーマンスを大幅に向上させることができます。

GPU は、PCI バスと呼ばれるデータ転送メカニズムの最後で使用されます。このバスはデータを PC ホスト メモリから各種の拡張カードにデータを転送するための効率的で広い帯域幅を使用できる方法ですが、GPU デバイスまたは CPU のグローバル メモリの全体の帯域に比べれば処理速度は大幅に遅くなります (詳細は「GPU パフォーマンスの測定」の例を参照してください)。さらに、GPU デバイスから MATLAB ホスト メモリへの転送を実行すると、MATLAB は他のステートメントを実行する前に、デバイス上で保留中のすべての処理が完了するまで待機します。これにより、アプリケーションのパフォーマンスが大幅に低下する可能性があります。一般的に、MATLAB ワークスペースと GPU の間でのデータ転送の回数は制限する必要があります。アプリケーションの起動時にデータを GPU に 1 回転送できる場合、GPU で可能なすべての計算を実行してから、その結果を最後に MATLAB に転送すると、通常は最高のパフォーマンスを実現できます。同様に、gpuArray の静的メソッド (gpuArray.zeros など) や、zeros などの関数の 'like' 構文 (gpuArray g での zeros(N,'like',g) など) を使用すると、GPU で直接データを作成できます。

GPU でのパフォーマンスの測定

GPU でパフォーマンスを測定するための最適な方法は、gputimeit を使用することです。この関数は入力として入力引数がない関数ハンドルを使用し、その関数について測定された実行時間を返します。この方法では、より適切な分解能を実現するためタイマー操作を繰り返すこと、初期化のオーバーヘッドを回避するため測定の前に関数を実行すること、タイミング関数のオーバーヘッドを抑制することなどのベンチマークに関する考慮事項に対応しています。また、gputimeit を使用すると GPU でのすべての操作が最後のタイミングの前に完了します。

たとえば、サイズが N の乱数行列 Alu 分解を計算するために必要な時間の測定について考えてみます。この操作は、lu 分解を実行する関数を定義し、関数ハンドルを gputimeit に渡すことにより行うことができます。

A = gpuArray.rand(N);
fh = @() lu(A);
gputimeit(fh,2); % 2nd arg indicates number of outputs

gputimeit を使用できない場合は、tictoc を使用してパフォーマンスを測定できます。ただし、GPU の正確なタイミングを取得するには、toc() を呼び出す前に操作が完了するまで待機しなければなりません。これを行う 2 つの方法があります。最後の GPU 出力で、toc() を呼び出す前に gather を呼び出すことができます。この方法では、時間測定が実行される前にすべての計算が完了します。また、gpuDevice を入力として使用する関数 wait() を使用することもできます。たとえば、行列 Alu 分解を計算するための時間を tictoc および wait を使用して測定する場合、以下のようにして行うことができます。

gd = gpuDevice();
tic();
[l,u] = lu(A);
wait(gd);
tLU = toc();

GPU での操作を伴う場合、MATLAB プロファイラーで得られた結果の処理には注意が必要です。プロファイラーでは CPU での実行時間しか表示されず、GPU での実行時間は表示されません。GPU コードのプロファイリング中に何が起きているかを判別するための最善の方法は、wait の呼び出しを各 GPU 操作またはコード内で該当する各セクションの直後に配置することです。通常、wait は非常に長い時間がかかっているように見えます。wait の実行時間は、実際にはプログラム内で wait の前に発生している GPU 操作の実行時間です。

GPU のパフォーマンス向上のためのベクトル化

この例では、CPU の代わりに GPU で関数を実行し、計算をベクトル化することによってパフォーマンスを向上させる方法を説明します。

行列の列に高速のたたみ込みを実行する関数について考えてみましょう。高速のたたみ込みは信号処理アプリケーションでは一般的な操作で、データの各列を時間領域から周波数領域に変換し、フィルター ベクトルの変換により乗算して時間領域に再度変換し、結果を出力行列に格納します。

function y = fastConvolution(data,filter)
[m,n] = size(data);
% Zero-pad filter to the column length of data, and transform
filter_f = fft(filter,m);

% Create an array of zeros of the same size and class as data
y = zeros(m,n,'like',data);

% Transform each column of data
for ix = 1:n
    af = fft(data(:,ix));
    y(:,ix) = ifft(af .* filter_f);
end
end

CPU 内でこの関数を特定のサイズのデータに対して実行し、実行時間を MATLAB 関数 timeit で測定します。関数 timeit は、起動やオーバーヘッドの把握など一般的なベンチマークの考慮事項に対応しています。

a = complex(randn(4096,100),randn(4096,100));  % Data input
b = randn(16,1);                               % Filter input
c = fastConvolution(a,b);                      % Calculate output
ctime = timeit(@()fastConvolution(a,b));       % Measure CPU time
disp(['Execution time on CPU = ',num2str(ctime)]);

サンプル マシンでは、このコードにより以下の出力が表示されます。

Execution time on CPU = 0.019335

次に、この関数を GPU で実行します。この操作は、入力データを通常の MATLAB 配列ではなく gpuArray に変更することにより簡単に実行できます。関数内で出力を作成する際に 'like' 構文を使用すると、data が gpuArray の場合に y が gpuArrayになります。

ga = gpuArray(a);                              % Move data to GPU
gb = gpuArray(b);                              % Move filter to GPU
gc = fastConvolution(ga,gb);                   % Calculate on GPU
gtime = gputimeit(@()fastConvolution(ga,gb));  % Measure GPU time
gerr = max(max(abs(gather(gc)-c)));            % Calculate error
disp(['Execution time on GPU = ',num2str(gtime)]);
disp(['Maximum absolute error = ',num2str(gerr)]);

同じマシンで、このコードにより以下の出力が表示されます。

Execution time on CPU = 0.019335
Execution time on GPU = 0.027235
Maximum absolute error = 1.1374e-14

残念ながら、この問題により GPU は CPU より処理が遅くなります。その理由は、for ループが FFT、乗算、逆 FFT の各操作を長さが 4096 の各列に個別に実行しているためです。パフォーマンスを向上させるための最適な方法は、コードをベクトル化して MATLAB 関数を 1 回呼び出すだけで多くの計算を実行できるようにすることです。FFT 操作と IFFT 操作は簡単にベクトル化できます。fft(A) は行列 A の各列の FFT を計算します。MATLAB バイナリ スカラー拡張関数 bsxfun を使用すると、行列内のすべての列でフィルターの乗算を同時に実行できます。ベクトル化された関数は次のようになります。

function y = fastConvolution_v2(data,filter)
m = size(data,1);
% Zero-pad filter to the length of data, and transform
filter_f = fft(filter,m);

% Transform each column of the input
af = fft(data);

% Multiply each column by filter and compute inverse transform
y = ifft(bsxfun(@times,af,filter_f));
end

ベクトル化された関数を使用して同じ実験を実行します。

a = complex(randn(4096,100),randn(4096,100));   % Data input
b = randn(16,1);                                % Filter input
c = fastConvolution_v2(a,b);                    % Calculate output
ctime = timeit(@()fastConvolution_v2(a,b));     % Measure CPU time
disp(['Execution time on CPU = ',num2str(ctime)]);

ga = gpuArray(a);                               % Move data to GPU
gb = gpuArray(b);                               % Move filter to GPU
gc = fastConvolution_v2(ga, gb);                % Calculate on GPU
gtime = gputimeit(@()fastConvolution_v2(ga,gb));% Measure GPU time
gerr = max(max(abs(gather(gc)-c)));             % Calculate error
disp(['Execution time on GPU = ',num2str(gtime)]);
disp(['Maximum absolute error = ',num2str(gerr)]);
Execution time on CPU = 0.010393
Execution time on GPU = 0.0020537
Maximum absolute error = 1.1374e-14

結論として、コードをベクトル化すると CPU と GPU の両方のバージョンで処理速度が向上します。しかし、ベクトル化の効果は CPU より GPU バージョンの方が高くなります。パフォーマンスが向上した CPU バージョンの処理速度は元の約 2 倍ですが、GPU バージョンの場合は元の 13 倍になります。元のバージョンでは GPU のコード処理速度は CPU より 40% 遅くなっていますが、パフォーマンスが向上したバージョンでは約 5 倍速くなっています。

この情報は役に立ちましたか?