You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在CUDA的Hillis-Steele扫描算法中正确使用__syncthreads()?

Understanding __syncthreads() in Hillis-Steele Scan & Correct CUDA Implementation

Let's break down your questions one by one, starting with the core issue around __syncthreads(), then why your current code works (by accident), and finally how to implement Hillis-Steele properly in CUDA.

1. What happens when threads in the same warp call __syncthreads() different numbers of times?

First, a critical rule to remember: __syncthreads() requires every thread in the entire block to reach that synchronization point. If even one thread skips the __syncthreads() call while others wait for it, you enter undefined behavior territory. This can lead to:

  • Deadlocks where your kernel hangs indefinitely
  • Incorrect results due to unsynchronized memory accesses
  • Hardware-level errors that crash your program

In your code, thread 0 never enters the loop (since idx <= 0 is false for the initial idx=1), so it never calls __syncthreads(). Meanwhile, threads 1 and above call it multiple times. When those threads hit __syncthreads(), they wait for thread 0 to reach the same point—but thread 0 has already moved past the loop and is sitting at the end of the kernel. This is a clear violation of CUDA's synchronization rules.

2. Why does your current code produce correct results?

This is purely a coincidence caused by your all-1s input and how unsynchronized memory accesses happened to play out on your hardware.

Hillis-Steele works by accumulating values in stages, but your code's unsynchronized steps didn't break the all-1s case because every addition is just adding 1 to the previous value. If you tested with a non-uniform input (e.g., [1,2,3,4,...]), you'd almost certainly get wrong results or a deadlock. Your code is not correct—it just appears to work for this specific input.

3. How to properly synchronize threads that don't execute the same code?

The fix is to ensure all threads execute every iteration of the loop, and only skip the computation when it's not relevant to their thread ID. This way, every thread reaches each __syncthreads() call, avoiding undefined behavior.

For Hillis-Steele, the number of iterations needed is log2(block_size) (rounded up to the next power of two if your block size isn't a power of two). In each iteration k, we check if the thread's index is at least 2^k—if so, we perform the addition; otherwise, we do nothing. All threads then hit __syncthreads() together.

4. Correct Hillis-Steele Scan Implementation

Here's a fixed version of your code that follows these rules:

import pycuda.driver as cuda
import pycuda.autoinit
import numpy as np
from pycuda.compiler import SourceModule

mod = SourceModule('''
__global__ void scan(int *addresses, int block_size) {
    // Calculate the number of steps needed (log2 of next power of two)
    int max_steps = 0;
    int temp = block_size;
    while (temp >>= 1) max_steps++;
    
    for (int k = 1; k <= max_steps; k++) {
        int idx = 1 << (k-1); // Equivalent to 2^(k-1)
        if (threadIdx.x >= idx) {
            addresses[threadIdx.x] += addresses[threadIdx.x - idx];
        }
        __syncthreads(); // All threads reach this sync point every iteration
    }
}
''')

func = mod.get_function("scan")

// Use a non-uniform input to verify correctness
addresses_h = np.arange(1, 897, dtype='i4')  // [1,2,3,...,896]
addresses_d = cuda.to_device(addresses_h)

threads_x = 896
func(addresses_d, np.int32(threads_x), block=(threads_x, 1, 1), grid=(1, 1))

addresses_h = cuda.from_device(addresses_d, addresses_h.shape, addresses_h.dtype)

// Verify prefix sum results (expected value at index i is sum(1..i+1) = (i+1)*(i+2)/2)
for i in range(len(addresses_h)):
    expected = (i+1)*(i+2)//2
    assert addresses_h[i] == expected, f"Index {i}: got {addresses_h[i]}, expected {expected}"

print("All tests passed!")

Key improvements:

  • All threads run every loop iteration, so every thread calls __syncthreads() the same number of times.
  • We calculate the exact number of steps needed based on the block size.
  • We use a non-uniform input to confirm the implementation works beyond the all-1s case.

Bonus Notes:

  • For block sizes that aren't powers of two, this implementation still works because we round up to the next power of two for steps, and threads beyond the original array size won't modify values (since their index checks will fail).
  • If you need larger scans (beyond a single block), you'll need to implement a multi-block scan (combining per-block prefix sums with a second pass), but that's beyond the scope of your original question.

内容的提问来源于stack exchange,提问作者jmbenlloch

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:39:32