В этом примере показано, как измерить некоторые ключевые характеристики эффективности графический процессор.
Графические процессоры могут использоваться для ускорения некоторых типов расчетов. Однако эффективность графический процессор сильно варьируется между различными устройствами 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 и сколько других вещей ее используют. Однако существуют также некоторые накладные расходы, которые включаются в измерения, в частности накладные расходы на вызов функции и время выделения массива. Поскольку они присутствуют в любом «реальном мире» использовании графический процессор, их разумно включать.
В следующих тестах выделяется память и данные отправляются на графический процессор с помощью 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
Теперь сравните его с тем же кодом, что и на центральном процессоре.
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-хоста. В идеале программы должны передавать данные на графический процессор, затем делать с ним как можно больше, находясь на графическом процессоре, и возвращать их на хост только по завершении. Еще лучше было бы создать данные о графическом процессоре, чтобы начать с.
Для операций, где количество расчетов с плавающей точкой, выполняемых на элемент, считанный из или записанный в память, велико, скорость памяти гораздо менее важна. В этом случае количество и скорость модулей с плавающей точкой является ограничивающим фактором. Эти операции, как говорят, имеют высокую «вычислительную плотность».
Хорошим тестом вычислительной эффективности является умножение на матрицу. Для умножения двух матриц, общее количество вычислений с плавающей точкой является
.
Считываются две входные матрицы и записывается одна результирующая матрица, для общего количества элементов, считанных или записанных. Это дает вычислительную плотность (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, включая сравнение между различными графическими процессорами, доступны в GPUBench в MATLAB ® Central File Exchange.