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:

  1. 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.
  2. 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.
  3. Step 3 (Offset 4): Each lane \(i\) reads from lane \(i - 4\) and accumulates.
  4. 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_SubgroupInvocationID less 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.