メインコンテンツ

GPU のメモリ帯域幅と処理能力の測定

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

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

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

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

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

これらのメトリクスを評価した後、GPU とホスト CPU のパフォーマンスを比較できます。この比較により、GPU が CPU と比べ有利となるにはどれ程のデータや計算が必要なのかがわかります。

GPU の設定の確認

GPU が利用可能かどうかをチェックします。

gpu = gpuDevice;
disp(gpu.Name + " GPU detected and available.")
NVIDIA RTX A5000 GPU detected and available.

ホスト/GPU の帯域幅の測定

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

テスト パラメーターを定義するには次を行います。

  • 倍精度数の格納に必要なバイト数を表す変数を作成する。

  • 利用可能な GPU メモリの 1/4 を最大サイズとするサイズのベクトルを作成する。テストでは、このベクトルをループ処理して、サイズが増加していく配列を作成します。MATLAB® では要素数が 231-1 を超える配列は GPU で使用できないため、作成される配列がこれよりも大きくなるサイズは削除してください。

sizeOfDouble = 8;

maxSize = 0.25*gpu.AvailableMemory;
maxNumTests = 15;
sizes = logspace(4,log10(maxSize),maxNumTests);
sizes(sizes/sizeOfDouble > intmax) = [];

ホスト/GPU の帯域幅を測定するには、sizes の各配列サイズについて次を行います。

  • ホストと GPU で倍精度の乱数データを作成する。

  • gputimeit関数を使用して、gpuArray関数を使用したメモリ割り当てとホストから GPU へのデータ送信にかかる時間を測定する。

  • timeit 関数を使用して、gather関数を使用したメモリ割り当てと GPU からホストへのデータ送信にかかる時間を測定する。

  • 送信されたデータの量を測定時間で除算して帯域幅を求める。

numTests = numel(sizes);
numElements = floor(sizes/sizeOfDouble);
sendTimes = inf(1,numTests);
gatherTimes = inf(1,numTests);

for idx=1:numTests
    disp("Test " + idx + " of " + numTests + ". Timing send and gather for array with " + numElements(idx) + " elements.")

    % Generate random data on GPU and host.
    gpuData = randi([0 9],numElements(idx),1,"gpuArray");
    hostData = gather(gpuData);

    % Time sending data to GPU.
    sendFcn = @() gpuArray(hostData);
    sendTimes(idx) = gputimeit(sendFcn);

    % Time gathering data back from GPU.
    gatherFcn = @() gather(gpuData);
    gatherTimes(idx) = gputimeit(gatherFcn);
end
Test 1 of 15. Timing send and gather for array with 1250 elements.
Test 2 of 15. Timing send and gather for array with 3240 elements.
Test 3 of 15. Timing send and gather for array with 8398 elements.
Test 4 of 15. Timing send and gather for array with 21768 elements.
Test 5 of 15. Timing send and gather for array with 56425 elements.
Test 6 of 15. Timing send and gather for array with 146258 elements.
Test 7 of 15. Timing send and gather for array with 379107 elements.
Test 8 of 15. Timing send and gather for array with 982663 elements.
Test 9 of 15. Timing send and gather for array with 2547104 elements.
Test 10 of 15. Timing send and gather for array with 6602203 elements.
Test 11 of 15. Timing send and gather for array with 17113191 elements.
Test 12 of 15. Timing send and gather for array with 44358118 elements.
Test 13 of 15. Timing send and gather for array with 114978124 elements.
Test 14 of 15. Timing send and gather for array with 298028173 elements.
Test 15 of 15. Timing send and gather for array with 772501660 elements.
sendBandwidth = (sizes./sendTimes)/1e9;
gatherBandwidth = (sizes./gatherTimes)/1e9;

送信速度と収集速度のピークを求めます。このテストで使用されている GPU は PCI Express® version 4.0 をサポートしており、帯域幅の理論値がレーンあたり 1.97 GB/s です。NVIDIA® の演算カードで使用される 16 レーンのスロッの場合、帯域幅の理論値は 31.52 GB/s となります。

[maxSendBandwidth,maxSendIdx] = max(sendBandwidth);
[maxGatherBandwidth,maxGatherIdx] = max(gatherBandwidth);
fprintf("Achieved peak send speed of %.2f GB/s",maxSendBandwidth)
Achieved peak send speed of 10.13 GB/s
fprintf("Achieved peak gather speed of %.2f GB/s",maxGatherBandwidth)
Achieved peak gather speed of 4.28 GB/s

データ転送速度を配列サイズに対してプロットし、それぞれの場合のピークに丸印を付けます。データセットのサイズが小さいと、オーバーヘッドの影響が大きく出ます。データの量が大きくなると、PCI バスが制限要因となります。

figure
semilogx(sizes,sendBandwidth,MarkerIndices=maxSendIdx,Marker="o")
hold on
semilogx(sizes,gatherBandwidth,MarkerIndices=maxGatherIdx,Marker="o")
grid on
title("Data Transfer Bandwidth")
xlabel("Array size (bytes)")
ylabel("Transfer speed (GB/s)")
legend(["Send to GPU" "Gather from GPU"],Location="SouthEast")
hold off

メモリ使用量の多い演算における読み取り速度と書き込み速度の測定

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

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

GPU をリセットして前のセクションで割り当てられた GPU 配列のメモリを消去します。

reset(gpu)

サイズのベクトルを作成します。

sizes = logspace(4.5,log10(maxSize),maxNumTests);
sizes(sizes/sizeOfDouble > intmax) = [];

GPU での GPU メモリの読み取りと書き込みの速度を測定します。さらに、ホストでのホスト メモリの読み取りと書き込みの速度を測定します。sizes の各配列サイズについて次を行います。

  • GPU とホストで倍精度の乱数データを作成する。

  • gputimeit 関数を使用して、GPU での plus 関数の実行時間を測定する。

  • timeit 関数を使用して、ホストでの plus 関数の実行時間を測定する。

  • 取得されたデータと書き込まれたデータの量を測定時間で除算して読み取りと書き込みの帯域幅を求める。

numTests = numel(sizes);
numElements = floor(sizes/sizeOfDouble);
memoryTimesGPU = inf(1,numTests);
memoryTimesHost = inf(1,numTests);

for idx=1:numTests
    disp("Test " + idx + " of " + numTests + ". Timing plus operation on GPU and CPU for arrays with " + numElements(idx) + " elements.")

    % Generate random data on GPU and host.
    gpuData = randi([0 9],numElements(idx),1,"gpuArray");
    hostData = gather(gpuData);

    % Time the plus function on GPU.
    plusFcn = @() plus(gpuData,1.0);
    memoryTimesGPU(idx) = gputimeit(plusFcn);

    % Time the plus function on host.
    plusFcn = @() plus(hostData,1.0);
    memoryTimesHost(idx) = timeit(plusFcn);
end
Test 1 of 15. Timing plus operation on GPU and CPU for arrays with 3952 elements.
Test 2 of 15. Timing plus operation on GPU and CPU for arrays with 9437 elements.
Test 3 of 15. Timing plus operation on GPU and CPU for arrays with 22530 elements.
Test 4 of 15. Timing plus operation on GPU and CPU for arrays with 53788 elements.
Test 5 of 15. Timing plus operation on GPU and CPU for arrays with 128416 elements.
Test 6 of 15. Timing plus operation on GPU and CPU for arrays with 306583 elements.
Test 7 of 15. Timing plus operation on GPU and CPU for arrays with 731942 elements.
Test 8 of 15. Timing plus operation on GPU and CPU for arrays with 1747449 elements.
Test 9 of 15. Timing plus operation on GPU and CPU for arrays with 4171886 elements.
Test 10 of 15. Timing plus operation on GPU and CPU for arrays with 9960023 elements.
Test 11 of 15. Timing plus operation on GPU and CPU for arrays with 23778702 elements.
Test 12 of 15. Timing plus operation on GPU and CPU for arrays with 56769618 elements.
Test 13 of 15. Timing plus operation on GPU and CPU for arrays with 135532606 elements.
Test 14 of 15. Timing plus operation on GPU and CPU for arrays with 323572501 elements.
Test 15 of 15. Timing plus operation on GPU and CPU for arrays with 772501660 elements.
memoryBandwidthGPU = 2*(sizes./memoryTimesGPU)/1e9;
memoryBandwidthHost = 2*(sizes./memoryTimesHost)/1e9;

読み取り速度と書き込み速度のピークを求めます。

[maxBWGPU,maxBWIdxGPU] = max(memoryBandwidthGPU);
[maxBWHost,maxBWIdxHost] = max(memoryBandwidthHost);
fprintf("Achieved peak read+write speed on the GPU: %.2f GB/s",maxBWGPU)
Achieved peak read+write speed on the GPU: 678.83 GB/s
fprintf("Achieved peak read+write speed on the host: %.2f GB/s",maxBWHost)
Achieved peak read+write speed on the host: 59.22 GB/s

読み取り速度と書き込み速度を配列サイズに対してプロットし、それぞれの場合のピークに丸印を付けます。このプロットを上記のデータ転送プロットと比較すると、通常 GPU では、ホストからデータを取得するよりずっと速くメモリからの読み取りとメモリへの書き込みができることは明らかです。したがって、ホストから GPU へ、または GPU からホストへのメモリ転送の数を最小化することが重要です。理想は、最初にプログラムによって GPU でデータを作成することです。それ以外の場合は、プログラムによってデータを GPU に転送し、GPU でデータの処理をできるだけ行ってから、完了したときにだけホストに戻すことが望まれます。

figure
semilogx(sizes,memoryBandwidthGPU,MarkerIndices=maxBWIdxGPU,Marker="o")
hold on
semilogx(sizes,memoryBandwidthHost,MarkerIndices=maxBWIdxHost,Marker="o")

grid on
title("Read+Write Bandwidth")
xlabel("Array size (bytes)")
ylabel("Speed (GB/s)")
legend(["GPU" "Host"],Location="NorthWest")
hold off

計算量の多い演算における処理能力の測定

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

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

FLOP(N)=2N3-N2.

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

GPU をリセットして前のセクションで割り当てられた GPU 配列のメモリを消去し、サイズのベクトルを作成します。

reset(gpu)
sizes = logspace(4,log10(maxSize)-1,maxNumTests);
sizes(sizes/sizeOfDouble > intmax) = [];

倍精度

MATLAB では倍精度または単精度で計算を実行できます。倍精度の代わりに単精度で計算を実行することにより、GPU で実行されるコードのパフォーマンスを改善できます。これは、ほとんどの GPU カードが、高い単精度のパフォーマンスを要求するグラフィックス表示用に設計されているためです。これに対し、CPU は汎用コンピューティング用に設計されているため、倍精度から単精度に切り替えてもこの向上は見られません。データの単精度への変換と単精度データでの算術演算の実行の詳細については、浮動小数点数を参照してください。GPU での単精度計算に適する代表的なワークフローの例には、イメージ処理と機械学習があります。ただし、線形代数問題などの他の種類の計算には、一般に倍精度の処理が必要です。

単精度と倍精度を比較した GPU の相対的なパフォーマンスの近似測定を確認するには、デバイスの SingleDoubleRatio プロパティをクエリします。このプロパティは、デバイス上の倍精度浮動小数点演算装置 (FPU) に対する単精度 FPU の比率を示します。ほとんどのデスクトップ GPU は倍精度の 24 倍または 32 倍の単精度浮動小数点演算装置を備え、64 倍のものもあります。さらに、一般的な深層学習演算を高速化する特殊なコアを搭載した GPU もあります。たとえば、Ampere アーキテクチャ以降の NVIDIA データ センター GPU (A100 および H100) は、倍精度の行列乗算を高速化できる Tensor Cores を搭載しています。それらのデータ センター GPU では、行列乗算について、SingleDoubleRatio プロパティが単精度と倍精度を比較した相対的なパフォーマンスを正確に表さないことがあります。

gpu.SingleDoubleRatio
ans = 
32

倍精度の処理能力を測定するには、sizes の各配列サイズについて次を行います。

  • GPU とホストで倍精度の乱数データを作成する。

  • gputimeit 関数を使用して、GPU での plus 関数の実行時間を単精度と倍精度で測定する。

  • timeit 関数を使用して、ホストでの plus 関数の実行時間を単精度と倍精度で測定する。

  • 取得されたデータと書き込まれたデータの量を測定時間で除算して読み取りと書き込みの帯域幅を求める。

numTests = numel(sizes);
NDouble = floor(sqrt(sizes/sizeOfDouble));
mmTimesHostDouble = inf(1,numTests);
mmTimesGPUDouble = inf(1,numTests);

for idx=1:numTests
    disp("Test " + idx + " of " + numTests + ". Timing double-precision matrix-matrix multiplication with " + NDouble(idx)^2 + " elements.")
    
    % Generate random data on GPU.
    A = rand(NDouble(idx),"gpuArray");
    B = rand(NDouble(idx),"gpuArray");
    
    % Time the matrix multiplication on GPU.
    mmTimesGPUDouble(idx) = gputimeit(@() A*B);

    % Gather the data and time matrix multiplication on the host.
    A = gather(A);
    B = gather(B);
    mmTimesHostDouble(idx) = timeit(@() A*B);
end
Test 1 of 15. Timing double-precision matrix-matrix multiplication with 1225 elements.
Test 2 of 15. Timing double-precision matrix-matrix multiplication with 2601 elements.
Test 3 of 15. Timing double-precision matrix-matrix multiplication with 5776 elements.
Test 4 of 15. Timing double-precision matrix-matrix multiplication with 12321 elements.
Test 5 of 15. Timing double-precision matrix-matrix multiplication with 26569 elements.
Test 6 of 15. Timing double-precision matrix-matrix multiplication with 57121 elements.
Test 7 of 15. Timing double-precision matrix-matrix multiplication with 123201 elements.
Test 8 of 15. Timing double-precision matrix-matrix multiplication with 265225 elements.
Test 9 of 15. Timing double-precision matrix-matrix multiplication with 570025 elements.
Test 10 of 15. Timing double-precision matrix-matrix multiplication with 1227664 elements.
Test 11 of 15. Timing double-precision matrix-matrix multiplication with 2637376 elements.
Test 12 of 15. Timing double-precision matrix-matrix multiplication with 5673924 elements.
Test 13 of 15. Timing double-precision matrix-matrix multiplication with 12201049 elements.
Test 14 of 15. Timing double-precision matrix-matrix multiplication with 26234884 elements.
Test 15 of 15. Timing double-precision matrix-matrix multiplication with 56415121 elements.

倍精度の処理能力のピークを求めます。

mmFlopsHostDouble = (2*NDouble.^3 - NDouble.^2)./mmTimesHostDouble;
[maxFlopsHostDouble,maxFlopsHostDoubleIdx] = max(mmFlopsHostDouble);
mmFlopsGPUDouble = (2*NDouble.^3 - NDouble.^2)./mmTimesGPUDouble;
[maxFlopsGPUDouble,maxFlopsGPUDoubleIdx] = max(mmFlopsGPUDouble);
fprintf("Achieved peak double-precision processing power on the GPU: %.2f TFLOPS",maxFlopsGPUDouble/1e12)
Achieved peak double-precision calculation rate on the GPU: 0.41 TFLOPS
fprintf("Achieved peak double-precision processing power on the host: %.2f TFLOPS",maxFlopsHostDouble/1e12)
Achieved peak double-precision calculation rate on the host: 0.39 TFLOPS

単精度

single関数を使用してデータを単精度に変換できます。また、zerosonesrandeye などの作成関数で、データ型として single を指定することもできます。

単精度の処理能力を測定します。rand 関数を使用し、データ型として single を指定して、単精度の乱数データを生成します。

NSingle = floor(sqrt(sizes/(sizeOfDouble/2)));
mmTimesHostSingle = inf(1,numTests);
mmTimesGPUSingle = inf(1,numTests);

for idx=1:numTests
    disp("Test " + idx + " of " + numTests + ". Timing single-precision matrix-matrix multiplication with " + NSingle(idx)^2 + " elements.")

    % Generate random, single-precision data on GPU.
    A = rand(NSingle(idx),"single","gpuArray");
    B = rand(NSingle(idx),"single","gpuArray");

    % Time matrix multiplication on GPU.
    mmTimesGPUSingle(idx) = gputimeit(@() A*B);

    % Gather the data and time matrix multiplication on the host.
    A = gather(A);
    B = gather(B);
    mmTimesHostSingle(idx) = timeit(@() A*B);
end
Test 1 of 15. Timing single-precision matrix-matrix multiplication with 2500 elements.
Test 2 of 15. Timing single-precision matrix-matrix multiplication with 5329 elements.
Test 3 of 15. Timing single-precision matrix-matrix multiplication with 11449 elements.
Test 4 of 15. Timing single-precision matrix-matrix multiplication with 24649 elements.
Test 5 of 15. Timing single-precision matrix-matrix multiplication with 53361 elements.
Test 6 of 15. Timing single-precision matrix-matrix multiplication with 114244 elements.
Test 7 of 15. Timing single-precision matrix-matrix multiplication with 247009 elements.
Test 8 of 15. Timing single-precision matrix-matrix multiplication with 529984 elements.
Test 9 of 15. Timing single-precision matrix-matrix multiplication with 1140624 elements.
Test 10 of 15. Timing single-precision matrix-matrix multiplication with 2455489 elements.
Test 11 of 15. Timing single-precision matrix-matrix multiplication with 5276209 elements.
Test 12 of 15. Timing single-precision matrix-matrix multiplication with 11350161 elements.
Test 13 of 15. Timing single-precision matrix-matrix multiplication with 24403600 elements.
Test 14 of 15. Timing single-precision matrix-matrix multiplication with 52475536 elements.
Test 15 of 15. Timing single-precision matrix-matrix multiplication with 112848129 elements.

単精度の処理能力のピークを求めます。

mmFlopsHostSingle = (2*NSingle.^3 - NSingle.^2)./mmTimesHostSingle;
[maxFlopsHostSingle,maxFlopsHostSingleIdx] = max(mmFlopsHostSingle);
mmFlopsGPUSingle = (2*NSingle.^3 - NSingle.^2)./mmTimesGPUSingle;
[maxFlopsGPUSingle,maxFlopsGPUSingleIdx] = max(mmFlopsGPUSingle);
fprintf("Achieved peak single-precision processing power on the GPU: %.2f TFLOPS",maxFlopsGPUSingle/1e12)
Achieved peak single-precision calculation rate on the GPU: 16.50 TFLOPS
fprintf("Achieved peak single-precision processing power on the host: %.2f TFLOPS",maxFlopsHostSingle/1e12)
Achieved peak single-precision calculation rate on the host: 0.83 TFLOPS

倍精度と単精度の処理能力を配列サイズに対してプロットし、それぞれの場合のピークに丸印を付けます。

figure
loglog(NDouble.^2,mmFlopsGPUDouble,MarkerIndices=maxFlopsGPUDoubleIdx,Marker="o")
hold on
loglog(NSingle.^2,mmFlopsGPUSingle,MarkerIndices=maxFlopsGPUSingleIdx,Marker="o")
loglog(NDouble.^2,mmFlopsHostDouble,MarkerIndices=maxFlopsHostDoubleIdx,Marker="o")
loglog(NSingle.^2,mmFlopsHostSingle,MarkerIndices=maxFlopsHostSingleIdx,Marker="o")

grid on
title("Matrix-Matrix Multiply")
xlabel("Matrix size (numel)")
ylabel("processing power (FLOPS)")
legend(["GPU double" "GPU single" "Host double" "Host single"],Location="SouthEast")
hold off

まとめ

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

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

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

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

  • GPU では倍精度よりも単精度の方が計算の実行が速く、多くの場合ははるかに速い。

それぞれのテストにおいて、GPU がホスト CPU のパフォーマンスを上回るには大きな配列が必要であるという点は注目に値します。GPU の利用は、何百万もの要素を同時に扱う際に最も有利となります。

さまざまな GPU 間の比較を含む、より詳細な GPU ベンチマークについては、MATLAB Central の File Exchange にある GPUBench を参照してください。

参考

| | |

トピック