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.
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 size16x16
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 new16x16
workgroup just for that last row/column $\endgroup$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 theglDispatchCompute
parameters such that they either match your number of pixels/ data items or exceed them. $\endgroup$