Как правильно использовать закрепленную память в ArrayFire?

#c #arrayfire

#c #arrayfire

Вопрос:

При использовании закрепленной памяти в ArrayFire я получаю низкую производительность.

Я пробовал различные методы создания закрепленной памяти и массивов из нее, например. cudaMallocHost. Использование cudaMallocHost с cudaMemcpy работает довольно быстро (несколько сотен юзек.), но затем создание / инициализация массива arrayfire было действительно медленным (~ 2-3 сек.). Наконец, я придумал следующий метод, и выделение занимает ~ 2-3 секунды., но его можно переместить в другое место. Инициализация массива данными хоста выполнена удовлетворительно (100-200 usec.), но теперь операции (в данном случае FFT) выполняются мучительно медленно: ~ 400 мс. Я должен добавить, что входной сигнал имеет переменный размер, но для синхронизации я использовал 64 КБ выборок (сложные удвоения). Кроме того, я не предоставляю свою функцию синхронизации для краткости, но это не проблема, я рассчитал время, используя другие методы, и результаты согласуются.

 // Use the Frequency-Smoothing method to calculate the full 
// Spectral Correlation Density
// currently the whole function takes ~ 2555 msec. w/ signal 64K samples
// and window_length = 400 (currently not implemented)
void exhaustive_fsm(std::vector<std::complex<double>> signal, uint16_t window_length) {

  // Allocate pinned memory (eventually move outside function)
  // 2192 ms.
  af::af_cdouble* device_ptr = af::pinned<af::af_cdouble>(signal.size());

  // Init arrayfire array (eventually move outside function)
  // 188 us.
  af::array s(signal.size(), device_ptr, afDevice);

  // Copy to device
  // 289 us.
  s.write((af::af_cdouble*) signal.data(), signal.size() * sizeof(std::complex<double>), afHost);

  // FFT
  // 351 ms. equivalent to:
  // af::array fft = af::fft(s, signal.size());
  af::array fft = zrp::timeit(amp;af::fft, s, signal.size());
  fft.eval();

  // Convolution

  // Copy result to host

  // free memory (eventually move outside function)
  // 0 ms.
  af::freePinned((void*) s.device<af::af_cdouble>());

  // Return result
}
  

Как я уже говорил выше, БПФ занимает ~ 400 мс. Эта функция с использованием Armadillo занимает ~ 110 мс. включая свертку, БПФ с использованием FFTW занимает около 5 мс. Также на моей машине, используя пример ArrayFire FFT, я получаю следующие результаты (модифицированные для использования c64)

             A             = randu(1, N, c64);)
  

Тест 1-by-N CX fft

    1 x  128:                    time:     29 us.
   1 x  256:                    time:     31 us.
   1 x  512:                    time:     33 us.
   1 x 1024:                    time:     41 us.
   1 x 2048:                    time:     53 us.
   1 x 4096:                    time:     75 us.
   1 x 8192:                    time:    109 us.
   1 x 16384:                   time:    179 us.
   1 x 32768:                   time:    328 us.
   1 x 65536:                   time:    626 us.
   1 x 131072:                  time:   1227 us.
   1 x 262144:                  time:   2423 us.
   1 x 524288:                  time:   4813 us.
   1 x 1048576:                 time:   9590 us.
  

Итак, единственное различие, которое я вижу, — это использование закрепленной памяти. Есть идеи, где я ошибаюсь? Спасибо.

Редактировать

Я заметил, что при запуске eaxample AF FFT возникает значительная задержка перед распечаткой в первый раз (даже если время не включает эту задержку). Итак, я решил создать класс и переместить все выделения / освобождения в ctor / dtor. Из любопытства я также поместил FFT в ctor, потому что я также заметил, что если я запустил второй FFT, это заняло ~ 600 usec. соответствует моим тестам. Конечно, запуск «предварительного» БПФ, похоже, что-то «инициализирует», и последующие БПФ выполняются намного быстрее. Должен быть способ получше, я, должно быть, чего-то не хватает.

Ответ №1:

Я прадип и один из разработчиков ArrayFire.

Во-первых, все серверные части функций ArrayFire (CUDA amp; OpenCL) требуют некоторой стоимости запуска, которая включает прогрев устройства и / или кэширование ядра (ядра кэшируются при первом вызове определенной функции). По этой причине вы замечаете улучшение времени выполнения после первого запуска. По этой же причине мы почти всегда настоятельно рекомендуем использовать нашу встроенную функцию timeit для определения времени выполнения кода arrayfire по мере его усреднения за несколько запусков, а не при первом запуске.

Как вы уже догадались из своих экспериментов, всегда лучше контролировать распределение закрепленной памяти. Если вы еще не знаете о компромиссах, связанных с использованием закрепленной памяти, вы можете начать с этого сообщения в блоге от NVIDIA (это в равной степени относится к закрепленной памяти из серверной части OpenCL, с любыми ограничениями, связанными с конкретным поставщиком, конечно). Общее руководство, предложенное в сообщении с гиперссылкой, заключается в следующем:

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

Если возможно, я бы выбрал следующий маршрут, чтобы использовать закрепленную память для ваших БПФ

  1. Инкапсулируйте закрепленные выделения / освобождения в формат RAII, что вы уже делаете сейчас из вашего отредактированного описания.
  2. Выделите закрепленную память только один раз, если это возможно — если размер ваших данных статичен.

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

af::af_cdouble* device_ptr = af::закрепленный(signal.size());

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

af::массив s(signal.size(), device_ptr, afDevice);

Поскольку af::pinned не выделяет память устройства, это не указатель на устройство, а перечисление является afHost. Итак, вызов будет af::array s(signal.size(), ptr);

Вы используете s.write правильно само по себе, но я считаю, что в вашем случае использования это не требуется.

Следующее, что я бы сделал.

  • Используйте конструкцию RAII для указателя, возвращаемого af::pinned , и выделяйте его только один раз. Убедитесь, что у вас не слишком много выделений с блокировкой страниц.
  • Используйте выделение с блокировкой страницы в качестве обычного выделения хоста, а не std::vector<complex> потому что это память хоста, просто с блокировкой страницы. Это потребовало бы написания некоторого дополнительного кода на стороне вашего хоста, если вы каким-то образом работаете с std::vector . В противном случае вы можете просто использовать RAIIed-pinned-pointer для хранения ваших данных.
  • Все, что вам нужно сделать для передачи ваших данных fft на устройство, это af::array s(size, ptr)

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

Комментарии:

1. Спасибо, прадип. Я вроде как понял, что первый вызов прошел, так сказать, «разминку». Я действительно думаю, что timeit следует переименовать в benchit или что-то в этом роде, поскольку это не просто функция синхронизации. В связи с этим возникает проблема устранения этого прогрева. timeэто дает своего рода оптимальное время, но крайне важно знать фактическую производительность каждого вызова. Я полагаю, я просто буду уверен, что сделаю хотя бы один вызов функции перед фактическим использованием.

2. теперь, когда я думаю об этом, это может показаться немного двусмысленным. Не могли бы вы, пожалуйста, поднять вопрос на нашей странице github относительно рефакторинга API. Мы можем продолжить обсуждение на github. Спасибо.