Параллельная редукция: дерево сложения за O(log n)

Урок подробно разбирает, почему параллельная редукция занимает всего log n шагов.

Дерево редукции — схема, где на каждом шаге пары соседних элементов складываются одновременно, вдвое сокращая массив, пока не останется одно значение.

Идея дерева

Последовательная сумма n чисел — это n-1 сложений строго по очереди. Но сложения независимы попарно! На первом шаге можно одновременно сложить (a0+a1), (a2+a3), (a4+a5), (a6+a7) — четыре сложения сразу, массив сократился вдвое. На втором шаге — два сложения, на третьем — одно. Для 8 элементов это 3 шага вместо 7. В общем случае — log₂(n) шагов. Это и есть параллельная редукция: глубина дерева равна логарифму.

шаг 0:  3   1   7   0   4   1   6   3
          \ /     \ /     \ /     \ /
шаг 1:    4       7       5       9
            \     /         \     /
шаг 2:      11             14
                \         /
шаг 3:           25

Пошаговая эмуляция

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

import math

def reduce_tree(a):
    a = a[:]
    step = 0
    while len(a) > 1:
        step += 1
        b = []
        for i in range(0, len(a), 2):
            if i + 1 < len(a):
                b.append(a[i] + a[i+1])   # пара складывается (параллельно на ядрах)
            else:
                b.append(a[i])            # нечётный хвост проходит дальше
        print(f"шаг {step}: {b}")
        a = b
    return a[0]

data = [3, 1, 7, 0, 4, 1, 6, 3]
print("массив:", data)
print("сумма =", reduce_tree(data))
print("шагов = log2(8) =", int(math.log2(len(data))))

Вывод:

массив: [3, 1, 7, 0, 4, 1, 6, 3]
шаг 1: [4, 7, 5, 9]
шаг 2: [11, 14]
шаг 3: [25]
сумма = 25
шагов = log2(8) = 3

Работа и глубина

Заметьте: общее число сложений всё равно n-1 (на 8 элементах — 7: четыре + два + одно). Это работа (work) — она не уменьшилась. Уменьшилась глубина (depth/span) — длина самой длинной цепочки зависимых операций: с n-1 до log n. Параллелизм возникает именно из малой глубины: за log n шагов мы заканчиваем, если есть достаточно ядер. Об анализе work и span — отдельный урок в конце курса.

Как работает под капотом

На GPU редукция — каноничный приём. Тысячи потоков складывают пары в разделяемой памяти блока, синхронизируясь после каждого шага дерева. Хитрость — избежать простоя: после первого шага половина потоков становится не нужна, после второго — три четверти. Поэтому продвинутые реализации заставляют каждый поток сначала сложить несколько элементов (повышая работу на поток), и лишь потом включают дерево — это уменьшает число шагов с синхронизацией.

Различие между работой и глубиной, которое так наглядно проявляется в дереве редукции, — это, пожалуй, главная мысль всего курса. Последовательный мир знает только одну меру — число операций. Параллельный мир добавляет вторую — длину цепочки зависимостей, которую нельзя обойти никаким числом процессоров. Дерево редукции делает ровно столько же сложений, сколько последовательная сумма, но выстраивает их так, что независимые сложения оказываются на одном уровне и идут разом. Перестроить вычисление, сократив глубину при той же работе, — это и есть суть проектирования параллельного алгоритма, и редукция — самый чистый её пример.

Частые ошибки

  • Забыть про нечётный хвост: если элементов не степень двойки, последний непарный элемент нужно аккуратно протащить дальше.
  • Думать, что дерево уменьшает число операций — оно уменьшает глубину, а работа та же n-1.
  • Синхронизировать после каждого крошечного шага на GPU, не укрупнив работу на поток — лишний overhead.

Итоги

  • Дерево редукции сворачивает n элементов за log n параллельных шагов вместо n-1.
  • Работа остаётся n-1, уменьшается глубина (длина цепочки зависимостей).
  • Параллелизм рождается из малой глубины; редукция — базовый приём на GPU.
Проверьте себя
1. Сколько параллельных шагов нужно дереву редукции для n элементов?
An-1
Blog2(n)
Cn
Dsqrt(n)
2. Что уменьшает дерево редукции по сравнению с последовательной суммой?
AЧисло операций (работу)
BГлубину — длину цепочки зависимых операций
CОбъём данных
DТочность результата
3. Что делать с нечётным хвостом, когда элементов не степень двойки?
AОтбросить его
BПротащить непарный элемент дальше без сложения
CУдвоить его
DПрограмма не сработает