Разве дивергенция отрасли действительно так плохо?

Я видел много вопросов, разбросанных по всему Интернету о разветвленности веток, и о том, как избежать этого. Однако даже после чтения десятков статей о том, как работает CUDA, , я не вижу, как избежать в большинстве случаев отклонения от ветки.. Прежде чем кто-нибудь скачет на меня с протянутыми когтями, позвольте мне описать, что я считаю "большинством дел".

Мне кажется, что большинство случаев расхождения ветвей связаны с рядом действительно различных блоков кода. Например, мы имеем следующий сценарий:

if (A):
  foo(A)
else:
  bar(B)

Если у нас есть два потока, которые сталкиваются с этим расхождением, поток 1 будет выполняться первым, пройдя путь A. После этого поток 2 примет путь B. Чтобы удалить расхождение, мы можем изменить вышеприведенный блок, чтобы читать как это

foo(A)
bar(B)

Предполагая, что безопасно вызывать foo(A) в потоке 2 и bar(B) в потоке 1, можно ожидать, что производительность улучшится. Однако здесь я вижу это:

В первом случае потоки 1 и 2 выполняются последовательно. Вызовите эти два тактовых цикла.

Во втором случае потоки 1 и 2 выполняют foo(A) параллельно, затем выполняйте bar(B) параллельно. Это по-прежнему выглядит как два такта, разница в том, что в первом случае, если foo(A) включает чтение из памяти, я думаю, что поток 2 может начать выполнение во время этой задержки, что приводит к скрытию скрытия. Если это так, расходящийся код ветки быстрее.

Ответ 1

Вы предполагаете (по крайней мере, это пример, который вы даете, и единственную ссылку, которую вы делаете), что единственный способ избежать разветвления ветвей - разрешить всем потокам выполнять весь код.

В этом случае я согласен, что не так много.

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

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

#define N 2 // number of pixel components
#define BLUE 0
#define GREEN 1
// pixel order: px0BL px0GR px1BL px1GR ...


if (threadIdx.x & 1)  foo(pixel(N*threadIdx.x+BLUE));
else                  bar(pixel(N*threadIdx.x+GREEN));

Это означает, что каждый альтернативный поток принимает заданный путь, будь то foo или bar. Итак, теперь мой warp занимает в два раза больше времени.

Однако, если я изменил свои пиксельные данные так, чтобы цветовые компоненты были смежными, возможно, в кусках 32 пикселя:      BL0 BL1 BL2... GR0 GR1 GR2...

Я могу написать аналогичный код:

if (threadIdx.x & 32)  foo(pixel(threadIdx.x));
else                   bar(pixel(threadIdx.x));

По-прежнему кажется, что у меня есть возможность расхождения. Но так как расхождение происходит на границах деформации, то деформация дает либо путь if, либо путь else, поэтому фактическая дивергенция не возникает.

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