为什么内存屏障不同步共享内存而屏障会呢?

15
以下GLSL计算着色器简单地将inImage复制到outImage。它是从更复杂的后处理通道派生而来的。
main()的前几行中,单个线程将64个像素的数据加载到共享数组中。然后,在同步之后,每个64个线程中的一个线程向输出图像写入一个像素。
根据我的同步方式,我会得到不同的结果。我最初认为memoryBarrierShared()会是正确的调用,但它产生了以下结果: Unsynchronized result 这与没有同步或使用memoryBarrier()相同。
如果我使用barrier(),我会得到以下(期望的)结果: enter image description here 条纹宽度为32像素,如果我将工作组大小更改为小于或等于32,我会得到正确的结果。
这里发生了什么?我误解了memoryBarrierShared()的目的吗?为什么barrier()有效?
#version 430

#define SIZE 64

layout (local_size_x = SIZE, local_size_y = 1, local_size_z = 1) in;

layout(rgba32f) uniform readonly  image2D inImage;
uniform writeonly image2D outImage;

shared vec4 shared_data[SIZE];

void main() {
    ivec2 base = ivec2(gl_WorkGroupID.xy * gl_WorkGroupSize.xy);
    ivec2 my_index = base + ivec2(gl_LocalInvocationID.x,0);

    if (gl_LocalInvocationID.x == 0) {
        for (int i = 0; i < SIZE; i++) {
            shared_data[i] = imageLoad(inImage, base + ivec2(i,0));
        }
    }

    // with no synchronization:   stripes
    // memoryBarrier();        // stripes
    // memoryBarrierShared();  // stripes
    // barrier();              // works

    imageStore(outImage, my_index, shared_data[gl_LocalInvocationID.x]);
}

那么如何显示它呢?也许计算着色器工作正常,但图像本身在计算着色器调用和后续显示之间没有同步,即主机端是否需要适当的glMemoryBarrier(不确定)。 - Christian Rau
我正在使用一个封装了OpenGL并处理与GPU的所有通信的库。因此,将显示代码合并到我的MWE中很困难。由于当允许每个线程执行单个“imageLoad”后跟一个“imageStore”时,我也可以获得正确的结果,所以我不认为主机-设备同步是问题所在。 - James Wilcox
“由于当允许每个线程执行单个imageLoad后跟随一个imageStore时,我也可以获得正确的结果,因此我认为主机-设备同步不是问题所在。” - 好吧,如果真的是跨通道同步失败了,那么未定义行为的副作用可能会在某些配置下正常工作。 glMemoryBarrier仍然是设备-设备同步,而不是主机-设备同步。 - Christian Rau
当然,您是正确的,未定义的行为在某些配置中可能有效。无论如何,在我的传递和下一个传递之间加入了glMemoryBarrier(GL_ALL_BARRIER_BITS),但结果没有改变。 - James Wilcox
深入挖掘后,似乎您需要使用“屏障(barrier)”。 - Christian Rau
1个回答

26
问题在于图像加载存储和相关操作的实现无法确保着色器仅更改其专用输出值(例如片段着色器后的帧缓冲区)的数据。对于计算着色器而言,情况更加如此,因为它们没有专用输出,只能通过向可写存储器(如图像、存储缓冲区或原子计数器)写入数据来输出内容。否则,尝试访问纹理的片段着色器可能无法获得由前一个传递中使用图像存储操作写入该纹理的最新数据。
这可能会导致计算着色器正常运行,但与随后的显示(或其他)传递同步时失败。为此,可以使用 glMemoryBarrier 函数。根据在显示传递(或更准确地说是在计算着色器传递后读取图像数据的传递)中读取图像数据的方式,需要向此函数提供不同的标志。如果使用纹理进行读取,请使用 GL_TEXTURE_FETCH_BARRIER_BIT​​;如果再次使用图像加载,请使用 GL_SHADER_IMAGE_ACCESS_BARRIER_BIT​​;如果使用 glBlitFramebuffer 进行显示,请使用 GL_FRAMEBUFFER_BARRIER_BIT​​ ...
虽然我对图像加载/存储和手动内存同步没有太多经验,但这只是我理论上的想法。如果有人知道更好的方法或者您已经使用了适当的 glMemoryBarrier,请随时纠正我。同样,这可能不是您唯一的错误(如果有的话)。但链接维基百科文章中的最后两个要点实际上涉及到您的用例,并且在我看来清楚地表明您需要某种类型的 glMemoryBarrier
  • 在一个渲染传递中写入图像变量并由稍后的着色器读取的数据不需要使用 coherent 变量或 memoryBarrier()。调用带有 GL_SHADER_IMAGE_ACCESS_BARRIER_BIT​​ 标志的 glMemoryBarrier 在渲染通道之间设置SHADER_IMAGE_ACCESS_BARRIER_BIT​是必要的。

  • 在一个渲染通道中由着色器写入数据并在以后的机制(例如从顶点或索引缓冲区拉取)中由另一个机制读取的数据不需要使用coherent变量或memoryBarrier()。但需要在通道之间的障碍物中调用glMemoryBarrier并设置相应的位。


编辑: 实际上计算着色器的维基文章说:

共享变量访问使用不一致内存访问的规则。 这意味着用户必须执行某些同步操作,以确保共享变量是可见的。

所有共享变量都被隐式声明为coherent​,因此您不需要(也不能)使用该限定符。但是,您仍然需要提供适当的内存屏障。

计算着色器可以使用常规的内存屏障集,但它们还可以访问memoryBarrierShared()​;,这个屏障专门用于共享变量排序。groupMemoryBarrier()​的作用类似于memoryBarrier()​​,它为所有类型的变量排序内存写入,但仅为当前工作组排序读/写。

虽然在工作组内的所有调用都被认为是“并行”执行的,但这并不意味着您可以假设它们全部以锁定的方式执行。如果您需要确保调用已写入某个变量以便您可以读取它,则需要与调用同步执行,而不仅仅是发出内存屏障(尽管您仍然需要内存屏障)。

要在工作组内的调用之间同步读取和写入,您必须使用barrier()函数。这将强制进行同步操作,以使调用与其他调用同步执行,而不仅仅是发出内存屏障(尽管您仍然需要内存屏障)。

在工作组中,所有调用之间需要显式同步。只有当所有其他调用都到达这个障碍点时,工作组内的执行才会继续进行。一旦通过了barrier()​,之前在组内所有调用中共享的变量将可见。

因此,实际上需要在这里使用barrier,而memoryBarrierShared是不够的(尽管您不需要同时使用两者,因为最后一句话已经说明了)。内存屏障只会同步内存,但它无法停止线程的执行来跨越它。因此,如果第一个线程已经写入了某些内容,其他线程就不会读取共享内存中的任何旧缓存数据,但它们仍然可以在第一个线程尝试写入任何内容之前到达读取点。

这实际上非常适合对于块大小为32及以下的情况以及前32个像素起作用的事实。至少在NVIDIA硬件上,32是warp大小,因此可以完美地锁定步骤操作的线程数量。因此,前32个线程(或32个线程的每个块)总是完全并行工作(在概念上是这样),因此它们不会引入任何竞争条件。这也是为什么如果您知道在单个warp内工作,则实际上不需要任何同步的常见优化策略。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接