Views, memory spaces, mirrors, layouts, roofline
After this lecture, you can
deep_copy patternLayoutRight / LayoutLeft) and index orders that perform on both architecturesstd::shared_ptr) – assignment shares, does not copyKokkos::deep_copy(dst, src) for actual data transferRuntime-sized dimensions must come first. Use compile-time extents when sizes are known to help the compiler.
| Kokkos space | Maps to |
|---|---|
HostSpace |
CPU RAM |
CudaSpace / HIPSpace |
GPU global memory |
CudaUVMSpace / HIPManagedSpace |
Unified memory |
ScratchMemorySpace |
Shared memory / LDS |
Every view has a memory space set at compile time:
Figure 1: Host and device are separate address spaces; the only legal way to move data is an explicit deep_copy. The red crosses mark the two illegal access patterns we are about to attempt.
When you call Kokkos::parallel_for(...):
parallel_for copies the functor (the closure, ~tens of bytes) to the device, runs the kernel, and releases the device-side functor copyThe view data is not copied – views are like pointers; the lambda captures them by value (the pointer copies, the data does not).
Wrong: the host for loop touches GPU memory directly – the first red X in Figure 1 (host code dereferences d_view).
Wrong: the CUDA kernel cannot dereference a host pointer – the second red X in Figure 1 (device kernel dereferences h_view).
We need a way to keep both copies and move data between them on demand.
The canonical Kokkos recipe for I/O:
// 1. Allocate the device view
using view_t = View<double*, Kokkos::CudaSpace>;
view_t view("data", size);
// 2. Create a host-side mirror
auto host_view = Kokkos::create_mirror_view(view);
// 3. Populate on the host (from file, etc.)
for (int i = 0; i < size; ++i) host_view(i) = read_from_file();
// 4. Deep copy host → device
Kokkos::deep_copy(view, host_view);
// 5. Run the kernel on the device
Kokkos::parallel_reduce("L", RangePolicy<Cuda>(0, size),
KOKKOS_LAMBDA(const int i, double& s) { s += view(i); }, sum);
// 6. Deep copy device → host (if needed for output)
Kokkos::deep_copy(host_view, view);create_mirror vs create_mirror_viewcreate_mirror – always allocates a new array on the hostcreate_mirror_view – allocates only if needed: if the view already lives on the host, the mirror simply references the same dataThe _view form is the one you usually want – it makes the same code do nothing extra on a CPU build and the right thing on a GPU build.
Reminder: Kokkos never performs a hidden deep copy.
A processor juggles two very different “operands”:
The architectures differ in whether a thread individually issues and executes its instructions.
CPUs fetch memory in 64-byte cache lines. Access data[0] → data[0]–data[7] (doubles) loaded into L1.
Sequential: ~87% hit rate, effective latency ≈ L1 (few ns)
Random: ~0% hit rate, every access pays full DRAM latency (100–300 ns)
⇒ 10–100× performance difference from access pattern alone.
32 threads in a warp issue loads simultaneously.
Coalesced (thread \(i\) reads data[i]):
→ 32 addresses merge into one 128-byte transaction. Full bandwidth.
Non-coalesced (thread \(i\) reads data[i * STRIDE]):
→ Up to 32 separate transactions. 32× bandwidth waste.
Rule: ensure unit-stride access within a warp.
Figure 2: CPU: thread \(t\)’s next access at \(i+1\) → cached. GPU: thread \(t+1\)’s current access at \(i+1\) → coalesced.
Both reward unit-stride memory access – but along different axes of your data.
⇒ You usually need different memory layouts on CPU and GPU. Pure portable code (OpenMP, OpenACC) can’t help with this. Kokkos can.
A View’s data is a flat 1-D buffer. The shape (n×n) and layout only define a map from indices to a linear offset — they move no bytes.
LayoutRight: offset = \(i\cdot n + j\)LayoutLeft : offset = \(i + j\cdot n\)A(1,2) is offset 5 under Right, 7 under Left.
View<T**, Layout, Space> – the layout is a template parameter.
Figure 3: LayoutRight: the rightmost index is stride-1. LayoutLeft: the leftmost index is stride-1.
| Layout | Stride-1 axis | Default for |
|---|---|---|
LayoutRight |
rightmost (row-major, C-style) | HostSpace |
LayoutLeft |
leftmost (column-major, Fortran-style) | CudaSpace / HIPSpace |
If you don’t specify a layout, Kokkos picks the right one for the memory space – that is the whole point of letting Kokkos choose.
Kokkos maps work indices differently per execution space:
OpenMP, Threads): contiguous chunks per thread → cachingCuda, HIP): strided across threads in a warp → coalescingRule of thumb: write your views so the iteration index is the first index of every view access. Combined with the default layouts, this gives cached access on CPU and coalesced access on GPU – the same source code.
Figure 4: Bandwidth (GB/s) vs. number of rows N for the inner-product kernel ⟨y|Ax⟩, run on Haswell (HSW), Knights Landing (KNL) and Pascal P100. LayoutLeft is fast on the GPU; LayoutRight is fast on the CPUs. From the Sandia Kokkos tutorial (SAND2020-7475).
Arithmetic intensity: \(I = \text{flops} / \text{bytes transferred}\)
\[P \leq \min(\pi,\; I \cdot \beta)\]
Ridge point for A100: \(I^* = 9.7\text{ Tflop/s} / 2.0\text{ TB/s} \approx 4.9\) flop/byte.
| Operation | \(I\) (flop/byte) |
|---|---|
| Vector addition | 0.04 |
| Dot product | 0.13 |
| 5-point stencil | ~0.3 |
| LBM collision | ~0.7 |
| Dense GEMM (\(N\)=1000) | ~80 |
Everything except dense matrix-matrix multiply is deeply memory-bound.
⇒ Optimize memory access patterns, not arithmetic.
LayoutLeft vs LayoutRight) directly impacts performancedeep_copy are how you bridge host and device cleanlyperf stat -e cache-misses (CPU) or bandwidth measurement (GPU)HPC: Parallelization on GPUs and Accelerators