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

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

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

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

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

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

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

Setup

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.

Проверка полосы пропускания хоста/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-хоста. В идеале программы должны передавать данные на графический процессор, затем делать с ним как можно больше, находясь на графическом процессоре, и возвращать их на хост только по завершении. Еще лучше было бы создать данные о графическом процессоре, чтобы начать с.

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

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

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

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

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

Для просмотра документации необходимо авторизоваться на сайте