7.5. 内存栅栏函数
CUDA编程模型假设设备采用弱序内存模型(weakly-ordered memory model),这意味着CUDA线程将数据写入共享内存、全局内存、页锁定主机内存或对等设备内存的顺序,并不一定是另一个CUDA线程或主机线程观测到的写入顺序。若两个线程在没有同步的情况下对同一内存位置进行读写操作,将导致未定义行为。
在以下示例中,线程1执行writeXY(),而线程2执行readXY()。
__device__ int X = 1, Y = 2;
__device__ void writeXY()
{
X = 10;
Y = 20;
}
__device__ void readXY()
{
int B = Y;
int A = X;
}
两个线程同时从相同的内存位置X和Y进行读写操作。任何数据竞争都属于未定义行为,没有明确的语义。最终A和B的结果值可能是任意的。
内存栅栏函数可用于强制对内存访问施加顺序一致性排序(sequentially-consistent ordering)。不同内存栅栏函数的区别在于其强制排序的作用域范围,但它们都与所访问的内存空间类型无关(共享内存、全局内存、页锁定主机内存以及对等设备的内存)。
void __threadfence_block();
等同于cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_block)并确保:
-
在调用
__threadfence_block()之前,调用线程对所有内存的写入操作,将被调用线程所在块内的所有线程视为发生在调用__threadfence_block()之后调用线程对所有内存的写入操作之前; -
调用线程在调用
__threadfence_block()之前对所有内存的所有读取操作,都将在调用__threadfence_block()之后对所有内存的所有读取操作之前完成排序。
void __threadfence();
等同于cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_device)
,并确保调用线程在调用__threadfence()之后对所有内存的写入操作,不会被设备中的任何线程观察到发生在调用__threadfence()之前该线程对所有内存的写入操作之前。
void __threadfence_system();
等同于cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_system),并确保调用线程在调用__threadfence_system()之前对所有内存的写入操作,会被设备中的所有线程、主机线程以及对等设备中的所有线程观察到,且这些写入操作都发生在调用线程在调用__threadfence_system()之后对所有内存的写入操作之前。
__threadfence_system() 仅在计算能力2.x及以上的设备上受支持。
在前面的代码示例中,我们可以像这样在代码中插入fences:
__device__ int X = 1, Y = 2;
__device__ void writeXY()
{
X = 10;
__threadfence();
Y = 20;
}
__device__ void readXY()
{
int B = Y;
__threadfence();
int A = X;
}
对于这段代码,可以观察到以下结果:
-
A等于 1 且B等于 2, -
A等于 10 且B等于 2, -
A等于 10 且B等于 20。
第四种结果是不可能的,因为第一次写入必须在第二次写入之前可见。如果线程1和2属于同一个块,使用__threadfence_block()就足够了。如果线程1和2不属于同一个块,当它们是来自同一设备的CUDA线程时必须使用__threadfence(),当它们是来自两个不同设备的CUDA线程时必须使用__threadfence_system()。
一个常见的使用场景是线程消费由其他线程产生的数据,如下面的内核代码示例所示,该内核在一次调用中计算包含N个数字的数组总和。每个线程块首先对数组的子集进行求和,并将结果存储在全局内存中。当所有线程块完成后,最后一个完成的线程块从全局内存中读取这些部分和并进行求和以获得最终结果。为了确定哪个线程块最后完成,每个线程块原子地递增一个计数器以表示它已完成计算并存储了其部分和。最后一个线程块是接收到计数器值等于gridDim.x-1的那个。如果在存储部分和与递增计数器之间没有设置内存屏障,计数器可能在部分和被存储之前递增,因此可能达到gridDim.x-1,导致最后一个线程块在实际更新内存中的部分和之前就开始读取它们。
内存栅栏函数仅影响线程对内存操作的顺序;它们本身并不确保这些内存操作对其他线程可见。在下面的代码示例中,通过将result变量声明为volatile,确保了对其内存操作的可见性。
__device__ unsigned int count = 0;
__shared__ bool isLastBlockDone;
__global__ void sum(const float* array, unsigned int N,
volatile float* result)
{
// Each block sums a subset of the input array.
float partialSum = calculatePartialSum(array, N);
if (threadIdx.x == 0) {
// Thread 0 of each block stores the partial sum
// to global memory. The compiler will use
// a store operation that bypasses the L1 cache
// since the "result" variable is declared as
// volatile. This ensures that the threads of
// the last block will read the correct partial
// sums computed by all other blocks.
result[blockIdx.x] = partialSum;
// Thread 0 makes sure that the incrementing
// of the "count" variable is only performed after
// the partial sum has been written to global memory.
__threadfence();
// Thread 0 signals that it is done.
unsigned int value = atomicInc(&count, gridDim.x);
// Thread 0 determines if its block is the last
// block to be done.
isLastBlockDone = (value == (gridDim.x - 1));
}
// Synchronize to make sure that each thread reads
// the correct value of isLastBlockDone.
__syncthreads();
if (isLastBlockDone) {
// The last block sums the partial sums
// stored in result[0 .. gridDim.x-1]
float totalSum = calculateTotalSum(result);
if (threadIdx.x == 0) {
// Thread 0 of last block stores the total sum
// to global memory and resets the count
// variable, so that the next kernel call
// works properly.
result[0] = totalSum;
count = 0;
}
}
}
7.6. 同步函数
void __syncthreads();
等待线程块中的所有线程都到达此点,并且这些线程在__syncthreads()之前对全局和共享内存的所有访问操作对块内所有线程可见。
__syncthreads() 用于协调同一线程块内各线程之间的通信。当块内的某些线程访问共享内存或全局内存中的相同地址时,这些内存访问可能存在读后写、写后读或写后写的风险。通过在访问之间同步线程,可以避免这些数据风险。
__syncthreads() 允许在条件代码中使用,但前提是该条件在整个线程块中的评估结果必须完全一致,否则代码执行可能会挂起或产生意外的副作用。
计算能力2.x及以上的设备支持以下三种__syncthreads()变体。
int __syncthreads_count(int predicate);
与__syncthreads()相同,但额外具有评估块内所有线程的谓词功能,并返回谓词评估为非零的线程数量。
int __syncthreads_and(int predicate);
与__syncthreads()相同,但额外具备一个特性:它会评估块内所有线程的谓词条件,当且仅当所有线程的谓词评估结果均为非零值时,该函数才会返回非零值。
int __syncthreads_or(int predicate);
与__syncthreads()相同,但额外具有一个特性:它会评估块中所有线程的谓词条件,当且仅当任意线程的谓词评估结果非零时返回非零值。
void __syncwarp(unsigned mask=0xffffffff);
将导致执行线程等待,直到掩码(mask)中指定的所有warp通道都执行了__syncwarp()(使用相同的掩码)后才会继续执行。每个调用线程必须在掩码中设置自己的位,并且掩码中指定的所有未退出线程都必须使用相同的掩码执行相应的__syncwarp(),否则结果将是未定义的。
执行__syncwarp()可确保参与屏障的线程之间的内存顺序。因此,warp内希望通过内存通信的线程可以先存储到内存,执行__syncwarp(),然后安全地读取warp中其他线程存储的值。
【注意】
对于.target sm_6x或更低版本,掩码中的所有线程必须在收敛时执行相同的__syncwarp(),且掩码中所有值的并集必须等于活动掩码。否则,行为将是未定义的。