同步和全球记忆

时间:2017-01-07 18:56:16

标签: cuda synchronization global-variables

我写了一个小内核,使用并行缩减来求和2 ^ k个元素。 这里没什么新东西....我的矢量存储在全局存储器中,我将矢量的每个部分分配给不同的块并将每个块减少到一个位置。其余的我在CPU中做。

__global__ void sum(real *v, long int s){

    long int ix     =  threadIdx.x;
    long int shift = blockIdx.x*blockDim.x;

    long int h = blockDim.x/2;
    while (h >= 1){
        if (ix < h){
            v[ix +  shift] = v[2*ix + shift] + v[2*ix + 1 + shift];
        }
        __syncthreads(); 
        h = h / 2;
    }
}

代码有效。然而,仔细检查后,我意识到它可能不起作用。所以我很困惑....可能是thread_id = 1,它对元素2和3求和,在thread_id = 0能够读取元素0和1之前将其和值写入位置1,从而使结果无效。 / p>

我会认为,为了安全起见,代码必须是

__global__ void sumsafe(real *v, long int s){
    long int ix     =  threadIdx.x;
    long int shift = blockIdx.x*blockDim.x;
    real x = 0;
    long int h = blockDim.x/2;
    while (h >= 1){
        if (ix < h){
            x = v[2*ix + shift] + v[2*ix + 1 + shift];
        }
        __syncthreads(); 
        if (ix < h){
            v[ix +  shift] = x;
        }
        __syncthreads();
        h = h / 2;
    }
}

所以我保证所有线程在开始更改它们之前读取它们的值。但正如我所说......两个代码都有效...他们的时间实际上也差不多。

为什么会这样?

我知道GPU并不能保证一个线程写入全局内存的内容对其他线程不可见。但它并不能保证这种情况永远不会发生。

任何想法!?我正在研发GTX 1080。

1 个答案:

答案 0 :(得分:4)

你确实很“幸运”,因为CUDA不能保证warp的执行顺序。以下描述(这是猜想)不应被解释为声明您所展示的是一个好主意。没有人应该像这样减少。

但是对于一个小的测试用例(没有其他代码,并且在单个数据块上运行),我希望这可以工作。

从全局内存中读取通常是高延迟的。当执行遇到这行代码时:

        v[ix +  shift] = v[2*ix + shift] + v[2*ix + 1 + shift];

将转换为SASS指令,如下所示:

LD  R0, v[2*ix + shift]        (let's call this LD0)
LD  R1, v[2*ix + 1 + shift];   (let's call this LD1)
ADD R3, R0, R1
ST  v[ix + shift], R3

现在,前两个LD操作不会导致停顿。但是,如果R1和R0无效,则ADD操作将导致停顿(无法发出)。

停顿的结果将是SM中的warp调度引擎将寻找其他可用的工作。其他可用的工作可能构成上述其他经线的代码。

由于ADD指令不能发出,直到读取完成,并且由于warp调度程序对停顿的响应,读取(跨warp)都被有效地背靠背发出,因此读取在ADD指令完成发布时,操作将倾向于所有完成,这意味着所有读取都在所有ADD操作发出时完成(并且ST不能发布直到其相应的ADD完成)。 ADD也具有流水线延迟,因此ADD操作也可能按顺序发出(但这里的流水线延迟可能会增加危险的可能性),并且在相应的ADD操作完成之前不能发出给定的ST操作。净效应可能是:

LD0 W0
LD1 W0
LD0 W1
LD1 W1
...   (all LD0 and LD1 get issued across all warps W0..WN)
<read latency stall --  eventually the first 2 LD0 and LD1 complete>
ADD W0
<read pipeline latency - 1 cycle>
ADD W1
<read pipeline latency - 1 cycle>
ADD W2
...
<add pipeline latency>
ST W0
<add pipeline latency>
ST W1
...

延迟的结果是在任何ADD操作开始之前,所有读取都以高可能性发布到全局内存。由于管道效应,所有读取操作也可能在任何ST操作开始之前完成(可能?),导致这种有限测试用例的可能性,即不会发生实际的危险错误。

我希望即使数据在L2缓存中,来自L2缓存的读取延迟仍然足以允许上述工作。我怀疑如果数据在L1缓存中,来自L1缓存的读取延迟(并假设最大补充的warp)可能不足以导致上述描述保持,但我没有仔细检查算法。由于ADD流水线延迟是固定的,但是从LD到ST操作的危险由ADD操作的数量与ADD流水线延迟相比确定,实际的危险概率随着在线程块中加载更多扭曲而增加。

请注意,以上所有描述都试图解压缩while循环的单个迭代的行为。 __syncthreads()的{​​{3}}应保证迭代i+1的读取不会被(见证失败)迭代i的写入所破坏。