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.

13.4 Stream Compaction

Scan implementations often operate on predicates, truth values (0 or 1) computed by evaluating a condition. As mentioned at the beginning of the chapter, an exclusive scan of predicates implements stream compaction, a class of parallel problems where only the “interesting” elements of an input array are written to the output: where the predicate is 1 for an interesting element, its exclusive-scan value gives that element’s output index.

Because we already have a single-pass scan, compaction can be done in a single pass too – which is how CUB’s DeviceSelect works. As an example, let’s compact an array of int down to its odd values5. One thread block processes one tile of b elements: it evaluates the predicate (isOdd()) and inclusive-scans the resulting 0/1 flags in shared memory. That scan gives each kept element its index among the tile’s survivors, and its last element is the tile’s keep count. The cooperative look-back of Section 13.3.4 then supplies the number of elements kept by all earlier tiles – this tile’s base output index – and each surviving element is scattered to out[base + within-tile index]. The highest-numbered tile writes the grand total.

The only differences from the decoupled-look-back scan are that we scan the 0/1 predicate flags rather than the input values, and scatter the survivors rather than writing a scanned value in place. Listing 13-17 gives the kernel.

template<class T>__host__ __device__ boolisOdd( T x ){    return x & 1;} template<class T>__global__ voidstreamCompact_odd_kernel(    T *out,    int *outCount,                    // total kept (written by the last tile)    const T *in,    volatile scanStatus *status,      // one descriptor per tile (SCAN_X-initialized)    uint32_t *tileCounter,            // one global counter, 0-initialized    uint32_t numTiles,    size_t N ){    extern __shared__ int sPartials[];   // blockDim.x predicate flags    __shared__ uint32_t s_tile;    __shared__ int s_base;               // number of elements kept by earlier tiles     if ( threadIdx.x == 0 )        s_tile = atomicAdd( tileCounter, 1 );    __syncthreads();    const uint32_t tile = s_tile;    const size_t gidx = (size_t) tile * blockDim.x + threadIdx.x;     //    // Evaluate the predicate, then inclusive-scan the 0/1 flags in shared    // memory (Kogge-Stone). sPartials[blockDim.x-1] is the tile's keep count.    //    T value = (T) 0;    int pred = 0;    if ( gidx < N ) {        value = in[gidx];        pred = isOdd( value ) ? 1 : 0;    }    sPartials[threadIdx.x] = pred;    __syncthreads();    for ( int off = 1; off < blockDim.x; off <<= 1 ) {        int add = ( threadIdx.x >= off ) ? sPartials[threadIdx.x - off] : 0;        __syncthreads();        sPartials[threadIdx.x] += add;        __syncthreads();    }    const int aggregate = sPartials[blockDim.x - 1];     //    // Cooperative look-back over the per-tile keep counts: s_base is the number    // of elements kept by every earlier tile -- this tile's base output index.    //    scanCoopLookback<int>( status, tile, aggregate, s_base );    __syncthreads();     //    // Scatter. sPartials[threadIdx.x] is the inclusive keep count, so    // sPartials[threadIdx.x]-1 is this element's index among the tile's kept    // elements; s_base offsets it into the global output.    //    if ( gidx < N && pred )        out[s_base + sPartials[threadIdx.x] - 1] = value;     if ( tile == numTiles - 1 && threadIdx.x == 0 )        *outCount = s_base + aggregate;}
Listing 13-17. streamCompact_odd_kernel() (source on GitHub)

Listing 13-18 gives the host function; like the scan’s host code (Listing 13-15), it allocates the per-tile descriptor array and the tile counter, initializes them, and makes a single kernel launch.

template<class T>voidstreamCompact_odd( T *out, int *outCount, const T *in, size_t N, int b ){    cudaError_t status_cudart;    scanStatus *gStatus = 0;    uint32_t *tileCounter = 0;     if ( N == 0 )        return;     uint32_t numTiles = (uint32_t) ( ( N + b - 1 ) / b );     //    // gStatus and tileCounter are transient per-call scratch.  Allocate and    // zero them stream-ordered on the default stream so repeated calls (e.g.    // a timing loop) recycle the same pool memory instead of paying for a    // synchronizing cudaMalloc/cudaFree on every invocation.    //    cuda(MallocAsync( &gStatus, numTiles * sizeof(scanStatus), 0 ) );    cuda(MemsetAsync( gStatus, 0, numTiles * sizeof(scanStatus), 0 ) );   // SCAN_X == 0    cuda(MallocAsync( &tileCounter, sizeof(uint32_t), 0 ) );    cuda(MemsetAsync( tileCounter, 0, sizeof(uint32_t), 0 ) );     streamCompact_odd_kernel<T><<<numTiles, b, b * sizeof(int)>>>(        out, outCount, in, gStatus, tileCounter, numTiles, N ); Error_cudart:    if ( gStatus )     cudaFreeAsync( gStatus, 0 );    if ( tileCounter ) cudaFreeAsync( tileCounter, 0 );}
Listing 13-18. streamCompact_odd() host function. (source on GitHub)

  1. The code is easily modified to evaluate more-complicated predicates.↩︎