Mapped pinned memory can be used to overlap PCI Express transfers and kernel processing – especially for device→host copies, where there is no need to cover the long latency to host memory. Mapped pinned memory has stricter alignment requirements than the native GPU memcpy, since they must be coalesced. Uncoalesced memory transactions run 2-6 times slower when using mapped pinned memory.
A naïve port of our concurrencyMemcpyKernelMapped.cu program yields
an interesting result: on a cg1.4xlarge instance in Amazon EC2, mapped
pinned memory runs very slowly for values of cycles below 64:
| Cycles | Mapped | Streamed | Speedup |
|---|---|---|---|
| 8 | 95.15 | 43.61 | 0.46 |
| 16 | 96.70 | 43.95 | 0.45 |
| 24 | 95.45 | 44.27 | 0.46 |
| 32 | 97.54 | 44.61 | 0.46 |
| 40 | 94.09 | 44.93 | 0.48 |
| 48 | 94.25 | 45.26 | 0.48 |
| 56 | 95.18 | 46.19 | 0.49 |
| 64 | 28.22 | 49.29 | 1.75 |
| 72 | 31.58 | 52.38 | 1.66 |
| ... | |||
| 208 | 92.59 | 104.60 | 1.13 |
| 216 | 96.11 | 107.68 | 1.12 |
For small values of cycles, the kernel takes as long to
run as if cycles were greater than 200! Only NVIDIA can
discover the reason for this performance anomaly for certain, but it is
not difficult to work around: by unrolling the inner loop of the kernel,
we create more work per thread, and performance improves.
template<const int unrollFactor>__device__ voidAddKernel_helper( int *out, const int *in, size_t N, int increment, int cycles ){ for ( size_t i = unrollFactor*blockIdx.x*blockDim.x+threadIdx.x; i < N; i += unrollFactor*blockDim.x*gridDim.x ) { int values[unrollFactor]; for ( int iUnroll = 0; iUnroll < unrollFactor; iUnroll++ ) { size_t index = i+iUnroll*blockDim.x; values[iUnroll] = in[index]; } for ( int iUnroll = 0; iUnroll < unrollFactor; iUnroll++ ) { for ( int k = 0; k < cycles; k++ ) { values[iUnroll] += increment; } } for ( int iUnroll = 0; iUnroll < unrollFactor; iUnroll++ ) { size_t index = i+iUnroll*blockDim.x; out[index] = values[iUnroll]; } }} __device__ voidAddKernel( int *out, const int *in, size_t N, int increment, int cycles, int unrollFactor ){ switch ( unrollFactor ) { case 1: return AddKernel_helper<1>( out, in, N, increment, cycles ); case 2: return AddKernel_helper<2>( out, in, N, increment, cycles ); case 4: return AddKernel_helper<4>( out, in, N, increment, cycles ); }}
Note that this version of AddKernel() is functionally identical to
the previous one6 – it just computes
unrollFactor outputs per loop iteration. Since the unroll
factor is a template parameter, the compiler can use registers to hold
the values array and the innermost for loops can be unrolled
completely.
For unrollFactor==1, this implementation is identical to
that of Listing 6-3. For unrollFactor==2, mapped pinned
formulation shows some improvement over the streamed formulation; the
tipping point drops from cycles==64 to
cycles==48. For unrollFactor==4, performance
is uniformly better than the streamed version:
| Cycles | Mapped | Streamed | Speedup |
|---|---|---|---|
| 8 | 36.73 | 43.77 | 1.19 |
| 16 | 34.09 | 44.23 | 1.30 |
| 24 | 32.21 | 44.72 | 1.39 |
| 32 | 30.67 | 45.21 | 1.47 |
| 40 | 29.61 | 45.90 | 1.55 |
| 48 | 26.62 | 49.04 | 1.84 |
| 56 | 32.26 | 53.11 | 1.65 |
| 64 | 36.75 | 57.23 | 1.56 |
| 72 | 41.24 | 61.36 | 1.49 |
These values are given for 32M integers, so the program reads and writes 128MB of data. For cycles==48, the program runs in 26ms. To achieve that effective bandwidth rate (more than 9GB/s over PCI Express 2.0), the GPU is concurrently reading and writing over PCI Express while performing the kernel processing!
The slowdown for small cycles, and the loop unrolling
that works around it, belong to the hardware of the first edition. A
modern GeForce RTX 3060 shows no such cliff: the naïve mapped-pinned
kernel of Listing 6-3 runs neck-and-neck with the streamed formulation –
both about 6ms for 32M integers – at every value of cycles,
so the unrolling of Listing 6-6 is no longer needed to make mapped
pinned memory competitive.
Except that as written, N must be divisible
by unrollFactor. This limitation is easily addressed, of
course, with a small change to the for loop and a bit of
cleanup code afterward.↩︎