# Reduce (parallel pattern)

Reduce is a collective communication primitive used in the context of a parallel programming model to combine multiple vectors into one, using an associative binary operator ${\displaystyle \oplus }$. Every vector is present at a distinct processor in the beginning. The goal of the primitive is to apply the operator in the order given by the processor-indices to the vectors until only one is left. The reduction of sets of elements is an integral part of programming models such as Map Reduce, where a function is applied (mapped) to all elements before they are reduced. Other parallel algorithms use reduce as a primary operation to solve more complex problems. The Message Passing Interface implements it in the operations MPI_Reduce and MPI_Allreduce, with the difference that the result is available at one (root) processing unit or all of them. Closely related to reduce is the broadcast operation, which distributes data to all processors. Many reduce algorithms can be used for broadcasting by reverting them and omitting the operator.

## Definition

Formally, reduce takes an associative (but not necessarily commutative) operator ${\displaystyle \oplus }$, which can be evaluated in constant time and an input set ${\displaystyle V=\{v_{0}={\begin{pmatrix}e_{0}^{0}\\\vdots \\e_{0}^{m-1}\end{pmatrix}},v_{1}={\begin{pmatrix}e_{1}^{0}\\\vdots \\e_{1}^{m-1}\end{pmatrix}},\dots ,v_{p-1}={\begin{pmatrix}e_{p-1}^{0}\\\vdots \\e_{p-1}^{m-1}\end{pmatrix}}\}}$of ${\displaystyle p}$ vectors with ${\displaystyle m}$ elements each. The total size of a vector is defined as ${\displaystyle n}$. The result ${\displaystyle r}$ of the operation is the combination of the elements ${\displaystyle r={\begin{pmatrix}e_{0}^{0}\oplus e_{1}^{0}\oplus \dots \oplus e_{p-1}^{0}\\\vdots \\e_{0}^{m-1}\oplus e_{1}^{m-1}\oplus \dots \oplus e_{p-1}^{m-1}\end{pmatrix}}={\begin{pmatrix}\bigoplus _{i=0}^{p-1}e_{i}^{0}\\\vdots \\\bigoplus _{i=0}^{p-1}e_{i}^{m-1}\end{pmatrix}}}$ and has to be stored at a specified root processor at the end of the execution. For example, the result of a reduction on the set ${\displaystyle \{3,5,7,9\}}$, where all vectors have size one is ${\displaystyle 3+5+7+9=24}$. If the result ${\displaystyle r}$ has to be available at every processor after the computation has finished, it is often called Allreduce. An optimal sequential linear-time algorithm for reduction can apply the operator successively from front to back, always replacing two vectors with the result of the operation applied to all its elements, thus creating an instance that has one vector less. It needs ${\displaystyle (p-1)\cdot m}$ steps until only ${\displaystyle r}$ is left. Sequential algorithms can not perform better than linear time, but parallel algorithms leave some space left to optimize.

## Binomial tree algorithms

Regarding parallel algorithms, there are two main models of parallel computation, the parallel random access machine as an extension of the RAM with shared memory between processing units and the bulk synchronous parallel computer which takes communication and synchronization into account. Both models have different implications for the time-complexity, therefore two algorithms will be shown.

### PRAM-algorithm

This algorithm represents a widely spread method to handle inputs where ${\displaystyle p}$ is a power of two. The reverse procedure is often used for broadcasting elements.[1][2][3]

Visualization of the algorithm executed on eight elements using addition as the operator
for ${\displaystyle k\gets 0}$ to ${\displaystyle \lceil \log _{2}p\rceil -1}$ do
for ${\displaystyle i\gets 0}$ to ${\displaystyle p-1}$ do in parallel
if ${\displaystyle p_{i}}$ is active then
if bit ${\displaystyle k}$ of ${\displaystyle i}$ is set then
set ${\displaystyle p_{i}}$ to inactive
else if ${\displaystyle i+2^{k}
${\displaystyle x_{i}\gets x_{i}\oplus ^{\star }x_{i+2^{k}}}$

The binary operator for vectors is defined such that ${\displaystyle {\begin{pmatrix}e_{i}^{0}\\\vdots \\e_{i}^{m-1}\end{pmatrix}}\oplus ^{\star }{\begin{pmatrix}e_{j}^{0}\\\vdots \\e_{j}^{m-1}\end{pmatrix}}={\begin{pmatrix}e_{i}^{0}\oplus e_{j}^{0}\\\vdots \\e_{i}^{m-1}\oplus e_{j}^{m-1}\end{pmatrix}}}$. The algorithm further assumes that in the beginning ${\displaystyle x_{i}=v_{i}}$ for all ${\displaystyle i}$ and ${\displaystyle p}$ is a power of two and uses the processing units ${\displaystyle p_{0},p_{1},\dots p_{n-1}}$. In every iteration, half of the processing units become inactive and do not contribute to further computations. The figure shows a visualization of the algorithm using addition as the operator. Vertical lines represent the processing units where the computation of the elements on that line take place. The eight input elements are located on the bottom and every animation step corresponds to one parallel step in the execution of the algorithm. An active processor ${\displaystyle p_{i}}$ evaluates the given operator on the element ${\displaystyle x_{i}}$ it is currently holding and ${\displaystyle x_{j}}$ where ${\displaystyle j}$ is the minimal index fulfilling ${\displaystyle j>i}$, so that ${\displaystyle p_{j}}$ is becoming an inactive processor in the current step. ${\displaystyle x_{i}}$ and ${\displaystyle x_{j}}$ are not necessarily elements of the input set ${\displaystyle X}$ as the fields are overwritten and reused for previously evaluated expressions. To coordinate the roles of the processing units in each step without causing additional communication between them, the fact that the processing units are indexed with numbers from ${\displaystyle 0}$ to ${\displaystyle p-1}$ is used. Each processor looks at its ${\displaystyle k}$-th least significant bit and decides whether to get inactive or compute the operator on its own element and the element with the index where the ${\displaystyle k}$-th bit is not set. The underlying communication pattern of the algorithm is a binomial tree, hence the name of the algorithm.

Only ${\displaystyle p_{0}}$ holds the result in the end, therefore it is the root processor. For an Allreduce-operation the result has to be distributed, which can be done by appending a broadcast from ${\displaystyle p_{0}}$. Furthermore, the number ${\displaystyle p}$ of processors is restricted to be a power of two. This can be lifted by padding the number of processors to the next power of two. There are also algorithms that are more tailored for this use-case.[4]

#### Runtime analysis

The main loop is executed ${\displaystyle \lceil \log _{2}p\rceil }$ times, the time needed for the part done in parallel is in ${\displaystyle {\mathcal {O}}(m)}$ as a processing unit either combines two vectors or becomes inactive. Thus the parallel time ${\displaystyle T(p,m)}$ for the PRAM is ${\displaystyle T(p,m)={\mathcal {O}}(log(p)\cdot m)}$. The strategy for handling read and write conflicts can be chosen as restrictive as an exclusive read and exclusive write (EREW). The efficiency ${\displaystyle S(p,m)}$ of the algorithm is ${\displaystyle S(p,m)\in {\mathcal {O}}({\frac {T_{seq}}{T(p,m)}})={\mathcal {O}}({\frac {p}{log(p)}})}$ and therefore the efficiency is ${\displaystyle E(p,m)\in {\mathcal {O}}({\frac {S(p,m)}{p}})={\mathcal {O}}({\frac {1}{log(p)}})}$. The efficiency suffers because of the fact that half of the active processing units become inactive after each step, so ${\displaystyle {\frac {p}{2^{i}}}}$ units are active in step ${\displaystyle i}$.

### Distributed memory algorithm

In contrast to the PRAM-algorithm, in the distributed memory model memory is not shared between processing units and data has to be exchanged explicitly between units, resulting in communication overhead that is accounted for. The following algorithm takes this into consideration.

for ${\displaystyle k\gets 0}$ to ${\displaystyle \lceil \log _{2}p\rceil -1}$ do
for ${\displaystyle i\gets 0}$ to ${\displaystyle p-1}$ do in parallel
if ${\displaystyle p_{i}}$ is active then
if bit ${\displaystyle k}$ of ${\displaystyle i}$ is set then
send ${\displaystyle x_{i}}$ to ${\displaystyle p_{i-2^{k}}}$
set ${\displaystyle p_{k}}$ to inactive
else if ${\displaystyle i+2^{k}
receive ${\displaystyle x_{i+2^{k}}}$
${\displaystyle x_{i}\gets x_{i}\oplus ^{\star }x_{i+2^{k}}}$

The only difference between the distributed algorithm and the PRAM version is the inclusion of explicit communication primitives, the operating principle stays the same.

#### Runtime analysis

A simple analysis for the algorithm uses the BSP-model and incorporates the time ${\displaystyle T_{start}}$ needed to initiate communication and ${\displaystyle T_{byte}}$ the time needed to send a byte. Then the resulting runtime is ${\displaystyle \Theta ((T_{start}+n\cdot T_{byte})\cdot log(p))}$, as ${\displaystyle m}$ elements of a vector are send in each iteration and have size ${\displaystyle n}$ in total.

## Pipeline-algorithm

Depiction of the pipeline-algorithm using addition as the operator on vectors of size four.

For distributed memory models, it can make sense to use pipelined communication. This is especially the case when ${\displaystyle T_{start}}$ is small in comparison to ${\displaystyle T_{byte}}$. Usually, linear pipelines split data or a task into smaller pieces and process them in stages. In contrast to the binomial tree algorithms, the pipelined algorithm uses the fact that the vectors are not inseparable, but the operator can be evaluated for single elements:[5]

for ${\displaystyle k\gets 0}$ to ${\displaystyle p+m-3}$ do
for ${\displaystyle i\gets 0}$ to ${\displaystyle p-1}$ do in parallel
if ${\displaystyle i\leq k
send ${\displaystyle x_{i}^{k-i}}$ to ${\displaystyle p_{i+1}}$
if ${\displaystyle i-1\leq k
receive ${\displaystyle x_{i-1}^{k+i-1}}$ from ${\displaystyle p_{i-1}}$
${\displaystyle x_{i}^{k+i-1}\gets x_{i}^{k+i-1}\oplus x_{i-1}^{k+i-1}}$

It is important to note that the send and receive operations have to be executed concurrently for the algorithm to work. The result vector is stored at ${\displaystyle p_{p-1}}$ at the end. The associated animation shows an execution of the algorithm on vectors of size four with five processing units. Two steps of the animation visualize one parallel execution step. The number of steps in the parallel execution are ${\displaystyle p+m-2}$, it takes ${\displaystyle p-1}$ steps until the last processing unit receives its first element and additional ${\displaystyle m-1}$ until all elements are received. Therefore, the runtime in the BSP-model is ${\displaystyle T(n,p,m)=(T_{start}+{\frac {n}{m}}T_{byte})(p+m-2)}$, assuming that ${\displaystyle n}$ is the total byte-size of a vector.

Although ${\displaystyle m}$ has a fixed value, it is possible to logically group elements of a vector together and reduce ${\displaystyle m}$. For example, a problem instance with vectors of size four can be handled by splitting the vectors into the first two and last two elements, which are always transmitted and computed together. In this case, double the volume is send each step, but the number of steps has roughly halved. It means that the parameter ${\displaystyle m}$ is halfed, while the total byte-size ${\displaystyle n}$ stays the same. The runtime ${\displaystyle T(p)}$ for this approach depends on the value of ${\displaystyle m}$, which can be optimized if ${\displaystyle T_{start}}$ and ${\displaystyle T_{byte}}$ are known. It is optimal for ${\displaystyle m={\sqrt {\frac {n\cdot (p-2)T_{byte}}{T_{start}}}}}$, assuming that this results in a smaller ${\displaystyle m}$ that divides the original one.

## Pipelined tree

The binomial tree and the pipeline both have their advantages and disadvantages, depending on the values of ${\displaystyle T_{start}}$ and ${\displaystyle T_{byte}}$ for the parallel communication. While the binomial tree algorithm is better suited for small vectors, the pipelined algorithm profits from a distribution of the elements to fewer processing units with more elements contained in one vector. Both approaches can be combined into one algorithm[6] which uses a tree as its underlying communication pattern and splits the computation of the operator into pieces at the same time. Instead of the binomial tree, a Fibonacci tree is used which has the property that the height of the trees rooted at its two children differ by one. It helps to balance the load on all processing units as each unit can only evaluate one operator in one iteration on one of its elements, but it has two child-processors it receives values from.

### Algorithm description

The animation shows the execution of such an algorithm in a full-duplex communication model. Communication links are represented by black lines between the vectors of elements and build a Fibonacci tree of size seven in this example. If an element is send to another processing unit the link is colored with the color of the corresponding element. An element that is received by a processor is added to the already existing element of same color (at the same index in the vector).

The algorithm itself propagates the partial sums from bottom to top until all elements are contained in the sum at the root processor on top. In the first step of the execution, the processing units which are leafs in the underlying tree send their first elements to their parent. This is similar to the send operations of the binomial tree algorithm with the key difference that the leaf units each have two more elements which have to be send and therefore do not become inactive, but can continue to send elements, which is analogous to the pipelined approach and improves efficiency. Processing units that are not leafs start to send their elements in order of the indices in the vector once they have received an element from a child. In the example they send green, blue and red elements in this order. If two processors compete to send their elements to the same processor, then the element of the right child is received first. Because of the structure of the Fibonacci tree all processors send or receive elements while the "pipeline" is filled. The pipeline is filled from the point where each unit has received an element and until the leaf units have no more elements to send.

### Runtime

Each iteration of the algorithm takes at most time ${\displaystyle {\frac {n}{m}}\cdot T_{byte}+T_{start}}$. The height of the tree factors into the time it needs to fill the pipeline and for Fibonacci trees it is known to be about ${\displaystyle h=log_{\phi }p}$ where ${\displaystyle \phi ={\frac {1+{\sqrt {5}}}{2}}}$ is the golden ratio. Once the pipeline is filled, all processors are active in each step. Because inner nodes have two children, they have to receive ${\displaystyle 2\cdot m}$ elements. Therefore, the runtime of the algorithm is ${\displaystyle T(n,p,m)\approx ({\frac {n}{m}}\cdot T_{byte}+T_{start})(h+2\cdot k-2)}$. It is minimal if the number of elements in a vector is chosen such that ${\displaystyle m={\sqrt {\frac {n\cdot (h-3)T_{byte}}{3\cdot T_{start}}}}}$.

## Applications

Reduction is one of the main collective operations implemented in the Message Passing Interface, where performance of the used algorithm is important and evaluated constantly for different use cases.[7]

MapReduce relies heavily on efficient reduction algorithms to process big data sets, even on huge clusters.[8][9]

Some parallel sorting algorithms use reductions to be able to handle very big data sets.[10]