0
$\begingroup$

I've been going round in circles trying to understand OpenGL compute shader "work groups" and it's doing my head in. It's all about as clear as mud.

In my compute program I have to set a local size with something like this (for example):

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

So what happens if I need to use this shader on an image that's 17x17 pixels? There doesn't seem to be any way to tell OpenGL the actual size of the computation. glDispatchCompute() accepts 3 numbers but none of them are the number "17"?

Do I have to check the coordinates returned by gl_GlobalInvocationID in the shader to see if they're in range?

$\endgroup$
2
  • 1
    $\begingroup$ glDispatchCompute tells how many workgroups you want to launch in each dimension. local_size sets the size of those workgroups in each dimension. If you launch only 1 workgroup in X,Y with the size 16x16 Than whatever you do will happen for only that part of the image. The 17th row/column will be left blank. If you launch more than 1 workgroup than it'll cover that area as well though it's not going to be optimized as you'd be assigning a new 16x16 workgroup just for that last row/column $\endgroup$ Commented May 3, 2023 at 7:19
  • 1
    $\begingroup$ The general rule used to be to set local_size anywhere from 32 (NVIDIA) to 64 (AMD) or it's multiple. Preferably 32/64 due to the way architecture was designed. See what works for your gpu now or read the specification on what the wavefront/SM size is. Then use the glDispatchCompute parameters such that they either match your number of pixels/ data items or exceed them. $\endgroup$ Commented May 3, 2023 at 7:30

2 Answers 2

1
$\begingroup$

First of all, let me explain how local_size_x local_size_y local_size_z and glDispatchCompute(x, y, z) are related to each other:

The workgroup size can be defined within the shader with the following command: layout (local_size_x = ..., local_size_y = ..., local_size_z = ...) in; The workgroup size is defined by the multiplication of all 3 local_sizes: workgroupSize = local_size_x * local_size_y * local_size_z

The glDispatchCompute(x, y, z) command executes the defined workgroup x * y * z times.

For example when saying: layout (local_size_x = 2, local_size_y = 3, local_size_z = 4) in; with glDispatchCompute(5, 6, 7) you will have 5 * 6 * 7 = 210 workgroups where workgroup has 2 * 3 * 4 = 24 invocations each.

Within the compute shader you have the following built-in input variables: The following information is copied from khronos website

in uvec3 gl_NumWorkGroups; // This variable contains the number of work groups passed to the dispatch function.
in uvec3 gl_WorkGroupID; // This is the current work group for this shader invocation. Each of the XYZ components will be on the half-open range [0, gl_NumWorkGroups.XYZ).
in uvec3 gl_LocalInvocationID; // This is the current invocation of the shader within the work group. Each of the XYZ components will be on the half-open range [0, gl_WorkGroupSize.XYZ).
in uvec3 gl_GlobalInvocationID; // This value uniquely identifies this particular invocation of the compute shader among all invocations of this compute dispatch call. It's a short-hand for the math computation: gl_WorkGroupID * gl_WorkGroupSize + gl_LocalInvocationID;
in uint  gl_LocalInvocationIndex; // This is a 1D version of gl_LocalInvocationID. It identifies this invocation's index within the work group. It is short-hand for this math computation: 
// gl_LocalInvocationIndex =
//     gl_LocalInvocationID.z * gl_WorkGroupSize.x * gl_WorkGroupSize.y +
//     gl_LocalInvocationID.y * gl_WorkGroupSize.x + 
//     gl_LocalInvocationID.x;
const uvec3 gl_WorkGroupSize;   // GLSL ≥ 4.30. The gl_WorkGroupSize variable is a constant that contains the local work-group size of the shader, in 3 dimensions. It is defined by the layout qualifiers local_size_x/y/z. This is a compile-time constant.

With these built-in variables you can figue out, which invocation is the current.

Each workgroup shares memory where each invocation within that workgroup can access to. This memory can be defined in the following way:

shared vec3 variableName;

The variable can NOT be initialized by declaration! The shared memory is faster in access than the global memory (ShaderStorageBufferObjects [SSBO]). So after identifying which invocation belongs to which pixel within an image, each invocation can load the one pixel data from the image (global memory (slow)) and stores the values to shared memory, so that other invocations within the same workgroup can access them. This is faster than telling each invocation to load multiple pixels directly from the image... Have in mind, this shared memory is very limited!

Lets go back to the workgroup size. The values of the example from above were choosen to let you understand how the workgroup size and the glDispatchCompute()command are related to each other. But this workgroup size is choosen badly! And following reason tells you why: On GPUs the work units (each executing one invocation) are grouped to so called warps (Nvidia) and waves (AMD). The vendor (Nvidia and AMD) have different sizes for these warps | waves. Nvidia group 32 work units to one warp, where AMD group 64 work units to one wave. Your workgroup size should be a multiple of your vendors warp(Nvidia), wave(AMD) size. Because when for example defining the workgroup size to be 1, the other 31(Nvidia) 63(AMD) work units will be blocked...

Thats not all... The next importent thing which you need to know when defining the workgroup size is: Each work unit within a workgroup calculate the same arithmetic operation. So when you have a conditional state within your shader (if(...){...} else {...}) then it would be good, if each invocation within the workgroup goes to the same branch (all should go to the if or all should go to the else branch). Because otherwise, when some invocations within the workgroup go to the if branch, all other invocation which would go to the else branch will wait until the if branch has finished their work. Then the else branch will start its work while the invocations from the if branch will wait till it is finished. So your shader will be very slow.

As you can see, when defining the workgroup size several aspects have to be taken care of.

$\endgroup$
3
  • $\begingroup$ So I have to do "if ((gl_GlobalInvocationID.x < 17) && (gl_GlobalInvocationID.y < 17))" in my shader? $\endgroup$ Commented May 3, 2023 at 20:00
  • $\begingroup$ Yes. You check if the global invocation ID hits a pixel within your texture. And if it does, you do your calculations $\endgroup$
    – Thomas
    Commented May 4, 2023 at 4:50
  • $\begingroup$ Thanks for the explanation. $\endgroup$ Commented May 5, 2023 at 12:50
-1
$\begingroup$

Short answer: Yes I have to check the coordinates.

If I set a 16x16x1 local size then use it to process a 17x17 image I have to use glDispatchCompute(2,2,1).

This generates four workgoups with size 16x16 even though the blocks to be processed have sizes 16x16, 16x1, 1x16 and 1x1

Inside the shader I have to check the range of the xy coordinates to make sure I don't do an imageStore() outside the image.

$\endgroup$
3
  • 1
    $\begingroup$ in case they do not communicate with each other, a smaller workgroup size could be better: For example when setting the workgroup size to 8x8x1 and execute glDispatchCompute(3,3,1) you generate 9 workgroups with 64 invocations each. where the used invocations are: 8x8, 8x8, 8x1, 8x8, 8x8, 8x1, 1x8, 1x8, 1x1. total invocations: 576, used invocations: 289. And the 16x16x1 solution: total invocations: 1024. Used invocations: 289 $\endgroup$
    – Thomas
    Commented May 5, 2023 at 12:56
  • $\begingroup$ Or the best option for a 17x17 image: use a workgroup size of 17x17... generated invocations: 289 blocked work units: 31 (Nvidia) 31(AMD). You only should stick to a workgroup size of 32/64 in case of using multiple workgroups... in this case we only need 1 workgroup. $\endgroup$
    – Thomas
    Commented May 5, 2023 at 13:03
  • $\begingroup$ Sure, but the question isn't about work optimal group sizes. I wasn't sure if the driver would call the shader outside the area of the image or not. In hindsight it makes sense that the driver shouldn't assume that I want exactly one shader invocation per pixel in the output image. The available documentation is a bit opaque though. $\endgroup$ Commented May 6, 2023 at 14:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.