nVidia 上的 OpenCL NDRange 尺寸顺序错误?

OpenCL NDRange dimensions order bug on nVidia?

我知道这些天 OpenCL 相当空闲——尤其是 NVidia 的 CUDA 实现。也就是说,我认为我在 Nvidia 中发现了一个重大错误,我想看看其他人是否注意到了同样的错误。使用 Linux 平台版本 OpenCL 1.2 CUDA 10.1.0 和 C++ 绑定 我一直遇到 NDRange 顺序的各种问题,我终于有了一个可以明确重现的简单内核问题:

void kernel test()
{
    printf("G0:%d   G1:%d   G2:%d   L0:%d   L1:%d   L2:%d\n", 
    get_global_id(0),
    get_global_id(1),
    get_global_id(2),
    get_local_id(0),
    get_local_id(1),
    get_local_id(2));
}

如果我用 3 个维度对这个内核进行排队:全局 (4,3,2) 和局部 (1,1,1):

queue.enqueueNDRangeKernel(kernel, cl::NullRange, 
                cl::NDRange(4, 3, 2), 
                cl::NDRange(1, 1, 1), 
                NULL, events);

它在 AMD/Intel 上随机正确输出以下内容(为清楚起见,随机输出排序):

G0:0   G1:0   G2:0   L0:0   L1:0   L2:0
G0:0   G1:0   G2:1   L0:0   L1:0   L2:0
G0:0   G1:1   G2:0   L0:0   L1:0   L2:0
G0:0   G1:1   G2:1   L0:0   L1:0   L2:0
G0:0   G1:2   G2:0   L0:0   L1:0   L2:0
G0:0   G1:2   G2:1   L0:0   L1:0   L2:0
G0:1   G1:0   G2:0   L0:0   L1:0   L2:0
G0:1   G1:0   G2:1   L0:0   L1:0   L2:0
G0:1   G1:1   G2:0   L0:0   L1:0   L2:0
G0:1   G1:1   G2:1   L0:0   L1:0   L2:0
G0:1   G1:2   G2:0   L0:0   L1:0   L2:0
G0:1   G1:2   G2:1   L0:0   L1:0   L2:0
G0:2   G1:0   G2:0   L0:0   L1:0   L2:0
G0:2   G1:0   G2:1   L0:0   L1:0   L2:0
G0:2   G1:1   G2:0   L0:0   L1:0   L2:0
G0:2   G1:1   G2:1   L0:0   L1:0   L2:0
G0:2   G1:2   G2:0   L0:0   L1:0   L2:0
G0:2   G1:2   G2:1   L0:0   L1:0   L2:0
G0:3   G1:0   G2:0   L0:0   L1:0   L2:0
G0:3   G1:0   G2:1   L0:0   L1:0   L2:0
G0:3   G1:1   G2:0   L0:0   L1:0   L2:0
G0:3   G1:1   G2:1   L0:0   L1:0   L2:0
G0:3   G1:2   G2:0   L0:0   L1:0   L2:0
G0:3   G1:2   G2:1   L0:0   L1:0   L2:0

这符合规范。但是,如果我使用 NVidia 安排具有相同尺寸的完全相同的内核,我将得到以下输出:

G0:0   G1:0   G2:0   L0:0   L1:0   L2:0
G0:0   G1:0   G2:0   L0:0   L1:1   L2:0
G0:0   G1:0   G2:1   L0:0   L1:0   L2:0
G0:0   G1:0   G2:1   L0:0   L1:1   L2:0
G0:0   G1:0   G2:2   L0:0   L1:0   L2:0
G0:0   G1:0   G2:2   L0:0   L1:1   L2:0
G0:1   G1:0   G2:0   L0:0   L1:0   L2:0
G0:1   G1:0   G2:0   L0:0   L1:1   L2:0
G0:1   G1:0   G2:1   L0:0   L1:0   L2:0
G0:1   G1:0   G2:1   L0:0   L1:1   L2:0
G0:1   G1:0   G2:2   L0:0   L1:0   L2:0
G0:1   G1:0   G2:2   L0:0   L1:1   L2:0
G0:2   G1:0   G2:0   L0:0   L1:0   L2:0
G0:2   G1:0   G2:0   L0:0   L1:1   L2:0
G0:2   G1:0   G2:1   L0:0   L1:0   L2:0
G0:2   G1:0   G2:1   L0:0   L1:1   L2:0
G0:2   G1:0   G2:2   L0:0   L1:0   L2:0
G0:2   G1:0   G2:2   L0:0   L1:1   L2:0
G0:3   G1:0   G2:0   L0:0   L1:0   L2:0
G0:3   G1:0   G2:0   L0:0   L1:1   L2:0
G0:3   G1:0   G2:1   L0:0   L1:0   L2:0
G0:3   G1:0   G2:1   L0:0   L1:1   L2:0
G0:3   G1:0   G2:2   L0:0   L1:0   L2:0
G0:3   G1:0   G2:2   L0:0   L1:1   L2:0

NVidia 对 global/local 维度的解释似乎是交错的,这与规范不符。这似乎也不涉及 C++ 绑定。本地 ID 只能为零,并且 get_global_id(1) 始终为零。

我知道 NVidia 不太关心 OpenCL,但这似乎是一个相当大的问题。还有其他人遇到过这样的事情吗?这不是 printf 的同步问题。我在实际数据用例中注意到了它,构建这个内核只是为了演示它。

虽然很难详细验证,但我会post它作为答案,因为根据我的观察,似乎说明问题:

tl;dr: 原因几乎可以肯定是由于 printf.

中缺少同步

首先,我观察到与您相同的行为:在 AMD 上,输出似乎是正确的。在 NVIDIA 上,这似乎是令人恼火的错误。所以我很好奇,扩展了内核,也打印了 get_local_size:

void kernel test()
{
    printf("G0:%d   G1:%d   G2:%d   L0:%d   L1:%d   L2:%d  S0:%d  S1:%d  S2:%d\n", 
        get_global_id(0),
        get_global_id(1),
        get_global_id(2),
        get_local_id(0),
        get_local_id(1),
        get_local_id(2),
        get_local_size(0),
        get_local_size(1),
        get_local_size(2));
}

现在,get_local_id当然必须小于这个大小,否则大多数内核都会崩溃。在 AMD 上,输出非常干净:

platform AMD Accelerated Parallel Processing
device Spectre
G0:0   G1:0   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:1   G1:0   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:2   G1:0   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:3   G1:0   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:0   G1:1   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:1   G1:1   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:2   G1:1   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:3   G1:1   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:0   G1:2   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:1   G1:2   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:2   G1:2   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:3   G1:2   G2:0   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:0   G1:0   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:1   G1:0   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:2   G1:0   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:3   G1:0   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:0   G1:1   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:1   G1:1   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:2   G1:1   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:3   G1:1   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:0   G1:2   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:1   G1:2   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:2   G1:2   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1
G0:3   G1:2   G2:1   L0:0   L1:0   L2:0  S0:1  S1:1  S2:1

在 NVIDIA 上,输出为

platform NVIDIA CUDA
device GeForce GTX 970
G0:3   G1:0   G2:2   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:3   G1:0   G2:1   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:3   G1:0   G2:0   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:0   G1:0   G2:2   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:0   G1:0   G2:1   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:2   G1:0   G2:0   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:2   G1:0   G2:1   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:2   G1:0   G2:2   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:1   G1:0   G2:1   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:3   G1:0   G2:0   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:1   G1:0   G2:0   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:3   G1:0   G2:1   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:0   G1:0   G2:2   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:1   G1:0   G2:2   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:3   G1:0   G2:2   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:0   G1:0   G2:1   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:2   G1:0   G2:1   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:0   G1:0   G2:0   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:2   G1:0   G2:0   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:0   G1:0   G2:0   L0:0   L1:0   L2:0  S0:0  S1:0  S2:0
G0:2   G1:0   G2:2   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:1   G1:0   G2:2   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:1   G1:0   G2:1   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0
G0:1   G1:0   G2:0   L0:0   L1:1   L2:0  S0:0  S1:0  S2:0

现在,不可能是正确的:本地工作大小始终为 0!

经过一些进一步的测试(例如使用 2D 内核和不同的数字),输出 generally 似乎根本没有任何意义。所以我尝试了这个内核:

void kernel test()
{
    printf("G0:%d\n", get_global_id(0));
    printf("G1:%d\n", get_global_id(1));
    printf("G2:%d\n", get_global_id(2));
    printf("L0:%d\n", get_local_id(0));
    printf("L1:%d\n", get_local_id(1));
    printf("L2:%d\n", get_local_id(2));
    printf("S0:%d\n", get_local_size(0));
    printf("S1:%d\n", get_local_size(1));
    printf("S2:%d\n", get_local_size(2));
}

在NVIDIA上,则输出为

platform NVIDIA CUDA
device GeForce GTX 970
G0:1
G0:1
G0:1
G0:2
G0:2
G0:2
G0:2
G0:2
G0:3
G0:2
G0:3
G0:3
G0:0
G0:3
G0:3
G0:0
G0:0
G0:3
G0:0
G0:0
G0:0
G0:1
G0:1
G0:1
G1:2
G1:2
G1:0
G1:0
G1:1
G1:2
G1:2
G1:1
G1:1
G1:1
G1:0
G1:0
G1:2
G1:1
G1:0
G1:0
G1:2
G1:1
G1:1
G1:0
G1:2
G1:2
G1:0
G1:1
G2:0
G2:0
G2:1
G2:1
G2:0
G2:0
G2:1
G2:0
G2:0
G2:0
G2:0
G2:0
G2:1
G2:1
G2:0
G2:1
G2:1
G2:1
G2:1
G2:0
G2:1
G2:0
G2:1
G2:1
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L0:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L1:0
L2:0
L1:0
L1:0
L1:0
L1:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
S0:1
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
L2:0
S0:1
S0:1
S0:1
S0:1
S0:1
S0:1
S0:1
S0:1
S0:1
S0:1
S0:1
S0:1
S0:1
S1:1
S0:1
S0:1
S0:1
S0:1
S0:1
S1:1
S0:1
S0:1
S0:1
S0:1
S0:1
S1:1
S1:1
S1:1
S1:1
S1:1
S1:1
S1:1
S1:1
S1:1
S1:1
S2:1
S1:1
S1:1
S1:1
S2:1
S1:1
S1:1
S1:1
S1:1
S1:1
S2:1
S1:1
S1:1
S2:1
S2:1
S1:1
S1:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1
S2:1

重点是:每个人输出正确!。问题似乎是将所有内容都放入一个 printf 会弄乱一些内部缓冲区。

这当然很可惜。它基本上不可能将 printf 用于内核内部合理使用的唯一目的,即 调试 ...


旁白:规范在这一点上仍然有点难以解释——至少在决定观察到的行为是"right"还是"wrong"时是这样。来自 Khronos documentation of printf :

In the case that printf is executed from multiple work-items concurrently, there is no guarantee of ordering with respect to written data. For example, it is valid for the output of a work-item with a global id (0,0,1) to appear intermixed with the output of a work-item with a global id (0,0,4) and so on.

NVIDIA documentation of the CUDA printf implementation 还包含一些免责声明并讨论了一些可能被覆盖的缓冲区,但是将其(在规范的技术层面上)映射到 OpenCL 行为是困难的...