NVIDIA CUDA Documentation for Warp Shuffle Functions (link)
When working with CUDA, we often need threads within the same warp to exchange data efficiently. CUDA's warp shuffle intrinsics allow threads in a warp to directly share values through registers without using shared memory, offering a fast alternative for intra-warp communication. These intrinsics operate at the warp level (typically 32 threads), enabling simultaneous exchange of values among all active threads in that warp.
Note: The older function
__shfl,__shfl_up,__shfl_down, and__shfl_xor(without the_syncsuffix) have been deprecated since CUDA 9.0. On modern devices (compute capability 7.x or higher), only the newer_syncversions are available, so it's best to use the_syncintrinsics in all new code.
Warp Basics: A warp is a group of 32 threads that execute in lockstep. Each thread in a warp is called a lane, identified by a lane index from 0 to 31. In code, you can determine the lane index as
lane_id = threadIdx.x % 32and the warp ID aswarp_id = threadIdx.x / 32. Understanding lane indices is useful when using shuffle operations (for example, to specify which thread's value to access).
Warp shuffle intrinsics are special CUDA built-in functions that exchange a variable between threads within a warp. Unlike using shared memory and __syncthreads() for communication, shuffle operations move data directly between registers of threads, significantly reducing latency and avoiding block-wide synchronization. Each thread can exchange 4 or 8 bytes of data (depending on the data type) simultaneously with others in the warp. This capability is supported on devices of compute capability 5.0 (Maxwell) or higher.
Key characteristics of shuffle intrinsics include:
- Supported Data Types: The template type
Tfor these intrinsics can be any 32-bit or 64-bit primitive type (e.g.,int,unsigned int,long,long long,float,double) as well as half-precision types (__half,__half2) or brain-float types (__nv_bfloat16,__nv_bfloat162) with the appropriate headers. - Active Mask: All
_syncvariants require amaskof typeunsigned intas the first argument. This bit-mask indicates which threads in the warp are participating in the shuffle. Typically, if you want all 32 lanes to participate, you can use0xFFFFFFFF(which has all 32 bits set) as the mask. If only a subset of lanes are active (for example, in a warp that is partially used), you should use an appropriate mask (often obtained via the__activemask()intrinsic) to include only those active threads. All threads named in the mask must execute the same shuffle instruction with the same mask simultaneously, otherwise the result is undefined. In other words, the warp should be converged for the operation - divergence or mismatched calls will lead to unpredictable behavior. - No Memory Fence: These intrinsics do not impose any memory barrier and do not guarantee any ordering of memory operations. They strictly deal with register exchange. This means you may still need explicit synchronization or memory fences if you mix shuffles with memory accesses that need reordering, but the shuffle itself just transfers data among threads' registers.
- Width Parameter: Each shuffle function has an optional
widthparameter (default iswarpSize, i.e. 32 on current hardware). Thewidthmust be a power of two between 1 and 32. This allows dividing a warp into smaller subsections (e.g., treating a warp as two groups of 16 threads, ifwidth=16). Whenwidthis less than warp size, each subgroup of threads acts as its own mini-warp for the shuffle - threads do not exchange values outside their subgroup. Any thread index (srcLane, etc.) is effectively taken modulowidthto find the source lane within the subgroup. Using a smaller width is useful if you want to perform separate shuffle operations on independent havles or quarters of a warp.
With those general rules in mind, let's look at each of the four shuffle intrinsics and what they do.
CUDA provides four variants of shuffle intrinsics, each implementing a different pattern of data exchange among warp lanes
This is the basic direct indexed shuffle. Each thread specifies a source lane ID (srcLane), and the intrinsic returns the value of var held by the thread at that lane index within the warp. In simpler terms, you can pull a value from any other thread in the warp by its lane ID.
- Usage: Every thread provides its own
var(the value it currently holds) and asrcLaneindex. After the shuffle, each thread gets the value that was originally in thesrcLaneposition. For example, if thread 5 calls__shfl_sync(mask, x, 0), it will receive the value ofxfrom lane 0 of the warp (assuming lane 0 is active and included in the mask). - Out-of-Range Index: If
srcLaneis outside[0, width-1], it will wrap around within the specified width (effectively usingsrcLane % width). This wrapping behavior is handy when performing repeated patterns, but usually you'll use a valid lane index.
Use case example: Broadcasting a value from one thread to the entire warp. If you want all threads in a warp to have a certain value that is initially held by lane 0, you can do:
int laneValue = (threadIdx.x % 32 == 0) ? myValue : 0;
int broadcasted = __shfl_sync(0xFFFFFFFF, laneValue, 0);
// now 'broadcasted' in every lane contains the value from lane 0In the above snippet, lane 0 provides myValue and all other lanes get a copy of that value in broadcasted. This pattern effectively broadcasts lane 0's data to the rest of the warp.
This shuffle shifts values upwards to lower-numbered lanes. Each thread obtains a value from another thread with a lane ID that is a fixed offset (delta) lower than its own. In other words, a thread will get the value from the thread delta positions ahead of it (with a smaller lane index) in the warp.
- Usage: If a thread’s lane ID is i,
__shfl_up_sync(mask, val, delta)will return the value from lane i – delta. For example, ifdelta=1, then lane 5 receives the value from lane 4, lane 4 gets it from lane 3, and so on. The lowestdeltalanes (those that don’t have any lower-index thread to draw from) will remain unchanged (they effectively can’t shuffle from before the start of the warp). - No Wrap-Around: There is no wrap-around with
__shfl_up_sync. Threads at the beginning of a warp subsection simply don’t get new data from this operation (their values remain the same, since there is no lane with a negative index). This makes it useful for algorithms like inclusive scans or performing prefix operations within a warp.
Think of __shfl_up_sync as everyone passing a value to the neighbor with the next lower lane index. If you imagine a warp’s lanes ordered from 0 to 31, each lane passes a value up to the one before it.
This is the opposite of shuffle up – it shifts values downwards to higher-numbered lanes. Each thread gets the value from another thread that is delta positions behind it (with a higher lane ID) .
- Usage: If a thread’s lane ID is i,
__shfl_down_sync(mask, val, delta)returns the value from lane i + delta. For example, withdelta=1, lane 4 would receive the value from lane 5, lane 5 from lane 6, etc. The highestdeltalanes in the warp (e.g., lanes 31, 30, ... ifdelta=1) will remain unchanged because there’s no thread with a higher index to provide them a value. - No Wrap-Around: Just like the up variant, there’s no wrap-around. Values don’t cycle from the end of the warp to the front. Lanes at the end simply retain their original values if there’s no source lane beyond them within the specified width.
Use __shfl_down_sync for algorithms that propagate a value down the lane indices. It’s commonly used in warp-level reduction patterns – for example, to accumulate a sum from all lanes by repeatedly halving the distance (delta) and adding values from the upper half of the lanes to the lower half.
This shuffle uses a bitwise XOR of the lane index to choose the source. Each thread calculates a source lane as srcLane = laneID ^ laneMask, and gets the value from that source. The pattern created by XOR can be thought of as pairing threads whose lane IDs differ in the bits specified by laneMask.
- Usage: The
laneMaskis typically a power of two, which means it selects a particular bit of the lane ID. For instance, if you uselaneMask = 1, then each lane will XOR its index with 1: this pairs up threads like 0<->1, 2<->3, 4<->5, etc. IflaneMask = 2, it pairs 0<->2, 1<->3, 4<->6, 5<->7, and so on (binary 10 flips the second-least-significant bit of the index). In general,laneMaskdefines which bit to toggle in the lane index to find the parter thread. - Width Groups: If
width < warpSize, then each group ofwidththreads acts independently. A thread can XOR with others within its own group or in earlier groups, but not with threads in a later group. If an XOR points to a thread outside the group (i.e. a higher-indexed group), the thread will just get its own value back (since those threads are not in its active subgraph). - Butterfly Pattern: XOR shuffles create a butterfly communication pattern. This is particularly useful in tree-based reductions and broadcasts. For example, to do a warp-level reduction (sum of all values in the warp), one can use a sequence of
__shfl_xor_synccalls withlaneMask = 16, 8, 4, 2, 1, combining values at each step. This effectively performs a tree reduction where at each step, threads exchange data with a partner at a distance that is a power of two, and accumulate results.
In summary, __shfl_xor_sync is the most flexible shuffle and often the most powerful for parallel algorithms, as it can perform the pairwise exchanges needed for many divide-and-conquer strategies.
The main benefit of warp shuffle functions is performance. They allow threads to share data without the overhead of shared memory or block-level synchronization. In scenarios where only a warp's threads need to cooperate (which is often the case for small reductions, prefix sums, shuffles, etc.), using these intrinsics can drastically reduce latency. By avoiding __syncthreads() and shared memory, you eliminate unnecessary stalling of other warps and extra memory traffic.
For example, consider summing 32 values (one per thread in a warp). A naive approach might use shared memory to accumulate partial sums and require a block sync. With shuffle intrinsics, each thread can obtain values from its peers in a tree-like fashion (using XOR or down shuffles) and compute the sum entirely in registers within a few steps - no shared memory needed. NVIDIA's documentation even provides a warp-level broadcast example using __shfl_sync to share a value from one thread to all others.
Another use case is transposing data within a warp or rearranging elements. As illustrated by a 4x4 matrix transpose example, warp shuffles let half a warp rearrange data among themselves while the other half can remain idle, without forcing an entire block to synchronize. This fine-grained communication is a cornerstone of advanced optimization techniques where different warps in a block perform different tasks in parallel.
CUDA's shuffle intrinsics are powerful tools for GPU developers, enabling fast data exchanges at the warp level. They should be used when threads within the same warp need to share data or perform collective operations like reductions, scans, or broadcasts. By leveraging these intrinsics, experienced programmers can write warp-synchronous algorithms that avoid unnecessary memory accesses and block-level synchronization, leading to more efficient GPU kernels. Just remember to use the _sync versions with proper mask (often 0xFFFFFFFF for a full warp) and ensure all intended threads participate in lockstep. With these in hand, you can implement clever warp-level routines that make that the most of NVIDIA GPU architecture - achieving speed-ups by communicating through the warp's internal network rather than lower memory.