Как правильно использовать закрепленную память в ArrayFire? - PullRequest
0 голосов
/ 31 марта 2019

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

Я пробовал различные способы создания закрепленной памяти и создания массивов из нее, например. cudaMallocHost. Использовать cudaMallocHost с cudaMemcpy довольно быстро (несколько сотен раз), но затем создание / инициализация массива arrayfire было очень медленным (~ 2-3 секунды). Наконец, я придумал следующий метод, и выделение занимает ~ 2-3 секунды, но его можно перенести в другое место. Инициализация массива с данными хоста удовлетворительна (100–200 мксек), но теперь операции (в данном случае FFT) мучительно медленные: ~ 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 мсек. включая свертку, FFT с использованием FFTW занимает около 5 мсек. Также на моей машине с использованием примера ArrayFire FFT я получаю следующие результаты (модифицированные для использования c64)

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

Контрольная отметка 1-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.

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

EDIT

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

1 Ответ

1 голос
/ 01 апреля 2019

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

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

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

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

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

  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)

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

...