Overview

5 The Reduction pattern

Reduction is the parallel pattern for turning many values into one result, such as a sum, maximum, minimum, product, or count. The central requirement is that the combining operation be associative, so the values can be regrouped without changing the answer; an identity element is also needed so extra threads can safely contribute “nothing” when there are more threads than data. This idea shows up everywhere, from spreadsheet-style totals and database aggregates to GPU tensor operations and large-scale frameworks for distributed data processing.

The chapter first shows why a straightforward parallel port fails. A single running total creates a loop-carried dependency in the sequential version, and making every thread update one shared variable leads to race conditions because read-modify-write is not atomic. Although atomic operations fix correctness, using one atomic update per thread serializes the work and largely destroys the benefit of parallelism. The better answer is to reduce in stages: each block computes a local subtotal, and then those partial results are merged safely.

The efficient solution is a tree-based reduction implemented inside a block using shared memory and synchronization barriers. Threads first load values from global memory into shared memory, padding out-of-range positions with the operation’s identity, then repeatedly combine pairs while the stride shrinks by half each round until one value remains in sdata[0]. Finally, one thread per block uses an atomic operation to add the block’s subtotal to the global result. The chapter closes by showing how the same structure adapts to max and min, warning about floating-point rounding differences, and highlighting common mistakes such as missing barriers, forgetting initialization, using non-atomic updates, or mismatching shared-memory size and block size.

A knockout tournament halves the number of teams every round until one champion remains; tree reduction does the same with numbers, halving the active elements each round (stride 4 → 2 → 1) until a single sum is left. Eight numbers collapse to one in just three rounds, instead of the seven steps a sequential sum would take.
The GPU memory hierarchy. Registers are the fastest; each thread has its own dedicated registers. All the threads in one block can read and write to the same shared memory. Global memory is reachable by every thread across all blocks, but it lives off-chip, which makes it by far the slowest.
The tree reduction kernel in three acts: load, reduce, write back. Act 1 copies global memory into shared memory so the fast reduction can happen on-chip. Act 2 runs three rounds of stride-based addition (stride 4, 2, then 1), collapsing eight values down to one. Act 3 uses atomicAdd to safely write the block's result (36) back to global memory.
Act 1 (Load): staging data in shared memory for fast access. Each thread reads one value from slow global memory into its private register, then writes it to shared memory, a two-step relay that happens in parallel across all eight threads simultaneously. Once sdata is populated, the reduction can run entirely on-chip without touching global memory again.
Act 2 (Reduce): three rounds of pairwise addition in shared memory. Each round, active threads read two values from sdata into registers, add them, and write the result back, halving the number of active threads each time (4 → 2 → 1). By the end of Round 3, the block's total sum (36) sits in sdata[0], ready to be written back to global memory.
Act 3 (Write back): one thread per block delivers the result. Thread 0 reads the block's sum from sdata[0] into a register, while T1–T7 sit idle. It then uses atomicAdd to safely add that value to the global result, necessary because multiple blocks may be writing to the same location simultaneously.
atomicAdd guarantees the right answer, regardless of which block goes first. Block 0 and Block 1 both race to update the same result in global memory with their partial sums (36 and 24). Whether Block 0 arrives first (0 → 36 → 60) or Block 1 does (0 → 24 → 60), the final sum lands on 60; atomicAdd serializes the updates so no contribution is ever lost.
Why atomicAdd is safe: read-modify-write as one indivisible step. When two blocks target the same address, the hardware serializes their requests: while one block's three steps (read → modify → write) run, the others wait, and the next one begins by reading the updated value, so no update is lost. When blocks target different addresses, their read-modify-writes run fully in parallel. The order in which same-address requests are served is unpredictable, but the final sum is always correct.
End-to-end reduction of 16M floats on a Tesla T4 (transfers included). CPU 52 ms; GPU-atomic 47.5 ms (1.10× over CPU); GPU-tree 14.8 ms (3.52×). The basis is end-to-end on purpose: the atomic bar sitting almost level with the CPU bar is the point. A direct GPU port of the sequential loop barely beats the CPU. Measured on a Google Colab Tesla T4 with CUDA 12.8 (nvcc -O3), 2²⁴ single-precision values, 256 threads per block; the text explains what each timing covers. The numbers compare the three approaches on one machine, not the speedup to expect elsewhere.

Summary

  • The Reduction pattern combines many values into one using an associative operation.
  • Naive parallel reduction creates race conditions and gives unpredictable, wrong results.
  • Tree-based reduction takes O(log n) depth (the number of rounds that must run one after another) instead of O(n), enabling large speedups when enough threads are available.
  • Shared memory is a fast, per-block scratch space for thread communication.
  • Padding out-of-bounds threads with the identity element keeps partial blocks correct, and the halving stride needs a power-of-2 block size.
  • __syncthreads() is a barrier that synchronizes all threads in a block; skipping one is the most common reduction bug.
  • Atomic operations prevent race conditions when combining results across blocks.
  • Structure matters more than hardware: on 16M floats the atomic-per-element version only ties the CPU, while the tree reduction runs 3.52× faster.
  • The same tree code works for other associative operations: swap the operator and its identity element to get max, min, product, or count, provided the type and the final atomic (or a second pass) support that operation.
  • NumPy’s sum(), PyTorch’s max(), and SQL’s COUNT()are all examples of the Reduction pattern at work.
  • The same idea shows up across the parallel family: MPI_Allreduce combines partial sums across a cluster, and OpenMP’s reduction clause does it on a multicore CPU.

FAQ

What is the reduction pattern in parallel programming?Reduction is a pattern that combines many input values into a single output value, such as a sum, maximum, minimum, product, or count. It works by repeatedly merging partial results until only one value remains.
Why can’t a reduction always be parallelized directly?Because a naive parallel reduction often causes race conditions when multiple threads try to update the same result at the same time. The operation may be correct mathematically, but the shared update must be coordinated to avoid lost writes.
What property must an operation have to be used in a reduction?The operation must be associative, meaning the way values are grouped does not change the result. Examples include addition, max, min, and multiplication. Subtraction is not associative, so it is not suitable for reduction.
Why is the identity element important in reduction?The identity element is a neutral value that does not change the result when combined with another value, such as 0 for sum, 1 for product, INT_MIN for max, and INT_MAX for min. It is also useful as padding when a thread has no real data to process.
What goes wrong in the naive kernel where every thread does *result += data[idx]?That expression is a read-modify-write sequence, not a single atomic action. If two threads interleave their reads and writes, one update can overwrite the other, producing an incorrect total.
How does tree reduction improve performance?Tree reduction combines values in rounds, halving the number of active elements each round. This reduces the number of sequential steps from n to about log₂(n), while still performing the same total work overall.
Why does the book use shared memory in the reduction kernel?Shared memory is fast and available to all threads within the same block, making it ideal for intermediate reduction results. Threads can cooperate on the whiteboard-like shared memory instead of repeatedly accessing slow global memory.
Why is __syncthreads() needed inside the reduction loop?__syncthreads() acts as a barrier so all threads in the block finish one reduction round before any thread starts the next. Without it, some threads may read values that other threads have not written yet, causing wrong answers.
Why is atomicAdd used at the end of the kernel?Each block produces a partial sum, and multiple blocks may try to add their partial sums to the same global result. atomicAdd makes that final update indivisible, preventing blocks from overwriting each other’s contributions.
What are the most common mistakes in reduction kernels?The main mistakes are missing __syncthreads(), forgetting to initialize the result, using regular addition instead of an atomic update across blocks, and declaring shared memory with the wrong size for the block. Each of these can produce incorrect or inconsistent results.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Grokking Parallel Programming ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Grokking Parallel Programming ebook for free