Blelloch Exclusive Scan in GLSL Compute Shaders
The Blelloch exclusive prefix scan is a work-efficient parallel
algorithm designed to compute prefix sums with \(O(N)\) total additions across \(O(\log N)\) parallel steps. This guide
details how to implement the Blelloch algorithm inside a single GLSL
compute shader workgroup using shared memory (shared) and
workgroup execution barriers (barrier()). By splitting
execution into an up-sweep (reduction) phase and a down-sweep phase,
threads within a workgroup cooperatively calculate exclusive running
sums entirely on the GPU.
Algorithm Phases
The Blelloch scan operates on an array of size \(N\) (where \(N\) is a power of two) mapped across \(N / 2\) invocation threads:
- Up-Sweep (Reduce Phase): Traverses the array from leaves to root, building a binary reduction tree in-place. At each step, partial sums are stored at specific indices.
- Clear the Root: The last element of the array (\(N - 1\)) is explicitly set to zero, serving as the identity element for exclusive prefix sums.
- Down-Sweep Phase: Traverses back down the tree from the root to the leaves. At each step, each node passes its current value to its left child and updates its right child with the sum of its old value and the left child's value.
GLSL Implementation
Below is a complete GLSL compute shader function demonstrating an
exclusive Blelloch scan on an array of size
SCAN_SIZE = 1024 using a local workgroup size of
512 threads.
#version 450
#define WORKGROUP_SIZE 512
#define SCAN_SIZE (WORKGROUP_SIZE * 2)
layout(local_size_x = WORKGROUP_SIZE) in;
// Workgroup shared memory for local scanning
shared uint shared_data[SCAN_SIZE];
void blelloch_exclusive_scan(uint local_id) {
// -------------------------------------------------------------
// Phase 1: Up-Sweep (Reduction)
// -------------------------------------------------------------
uint stride = 1;
for (uint d = SCAN_SIZE >> 1; d > 0; d >>= 1) {
barrier();
if (local_id < d) {
uint ai = stride * (2 * local_id + 1) - 1;
uint bi = stride * (2 * local_id + 2) - 1;
shared_data[bi] += shared_data[ai];
}
stride <<= 1;
}
// -------------------------------------------------------------
// Phase 2: Clear Root
// -------------------------------------------------------------
if (local_id == 0) {
shared_data[SCAN_SIZE - 1] = 0;
}
// -------------------------------------------------------------
// Phase 3: Down-Sweep
// -------------------------------------------------------------
for (uint d = 1; d < SCAN_SIZE; d <<= 1) {
stride >>= 1;
barrier();
if (local_id < d) {
uint ai = stride * (2 * local_id + 1) - 1;
uint bi = stride * (2 * local_id + 2) - 1;
uint temp = shared_data[ai];
shared_data[ai] = shared_data[bi];
shared_data[bi] += temp;
}
}
barrier();
}
void main() {
uint local_id = gl_LocalInvocationID.x;
// Load elements into shared memory (each thread loads 2 elements)
shared_data[2 * local_id] = 1; // Replace with input buffer value
shared_data[2 * local_id + 1] = 1; // Replace with input buffer value
// Perform scan in-place
blelloch_exclusive_scan(local_id);
// shared_data now holds the exclusive prefix sum of the elements
}Execution Details
- Index Calculation: The indices
aiandbitarget the left and right children at each level of the virtual binary tree. - Barriers: Calls to
barrier()ensure that all active threads read the previous iteration's results before any thread writes to shared memory for the current step. - Exclusive vs. Inclusive: The algorithm produces an
exclusive scan because the last element is set to
0before the down-sweep. If an inclusive scan is required, the original array elements can be added back, or each thread can shift results left by one position and append the reduction sum at the end.