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

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

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

// 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(&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.

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

ИЗМЕНИТЬ

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


person ZRP    schedule 31.03.2019    source источник


Ответы (1)


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

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

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

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

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

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

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

af::af_cdouble* device_ptr = af::pinned(signal.size());

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

af::array 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)

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

person pradeep    schedule 01.04.2019
comment
Спасибо, Прадип. Я как бы понял, что у первого звонка была, так сказать, разминка. Я действительно думаю, что timeit следует переименовать в Benchit или что-то в этом роде, так как это не просто функция синхронизации. Что поднимает вопрос об устранении этой разминки. время дает своего рода лучшее время, но крайне важно знать реальную производительность каждого вызова. Я полагаю, что я просто обязательно сделаю хотя бы один вызов функции до фактического использования. - person ZRP; 03.04.2019
comment
время это может быть немного двусмысленным сейчас, когда я думаю об этом. Не могли бы вы поднять вопрос на нашей странице github относительно рефакторинга API. Мы можем продолжить обсуждение на github. Спасибо. - person pradeep; 15.04.2019