Правильный способ записи функций ядра в CUDA?

Я как раз собираюсь приступить к преобразованию программы, которую я написал в CUDA, чтобы, надеюсь, увеличить скорость обработки.

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

void main ()
{
  *initialization of variables*
  function1()
  function2()
  function3()
  print result;
}

эти функции по своей сути являются последовательными, поскольку funtion2 зависит от результатов funtion1.

хорошо, теперь я хочу преобразовать эти функции в ядра и выполните задачи в функциях параллельно.

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

очевидно, что я сохраню все переменные времени выполнения в памяти GPU, чтобы ограничить объем передачи данных, поэтому Я даже беспокоюсь о времени, которое требуется между вызовами ядра?

Я надеюсь, что этот вопрос ясен, если нет, пожалуйста, попросите меня уточнить. Спасибо.

и вот дополнительный вопрос, чтобы я мог проверить свое здравомыслие. В конечном счете, вход этой программы является видеофайлом, и через различные функции каждый кадр приведет к результату. Мой план состоит в том, чтобы захватить несколько кадров за раз (скажем, 8 уникальных кадров), а затем разделить общее количество блоков, которые у меня есть среди этих 8 кадров, а затем несколько потоков в блоках будут выполнять еще более параллельные операции с данными изображения, такие как векторное сложение, преобразования Фурье и т. д.
это правильный подход к проблеме?

3 ответов


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

если возможно, чтобы у вас была такая функция:

void process_single_video_frame(void* part_of_frame)
{
  // initialize variables
  ...
  intermediate_result_1 = function1(part_of_frame);
  intermediate_result_2 = function2(intermediate_result_1);
  intermediate_result_3 = function3(intermediate_result_2);
  store_results(intermediate_result_3);
}

и вы можете обрабатывать много part_of_frames в то же время. Скажем, несколько тысяч!--9-->

и function1(), function2() и function3() пройти через почти тот же код пути (то есть, поток программы не сильно зависит от содержания кадра),

тогда локальная память может сделать всю работу за вас. Локальная память-это тип памяти, которая хранится в глобальной памяти. Она отличается от глобальной памяти тонким, но глубоким образом... Память просто перемежается таким образом, что соседние потоки будут обращаться к соседним 32-битным словам, что позволяет полностью объединить доступ к памяти, если все потоки читаются из одного и того же местоположения их локальные массивы памяти.

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

какой-то псевдокод:

const int size_of_one_frame_part = 1000;

__global__ void my_kernel(int* all_parts_of_frames) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    int my_local_array[size_of_one_frame_part];
    memcpy(my_local_array, all_parts_of_frames + i * size_of_one_frame_part);
    int local_intermediate_1[100];
    function1(local_intermediate_1, my_local_array);
    ...
}

__device__ void function1(int* dst, int* src) {
   ...
}

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

Примечания:

  • первоначальная копия!--5--> от глобальной к локальной памяти не объединяется, но, надеюсь, у вас будет достаточно вычислений, чтобы скрыть это.

  • на устройствах вычислительных возможностей part_of_frame и другие промежуточные данные. Но при вычислительной способности >= 2.0 это расширилось до 512KiB, чего должно быть достаточно.


отвечая на некоторые из ваших вопросов:

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

в отношении распараллеливание:

Я думаю, что любая идея, которая позволяет запускать вычисления на несколько потоков данных-это хорошо. Чем больше ваш код напоминает шейдер, тем лучше (это означает, что он будет иметь необходимые характеристики для быстрого запуска на gpu). Идея с несколькими кадрами хороша. Некоторые подсказки об этом: минимизировать синхронизацию как можно больше, доступ к памяти как можно реже, попытаться увеличить отношение времени вычислений к времени запросов ввода-вывода, использовать регистры gpu / общую память, предпочитайте много-читать-от-одного к одному-пишет-ко-многим дизайнам.


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

однако, если ресурсов GPU недостаточно, размещение 3 функций в одном ядре может привести к снижению производительности. В этом случае, это лучше поместить каждую функцию в отдельное ядро.