How subgroupInclusiveAdd Works in GLSL
The subgroupInclusiveAdd function in GLSL computes an
inclusive prefix sum (also known as an inclusive scan) across all active
invocations within a single subgroup. By utilizing low-level hardware
communication pathways rather than shared memory or barrier
synchronizations, it produces a running total where each lane receives
the sum of its own value and the values of all preceding active lanes.
This article explains how GLSL subgroups operate, the mathematical
definition of an inclusive prefix sum, the low-level hardware mechanisms
used to pass values between lanes, and how control flow masking impacts
the result.
Subgroups and Lane Indexing
In modern GPU hardware, workgroups are divided into smaller,
fixed-size batches of threads called subgroups (referred to as warps on
NVIDIA GPUs or wavefronts on AMD GPUs). Typical subgroup sizes are 32 or
64 invocations. Within a subgroup, each invocation is assigned a unique
index accessible through the built-in variable
gl_SubgroupInvocationID, ranging from 0 to
gl_SubgroupSize - 1.
Because these invocations execute in lockstep on SIMD (Single
Instruction, Multiple Data) or SIMT (Single Instruction, Multiple
Threads) processing units, they can exchange data directly via hardware
registers without reading from or writing to shared memory
(workgroup memory).
The Inclusive Prefix Sum Operation
An inclusive prefix sum takes an input value \(x_i\) from each invocation \(i\) and computes an output \(y_i\) that includes the invocation's own value:
\[y_i = \sum_{k=0}^{i} x_k\]
Assuming an active subgroup where four lanes (IDs 0 through 3)
provide the input values [3, 1, 4, 2]:
- Lane 0: \(3 = 3\)
- Lane 1: \(3 + 1 = 4\)
- Lane 2: \(3 + 1 + 4 = 8\)
- Lane 3: \(3 + 1 + 4 + 2 = 10\)
The resulting values across the lanes are [3, 4, 8, 10].
This differs from subgroupExclusiveAdd, which excludes the
current lane's value and yields [0, 3, 4, 8].
Hardware Implementation via Register Shuffling
Rather than performing a sequential accumulation across lanes in
linear time (\(O(N)\)), GPU hardware
implements subgroupInclusiveAdd in parallel using register
shuffle instructions (such as shuffle_up or logarithmic
reduction networks). This process requires \(O(\log_2 N)\) steps, where \(N\) is the subgroup size.
A typical hardware execution follows the Kogge-Stone algorithm pattern across successive clock cycles:
- Step 1 (Offset 1): Each lane \(i\) reads the value from lane \(i - 1\) using cross-lane register shifting. If \(i \ge 1\), it adds that value to its current register.
- Step 2 (Offset 2): Each lane \(i\) reads the accumulated value from lane \(i - 2\). If \(i \ge 2\), it adds that value to its register.
- Step 3 (Offset 4): Each lane \(i\) reads from lane \(i - 4\) and accumulates.
- Subsequent Steps: The offset doubles in each stage (\(8, 16, 32\)) until the offset exceeds the subgroup size.
By the end of \(\log_2(\text{subgroup size})\) steps (e.g., 5 steps for a 32-lane warp), every lane holds the correct running total of all preceding lanes and itself.
Inactive Lanes and Control Flow Masking
Subgroup operations automatically account for inactive invocations caused by divergent branching or partially filled workgroups. The GPU tracks execution using an internal bitmask of active lanes.
When subgroupInclusiveAdd executes:
- Inactive lanes are excluded from the summation.
- The prefix sum only accumulates values from active lanes with a
gl_SubgroupInvocationIDless than or equal to the current lane's ID. - If lane 1 is inactive in the set
[lane 0: 3, lane 1: inactive, lane 2: 4], lane 2's inclusive sum evaluates to \(3 + 4 = 7\).
GLSL Usage
To use subgroupInclusiveAdd, the
GL_KHR_shader_subgroup_arithmetic extension must be
supported and enabled in the shader. The function works with signed
integers, unsigned integers, and floating-point scalars or vectors.
#version 450
#extension GL_KHR_shader_subgroup_arithmetic : enable
layout(local_size_x = 64) in;
void main() {
uint inputVal = 1u;
// Each invocation receives the sum of its value and all lower active lanes
uint runningTotal = subgroupInclusiveAdd(inputVal);
// runningTotal for Lane 0 = 1, Lane 1 = 2, Lane 2 = 3, etc.
}Because it bypasses the latency of writing to local storage and
issuing synchronization barriers, subgroupInclusiveAdd
serves as a primary building block for high-performance stream
compaction, memory allocation counters, and hierarchical prefix sums
across large compute buffers.