Library Composition: Kokkos+HYPRE
This demo uses two libraries that are already built into the ecpe4s/e4s-cuda:26.06 images – Kokkos and Hypre – and links a small application against both of them:
- Kokkos builds a right-hand-side vector on the GPU
- Hypre solves the resulting linear system with PCG
- Kokkos reduces the solution back on the GPU
Other platforms:
ecpe4s/e4s-cuda:26.06specifies the CUDA/NVIDIA-GPU x86_64 image used below, defaulting to cuda90 build targets. ecpe4s/e4s-cuda:26.06-cuda80 and ecpe4s/e4s-cuda:26.06-cuda120 are available for different GPU hardware. E4S also publishes CPU-only, Intel oneAPI, and ROCm images (see the Container Download page). Packages from the CPU image missing on a GPU image can be pulled in as a prebuilt, signed binaries from the E4S Spack build cache instead of compiled from source.
1. Run the E4S CUDA Container
Start the container in interactive mode with GPU access (Consider mounting a writable directory on the host to save your work):
docker run --gpus all -it --rm \
-w /tmp/work \
ecpe4s/e4s-cuda:26.06
Verify the GPU is visible from inside the container:
nvidia-smi
2. Discover What Spack Already Provides
Ask Spack what packages are installed in this image by explicit request:
spack find -x
Look for kokkos and hypre in the listing.
The container carries a broad set of tools. Build tools and other dependencies, every package Spack installed, will be shown by:
spack find
Check the exact variant strings before loading to find the package configured for your needs. For example, since GPU-enabled images typically add a +cuda variant you can try the search below. -l will print the unique spack hash assigned to the install and -v will print the variants:
spack find -lxv kokkos+cuda
spack find -lxv hypre+cuda
3. Load Kokkos and Hypre
Load both packages, matching the +cuda variant reported above, using the hash value so Spack knows the exact package you want. (Hash values will differ between different images).
KOKKOS_HASH=$(spack find --format '/{hash}' -x kokkos+cuda)
HYPRE_HASH=$(spack find --format '/{hash}' -x hypre+cuda)
spack load $KOKKOS_HASH
spack load $HYPRE_HASH
Confirm they’re loaded and record their install prefixes – your build will need these:
spack find --loaded
KOKKOS_DIR=$(spack location -i $KOKKOS_HASH)
HYPRE_DIR=$(spack location -i $HYPRE_HASH)
echo "KOKKOS_DIR=$KOKKOS_DIR"
echo "HYPRE_DIR=$HYPRE_DIR"
4. Write a Small App on Top of Both Libraries
Create and enter a project directory:
mkdir -p src
cd src
Create CMakeLists.txt:
cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.18)
project(e4s_dev_demo LANGUAGES CXX)
find_package(Kokkos REQUIRED)
add_executable(dev_demo main.cpp)
target_link_libraries(dev_demo PRIVATE Kokkos::kokkos)
if(DEFINED HYPRE_DIR)
target_include_directories(dev_demo PRIVATE ${HYPRE_DIR}/include)
target_link_directories(dev_demo PRIVATE ${HYPRE_DIR}/lib)
target_link_libraries(dev_demo PRIVATE HYPRE)
endif()
find_package(MPI REQUIRED)
target_link_libraries(dev_demo PRIVATE MPI::MPI_CXX)
EOF
Create main.cpp:
cat > main.cpp <<'EOF'
#include <Kokkos_Core.hpp>
#include <mpi.h>
#include <HYPRE.h>
#include <HYPRE_IJ_mv.h>
#include <HYPRE_parcsr_ls.h>
#include <cstdio>
#include <utility> // For std::make_pair
// Problem size -- change this and rebuild to see Step 6 in action.
static const int n = 1000;
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
Kokkos::initialize(argc, argv);
{
int rank, nprocs;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &nprocs);
int rows_per_rank = n / nprocs;
int remainder = n % nprocs;
int local_n = rows_per_rank + (rank < remainder ? 1 : 0);
int ilower = rank * rows_per_rank + (rank < remainder ? rank : remainder);
int iupper = ilower + local_n - 1;
// Build the right-hand side on the GPU with Kokkos.
Kokkos::View<double*> d_rhs("rhs", local_n);
Kokkos::parallel_for("fill_rhs", local_n, KOKKOS_LAMBDA(const int i) {
d_rhs(i) = 1.0;
});
// We'll feed d_rhs directly to HYPRE!
if (rank == 0) {
printf("Kokkos execution space: %s\n",
Kokkos::DefaultExecutionSpace::name());
}
// Hand the system over to Hypre for the actual solve.
HYPRE_Init();
// Explicitly instruct HYPRE to use GPU memory and execute on the GPU
HYPRE_SetMemoryLocation(HYPRE_MEMORY_DEVICE);
HYPRE_SetExecutionPolicy(HYPRE_EXEC_DEVICE);
HYPRE_IJMatrix A;
HYPRE_IJMatrixCreate(MPI_COMM_WORLD, ilower, iupper, ilower, iupper, &A);
HYPRE_IJMatrixSetObjectType(A, HYPRE_PARCSR);
HYPRE_IJMatrixInitialize(A);
// Pre-calculate non-zeros and allocate Host views
int max_nnz = 3 * local_n;
Kokkos::View<int*, Kokkos::HostSpace> h_ncols("ncols", local_n);
Kokkos::View<int*, Kokkos::HostSpace> h_rows("rows", local_n);
Kokkos::View<int*, Kokkos::HostSpace> h_cols("cols", max_nnz);
Kokkos::View<double*, Kokkos::HostSpace> h_vals("vals", max_nnz);
int nnz = 0;
for (int i = ilower; i <= iupper; i++) {
h_rows(i - ilower) = i;
int cols_count = 0;
if (i - 1 >= 0) { h_cols(nnz) = i - 1; h_vals(nnz) = -1.0; nnz++; cols_count++; }
h_cols(nnz) = i; h_vals(nnz) = 2.0; nnz++; cols_count++;
if (i + 1 < n) { h_cols(nnz) = i + 1; h_vals(nnz) = -1.0; nnz++; cols_count++; }
h_ncols(i - ilower) = cols_count;
}
// Mirror arrays to the GPU
Kokkos::View<int*> d_ncols("d_ncols", local_n);
Kokkos::View<int*> d_rows("d_rows", local_n);
Kokkos::View<int*> d_cols("d_cols", nnz);
Kokkos::View<double*> d_vals("d_vals", nnz);
Kokkos::deep_copy(d_ncols, h_ncols);
Kokkos::deep_copy(d_rows, h_rows);
// Deep copy only the populated subset of the contiguous arrays
Kokkos::deep_copy(Kokkos::subview(d_cols, std::make_pair(0, nnz)),
Kokkos::subview(h_cols, std::make_pair(0, nnz)));
Kokkos::deep_copy(Kokkos::subview(d_vals, std::make_pair(0, nnz)),
Kokkos::subview(h_vals, std::make_pair(0, nnz)));
// Pass the device pointers to HYPRE (batching everything in a single call)
HYPRE_IJMatrixSetValues(A, local_n, d_ncols.data(), d_rows.data(), d_cols.data(), d_vals.data());
HYPRE_IJMatrixAssemble(A);
HYPRE_ParCSRMatrix par_A;
HYPRE_IJMatrixGetObject(A, (void**)&par_A);
HYPRE_IJVector b, x;
HYPRE_IJVectorCreate(MPI_COMM_WORLD, ilower, iupper, &b);
HYPRE_IJVectorSetObjectType(b, HYPRE_PARCSR);
HYPRE_IJVectorInitialize(b);
HYPRE_IJVectorCreate(MPI_COMM_WORLD, ilower, iupper, &x);
HYPRE_IJVectorSetObjectType(x, HYPRE_PARCSR);
HYPRE_IJVectorInitialize(x);
// Create an array of global indices on the GPU
Kokkos::View<int*> d_idx("d_idx", local_n);
Kokkos::parallel_for("fill_idx", local_n, KOKKOS_LAMBDA(const int i) {
d_idx(i) = ilower + i;
});
// Create a device view of zeros for the initial guess, x
Kokkos::View<double*> d_x("d_x", local_n);
Kokkos::deep_copy(d_x, 0.0);
// Feed the device views directly to HYPRE
HYPRE_IJVectorSetValues(b, local_n, d_idx.data(), d_rhs.data());
HYPRE_IJVectorSetValues(x, local_n, d_idx.data(), d_x.data());
HYPRE_IJVectorAssemble(b);
HYPRE_IJVectorAssemble(x);
HYPRE_ParVector par_b, par_x;
HYPRE_IJVectorGetObject(b, (void**)&par_b);
HYPRE_IJVectorGetObject(x, (void**)&par_x);
HYPRE_Solver solver;
HYPRE_ParCSRPCGCreate(MPI_COMM_WORLD, &solver);
HYPRE_PCGSetMaxIter(solver, 200);
HYPRE_PCGSetTol(solver, 1e-8);
HYPRE_ParCSRPCGSetup(solver, par_A, par_b, par_x);
HYPRE_ParCSRPCGSolve(solver, par_A, par_b, par_x);
int its; double resid;
HYPRE_PCGGetNumIterations(solver, &its);
HYPRE_PCGGetFinalRelativeResidualNorm(solver, &resid);
if (rank == 0) {
printf("Hypre PCG converged in %d iterations, residual = %e\n", its, resid);
}
// Pull the solution back using device views
Kokkos::View<double*> d_sol("d_sol", local_n);
HYPRE_IJVectorGetValues(x, local_n, d_idx.data(), d_sol.data());
double sum = 0.0;
Kokkos::parallel_reduce("checksum", local_n, KOKKOS_LAMBDA(const int i, double& acc) {
acc += d_sol(i);
}, sum);
printf("Rank %d: sum of local solution values = %f\n", rank, sum);
HYPRE_ParCSRPCGDestroy(solver);
HYPRE_IJMatrixDestroy(A);
HYPRE_IJVectorDestroy(b);
HYPRE_IJVectorDestroy(x);
HYPRE_Finalize();
}
Kokkos::finalize();
MPI_Finalize();
return 0;
}
EOF
5. Build and Run on the GPU
Configure and build with CMake:
mkdir -p build && cd build
cmake .. -DKokkos_ROOT="$KOKKOS_DIR" -DHYPRE_DIR="$HYPRE_DIR" -DCMAKE_BUILD_TYPE=Release
cmake --build . -j
Run it with two MPI ranks:
mpirun -n 2 ./dev_demo
You should see the Kokkos execution space reported (ideally Cuda), a converged Hypre PCG solve, and a checksum printed per rank.
6. Iterate Quickly
Change the problem size in main.cpp:
static const int n = 1000; // try 100000
Rebuild and rerun:
cmake --build . -j
mpirun -n 2 ./dev_demo