ドキュメンテーション

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

GPU パフォーマンスの測定

この例では、GPU の主なパフォーマンス上の特徴をいくつか測定する方法を示します。

GPU は、特定タイプの計算を高速化するために使用できます。しかし GPU のパフォーマンスには、異なる GPU デバイス間で大きな違いがあります。GPU のパフォーマンスを数量化するために、3 つのテストが使用されます。

  • GPU へのデータ送信やそこからの再読み取りを、どの程度速く実行できるか

  • GPU カーネルがどの程度速くデータの読み取りと書き込みを実行できるか

  • GPU はどの程度速く計算を実行できるか

これらを測定した後、GPU のパフォーマンスをホスト CPU と比較できます。これによって、GPU が CPU と比べ有利となるにはどれ程のデータや計算が必要なのかについて、指針が与えられます。

設定

gpu = gpuDevice();
fprintf('Using a %s GPU.\n', gpu.Name)
sizeOfDouble = 8; % Each double-precision number needs 8 bytes of storage
sizes = power(2, 14:28);
Using a Tesla K40c GPU.

ホスト/GPU 帯域幅のテスト

最初のテストでは、GPU へのデータ送信と GPU からの読み取りの速さを推定します。GPU は PCI バスに接続されているため、この値は、PCI バスの速さとバスを使用する他の要素の数によって大きく左右されます。しかし、特に関数呼び出しオーバーヘッドと配列割り当て時間など、いくつかのオーバーヘッドも測定には含まれます。こうした値は GPU の "実際の" 使用には付き物なので、これを含めるのは理に適っています。

以下のテストでは、関数 gpuArray を使用して、GPU へのメモリ割り当てとデータ送信が行われます。gather を使用して、ホスト メモリへのメモリ割り当てとデータ再転送が行われます。

このテストで使用されている PCI Express v3 では、帯域幅の理論値がレーンあたり 0.99 GB/s です。つまり、NVIDIA の演算カードで使用される 16 レーンのスロット (PCIe3 x16) の場合、理論値は 15.75 GB/s となります。

sendTimes = inf(size(sizes));
gatherTimes = inf(size(sizes));
for ii=1:numel(sizes)
    numElements = sizes(ii)/sizeOfDouble;
    hostData = randi([0 9], numElements, 1);
    gpuData = randi([0 9], numElements, 1, 'gpuArray');
    % Time sending to GPU
    sendFcn = @() gpuArray(hostData);
    sendTimes(ii) = gputimeit(sendFcn);
    % Time gathering back from GPU
    gatherFcn = @() gather(gpuData);
    gatherTimes(ii) = gputimeit(gatherFcn);
end
sendBandwidth = (sizes./sendTimes)/1e9;
[maxSendBandwidth,maxSendIdx] = max(sendBandwidth);
fprintf('Achieved peak send speed of %g GB/s\n',maxSendBandwidth)
gatherBandwidth = (sizes./gatherTimes)/1e9;
[maxGatherBandwidth,maxGatherIdx] = max(gatherBandwidth);
fprintf('Achieved peak gather speed of %g GB/s\n',max(gatherBandwidth))
Achieved peak send speed of 6.18519 GB/s
Achieved peak gather speed of 3.31891 GB/s

下記のプロットでは、それぞれの場合のピークに丸印が付けられています。データセットのサイズが小さいと、オーバーヘッドの影響が大きく出ます。データの量が大きくなると、PCI バスが制限要因となります。

hold off
semilogx(sizes, sendBandwidth, 'b.-', sizes, gatherBandwidth, 'r.-')
hold on
semilogx(sizes(maxSendIdx), maxSendBandwidth, 'bo-', 'MarkerSize', 10);
semilogx(sizes(maxGatherIdx), maxGatherBandwidth, 'ro-', 'MarkerSize', 10);
grid on
title('Data Transfer Bandwidth')
xlabel('Array size (bytes)')
ylabel('Transfer speed (GB/s)')
legend('Send to GPU', 'Gather from GPU', 'Location', 'NorthWest')

メモリ使用量の多い演算のテスト

多くの演算では配列の各要素を使った計算は極めて少なく、したがって、データをメモリから取得するまたはそれを書き込むために要する時間が大半となります。oneszerosnantrue などの関数は出力の書き込みのみを行います。一方、transposetril などの関数は読み取りと書き込みの両方を行いますが、計算を一切行いません。plusminusmtimes などの単純な演算子も要素あたりの計算量は少なく、メモリ アクセス速度によってのみ制限を受けます。

関数 plus は、各浮動小数点演算につき 1 回のメモリ読み取りと 1 回のメモリ書き込みを行います。したがって、メモリへのアクセス速度によって制限を受け、読み取りと書き込み操作の速度の優れた指標となります。

memoryTimesGPU = inf(size(sizes));
for ii=1:numel(sizes)
    numElements = sizes(ii)/sizeOfDouble;
    gpuData = randi([0 9], numElements, 1, 'gpuArray');
    plusFcn = @() plus(gpuData, 1.0);
    memoryTimesGPU(ii) = gputimeit(plusFcn);
end
memoryBandwidthGPU = 2*(sizes./memoryTimesGPU)/1e9;
[maxBWGPU, maxBWIdxGPU] = max(memoryBandwidthGPU);
fprintf('Achieved peak read+write speed on the GPU: %g GB/s\n',maxBWGPU)
Achieved peak read+write speed on the GPU: 186.494 GB/s

ここで、同じコードを CPU で実行した場合と比較します。

memoryTimesHost = inf(size(sizes));
for ii=1:numel(sizes)
    numElements = sizes(ii)/sizeOfDouble;
    hostData = randi([0 9], numElements, 1);
    plusFcn = @() plus(hostData, 1.0);
    memoryTimesHost(ii) = timeit(plusFcn);
end
memoryBandwidthHost = 2*(sizes./memoryTimesHost)/1e9;
[maxBWHost, maxBWIdxHost] = max(memoryBandwidthHost);
fprintf('Achieved peak read+write speed on the host: %g GB/s\n',maxBWHost)

% Plot CPU and GPU results.
hold off
semilogx(sizes, memoryBandwidthGPU, 'b.-', ...
    sizes, memoryBandwidthHost, 'r.-')
hold on
semilogx(sizes(maxBWIdxGPU), maxBWGPU, 'bo-', 'MarkerSize', 10);
semilogx(sizes(maxBWIdxHost), maxBWHost, 'ro-', 'MarkerSize', 10);
grid on
title('Read+write Bandwidth')
xlabel('Array size (bytes)')
ylabel('Speed (GB/s)')
legend('GPU', 'Host', 'Location', 'NorthWest')
Achieved peak read+write speed on the host: 40.2573 GB/s

このプロットを上記のデータ転送プロットと比較すると、通常 GPU では、ホストからデータを取得するよりずっと速くメモリからの読み取りとメモリへの書き込みができることは明らかです。したがって、ホストから GPU へ、または GPU からホストへのメモリ転送の数を最小化することが重要です。理想としては、プログラムによってデータを GPU に転送し、GPU 内でできるだけの処理を行って、完了したときにだけホストに戻すことが望まれます。さらに望ましいのは、最初から GPU でデータを作成することです。

計算量の多い演算のテスト

メモリから読み取られる、またはメモリに書き込まれる各要素に対し浮動小数点計算が数多く実行される演算では、メモリ速度の重要性はずっと低くなります。この場合は、浮動小数点演算装置の数と速度が制限要因となります。こうした演算は "計算密度" が高いと言われます。

計算パフォーマンスのテストに適しているのは行列と行列の乗算です。2 つの 行列を乗算する場合、浮動小数点計算の合計数は次になります。

入力行列が 2 つ読み取られ、結果の行列が 1 つ書き込まれるため、合計で 個の要素の読み取りまたは書き込みが行われます。したがって、計算密度は (2N - 1)/3 フロップ/要素となります。先に使用した plus の計算密度が 1/2 フロップ/要素であることと比較してください。

sizes = power(2, 12:2:24);
N = sqrt(sizes);
mmTimesHost = inf(size(sizes));
mmTimesGPU = inf(size(sizes));
for ii=1:numel(sizes)
    % First do it on the host
    A = rand( N(ii), N(ii) );
    B = rand( N(ii), N(ii) );
    mmTimesHost(ii) = timeit(@() A*B);
    % Now on the GPU
    A = gpuArray(A);
    B = gpuArray(B);
    mmTimesGPU(ii) = gputimeit(@() A*B);
end
mmGFlopsHost = (2*N.^3 - N.^2)./mmTimesHost/1e9;
[maxGFlopsHost,maxGFlopsHostIdx] = max(mmGFlopsHost);
mmGFlopsGPU = (2*N.^3 - N.^2)./mmTimesGPU/1e9;
[maxGFlopsGPU,maxGFlopsGPUIdx] = max(mmGFlopsGPU);
fprintf(['Achieved peak calculation rates of ', ...
    '%1.1f GFLOPS (host), %1.1f GFLOPS (GPU)\n'], ...
    maxGFlopsHost, maxGFlopsGPU)
Achieved peak calculation rates of 72.5 GFLOPS (host), 1153.3 GFLOPS (GPU)

ここでプロットを行い、ピークがどこにくるかを確認します。

hold off
semilogx(sizes, mmGFlopsGPU, 'b.-', sizes, mmGFlopsHost, 'r.-')
hold on
semilogx(sizes(maxGFlopsGPUIdx), maxGFlopsGPU, 'bo-', 'MarkerSize', 10);
semilogx(sizes(maxGFlopsHostIdx), maxGFlopsHost, 'ro-', 'MarkerSize', 10);
grid on
title('Double precision matrix-matrix multiply')
xlabel('Matrix size (numel)')
ylabel('Calculation Rate (GFLOPS)')
legend('GPU', 'Host', 'Location', 'NorthWest')

まとめ

以上のテストで、GPU パフォーマンスの重要な特徴がいくつか明らかになります。

  • ホスト メモリと GPU メモリ間の転送は比較的低速である。

  • 優れた GPU のメモリ読み取り/書き込みは、ホスト CPU のメモリ読み取り/書き込みよりもずっと高速である。

  • データのサイズが十分大きい場合、GPU はホスト CPU よりもずっと速く計算を実行できる。

それぞれのテストにおいて、GPU をメモリ面で、または計算面で完全に飽和状態にするにはかなり大きな配列が必要であるという点は注目に値します。GPU の利用は、何百万もの要素を同時に扱う際に最も有利となります。

さまざまな GPU 間の比較を含む、より詳細な GPU ベンチマークは、MATLAB® Central の File ExchangeGPUBench で入手できます。