Измерение уровня графического процессора

В этом примере показано, как измерить некоторые ключевые показатели производительности графического процессора.

Графические процессоры могут использоваться, чтобы ускорить определенные типы расчетов. Однако эффективность графического процессора значительно различается в различных устройствах графического процессора. Для того, чтобы определить количество эффективности графического процессора, три теста используются:

  • Как быстро данные могут быть отправлены в графический процессор или читать назад из него?

  • Как быстро ядро графического процессора может читать и записать данные?

  • Как быстро графический процессор может выполнить расчеты?

После измерения их эффективность графического процессора может сравниться с центральным процессором хоста. Это предоставляет руководство относительно того, сколько данные или расчет требуются для графического процессора обеспечить преимуществу перед центральным процессором.

Настройка

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 описывает 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нулиNaNTRUE только запишите их выход, тогда как функции как transposetril оба чтения и запись, но не делают никакого расчета. Даже простые операторы как plusминус, mtimes, * сделайте так мало расчета на элемент, что они связаны только скоростью доступа к памяти.

Функциональный plus выполняет одно чтение памяти и одну запись памяти для каждой операции с плавающей точкой. Это должно поэтому быть ограничено скоростью доступа к памяти и обеспечивает хороший индикатор скорости read+write операции.

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

Сравнивание этого графика с передачей данных строит выше, ясно, что графические процессоры могут обычно читать из и писать в свою память намного быстрее, чем они могут получить данные из хоста. Поэтому важно минимизировать количество передач графического процессора хоста или памяти хоста графического процессора. Идеально, программы должны передать данные графическому процессору, затем сделать как можно больше с ним, в то время как на графическом процессоре, и возвращают его хосту только, когда завершенный. Еще лучше должен был бы создать данные по графическому процессору, чтобы запуститься с.

Тестирование в вычислительном отношении интенсивных действий

Для операций, где количество расчетов с плавающей точкой, выполняемых на элемент, считанный из или записанный в память, высоко, скорость памяти намного менее важна. В этом случае номер и скорость модулей с плавающей точкой являются ограничивающим фактором. Эти операции, как говорят, имеют высоко "вычислительную плотность".

Хороший тест вычислительной эффективности является матрицей - умножение матриц. Для умножения двух$N \times N$ матриц общее количество вычислений с плавающей точкой

$FLOPS(N) = 2N^3 - N^2$.

Две входных матрицы читаются, и одна получившаяся матрица записана, в общей сложности для$3N^2$ чтения элементов или записана. Это дает вычислительную плотность (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')

Заключения

Эти тесты показывают некоторые важные характеристики эффективности графического процессора:

  • Передачи от памяти хоста до памяти графического процессора и назад являются относительно медленными.

  • Хороший графический процессор может чтение-запись его память намного быстрее, чем центральный процессор хоста может чтение-запись его память.

  • Учитывая достаточно большие данные, графические процессоры могут выполнить вычисления намного быстрее, чем центральный процессор хоста.

Известно, что в каждом тесте довольно большие массивы потребовались, чтобы полностью насыщать графический процессор, ли ограниченный памятью или расчетом. Графические процессоры обеспечивают самое большое преимущество при работе с миллионами элементов целиком.

Более подробные сравнительные тесты графического процессора, включая сравнения между различными графическими процессорами, доступны в GPUBench на MATLAB® Central File Exchange.