Vulkan subgroupBarrier 不同步调用

Vulkan subgroupBarrier does not synchronize invokations

我有一个有点复杂的过程,其中包含嵌套循环和一个 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() performs a full memory and execution barrier - basically when an invocation returns from subgroupBarrier() we are guaranteed that every invocation executed the barrier before any return, and all memory writes by those invocations are visible to all invocations in the subgroup.

有趣的是,我尝试将 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)

我找到了我的代码不起作用的原因。所以事实证明我误解了 subgroupBarrier() 究竟是如何决定同步哪些线程的。如果线程处于非活动状态,则它不会参与屏障。不活动线程稍后是否会变为活动线程并最终到达屏障并不重要。

这两个循环是不等价的(尽管看起来是)

while(true){
   if(end_condition){
      break;
   }
}
subgroupBarrier();
some_function();

while(true){
   if(end_condition){
      subgroupBarrier();
      some_function();
      return;
   }
}

如果所有线程在完全相同的迭代中达到结束条件,则没有问题,因为所有线程同时处于活动状态。

当不同的线程可能在不同的迭代中退出循环时,就会出现此问题。如果线程 A 在 2 次迭代后通过结束条件并且线程 B 在 3 次迭代后通过结束条件,然后当 A 处于非活动状态并等待 B 完成时,它们之间将有一个完整的迭代。

第一种情况,A先到达break,然后B再到达break,最后两个线程都退出循环到达barrier。

在第二种情况下,A会先到达结束条件并执行if语句,而B会处于非活动状态,等待A完成。当 A 到达屏障时,它将是该时间点唯一活动的线程,因此它将通过屏障而不与 B 同步。然后 A 将完成 if 语句 reach return 的主体执行并变为非活动状态。然后 B 实际上会再次激活并完成其迭代的执行。然后在下一次迭代中它将达到结束条件和屏障,ti 将再次成为唯一的活动线程,因此屏障不必同步任何东西。