【发布时间】:2021-11-07 16:15:56
【问题描述】:
我有一个包含嵌套循环和subgroupBarrier 的复杂过程。
在简化的形式中,它看起来像
while(true){
while(some_condition){
if(end_condition){
atomicAdd(some_variable,1);
debugPrintfEXT("%d:%d",gl_SubgroupID,gl_SubgroupInvocationID.x);
subgroupBarrier();
if(gl_SubgroupInvocationID.x==0){
debugPrintfEXT("Finish! %d", some_variable);
// do some final stuff
}
return; // this is the only return in the entire procedure
}
// do some stuff
}
// do some stuff
}
总体而言,该过程是正确的,并且符合预期。所有子组线程 总是最终到达结束条件。但是,在我的日志中我看到了
0:2
0:3
0:0
Finish! 3
0:1
这不仅仅是日志显示无序的问题。我执行原子加法,它似乎也是错误的。在打印Finish! 之前,我需要所有线程完成所有原子操作。如果subgroupBarrier() 工作正常,它应该打印4,但在我的情况下它打印3。我一直在关注本教程
https://www.khronos.org/blog/vulkan-subgroup-tutorial
它说
void subgroupBarrier()执行完整的内存和执行屏障 - 基本上,当调用从subgroupBarrier()返回时,我们保证每次调用在任何返回之前都执行屏障,并且这些调用的所有内存写入对所有调用都可见子组。
有趣的是,我尝试将if(gl_SubgroupInvocationID.x==0) 更改为其他数字。例如if(gl_SubgroupInvocationID.x==3) 产生
0:2
0:3
Finish! 2
0:0
0:1
所以看起来subgroupBarrier() 完全被忽略了。
嵌套循环可能是问题的原因还是其他原因?
编辑:
我这里提供更详细的代码
#version 450
#extension GL_KHR_shader_subgroup_basic : enable
#extension GL_EXT_debug_printf : enable
layout (local_size_x_id = GROUP_SIZE_CONST_ID) in; // this is a specialization constant whose value always matches the subgroupSize
shared uint copied_faces_idx;
void main() {
const uint chunk_offset = gl_WorkGroupID.x;
const uint lID = gl_LocalInvocationID.x;
// ... Some less important stuff happens here ...
const uint[2] ending = uint[2](relocated_leading_faces_ending, relocated_trailing_faces_ending);
const uint[2] beginning = uint[2](offset_to_relocated_leading_faces, offset_to_relocated_trailing_faces);
uint part = 0;
face_offset = lID;
Face face_to_relocate = faces[face_offset];
i=-1;
debugPrintfEXT("Stop 1: %d %d",gl_SubgroupID,gl_SubgroupInvocationID.x);
subgroupBarrier(); // I added this just to test see what happens
debugPrintfEXT("Stop 2: %d %d",gl_SubgroupID,gl_SubgroupInvocationID.x);
while(true){
while(face_offset >= ending[part]){
part++;
if(part>=2){
debugPrintfEXT("Stop 3: %d %d",gl_SubgroupID,gl_SubgroupInvocationID.x);
subgroupBarrier();
debugPrintfEXT("Stop 4: %d %d",gl_SubgroupID,gl_SubgroupInvocationID.x);
for(uint i=lID;i<inserted_face_count;i+=GROUP_SIZE){
uint offset = atomicAdd(copied_faces_idx,1);
face_to_relocate = faces_to_be_inserted[i];
debugPrintfEXT("Stop 5: %d %d",gl_SubgroupID,gl_SubgroupInvocationID.x);
tmp_faces_copy[offset+1] = face_to_relocate.x;
tmp_faces_copy[offset+2] = face_to_relocate.y;
}
subgroupBarrier(); // Let's make sure that copied_faces_idx has been incremented by all threads.
if(lID==0){
debugPrintfEXT("Finish! %d",copied_faces_idx);
save_copied_face_count_to_buffer(copied_faces_idx);
}
return;
}
face_offset = beginning[part] + lID;
face_to_relocate = faces[face_offset];
}
i++;
if(i==removed_face_count||shared_faces_to_be_removed[i]==face_to_relocate.x){
remove_face(face_offset, i);
debugPrintfEXT("remove_face: %d %d",gl_SubgroupID,gl_SubgroupInvocationID.x);
face_offset+=GROUP_SIZE;
face_to_relocate = faces[face_offset];
i=-1;
}
}
}
基本上这段代码的作用相当于
outer1:for(every face X in polygon beginning){
for(every face Y to be removed from polygons){
if(X==Y){
remove_face(X);
continue outer1;
}
}
}
outer2:for(every face X in polygon ending){
for(every face Y to be removed from polygons){
if(X==Y){
remove_face(X);
continue outer2;
}
}
}
for(every face Z to be inserted in the middle of polygon){
insertFace(Z);
}
save_copied_face_count_to_buffer(number_of_faces_copied_along_the_way);
我的代码看起来如此复杂的原因是因为我以一种更可并行化的方式编写它,并试图最大限度地减少非活动线程的数量(考虑到通常同一子组中的线程必须执行相同的指令)。
我还添加了更多调试打印和更多障碍,只是为了看看会发生什么。这是我得到的日志
Stop 1: 0 0
Stop 1: 0 1
Stop 1: 0 2
Stop 1: 0 3
Stop 2: 0 0
Stop 2: 0 1
Stop 2: 0 2
Stop 2: 0 3
Stop 3: 0 2
Stop 3: 0 3
Stop 4: 0 2
Stop 4: 0 3
Stop 5: 0 2
Stop 5: 0 3
remove_face: 0 0
Stop 3: 0 0
Stop 4: 0 0
Stop 5: 0 0
Finish! 3 // at this point value 3 is saved (which is the wrong value)
remove_face: 0 1
Stop 3: 0 1
Stop 4: 0 1
Stop 5: 0 1 // at this point atomic is incremented and becomes 4 (which is the correct value)
【问题讨论】:
-
"我执行了原子加法,它似乎也是错误的。" 你怎么知道的?也就是除了的打印顺序,你凭什么说原子加法不正确呢?我对调试打印了解不多,但我不知道它们以任何方式尊重执行顺序。
-
因为原子操作是在
Finish调试打印之前执行的。如果在打印之前调用了 3 个原子操作,在打印之后调用了 1 个,那么日志将包含数字 3。我希望在打印之前执行所有原子操作,因此它应该在日志中显示 4。整个问题是屏障应该确保所有原子在打印之前执行 -
应该吗?标准中是否有任何内容要求打印语句遵循任何类型的同步?
-
子组屏障。如果您如此反对使用调试打印的想法,还有另一件事。我真的很关心
some_variable的最终值,我将它保存到缓冲区中,以便以后使用。我保存到缓冲区的点发生在屏障之后的 if 语句中。因此,错误的不仅仅是印刷。这也是我保存到该缓冲区中的值。 -
您是否启用了内存模型扩展?每次调用的
some_condition和end_condition的值是多少?我可以假设这是计算着色器吗?
标签: gpgpu vulkan barrier gpu-atomics