2013-10-03 2 views
9

У меня есть следующая проблема: мне нужно вычислить включенные проверки (например, prefix sums) значений на основе древовидной структуры на графическом процессоре. Эти проверки выполняются либо из корневого узла (сверху вниз), либо из листовых узлов (снизу вверх). Случай простой цепи - easily handled, но древовидная структура делает распараллеливание довольно сложным для эффективной реализации.Включенное сканирование на основе графического процессора на неуравновешенном дереве

Tree example

Например, после того, как сверху вниз включительно сканирования, (12) проведет (0)[op](6)[op](7)[op](8)[op](11)[op](12), и снизу вверх включительно сканирования, (8) проведет (8)[op](9)[op](10)[op](11)[op](12), где [op] является данный бинарный оператор (матрица сложения, умножение и т. д.).

Кроме того, нужно учитывать следующие моменты:

  • Для типичного сценария, длина различных ветвей не должна быть слишком длинной (~ 10), с чем-то вроде 5 до 10 ветвей, так что это то, что будет выполняться внутри блока, и работа будет разделена между потоками. Различные блоки будут просто обрабатывать разные значения узлов. Это, очевидно, не является оптимальным в отношении занятости, но это ограничение проблемы, которая будет решена через некоторое время. На данный момент я буду полагаться на Instruction-level parallelism.
  • Структура диаграммы не может быть изменена (она описывает реальную систему), поэтому она не может быть сбалансирована (или только путем изменения корня дерева, например, с использованием (6) в качестве нового корня). Тем не менее, типичное дерево не должно быть слишком неуравновешенным.
  • В настоящее время я использую CUDA для GPGPU, поэтому я открыт для любой библиотеки шаблонов с поддержкой CUDA, которая может решить эту проблему.
  • Узел данных уже находится в глобальной памяти, и результат будет использоваться другими ядрами CUDA, поэтому цель заключается в достижении этого, не делая этого огромным узким местом.
  • Не существует «цикла», то есть ветви не могут сливаться с деревом.
  • Структура дерева фиксирована и задана в фазе инициализации.
  • Одна бинарная операция может быть довольно высокой (например, умножение полиномиальных матриц, т.е. каждый элемент является многочленом заданного порядка).

В этом случае, какая была бы «лучшая» структура данных (для древовидной структуры) и лучшие алгоритмы (для включенных сканирований/префиксов) для решения этой проблемы?

+4

Высокий вопрос. Нам нужно больше этого типа вопроса и меньше основных вопросов по исправлению ошибок, которые мы получаем все время. –

+0

Размер проблемы настолько мал (10 ветвей x 10 заметок/ветвь). Я предполагаю, что даже если вы используете только 1 поток для вычисления всех префикс-сумм для всех узлов, затраты времени можно игнорировать по сравнению с другими частями кода, который выполняется на графическом процессоре. – kangshiyin

+0

@ Эрик: дело в том, что каждый узел фактически обрабатывает довольно много вычислений (например, полиномиальные матрицы, которые нужно умножать), поэтому последовательный алгоритм становится узким местом. Я пробовал это с помощью простых цепей, и более разумный логарифмический подход обеспечивал нестандартное ускорение. – BenC

ответ

3

Наверное, загадочная идея, но представьте, что вы вставляете узлы значения 0 в дерево, чтобы получить двумерную матрицу. Например, в вашем примере будет 3 узла нулевого значения ниже узла . Затем используйте один поток для перемещения каждого уровня матрицы по горизонтали. Для суммы префикса сверху вниз смещайте потоки таким образом, чтобы каждый нижний поток задерживался на максимальное количество ветвей, которые дерево может иметь в этом месте. Итак, вы получаете «волну» с наклонным краем, проходящим по матрице. Верхние потоки, находящиеся далее, вычисляют эти узлы во времени, чтобы они могли обрабатываться далее потоками, идущими дальше вниз. Вам понадобится то же количество потоков, что и дерево.

+0

Это напоминает мне то, что я делал при вычислении градиента частичного произведения. Я закончил создание параллельного вычислительного плана на CPU, который хранит соответствующие индексы массивов, используемые для создания временного массива, который содержит все возможные частичные продукты. Затем на каждом этапе вычисления графического процессора различное количество потоков будет вычислять значения на скользящих диагоналях на основе этих индексов для заполнения массива (стиль волнового фронта). Я был обеспокоен объединением памяти, но в этом сценарии это, похоже, не имело большого значения. Во всяком случае, я постараюсь оценить ваше предложение как можно скорее. – BenC

3

Я думаю, что параллельно Приставка сканирование может не подходит для вашего случая, потому что:

алгоритм сканирования параллельно Приставка увеличит общее количество [op] в вашем звене prefix sum, 16 входов параллельно Приставка сканирование требует 26 [op], в то время как для последовательной версии требуется только 15. Оптимальный параллельный алгоритм основан на предположении, что существует достаточно аппаратных ресурсов для параллельного запуска нескольких [op].

Вы можете оценить стоимость вашего [op] перед тем, как попробовать параллельное префиксное сканирование.

С другой стороны, поскольку размер дерева невелик, я думаю, вы могли бы просто рассмотреть свое дерево как 4 (количество листовых узлов) независимых простых цепей и использовать одновременное выполнение ядра для повышения производительности этих 4 префиксов ядро ​​сканирования

0-1-2-3 
0-4-5 
0-6-7-8-9-10 
0-6-7-8-11-12 
+0

Что касается вашего последнего комментария о параллельном выполнении ядра, это действительно то, что я рассмотрел для проблемы «root to leaves». Однако проблема «уходит в корень» потребует использования атомных операций, так как некоторые ветви будут одновременно изменять одинаковые узлы, если они имеют одинаковую длину. Количество филиалов должно быть довольно низким, поэтому это может сильно не повлиять на производительность. – BenC

+0

Для восходящего пути вы имеете в виду, что вам нужно хранить 2 суммы для узлов как '(8)'? один - 10-9-8, другой - 12-11-8? Если бы это было правдой, я бы предложил операцию на месте. – kangshiyin

+0

Нет, только один (как объяснялось в моем вопросе). Это сумма всех нижних узлов в данной ветви. – BenC

0

Я думаю, что в архитектуре Kepler GK110, вы можете вызвать ядра рекурсивно, которые они называют динамический параллелизм. Итак, если вам нужно вычислить сумму значений в каждом узле дерева, то поможет динамический параллелизм. Однако глубина рекурсии может быть ограничением.

+0

Я еще не пробовал динамический параллелизм, так как у меня есть только устройство CC 3.0. Я не знаю, как это отреагирует на ограниченную рекурсию. – BenC

0

Мое первое впечатление заключается в том, что вы могли бы организовать узлы дерева в одномерном массиве, аналогично тому, что предложил Эрик. А затем вы можете выполнить Segmented Prefix Sum Scan (http://en.wikipedia.org/wiki/Segmented_scan) над этим массивом.

Использование дерева узлов в качестве примера, 1-тусклый массив будет выглядеть следующим образом:

0-1-2-3-0-4-5-0-6-7-8-9-10-0-6-7-8-11-12 

И тогда вы бы параллельный массив флагов, указывающий, где начинается новый список (по списку, я имею в виду последовательность, начиная с корня и заканчивая листовым узлом):

1-0-0-0-1-0-0-1-0-0-0-0- 0-1-0-0-0- 0- 0 

Для восходящем случае, вы можете создать отдельный массив сегмента флаг следующим образом:

0-0-0-1-0-0-1-0-0-0-0-0- 1-0-0-0-0- 0- 1 

и повернуть его в обратном порядке, используя тот же алгоритм.

А как реализовать сегментированный Prefix Scan, я не выполнил один я, но я нашел несколько ссылок, которые могут быть информативными о том, как это сделать: http://www.cs.cmu.edu/afs/cs/academic/class/15418-s12/www/lectures/24_algorithms.pdf и http://www.cs.ucdavis.edu/research/tech-reports/2011/CSE-2011-4.pdf (стр.23)

+0

Действительно, можно предположить, что сгруппированная сумма префикса может работать. Существует также один из реализованных в [Thrust] (http://thrust.github.io/doc/group__segmentedprefixsums.html). Это просто означает избыточное вычисление и дополнительное запоминающее устройство (например, часть «0-6-7-8», которая вычисляется дважды для двух младших ветвей). – BenC

+0

Да, я заметил повторяющиеся сегменты. Возможно, можно было бы разработать систему для устранения дубликатов. Например, перед тем, как передать массив сегментированному сканированию, сначала выполните поиск повторяющихся сегментов и вытащите повторяющиеся разделы и замените их каким-то индикатором, который позволит вам восстановить полный список в конце. В некотором смысле, это будет похоже на сжатие входного списка, а затем его распаковку в конце. – MorbidFuzzball

+0

Кроме того, это не обязательно решает обратную проблему, когда несколько входов (выходящих из листьев) влияют на один «сливающий» узел, что приводит к возможным опасностям WAW. – BenC