В этом примере показано, как измерить некоторые ключевые характеристики производительности графического процессора.
Графические процессоры могут использоваться для ускорения определенных типов вычислений. Однако производительность GPU сильно варьируется между различными устройствами GPU. Для количественной оценки производительности графического процессора используются три теста:
Как быстро данные могут быть отправлены в графический процессор или считаны с него?
Насколько быстро ядро графического процессора может считывать и записывать данные?
Насколько быстро графический процессор может выполнять вычисления?
После их измерения производительность графического процессора можно сравнить с производительностью центрального процессора. Это обеспечивает руководство относительно того, какой объем данных или вычислений требуется графическому процессору для обеспечения преимущества по сравнению с ЦП.
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.
Первый тест оценивает, как быстро данные могут быть отправлены в графический процессор и считаны из него. Поскольку графический процессор подключен к шине PCI, это во многом зависит от того, насколько быстра шина PCI и сколько других устройств ее использует. Однако есть также некоторые накладные расходы, которые включены в измерения, в частности служебные расходы вызова функции и время распределения массива. Поскольку они присутствуют в любом «реальном» использовании GPU, разумно включать их.
В следующих тестах выделяется память и данные передаются в графический процессор с помощью gpuArray функция. Память выделяется, а данные передаются обратно в память хоста с помощью gather.
Обратите внимание, что PCI express v3, используемый в этом тесте, имеет теоретическую полосу пропускания 0.99GB/s на полосу. Для 16-полосных слотов (PCIe3 x16), используемых вычислительными картами NVIDIA, это дает теоретическую 15.75GB/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')

Многие операции выполняют очень мало вычислений с каждым элементом массива, и поэтому доминирует время, затрачиваемое на извлечение данных из памяти или их обратную запись. Функции, такие как 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: 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 для начала.
Для операций, в которых число вычислений с плавающей запятой, выполняемых на элемент, считанный из памяти или записанный в память, является высоким, скорость памяти гораздо менее важна. В этом случае число и скорость единиц с плавающей запятой являются ограничивающим фактором. Говорят, что эти операции имеют высокую «вычислительную плотность».
Хорошим тестом вычислительной производительности является умножение матрицы. Для умножения двух
матриц общее число вычислений с плавающей запятой равно
.
Считывают две входные матрицы и записывают одну результирующую матрицу для общего количества
считанных или записанных элементов. Это дает вычислительную плотность (2N - 1)/3 FLOP/элемент. Сравните это с plus как использовано выше, которая имеет вычислительную плотность 1/2 FLOP/элемент.
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 требовались достаточно большие массивы, ограниченные памятью или вычислениями. Графические процессоры обеспечивают наибольшее преимущество при работе сразу с миллионами элементов.
Более подробные тесты графического процессора, включая сравнение различных графических процессоров, доступны в GPUBench на сервере MATLAB ® Central File Exchange.