2.3 The CUDA programming model
2.3.1 Blocks and Threads
When a kernel method is executed, the execution is distributed over a grid of thread blocks as illustrated below in Fig. 2.2. Each of the thread blocks contains a subset of parallel CUDA threads which all in turn execute the kernel method.
Grid ... Block (0,0) ... Block (1,0) ... Block (2,0) ... Block (3,0) ... Block (0,1) ... Block (1,1) ... Block (2,1) ... Block (3,1)
Blocks
Within the CUDA grid, thread blocks can be arranged into a one-dimensional, two- dimensional or three-dimensional formation [30] (an example of a two-dimensional arrangement can be seen in Fig. 2.2). Each thread block contains a number of parallel threads that can only directly interact with threads within the thread block. Each block has a unique position in the grid and this can be determined at run time by each thread. The number of thread blocks and their dimensional arrangement can be defined by the application before or after compilation and will vary depending on the usage. For example, when working with images, a 2-dimensional arrangement is best suited as this matches the two-dimensional array holding the pixel data (although a one-dimensional array could be used with additional calculations to determine the position of the block within the grid).
NVIDIA note [30] that thread blocks must be required to execute independently and this is key to the mapping of thread blocks to SMs. There can be no inferred or presumed order of execution and parallelism between thread blocks. As a result each block must be self contained. The number of thread blocks chosen for each kernel execution should be sufficiently high so as to utilise all of the parallel cores available. During execution we must assume that each block could be the first or the last block to execute. This execution independence allows blocks to be distributed across a variable number of processing elements and executed in parallel [30]. However, as multiple blocks can execute in parallel we must be aware of data integrity when accessing memory outside of the block thus necessitating the use of atomic operators (see 2.3.3) that respect the order of access. Farber notes [29] that NVIDIA’s insistence on thread block independence might also be indicative of transparent scheduling execution across multiple devices in future releases.
Threads
Within each thread block is a collection of parallel threads. Older cards such
as the G80 can support up to 768 parallel threads per block and more recent Fermi cards increased that limit to 1024. The total number of threads per kernel execution is equal to the number of threads per thread block multiplied by the
number of thread blocks. For example executing 128 thread blocks each with 1024 threads would result in 131072 threads each executing the same kernel. Each parallel thread is only able to communicate with other threads in that block and this is facilitated by shared memory. Within a block, threads execute in parallel in smaller sub-blocks known as warps. Each thread warp contains either 16 or 32 threads depending on the compute version and generation of CUDA GPU. There is no guarantee as to what order the warps will execute in; however, within a warp threads can communicate directly with each other using warp level primitives such
as ballot(). As with thread blocks, the number of threads per block is configurable
at runtime but each thread block must have the same number of threads. As a general rule, CUDA applications must keep the GPU busy and achieve a high level of occupancy. NVIDIA recommend that at least 64 threads are used per thread block [30]. Threads can also be configured into a one-dimensional, two-dimensional or three-dimensional formation. The arrangement of threads within a thread block is user configurable and certain configurations may suit particular applications [30]. For optimum performance, all threads within a warp must follow the same execution path. Developers must pay attention to the control flow of each warp so as to avoid branching conditional statements. For example, if statements may cause the warp to branch as CUDA cannot process the resulting multiple paths from this branching statement in parallel. When threads follow different execution paths within a warp, the execution is serialised resulting in a loss of parallelism within the warp until after the branching region has finished execution. This process of warp serialisation due to branching is known as warp divergence and must be avoided if possible. To avoid warp divergence we must ensure that all threads strictly follow the same control flow throughout the kernel execution even if this results in performing redundant computation to align all threads within the warp.
The CUDA programming model allows applications to support fine-grained parallelism capable of instantiating hundreds of thousands of parallel threads for execution on a single task. In contrast CPU applications often apply coarse-grained parallelism typically focusing a significantly lower number of threads on distinctly different tasks. As each of the threads executes the same kernel, we can classify this parallel model of execution as single-program, multiple-data (SPMD) [5].