Prefer to read without ads? Become a member — from $10/month — and support the work. Already a member? Log in to read ad-free on this device.

6.7 Concurrent Kernel Processing

Fermi-class and later GPUs are capable of concurrently running multiple kernels, provided they are launched in different streams and with block sizes that are small enough that a single kernel will not fill the whole GPU. The code of Listing 6-5 (Lines 9-14) will cause kernels to run concurrently, provided the number of blocks in each kernel launch is small enough. Since the kernels can only communicate through global memory, we can add some instrumentation to AddKernel() to track how many kernels are running concurrently. Using a “kernel concurrency tracking” structure:

static const int g_maxStreams = 8;
typedef struct KernelConcurrencyData_st {
    int mask; // mask of active kernels
    int maskMax; // atomic max of mask popcount
    int masks[g_maxStreams];
    int count; // number of active kernels
    int countMax; // atomic max of kernel count
    int counts[g_maxStreams];
} KernelConcurrencyData;

we can add code to AddKernel() to “check in” and “check out” at the beginning and end of the function. The “check in” takes the “kernel id” parameter kid (a value in the range 0..NumStreams-1 passed to the kernel), OR’s a mask corresponding to the kernel ID into a global, and records that kernel’s view of the global. Note that atomicOr() returns the value that was in the memory location before the OR was performed.

Similarly, this code tracks the number of active kernels by incrementing kernelData-\>count and calling atomicMax() on a shared global.

    // check in, and record active kernel mask and count
    // as seen by this kernel.
    if ( kernelData && blockIdx.x==0 && threadIdx.x == 0 ) {
        int myMask = atomicOr( &kernelData->mask, 1<<kid );
        kernelData->masks[kid] = myMask | (1<<kid);
        int myCount = atomicAdd( &kernelData->count, 1 );
        atomicMax( &kernelData->countMax, myCount+1 );
        kernelData->counts[kid] = myCount+1;
    }

At the bottom of the kernel, similar code clears the mask and decrements the active-kernel count:

    // check out
    if ( kernelData && blockIdx.x==0 && threadIdx.x==0 ) {
        atomicAnd( &kernelData->mask, ~(1<<kid) );
        atomicAdd( &kernelData->count, -1 );
    }

The kernelData parameter refers to a __device__ variable declared at file scope:

__device__ KernelConcurrencyData g_kernelData;

Remember, the pointer to g_kernelData must be obtained by calling cudaGetSymbolAddress(). It is possible to write code that references &g_kernelData, but CUDA’s language integration will not correctly resolve the address.

The concurrencyKernelKernel.cu program adds support for a command line option blocksPerSM to specify the number of blocks with which to launch these kernels. It will generate a report on the number of kernels that were active. Two sample invocations of concurrencyKernelKernel are as follows:

$ ./concurrencyKernelKernel --blocksPerSM 2
Using 2 blocks per SM on GPU with 14 SMs = 28 blocks
Timing sequential operations... Kernel data:
    Masks: ( 0x1 0x0 0x0 0x0 0x0 0x0 0x0 0x0 )
    Up to 1 kernels were active: (0x1 0x0 0x0 0x0 0x0 0x0 0x0 0x0 )
Timing concurrent operations...
Kernel data:
    Masks: ( 0x1 0x3 0x7 0xe 0x1c 0x38 0x60 0xe0 )
    Up to 3 kernels were active: (0x1 0x2 0x3 0x3 0x3 0x3 0x2 0x3 )

$ ./concurrencyKernelKernel --blocksPerSM 3
Using 3 blocks per SM on GPU with 14 SMs = 42 blocks
Timing sequential operations... Kernel data:
    Masks: ( 0x1 0x0 0x0 0x0 0x0 0x0 0x0 0x0 )
    Up to 1 kernels were active: (0x1 0x0 0x0 0x0 0x0 0x0 0x0 0x0 )
Timing concurrent operations... Kernel data:
    Masks: ( 0x1 0x3 0x6 0xc 0x10 0x30 0x60 0x80 )
    Up to 2 kernels were active: (0x1 0x2 0x2 0x2 0x1 0x2 0x2 0x1 )

Note that blocksPerSM is the number of blocks specified to each kernel launch, so a total of numStreams\*blocksPerSM blocks are launched in numStreams separate kernels. You can see that the hardware can run more kernels concurrently when the kernel grids are smaller, but there is no performance benefit to concurrent kernel processing for the workload discussed in this chapter.