Я экспериментирую с использованием функции обратного вызова cuFFT для преобразования входного формата "на лету" (например, вычисления БПФ 8-битных целочисленных входных данных без предварительного преобразования входного буфера в float
). Во многих моих приложениях мне нужно рассчитать перекрывающиеся БПФ на входном буфере как описано в этом предыдущем вопросе SO. Как правило, смежные FFT могут перекрываться на 1/4 до 1/8 длины FFT.
cuFFT с его FFTW-подобным интерфейсом явно поддерживает этот с помощью параметра idist
функции cufftPlanMany()
. В частности, если я хочу рассчитать БПФ размером 32768 с перекрытием 4096 выборок между последовательными входами, я бы установил idist = 32768 - 4096
. Этот работает правильно в том смысле, что он дает правильный вывод.
Тем не менее, я вижу странное ухудшение производительности при использовании cuFFT таким образом. Я разработал тест, который реализует это преобразование формата и перекрывается двумя разными способами:
-
Явным образом скажите cuFFT о перекрывающемся характере ввода: установите
idist = nfft - overlap
, как я описал выше. Установите функцию обратного вызова нагрузки, которая просто выполняет преобразование отint8_t
доfloat
по мере необходимости в индексе буфера, предоставленном для обратного вызова. -
Не сообщайте cuFFT о перекрывающемся характере ввода; ложь ему dset
idist = nfft
. Затем пусть функция обратного вызова обрабатывает перекрытие путем вычисления правильного индекса, который должен быть прочитан для каждого входа FFT.
В этом примере GitHub доступен тестовая программа, реализующая оба этих подхода с тестами времени и эквивалентности. Я не воспроизводил все это здесь для краткости. Программа вычисляет партию из 1024 32768-точечных БПФ, которые перекрываются 4096 выборками; тип входных данных - 8-битные целые числа. Когда я запускаю его на своей машине (с графическим процессором GeForce GTX 660, используя CUDA 8.0 RC на Ubuntu 16.04), я получаю следующий результат:
executing method 1...done in 32.523 msec
executing method 2...done in 26.3281 msec
Метод 2 заметно быстрее, чего я не ожидал. Посмотрите на реализации функций обратного вызова:
Метод 1:
template <typename T>
__device__ cufftReal convert_callback(void * inbuf, size_t fft_index,
void *, void *)
{
return (cufftReal)(((const T *) inbuf)[fft_index]);
}
Метод 2:
template <typename T>
__device__ cufftReal convert_and_overlap_callback(void *inbuf,
size_t fft_index, void *, void *)
{
// fft_index is the index of the sample that we need, not taking
// the overlap into account. Convert it to the appropriate sample
// index, considering the overlap structure. First, grab the FFT
// parameters from constant memory.
int nfft = overlap_params.nfft;
int overlap = overlap_params.overlap;
// Calculate which FFT in the batch that we're reading data for. This
// tells us how much overlap we need to account for. Just use integer
// arithmetic here for speed, knowing that this would cause a problem
// if we did a batch larger than 2Gsamples long.
int fft_index_int = fft_index;
int fft_batch_index = fft_index_int / nfft;
// For each transform past the first one, we need to slide "overlap"
// samples back in the input buffer when fetching the sample.
fft_index_int -= fft_batch_index * overlap;
// Cast the input pointer to the appropriate type and convert to a float.
return (cufftReal) (((const T *) inbuf)[fft_index_int]);
}
Метод 2 имеет значительно более сложную функцию обратного вызова, которая даже включает целочисленное деление по не компилируемому значению времени! Я ожидал бы, что это будет намного медленнее, чем метод 1, но я вижу обратное. Есть ли хорошее объяснение этому? Возможно ли, что cuFFT структурирует свою обработку по-разному, когда вход перекрывается, что приводит к ухудшению производительности?
Похоже, что мне удастся достичь производительности, которая будет намного быстрее, чем метод 2, если вычисления индекса могут быть удалены из обратного вызова (но для этого требуется, чтобы перекрытие указывалось на cuFFT).
Изменить: После запуска моей тестовой программы под nvvp
, я вижу, что cuFFT определенно структурирует свои вычисления по-разному. Трудно понять имена символов ядра, но вызовы ядра разбиваются следующим образом:
Метод 1:
-
__nv_static_73__60_tmpxft_00006cdb_00000000_15_spRealComplex_compute_60_cpp1_ii_1f28721c__ZN13spRealComplex14packR2C_kernelIjfEEvNS_19spRealComplexR2C_stIT_T0_EE
: 3,72 мс -
spRadix0128C::kernel1Tex<unsigned int, float, fftDirection_t=-1, unsigned int=16, unsigned int=4, CONSTANT, ALL, WRITEBACK>
: 7.71 мс -
spRadix0128C::kernel1Tex<unsigned int, float, fftDirection_t=-1, unsigned int=16, unsigned int=4, CONSTANT, ALL, WRITEBACK>
: 12,75 мс (да, он вызывается дважды) -
__nv_static_73__60_tmpxft_00006cdb_00000000_15_spRealComplex_compute_60_cpp1_ii_1f28721c__ZN13spRealComplex24postprocessC2C_kernelTexIjfL9fftAxii_t1EEEvP7ComplexIT0_EjT_15coordDivisors_tIS6_E7coord_tIS6_ESA_S6_S3_
: 7.49 мс
Метод 2:
-
spRadix0128C::kernel1MemCallback<unsigned int, float, fftDirection_t=-1, unsigned int=16, unsigned int=4, L1, ALL, WRITEBACK>
: 5.15 мс -
spRadix0128C::kernel1Tex<unsigned int, float, fftDirection_t=-1, unsigned int=16, unsigned int=4, CONSTANT, ALL, WRITEBACK>
: 12,88 мс -
__nv_static_73__60_tmpxft_00006cdb_00000000_15_spRealComplex_compute_60_cpp1_ii_1f28721c__ZN13spRealComplex24postprocessC2C_kernelTexIjfL9fftAxii_t1EEEvP7ComplexIT0_EjT_15coordDivisors_tIS6_E7coord_tIS6_ESA_S6_S3_
: 7.51 мс
Интересно, что cuFFT вызывает два ядра для фактического вычисления БПФ с использованием метода 1 (когда cuFFT знает о перекрытии), но с помощью метода 2 (где он не знает, что БПФ перекрываются), он выполняет работа с одним. Для ядер, которые используются в обоих случаях, они, похоже, используют одни и те же параметры сетки между методами 1 и 2.
Я не понимаю, почему здесь нужно использовать другую реализацию, тем более, что шаг ввода istride == 1
. Он должен просто использовать другой базовый адрес при извлечении данных на входе преобразования; остальная часть алгоритма должна быть точно такой же, я думаю.
Изменить 2: Я вижу еще более странное поведение. Я случайно осознал, что если я не смогу самостоятельно уничтожить ручки cuFFT, я вижу различия в измеренной производительности. Например, я модифицировал тестовую программу, чтобы пропустить уничтожение дескрипторов cuFFT, а затем выполнить тесты в другой последовательности: метод 1, метод 2, затем метод 2 и метод 1 снова. Я получил следующие результаты:
executing method 1...done in 31.5662 msec
executing method 2...done in 17.6484 msec
executing method 2...done in 17.7506 msec
executing method 1...done in 20.2447 msec
Таким образом, производительность, похоже, меняется в зависимости от того, существуют ли другие планы cuFFT при создании плана для тестового примера! Используя профилировщик, я вижу, что структура запуска ядра не изменяется между двумя случаями; ядра просто все работают быстрее. У меня нет разумного объяснения этого эффекта.