Готов поспорить, вы можете легко ускорить свою программу в 10 раз, приняв CUDA. Но это 10x - это далеко не конец истории. Полностью оптимизированный код CUDA может дать вам 100-кратное ускорение. Чтобы написать высокооптимизированные ядра CUDA, нужно хорошо понимать некоторые концепции графического процессора. Однако я обнаружил, что некоторые концепции плохо объясняются в Интернете и могут легко запутать людей.

Эти концепции сбивают с толку, потому что

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

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

Графический процессор состоит из нескольких модулей, называемых SM (Streaming Multiprocessors). В качестве конкретного примера, GPU Titan V имеет 80 СМ. Каждый SM может одновременно выполнять множество потоков. В случае Titan V максимальное количество одновременных потоков для singleTitan V SM составляет 2048. Но эти потоки не совсем такие же, как потоки, выполняемые ЦП.

Эти темы сгруппированы. А группа нитей называется основой, которая содержит 32 потока. Таким образом, Titan V SM может выполнять 2048 потоков, но эти 2048 потоков сгруппированы в 2048/32 = 64 деформации.

Эти потоки отличаются от потока ЦП тем, что каждый из потоков ЦП может одновременно выполнять разные задачи. Но потоки графического процессора в одной деформации могут выполнять только одну и ту же задачу. Например, если вы хотите выполнить 2 операции, c = a + b и d = a * b. И вам нужно выполнить эти 2 вычисления на большом количестве данных. Вы не можете назначить одну деформацию для одновременного выполнения обоих расчетов. Все 32 потока должны работать над одним и тем же вычислением, прежде чем переходить к следующему вычислению, хотя данные, обрабатываемые потоками, могут быть разными, например, 1 + 2 и 45 + 17, все они должны работать над вычислением сложения, прежде чем переходить к следующему вычислению. умножение.

Это отличается от потоков ЦП, потому что, если у вас достаточно мощный ЦП для одновременной поддержки 32 потоков, каждый из них может работать с отдельным вычислением. Таким образом, концепция потока GPU сродни функции SIMD (Single Instruction, Multiple Data) ЦП.

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

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

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

С точки зрения программного обеспечения потоки графического процессора организованы в блоки. Блок - это чисто программная концепция, которой нет в конструкции оборудования. В отличие от организации физических потоков, warp. У блоков нет фиксированного количества потоков. Вы можете указать любое количество потоков до 1024 в блоке, но это не означает, что любой номер потока будет работать одинаково.

Допустим, мы хотим выполнить 330 вычислений. Один из естественных способов - запустить 10 блоков, и каждый блок работает над 33 вычислениями с 33 потоками. Но ведь каждые 32 потока сгруппированы в деформацию. Для завершения 33 расчетов задействованы 2 основы == 64 потока. Всего мы будем использовать 640 потоков.

Другой способ - запуск 11 блоков по 32 потока. На этот раз каждый блок может уместиться в единую основу. Итак, всего будет запущено 11 warps == 352 потока. Будут какие-то потери, но их будет меньше, чем в первом варианте.

Еще одна вещь, которую необходимо учитывать, - это количество SM, потому что каждый блок может быть выполнен только в пределах одного SM. Блок не может обрабатываться более чем одним SM. Если рабочая нагрузка очень велика, то есть у нас много блоков, мы могли бы использовать все доступные SM, и у нас еще есть работа, которую нужно сделать. В этом случае нам нужно будет запустить часть работы в качестве первого пакета, а затем завершить оставшуюся работу в следующих пакетах. Например, в случае с Titan V имеется 80 SM. И предположим, что у нас есть сложная работа, для завершения которой требуется 90 СМ. Сначала нам нужно будет запустить партию из 80 SM, а оставшуюся работу запустить с 10 SM в качестве второй партии. Но в этом случае при второй партии простаивают 70 СМ. Лучше всего настроить рабочую нагрузку для каждого SM, чтобы каждый из них мог выполнять меньше работы и быстрее закончить. Но всего на этот раз нужно 160 СМ. Хотя вам все равно нужно запустить 2 пакета вычислений, но поскольку каждый пакет может завершиться быстрее, общее время выполнения сокращается.

Наконец, если вы знакомы с маркетинговыми терминами NVIDIA, мощность графического процессора часто измеряется количеством ядер CUDA. Но когда вы изучаете программирование на CUDA, вы, вероятно, редко воспринимаете его как концепцию программирования. Что ж, ядро ​​CUDA на самом деле является искажением. Итак, опять же, в случае с Titan V, он имеет 80 (SM) * (2048) потоков / 32 (Threads / Warp) = 5120 ядер CUDA.