测量 GPU 性能
此示例展示了如何测量 GPU 的一些关键性能特征。
GPU 可用于加速某些类型的计算。然而,不同的 GPU 设备之间的 GPU 性能存在很大差异。为了量化 GPU 的性能,使用了三种测试:
数据可以多快被发送到 GPU 或从 GPU 读回?
GPU 内核读写数据的速度有多快?
GPU 执行计算的速度有多快?
测量这些之后,可以将 GPU 的性能与主机 CPU 进行比较。这为 GPU 需要多少数据或计算才能比 CPU 更具优势提供了指导。
设置
gpu = gpuDevice();
fprintf('Using an %s GPU.\n', gpu.Name)
Using an NVIDIA RTX A5000 GPU.
sizeOfDouble = 8; % Each double-precision number needs 8 bytes of storage
sizes = power(2, 14:28);
测试主机/GPU 带宽
第一个测试估计数据发送到 GPU 和从 GPU 读取的速度。由于 GPU 插入 PCI 总线,这在很大程度上取决于 PCI 总线的速度以及有多少其他东西正在使用它。然而,测量中还包含一些开销,特别是函数调用开销和数组分配时间。由于这些存在于 GPU 的任何“现实世界”使用中,因此将它们包括在内是合理的。
在以下测试中,使用 gpuArray
函数分配内存并将数据发送到 GPU。分配内存并使用 gather
将数据传输回主机内存。
请注意,此测试中使用的 GPU 支持 PCI Express® 版本 4.0,其每通道理论带宽为 1.97GB/s。对于 NVIDIA® 计算卡使用的 16 通道插槽,理论上可达到 31.52GB/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)
Achieved peak send speed of 9.5407 GB/s
gatherBandwidth = (sizes./gatherTimes)/1e9;
[maxGatherBandwidth,maxGatherIdx] = max(gatherBandwidth);
fprintf('Achieved peak gather speed of %g GB/s\n',max(gatherBandwidth))
Achieved peak gather speed of 4.1956 GB/s
在下图中,每种情况的峰值都被圈出来了。由于数据集较小,因此开销占主导地位。当数据量较大时,PCI 总线就成为限制因素。
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') hold off
测试内存密集型操作
许多操作对数组的每个元素进行的计算都很少,因此主要取决于从内存中获取数据或将其写回所花费的时间。ones
、zeros
、nan
、true
等函数只写入其输出,而 transpose
、tril
等函数既读取又写入,但不进行计算。即使是像 plus
、minus
、mtimes
这样的简单运算符,每个元素的计算也非常少,因此它们仅受内存访问速度的限制。
函数 plus
对每个浮点运算执行一次内存读取和一次内存写入。因此,它应该受到内存访问速度的限制,并提供读写操作速度的良好指示符。
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: 659.528 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)
Achieved peak read+write speed on the host: 71.0434 GB/s
% Plot CPU and GPU results. 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') hold off
将此图与上面的数据传输图进行比较,可以清楚地看出,GPU 从其内存读取和写入的速度通常比从主机获取数据的速度快得多。因此,尽量减少主机-GPU 或 GPU-主机内存传输的次数非常重要。理想情况下,程序应该将数据传输到 GPU,然后在 GPU 上尽可能多地处理数据,并仅在完成后将其带回主机。更好的方法是首先在 GPU 上创建数据。
测试计算密集型操作
对于从内存读取或写入每个元素执行的浮点计算次数较多的操作,内存速度就没那么重要了。在这种情况下,浮点单元的数量和速度是限制因素。据称这些操作具有很高的“计算密度”。
矩阵与矩阵相乘是计算性能的一个很好的测试。对于两个 矩阵的乘法,浮点计算的总数为
.
读取两个输入矩阵并写入一个结果矩阵,总共读取或写入 个元素。这给出了 (2N - 1)/3
FLOP/element 的计算密度。将其与上面使用的 plus
进行对比,其计算密度为 1/2
FLOP/element。
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 354.4 GFLOPS (host), 414.0 GFLOPS (GPU)
现在绘制它来查看峰值在哪里达到。
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') hold off
结论
这些测试揭示了 GPU 性能的一些重要特征:
从主机内存到 GPU 内存和返回的传输相对较慢。
良好的 GPU 读取/写入其内存的速度比主机 CPU 读取/写入其内存的速度快得多。
如果数据足够大,GPU 可以比主机 CPU 更快地执行计算。
值得注意的是,在每次测试中都需要相当大的数组才能完全饱和 GPU,无论是受到内存还是计算的限制。GPU 在同时处理数百万个元素时具有最大的优势。
更详细的 GPU 基准测试(包括不同 GPU 之间的比较)可在 MATLAB® 中央文件交换上的 GPUBench 中获取。