Затраты на вызовы OpenCL или CUDA? - PullRequest
4 голосов
/ 24 января 2012

Я пишу функцию, которая выполняет множество операций BLAS gemv.

Я бы хотел сделать это на GPU, и я попробовал с cuBlas.

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

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

Это время, которое требуется CUDA для настройки вызова и отправки его графическому процессору - не считая времени, которое фактически требуется для умножения матрицы на вектор.

Как бы я поступил так?

Ответы [ 4 ]

8 голосов
/ 24 января 2012

Обновление : Следующие результаты относятся к рукописному алгоритму FFT GPU на оборудовании 2005 года (nVidia 7800 GTX), но показывают принцип узких мест передачи CPU-GPU

Накладные расходы - это не сам вызов, а компиляция программы GPU и передача данных между GPU и хостом.Процессор сильно оптимизирован для функций, которые могут выполняться полностью в кэш-памяти, и задержка памяти DDR3 намного ниже, чем шины PCI-Express, которая обслуживает графический процессор.Я испытал это сам, когда писал процедуры FFT на GPU (до CUDA).Пожалуйста, посмотрите этот связанный вопрос .

N       FFTw (s)    GPUFFT (s)  GPUFFT MFLOPS   GPUFFT Speedup
8       0           0.00006     3.352705        0.006881
16      0.000001    0.000065    7.882117        0.010217
32      0.000001    0.000075    17.10887        0.014695
64      0.000002    0.000085    36.080118       0.026744
128     0.000004    0.000093    76.724324       0.040122
256     0.000007    0.000107    153.739856      0.066754
512     0.000015    0.000115    320.200892      0.134614
1024    0.000034    0.000125    657.735381      0.270512
2048    0.000076    0.000156    1155.151507     0.484331
4096    0.000173    0.000215    1834.212989     0.804558
8192    0.000483    0.00032     2664.042421     1.510011
16384   0.001363    0.000605    3035.4551       2.255411
32768   0.003168    0.00114     3450.455808     2.780041
65536   0.008694    0.002464    3404.628083     3.528726
131072  0.015363    0.005027    3545.850483     3.05604
262144  0.033223    0.012513    3016.885246     2.655183
524288  0.072918    0.025879    3079.443664     2.817667
1048576 0.173043    0.076537    2192.056517     2.260904
2097152 0.331553    0.157427    2238.01491      2.106081
4194304 0.801544    0.430518    1715.573229     1.861814

В таблице выше показаны временные характеристики реализации FFT на GPU по сравнению с реализацией CPU на основе размера ядра.Для меньших размеров преобладает передача данных в / из графического процессора.Меньшие ядра могут быть выполнены на CPU, некоторые реализации / размеры полностью в кеше.Это делает процессор лучшим выбором для небольших операций.

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

Что касается измерения эффекта в вашем примере, я бы предложил провести эксперимент, подобный описанному выше.Попробуйте рассчитать FLOPS, рассчитанные для каждого размера матрицы, и запустите тест на процессоре и графическом процессоре для матрицы разных размеров.Выведите в файл CSV размер, время и FLOPS для GPU против CPU.Для любого профилирования убедитесь, что вы выполняете несколько сотен итераций своего кода и все время, а затем делите общее время на итерации, чтобы получить время цикла.Попробуйте использовать матрицы различной формы, если ваш алгоритм позволяет (например, 10x100, а не 100x10).

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

Надеюсь, это поможет,

1 голос
/ 10 мая 2012

Чтобы найти издержки вызова, вызовите ядро ​​CUDA, которое делает как можно меньше.

for (int i=0; i<NLoops; i++) {
    gettimeofday(&cpuStart, 0); // get start time on CPU  

    // Call minimal CUDA kernel  

    gettimeofday(&cpuEnd, 0); // get end time on CPU 

    // save elapsed time
}

Следуйте приведенному выше коду Алекса П.

Чем меньше обработки вы выполняете в ядре, тем больше разница во времени будет связана только с накладными расходами на вызов.

Сделайте небольшой эксперимент, чтобы найти хорошее значение для NLoops (возможно, 1 000 000). Убедитесь, что истекшее время больше, чем интервал вашего таймера, иначе вы получите все нули. Если это произойдет, напишите некоторый код ядра, который выполняется за фиксированный интервал времени, который вы можете предсказать: (n циклов по x циклов каждый).

Трудно удалить все вычисления, не относящиеся к CUDA, которые могут происходить между cpuStart и cpuEnd (например, обработка прерываний), но выполнение нескольких прогонов и усреднение может дать хорошие результаты.

1 голос
/ 24 января 2012

Ваши матрицы уже на GPU?Если нет, то CUBLAS может передать их вам (это называется thunking), что является дополнительным расходом.

Кроме того, графические процессоры не очень хороши для таких небольших вычислений, то есть, вероятно, они будут медленнее, чем процессоры, поскольку у вас естьперенести ваш результат обратно.Если вы можете, используйте большие матрицы.В противном случае вы можете использовать потоки (cudaStream_t) для запуска нескольких параллельных вычислений на графическом процессоре.

Если вы хотите измерить время выполнения ядра в CUDA, вам нужно заключить это (или что-либо еще, что вычисляет)в графическом процессоре) в событиях, например, при использовании API времени выполнения CUDA:

cudaEvent_t start, stop;

cudaEventRecord(&start);

struct timeval cpuStart, cpuEnd;

gettimeofday(&cpuStart, 0); // get start time on CPU

// Do something with CUDA on the GPU, e.g. call kernels, transfer memory, ...

gettimeofday(&cpuEnd, 0); // get end time on CPU

double seconds = cpuEnd.tv_sec - cpuStart.tv_sec;
double microseconds = cpuEnd.tv_usec - cpuStart.tv_usec;
double cpuDuration = (seconds * 1.0e6 + microseconds) / 1.0e3; // in milliseconds

cudaEventRecord(&stop);

// Wait until the stop event occurred
cudaError_t eventResult;

do
{
  eventResult = cudaEventQuery(stop);
}
while (eventResult == cudaErrorNotReady);

// Assert there was no error; check the CUDA Toolkit Reference for further info
assert(cudaSuccess == eventResult); // requires #include <assert.h> or <cassert>

// Retrieve the time
float gpuDuration = 0.0; // in milliseconds
cudaEventElapsedTime(&gpuDuration, start, stop);

// Release the event objects
cudaEventDestroy(stop);
cudaEventDestroy(start);

Возможно, вы захотите проверять код ошибки каждого вызова CUDA (по крайней мере, с утверждением), как вы можете получитьошибки от предыдущих вызовов, приводящие к часам отладки ...

(Примечание: я в основном использую API-интерфейс драйвера CUDA, поэтому это может не работать из коробки. Извините за это.)

РЕДАКТИРОВАТЬ: Только что увидел, что вы хотите измерить сам вызов, а не продолжительность ядра.Вы можете сделать это, просто измеряя время в CPU для вызова - см. Обновленный код выше.Это работает только в Linux, потому что gettimeofday недоступен для Windows (AFAIK).

1 голос
/ 24 января 2012

Вы можете получить время в наносекундах с устройства, когда событие было поставлено в очередь, отправлено, запущено и завершено, используя clGetEventProfilingInfo для вашего события передачи в буфер.

Дополнительная информация и способ ее настройки здесь: http://www.khronos.org/registry/cl/sdk/1.0/docs/man/xhtml/clGetEventProfilingInfo.html

Я думаю, что для матриц 100x100 вам лучше придерживаться процессора для хруста.Если вам не нужно много умножать в одно и то же время, выгода от использования GPU вряд ли будет заметна из-за (небольших) накладных расходов на передачу и, как правило, гораздо меньшей тактовой частоты.Убедитесь, что вы настроили ваше ядро ​​так, чтобы использовать как можно больше локальных данных - на моем оборудовании 32 КБ на рабочую группу, и этого должно быть достаточно для размещения двух матриц 100x100.Встроенные функции точечного продукта также должны быть очень удобными.

В прошлом году в ADFS был потрясающий разговор об этом (см. SessionId: 2908) http://developer.amd.com/afds/pages/OLD/sessions.aspx Они подробно рассказывают об оптимизацииядро и жесткое кодирование оптимальных размеров.

...