为什么计算单源最短路径需要第二个CUDA内核?

时间:2014-11-24 14:08:35

标签: cuda

我参考了一篇研究论文,在CUDA上实现了单源最短路径算法。 有两个内核如下

__global__ void SSSP_kernel_1(Node* d_node, int* d_edges, int *d_weights, bool* d_mask, int* d_cost, int *d_costU, unsigned long long no_of_nodes) { 
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    if(tid < no_of_nodes && d_mask[tid]) { 
        d_mask[tid] = false;
        for(int i = d_node[tid].start; i < (d_node[tid].start + d_node[tid].num); i++) { 
            int id = d_edges[i];
            if(d_costU[id] > (d_cost[tid] + d_weights[i]))
            d_costU[id] = d_cost[tid] + d_weights[i]; 
        } 
    } 
} 

__global__ void SSSP_kernel_2(Node* d_node, int* d_edges, int *d_weights, bool* d_mask, int* d_cost, int *d_costU, unsigned long long no_of_nodes, bool *d_stop) { 
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    if(tid < no_of_nodes) {
        if(d_cost[tid] > d_costU[tid]) {
            d_cost[tid] = d_costU[tid];
            d_mask[tid] = true;
            *d_stop = false;
        }
        d_costU[tid] = d_cost[tid];
    }
}

使用中间数组计算成本,启动第二个内核以更新成本值。作者说&#34;在修改本身时更新成本 写入不一致会导致读取。&#34;但我不明白为什么。即使写入后读取,我也会更新(第8行,内核1)只有最小值,这是无论如何需要。我在这里错过了什么? 谢谢你的时间

编辑: 我提到的那篇论文 http://cvit.iiit.ac.in/papers/Pawan07accelerating.pdf(第7页)

2 个答案:

答案 0 :(得分:2)

由于同步需要,作者在两个内核中打破操作,因为这样他们可以使所有线程都可以看到成本更新。

在第一个内核中,访问d_mask选择的所有顶点,并评估其相应邻居的新成本。如果当前费用d_cost[tid]加上边缘权重d_weights[i]小于旧费用d_costU[id],则必须更新费用。必须在旧的成本变量d_costU[id]中执行更新,否则,如果它在当前变量d_cost[tid]中运行,则该更新仅对线程的子集可见。

第二个内核检查是否为每个顶点找到了较小的成本,如果是,则将其标记为需要访问并更新当前成本变量d_cost[tid]。然后将旧的成本变量d_costU[tid]设置为当前的d_cost[tid]

答案 1 :(得分:2)

仔细阅读你所链接的论文后,这是我的想法。对不起,这有点长,请耐心等待。

通过将两个内核合并为一个内核,我们得到以下实现,我希望您同意这一点:

__global__ void SSSP_kernel(Node* d_node, int* d_edges, int *d_weights,
                            bool* d_mask, int* d_cost, int *d_costU,
                            unsigned long long no_of_nodes, bool *d_stop) {
    int tid = blockIdx.x * blockDim.x + threadIdx.x;

    if (tid < no_of_nodes && d_mask[tid]) {
        d_mask[tid] = false;

        for (int i = d_node[tid].start; i < (d_node[tid].start + d_node[tid].num); i++) {
            int id = d_edges[i];

            if (d_cost[id] > (d_cost[tid] + d_weights[i])) {
                d_cost[id] = (d_cost[tid] + d_weights[i]);
                d_mask[id] = true;
                *d_stop = false;
            }
        }
    }
}

让我们假设这是作者在提及写后读写危险时所指的实现方式。如果这个假设是错误的,那么这个答案可能毫无意义。但是我们假设这是真的并继续:哪里可能发生写后读取危险?

正如Robert Crovella在他(现已删除)的回答中所说,内核中只有两个有趣的位置d_costd_mask。我们来看两个。

<强> 1。写入 d_cost

考虑以下代码节:

if (d_cost[id] > (d_cost[tid] + d_weights[i])) {
    d_cost[id] = (d_cost[tid] + d_weights[i]);
    ...
}

如果d_cost[tid]if条件和<{em>}之间写入,则可能会发生危险。结果是d_cost[id]的新值不一致。这有问题吗?我认为不是:d_cost[tid]的价值只能减少,因此不会违反条件。通过引入一个辅助变量可以解决这个精确的危险:

int cost = d_cost[tid] + d_weights[i];
if (d_cost[id] > cost) {
    d_cost[id] = cost;
    ...
}

危险仍然可能发生,但现在d_cost[id]的价值:可能会以更好的成本覆盖“更好的成本”(这会违反条件)。这有问题吗?同样,我认为不是:它只会延迟找到正确的解决方案,而不是阻止它。事实上,这个危险也可能发生在你发布的2内核版本中,你在评论中自己说了。这可以通过使用原子指令来解决(作者在论文中提到它)。

我认为这两种危险,其他条件相同,并不妨碍算法收敛到正确的解决方案,但它们会使成本矩阵处于不一致状态(暂时)。

<强> 2。写入 d_mask

每次更新节点id的费用时,都会将其标记为必须刷新邻居费用,其中包含以下行:

d_mask[id] = true;

这是对d_mask的一次写作。另一个写入是在检查是否必须刷新节点邻居时:

if (tid < no_of_nodes && d_mask[tid]) {
    d_mask[tid] = false;

那么如何对这些操作进行排序呢?

一个选项是,在true写入后,对false的写入可能会发生:d_mask[tid]的最终值为true。这不是问题,因为它不会影响算法的正确性:在下一次迭代中将再次访问节点(可能是冗余的),并且将刷新邻居。

但请考虑一下true写入发送之前发生的事件写入falsed_mask[tid]的最终值为false 。这意味着节点将在下一次迭代中再次访问 ,因此在当前迭代中计算的所有成本将被冻结,可能永远冻结。因此,必须使用在将掩码设置为true的线程上计算的最新可用成本正确计算它们。但由于SM无法同步其内存操作,因此这些可能尚不可用。这可能会阻止算法收敛到正确的解决方案。正如作者所提到的,这种危险会随着原子的使用而消失。

总之,写入后读取危险的出现是因为只有一部分节点从迭代到刷新被刷新:如果所有节点都在每一步都刷新,那么算法总会收敛到正确的解决方案。