deal.II version GIT relicensing-6834-g5b78e6bcdf 2026-10-01 11:20:01+00:00
\(\newcommand{\dealvcentcolon}{\mathrel{\mathop{:}}}\) \(\newcommand{\dealcoloneq}{\dealvcentcolon\mathrel{\mkern-1.2mu}=}\) \(\newcommand{\jump}[1]{\left[\!\left[ #1 \right]\!\right]}\) \(\newcommand{\average}[1]{\left\{\!\left\{ #1 \right\}\!\right\}}\)
Loading...
Searching...
No Matches
The step-104 tutorial program

This tutorial depends on step-22, step-64.

Table of contents
  1. Introduction
  2. The commented program
  1. Results
  2. The plain program

This program was contributed by Quang Hoang and Timo Heister, Clemson University.

We acknowledge support from the NVIDIA Academic Grant Program award "Sustainable NVIDIA GPU acceleration for open source FEM simulations in deal.II". Many thanks to Daniel Arndt, Rene Gassmöller, Martin Kronbichler, Ivan Prusak, and Bruno Turcksin for help with GPU support in deal.II and this tutorial.

Introduction

This tutorial implements a matrix-free algorithm that works on GPUs using Kokkos to solve the Stokes problem

\begin{align} -\Delta \mathbf{u} + \nabla p &= \mathbf{f} \quad \text{in } \Omega, \\ -\nabla \cdot \mathbf{u} &= 0 \quad \text{in } \Omega, \\ \mathbf{u} &= \mathbf{0} \quad \text{on } \partial\Omega, \end{align}

where we want to solve for velocity \(\mathbf{u}\) and pressure \(p\). The Stokes equations model the behavior of slow-moving, viscous flow, which is often relevant in applications of biofluid dynamics, microfluidics, and flow of geomaterials like ice or rock.

The algorithm that is implemented in this tutorial is matrix-free, meaning we use no assembled matrices. Matrix-free methods have been instrumental in speeding up finite element computations, as memory bandwidth is often a massive bottleneck when using matrix-based methods. Matrix-free methods exploit the fact that simple computations such as addition and multiplication are relatively quick compared to memory bandwidth, so instead of storing data in a matrix and using them to apply the action of the linear operator, matrix vector products are computed on the fly. This approach is discussed in detail in step-37 using the MatrixFree framework that runs on CPUs only.

Here, we use the Portable::MatrixFree class instead, which is a similar framework but implemented using Kokkos to run on GPUs and first introduced in step-64. Just as in that tutorial, which solves the Helmholtz equation instead of the Stokes equations, we use Portable::MatrixFree and Portable::FEEvaluation, which make use of the Kokkos library to implement a portable algorithm that works well on both GPUs and CPUs. Finite element simulations on GPUs offer several benefits compared to traditional CPU calculations, for example a higher compute performance and memory bandwidth for a specified power budget.

Unlike step-64, this tutorial solves for two solution variables, namely velocity \(\mathbf{u}\) and pressure \(p\), which means we will need two DoFHandlers and Portable::FEEvaluation objects, and the solution will be stored in a block vector with two blocks. Unlike earlier tutorials that assemble matrices, like step-22, we do not build an FESystem that represents the combination of velocity and pressure. Instead, the Portable::MatrixFree object is initialized with a vector of DoFHandlers. Later, we can reference the velocity and pressure by referring to the DoFHandler index 0 (velocity) and index 1 (pressure). These indices will be passed to Portable::FEEvaluation objects when constructed.

While this program runs without a GPU, using a workstation with one or more GPUs or a GPU cluster is strongly recommended. Unfortunately, installing deal.II with GPU support is non-trivial. Please see the deal.II GPU wiki page for installation instructions.

The Stokes system and linear solvers for it have been discussed in various tutorials including step-22 (discretization, Schur complement solver), step-32 and step-56 (using the same block preconditioner), and also step-55 (simpler block preconditioner, combined with AMG). Instead of repeating this here, we refer to the derivation in these tutorials and only highlight the differences.

Weak Form

While the usage of stable Taylor-Hood elements is the same as in step-22 and step-56, the discretization is slightly different because we consider the standard Laplacian instead of the deformation tensor acting on the velocity. This leads to the weak form

\begin{eqnarray*} (\nabla \mathbf{u}, \nabla \mathbf{v})_{\Omega} - (p, \nabla \cdot \mathbf{v})_{\Omega} &= (\mathbf{f}, \mathbf{v})_{\Omega},\\ -(q, \nabla \cdot \mathbf{u})_{\Omega} &=&0. \end{eqnarray*}

Linear system and preconditioning

Nevertheless, we arrive at the same block linear system

\begin{eqnarray*} \left(\begin{array}{cc} A & B^T \\ B & 0 \end{array}\right) \end{eqnarray*}

and use the same block preconditioner as in step-32 and step-56. We are left with the choice for the approximation of the inverse of the velocity or \(A\) block, the inverse of the Schur complement \(S\), the implementation of the \(B^T\) operator, and the Stokes operator, all of which we perform matrix-free.

Here, we choose to approximate the inverse of the velocity block by a single v-cycle using the matrix-free, global coarsening, geometric multigrid method. See [181] for the algorithmic details for the CPU implementation. As a smoother we use a Chebyshev iteration around the point-Jacobi scheme, i.e., the inverse of the diagonal of \(A\). This is discussed in step-37.

The inverse of the Schur complement \(S\) is approximated by a Chebyshev iteration applied to the pressure mass matrix (without using multigrid). We do not need to use a multigrid method because the mass matrix is much easier to solve, and because using multigrid on a significantly smaller problem would cause additional computational overhead.

While this is a slight variation on the preconditioners discussed in other tutorials, the same principles apply. Because we do not perform inner solves as done in step-32, the preconditioner is a linear operator. Therefore, we do not require the use of flexible GMRES. This saves precious device memory on the GPUs.

GPU Acceleration

Not all parts of this example run on the GPU. Partly, this is because GPU support in deal.II is still early and some parts could and most certainly will be ported to run on GPUs. Additionally, some steps in this program do not lend themselves to GPU acceleration because they are hard or impossible to parallelize and because they require IO, complex logic, or dynamic memory allocation.

As in step-64, we use the template parameter MemorySpace::Default and MemorySpace::Host to indicate where the solution data of a particular vector is stored. MemorySpace::Host always indicates the host memory (CPU or main memory), while MemorySpace::Default refers to the memory of the default compute device (either CPUs or GPUs depending on how the Kokkos library used by deal.II was configured). While LinearAlgebra::distributed::Vector has a default value for the memory space, which is MemorySpace::Host, we always specify the memory space explicitly in this example. Any function operating on vectors stored on the compute device have to run on the GPU as a kernel and has to be marked with the DEAL_II_HOST_DEVICE precompiler macro to let the compiler know that this code has to be compiled for the programming model of the relevant compute device. This implies that the code in these functions cannot make use of functionality that is not available on this device. Before the CPU can operate on a vector, the data has to be copied into system memory (by copying into a vector with MemorySpace::Host).

In short, we currently perform setup and postprocessing on the CPU, while the linear solve runs on the GPU. The example performs the following steps:

  1. Setup
    1. Generate and refine the mesh (CPU)
    2. Distribute DoFs, compute constraints (CPU)
    3. Assemble right-hand side vector (CPU)
    4. Create mesh hierarchy for multigrid (CPU)
    5. Setup Portable::MatrixFree operator: evaluate inverse Jacobians, mapping, cell geometry (CPU)
    6. Setup multigrid operators, smoothers, transfer (CPU)
  2. Solve
    1. GMRES algorithm (CPU logic, operations on GPU)
      • Vector operations: inner products, ... (GPU)
      • Matrix-vector product: Portable::MatrixFree (GPU)
      • GMG preconditioner: smoother, transfer, coarse solve (GPU)
  3. Postprocess
    1. Move solution to host memory
    2. Compute errors (currently CPU only)

Notice that roughly speaking the setup and postprocess happen on the CPU, with the linear solve step happening on the GPU. With complex problems, this will be the majority of the computational time.

Test case

Our test problem features a unit hypercube with the manufactured solution

\begin{align} u &= \pi \sin^2(\pi x) \sin(2\pi y), \\ v &= -\pi \sin(2\pi x) \sin^2(\pi y), \\ p &= \cos(\pi x) \cos(\pi y) \end{align}

in two dimensions and

\begin{align} u &= \pi \sin^2(\pi x)(\sin^2(\pi z)\sin(2\pi y)-\sin^2(\pi y)\sin(2\pi z)), \\ v &= \pi \sin^2(\pi y) (\sin^2(\pi x) \sin(2\pi z) - \sin^2(\pi z)\sin(2\pi x)), \\ w &= \pi \sin^2(\pi z) (\sin^2(\pi y) \sin(2\pi x) - \sin^2(\pi x)\sin(2\pi y)), \\ p &= \cos(\pi x)\cos(\pi y)\cos(\pi z) \end{align}

in three dimensions.

The commented program

  #include <deal.II/base/conditional_ostream.h>
  #include <deal.II/base/timer.h>
  #include <deal.II/distributed/tria.h>
  #include <deal.II/dofs/dof_handler.h>
  #include <deal.II/dofs/dof_tools.h>
  #include <deal.II/fe/fe_q.h>
  #include <deal.II/fe/fe_system.h>
  #include <deal.II/grid/grid_generator.h>
  #include <deal.II/lac/affine_constraints.h>
  #include <deal.II/lac/la_parallel_block_vector.h>
  #include <deal.II/lac/precondition.h>
  #include <deal.II/lac/solver_gmres.h>
  #include <deal.II/matrix_free/operators.h>
  #include <deal.II/matrix_free/portable_fe_evaluation.h>
  #include <deal.II/matrix_free/portable_matrix_free.h>
  #include <deal.II/multigrid/mg_coarse.h>
  #include <deal.II/multigrid/mg_matrix.h>
  #include <deal.II/multigrid/mg_smoother.h>
  #include <deal.II/multigrid/mg_transfer_global_coarsening.h>
  #include <deal.II/multigrid/mg_transfer_matrix_free.h>
  #include <deal.II/multigrid/multigrid.h>
  #include <deal.II/multigrid/portable_mg_transfer_global_coarsening.h>
  #include <deal.II/numerics/vector_tools.h>
  #include <deal.II/numerics/vector_tools_integrate_difference.h>
  namespace Step104
  {
  using namespace dealii;
*  *  *  struct InterferenceTaperTransform *  

The index of the velocity and pressure DoFHandler into the vector of DoFHandlers inside Portable::MatrixFree and the blocks of the solution vector.

  constexpr unsigned int velocity_dof_handler_index = 0;
  constexpr unsigned int pressure_dof_handler_index = 1;

Problem Definition

We start with the definition of the right-hand side and exact solution of the manufactured solution in 2 and 3 dimensions. In 2D the exact solution is given by

\begin{align*} u &= \pi \sin^2(\pi x) \sin(2 \pi y) \\ v &= -\pi \sin(2 \pi x) \sin^2(\pi y) \\ p &= \cos(\pi x) \cos(\pi y) \end{align*}

and in 3D by:

\begin{align*} u &= \pi \sin^2(\pi x)(\sin^2(\pi z)\sin(2\pi y)-\sin^2(\pi y)\sin(2\pi z)) \\ v &= \pi \sin^2(\pi y) (\sin^2(\pi x) \sin(2\pi z) - \sin^2(\pi z)\sin(2\pi x)) \\ w &= \pi \sin^2(\pi z) (\sin^2(\pi y) \sin(2\pi x) - \sin^2(\pi x)\sin(2\pi y)) \\ p &= \cos(\pi x)\cos(\pi y)\cos(\pi z) \end{align*}

The following classes define these in code.

  template <int dim, typename Number>
  class VelocityRightHandSide : public Function<dim, Number>
  {
  public:
  VelocityRightHandSide()
  : Function<dim, Number>(dim)
  {}
  virtual Number value(const Point<dim> &p,
  const unsigned int component = 0) const override;
  };
  template <int dim, typename Number>
  VelocityRightHandSide<dim, Number>::value(const Point<dim> &p,
  const unsigned int component) const
  {
  AssertIndexRange(component, dim);
  const double x = p[0];
  const double y = p[1];
  const double pi = numbers::PI;
  const double pi2 = pi * pi;
  const double sx = std::sin(pi * x);
  const double cx = std::cos(pi * x);
  const double sy = std::sin(pi * y);
  const double cy = std::cos(pi * y);
  if constexpr (dim == 2)
  {
  if (component == 0)
  return pi * cy * (16.0 * pi2 * sx * sx * sy - 4.0 * pi2 * sy - sx);
  else
  return pi * cx * (-16.0 * pi2 * sx * sy * sy + 4.0 * pi2 * sx - sy);
  }
  else
  {
  const double z = p[2];
  const double sz = std::sin(pi * z);
  const double cz = std::cos(pi * z);
  const double sx2 = sx * sx;
  const double sy2 = sy * sy;
  const double sz2 = sz * sz;
  const double cx2 = cx * cx;
  const double cy2 = cy * cy;
  const double cz2 = cz * cz;
  const double pi3 = pi * pi2;
  if (component == 0)
  return -16.0 * pi3 * sx2 * sy2 * sz * cz +
  16.0 * pi3 * sx2 * sy * sz2 * cy -
  4.0 * pi3 * sx2 * sy * cy * cz2 +
  4.0 * pi3 * sx2 * sz * cy2 * cz - pi * sx * cy * cz +
  4.0 * pi3 * sy2 * sz * cx2 * cz -
  4.0 * pi3 * sy * sz2 * cx2 * cy;
  else if (component == 1)
  return 16.0 * pi3 * sx2 * sy2 * sz * cz -
  4.0 * pi3 * sx2 * sz * cy2 * cz -
  16.0 * pi3 * sx * sy2 * sz2 * cx +
  4.0 * pi3 * sx * sy2 * cx * cz2 +
  4.0 * pi3 * sx * sz2 * cx * cy2 -
  4.0 * pi3 * sy2 * sz * cx2 * cz - pi * sy * cx * cz;
  else
  return -16.0 * pi3 * sx2 * sy * sz2 * cy +
  4.0 * pi3 * sx2 * sy * cy * cz2 +
  16.0 * pi3 * sx * sy2 * sz2 * cx -
  4.0 * pi3 * sx * sy2 * cx * cz2 -
  4.0 * pi3 * sx * sz2 * cx * cy2 +
  4.0 * pi3 * sy * sz2 * cx2 * cy - pi * sz * cx * cy;
  }
  }
  template <int dim, typename Number>
  class VelocitySolution : public Function<dim, Number>
  {
  public:
  VelocitySolution()
  : Function<dim, Number>(dim)
  {}
  virtual Number value(const Point<dim> &p,
  const unsigned int component = 0) const override;
  };
  template <int dim, typename Number>
  VelocitySolution<dim, Number>::value(const Point<dim> &p,
  const unsigned int component) const
  {
  AssertIndexRange(component, dim);
  const double x = p[0];
  const double y = p[1];
  const double pi = numbers::PI;
  const double sx = std::sin(pi * x);
  const double sy = std::sin(pi * y);
  const double s2x = std::sin(2.0 * pi * x);
  const double s2y = std::sin(2.0 * pi * y);
  if constexpr (dim == 2)
  {
  if (component == 0)
  return pi * sx * sx * s2y;
  else
  return -pi * s2x * sy * sy;
  }
  else
  {
  const double z = p[2];
  const double sz = std::sin(pi * z);
  const double s2z = std::sin(2.0 * pi * z);
  const double sx2 = sx * sx;
  const double sy2 = sy * sy;
  const double sz2 = sz * sz;
  const double dphidx = pi * s2x * sy2 * sz2;
  const double dphidy = pi * s2y * sx2 * sz2;
  const double dphidz = pi * s2z * sx2 * sy2;
  if (component == 0)
  return dphidy - dphidz;
  else if (component == 1)
  return dphidz - dphidx;
  else
  return dphidx - dphidy;
  }
  }
  template <int dim, typename Number>
  class PressureSolution : public Function<dim, Number>
  {
  public:
  PressureSolution()
  : Function<dim, Number>()
  {}
  virtual Number value(const Point<dim> &p,
  const unsigned int /*component*/ = 0) const override
  {
  const double pi = numbers::PI;
  if constexpr (dim == 2)
  return std::cos(pi * p[0]) * std::cos(pi * p[1]);
  else
  return std::cos(pi * p[0]) * std::cos(pi * p[1]) * std::cos(pi * p[2]);
  }
  };
virtual RangeNumberType value(const Point< dim > &p, const unsigned int component=0) const
Definition point.h:111
#define AssertIndexRange(index, range)
*  *  *  RotationFunction< dim, Number >::RotationFunction Number(dim)
constexpr double PI
Definition numbers.h:240
::VectorizedArray< Number, width > cos(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sin(const ::VectorizedArray< Number, width > &)

The velocity operator

The matrix-free operator for the velocity block \(A\) given by \((\nabla u,\nabla v)\) is defined by the class PortableMFVelocityOperator. It uses the class VelocityCellOperator, which is evaluated in parallel on each cell. On each cell, we define the action at each quadrature point with the small helper class VelocityOperatorQuad with operator().

  template <int dim, int fe_degree, typename Number>
  class VelocityOperatorQuad
  {
  public:
  *fe_eval,
  const int q_point) const
  {
  const auto gradient_u = fe_eval->get_gradient(q_point);
  fe_eval->submit_gradient(gradient_u, q_point);
  }
  };
  template <int dim,
  int degree_u,
  int degree_p,
  typename Number,
  int n_q_points_1d>
  class VelocityCellOperator
  {
  public:
  static const unsigned int n_q_points =
  ::Utilities::pow(n_q_points_1d, dim);
  {
  data, velocity_dof_handler_index);
  fe_u.read_dof_values(src);
  VelocityOperatorQuad<dim, degree_u, Number> quad_operation;
  data->for_each_quad_point(
  [&](const int q_point) { quad_operation(&fe_u, q_point); });
  fe_u.distribute_local_to_global(dst);
  }
  };
*  *  Point< dim > operator()(const Point< dim > &p) const * 
#define DEAL_II_HOST_DEVICE
Definition config.h:171
std::vector< index_type > data
Definition mpi.cc:734
Kokkos::View< Number *, MemorySpace::Default::kokkos_space > DeviceVector

This class finally provides the matrix-free operator for the velocity block. Note that we also compute the inverse diagonal of the operator, which is used in the Chebyshev smoother when we approximate A^{-1} with a GMG v-cycle. We note that Tvmult() is not implemented because it is not required for the smoother and \(A\) is symmetric anyway.

  template <int dim,
  int degree_u,
  int degree_p,
  typename Number = double,
  typename VectorType =
  int n_q_points_1d = degree_u + 1>
  class PortableMFVelocityOperator : public EnableObserverPointer
  {
  public:
  PortableMFVelocityOperator() = default;
  PortableMFVelocityOperator(
  std::shared_ptr<Portable::MatrixFree<dim, Number>> data_in)
  : data(data_in)
  {}
  void reinit(std::shared_ptr<Portable::MatrixFree<dim, Number>> data_in)
  {
  data = data_in;
  }
  void initialize_dof_vector(VectorType &vec) const
  {
  data->initialize_dof_vector(vec, velocity_dof_handler_index);
  }
  {
  return data->get_vector_partitioner(velocity_dof_handler_index)->size();
  }
  std::shared_ptr<DiagonalMatrix<
  get_matrix_diagonal_inverse() const
  {
  return inverse_diagonal_entries;
  }
  double el(const types::global_dof_index row,
  const types::global_dof_index col) const
  {
  (void)col;
  Assert(row == col, ExcNotImplemented());
  Assert(inverse_diagonal_entries.get() != nullptr &&
  inverse_diagonal_entries->m() > 0,
  ExcNotInitialized());
  return 1.0 / (*inverse_diagonal_entries)(row, row);
  }
  void vmult(VectorType &dst, const VectorType &src) const
  {
  dst = static_cast<Number>(0.);
  VelocityCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
  velocity_operator;
  data->cell_loop(velocity_operator, src, dst);
  data->copy_constrained_values(src, dst, velocity_dof_handler_index);
  }
  void Tvmult(VectorType & /* dst */, const VectorType & /* src */) const
  {
  AssertThrow(false, ExcNotImplemented());
  }
  {
  Assert(data.get() != nullptr, ExcNotInitialized());
  this->inverse_diagonal_entries =
  std::make_shared<DiagonalMatrix<VectorType>>();
  VectorType &inverse_diagonal =
  this->inverse_diagonal_entries->get_vector();
  data->initialize_dof_vector(inverse_diagonal, velocity_dof_handler_index);
  VelocityOperatorQuad<dim, degree_u, Number> velocity_operator_quad;
  MatrixFreeTools::
  compute_diagonal<dim, degree_u, degree_u + 1, dim, Number>(
  *data.get(),
  inverse_diagonal,
  velocity_operator_quad,
  velocity_dof_handler_index);
  Number *raw_diagonal = inverse_diagonal.get_values();
  Kokkos::parallel_for(
  "invert A diagonal",
  inverse_diagonal.locally_owned_size(),
  KOKKOS_LAMBDA(int i) {
  Assert(raw_diagonal[i] > 0.,
  ExcMessage("Diagonal entries of a positive definite operator "
  "should be positive"));
  raw_diagonal[i] = 1. / raw_diagonal[i];
  });
  }
  private:
  std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
  std::shared_ptr<DiagonalMatrix<VectorType>> inverse_diagonal_entries;
  };
#define Assert(cond, exc)
#define AssertThrow(cond, exc)
Tpetra::Vector< Number, LO, GO, NodeType< MemorySpace > > VectorType
void compute_diagonal(const MatrixFree< dim, Number, VectorizedArrayType > &matrix_free, VectorType &diagonal_global, const std::function< void(FEEvaluation< dim, fe_degree, n_q_points_1d, n_components, Number, VectorizedArrayType > &)> &cell_operation, const unsigned int dof_handler_index=0, const unsigned int quadrature_index=0, const unsigned int first_selected_component=0, const unsigned int first_vector_component=0)
STL namespace.

The Schur complement operator

The preconditioner requires a Schur complement approximation, which is here given by a mass matrix in the pressure space \((p,q)\). This is implemented in a very similar way to the velocity block above. A notable difference is that we select the pressure by passing a dof_handler_index of 1 instead of 0 to the FEEvaluation class.

  template <int dim,
  int degree_u,
  int degree_p,
  typename Number,
  int n_q_points_1d>
  class MassOperatorQuad
  {
  public:
  const int q_point) const
  {
  fe_eval->submit_value(fe_eval->get_value(q_point), q_point);
  }
  };
  template <int dim,
  int degree_u,
  int degree_p,
  typename Number,
  int n_q_points_1d>
  class MassCellOperator
  {
  public:
  static const unsigned int n_q_points =
  ::Utilities::pow(n_q_points_1d, dim);
  };
  template <int dim,
  int degree_u,
  int degree_p,
  typename Number,
  int n_q_points_1d>
  MassCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>::operator()(
  {
  data, pressure_dof_handler_index);
  fe_p.read_dof_values(src);
  fe_p.evaluate(EvaluationFlags::values);
  MassOperatorQuad<dim, degree_u, degree_p, Number, n_q_points_1d>
  quad_operation;
  data->for_each_quad_point(
  [&](const int &q_point) { quad_operation(&fe_p, q_point); });
  fe_p.integrate(EvaluationFlags::values);
  fe_p.distribute_local_to_global(dst);
  }
  template <int dim,
  int degree_u,
  int degree_p,
  typename Number = double,
  typename VectorType =
  int n_q_points_1d = degree_u + 1>
  class PortableMFMassOperator : public EnableObserverPointer
  {
  public:
  PortableMFMassOperator(
  const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
  : data(data_in)
  {}
  {
  return data->get_vector_partitioner(pressure_dof_handler_index)->size();
  }
  const types::global_dof_index col) const
  {
  (void)col;
  Assert(row == col, ExcNotImplemented());
  Assert(inverse_diagonal_entries.get() != nullptr &&
  inverse_diagonal_entries->m() > 0,
  ExcNotInitialized());
  return 1.0 / (*inverse_diagonal_entries)(row, row);
  }
  void vmult(VectorType &dst, const VectorType &src) const
  {
  dst = static_cast<Number>(0.);
  MassCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
  mass_operator;
  data->cell_loop(mass_operator, src, dst);
  data->copy_constrained_values(src, dst, pressure_dof_handler_index);
  }
  std::shared_ptr<DiagonalMatrix<VectorType>>
  get_matrix_diagonal_inverse() const
  {
  return inverse_diagonal_entries;
  }
  {
  this->inverse_diagonal_entries =
  std::make_shared<DiagonalMatrix<VectorType>>();
  VectorType &inverse_diagonal =
  this->inverse_diagonal_entries->get_vector();
  MassOperatorQuad<dim, degree_u, degree_p, Number, n_q_points_1d>
  quad_operation;
  MatrixFreeTools::
  compute_diagonal<dim, degree_p, n_q_points_1d, 1, Number>(
  *data.get(),
  inverse_diagonal,
  quad_operation,
  pressure_dof_handler_index);
  Number *raw_diagonal = inverse_diagonal.get_values();
  Kokkos::parallel_for(
  "invert Mass diagonal",
  inverse_diagonal.locally_owned_size(),
  KOKKOS_LAMBDA(int i) {
  Assert(raw_diagonal[i] > 0.,
  ExcMessage("Diagonal entries of a positive definite operator "
  "should be positive"));
  raw_diagonal[i] = 1. / raw_diagonal[i];
  });
  }
  private:
  std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
  std::shared_ptr<DiagonalMatrix<VectorType>> inverse_diagonal_entries;
  };
std::size_t size
Definition mpi.cc:733
*  *  *  *  std::vector< Number > ThermoPlasticMaterial< dim, ViscoplasticYieldLaw, Number >::get_state_parameters   const
Definition types.h:30
unsigned int global_dof_index
Definition types.h:92

The Stokes operator

The following set of classes provides the whole Stokes operator

\begin{eqnarray*} \begin{bmatrix} A & B^T \\ B & 0 \end{bmatrix}. \end{eqnarray*}

While structured in a similar way (class PortableMFStokesOperator uses StokesCellOperator and a lambda function for the action at each quadrature point), we now operate on Portable::DeviceBlockVector and use two Portable::FEEvaluation objects, one for the velocity and one for the pressure.

Note that we don't need support for computing the diagonal, as this is not needed in the block preconditioner.

  template <int dim,
  int degree_u,
  int degree_p,
  typename Number,
  int n_q_points_1d>
  class StokesCellOperator
  {
  public:
  static const unsigned int n_q_points =
  ::Utilities::pow(n_q_points_1d, dim);
  {
  data, velocity_dof_handler_index);
  data, pressure_dof_handler_index);
  fe_u.read_dof_values(src.block(0));
  fe_p.read_dof_values(src.block(1));
  fe_p.evaluate(EvaluationFlags::values);
  data->for_each_quad_point([&](const int &q_point) {
  const Tensor<2, dim, Number> gradient_u = fe_u.get_gradient(q_point);
  const Number pressure_value = fe_p.get_value(q_point);
  Tensor<2, dim, Number> velocity_term = gradient_u;
  for (unsigned int d = 0; d < dim; ++d)
  velocity_term[d][d] -= pressure_value;
  fe_u.submit_gradient(velocity_term, q_point);
  const Number pressure_term = -trace(gradient_u);
  fe_p.submit_value(pressure_term, q_point);
  });
  fe_p.integrate(EvaluationFlags::values);
  fe_u.distribute_local_to_global(dst.block(0));
  fe_p.distribute_local_to_global(dst.block(1));
  }
  };
  template <
  int dim,
  int degree_u,
  int degree_p,
  typename Number = double,
  typename VectorType =
  int n_q_points_1d = degree_u + 1>
  class PortableMFStokesOperator
  {
  public:
  PortableMFStokesOperator(
  const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
  : data(data_in)
  {}
  void vmult(VectorType &dst, const VectorType &src) const
  {
  dst = static_cast<Number>(0.);
  StokesCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
  stokes_operator;
  data->cell_loop(stokes_operator, src, dst);
  data->copy_constrained_values(src, dst);
  }
  private:
  std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
  };
constexpr Number trace(const SymmetricTensor< 2, dim2, Number > &)

The BT operator

The last ingredient of the block preconditioner is the action of the block \(B^T\) given by \(-(p,\nabla \cdot v)\). The operator reads pressure values \(p\) and produces a velocity (you can think of it as a rectangular matrix block). Therefore, the implementation is similar to the Stokes operator in that we work with two Portable::FEEvaluation objects and operate on Portable::DeviceBlockVector.

  template <int dim,
  int degree_u,
  int degree_p,
  typename Number,
  int n_q_points_1d>
  class BTCellOperator
  {
  public:
  static const unsigned int n_q_points =
  ::Utilities::pow(n_q_points_1d, dim);
  {
  data, velocity_dof_handler_index);
  data, pressure_dof_handler_index);
  fe_p.read_dof_values(src.block(1));
  fe_p.evaluate(EvaluationFlags::values);
  data->for_each_quad_point([&](const int &q_point) {
  const Number pressure_value = fe_p.get_value(q_point);
  fe_u.submit_divergence(-pressure_value, q_point);
  });
  fe_u.distribute_local_to_global(dst.block(0));
  }
  };
  template <
  int dim,
  int degree_u,
  int degree_p,
  typename Number = double,
  typename VectorType =
  int n_q_points_1d = degree_u + 1>
  class PortableMFBTOperator
  {
  public:
  PortableMFBTOperator(
  const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
  : data(data_in)
  {}
  void vmult(VectorType &dst, const VectorType &src) const
  {
  dst = static_cast<Number>(0.);
  BTCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
  cell_operator;
  data->cell_loop(cell_operator, src, dst);

Instead of copying constrained values, zero them out. The BT operator does not receive an input velocity to copy values from and zeroing out is the correct operation for boundary and hanging nodes for an update of the velocity:

  data->set_constrained_values(0.0,
  dst.block(0),
  velocity_dof_handler_index);
  }
  private:
  std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
  };

The Preconditioner BlockSchurPreconditioner

The following class implements the block preconditioner. The class takes the types of the operators for \(A^{-1}\), \(S^{-1}\), and \(B^T\) as template arguments. This is the same preconditioner used in step-32 and step-56.

We keep a temporary vector tmp inside this class to avoid reallocating memory in every vmult call. It has to be mutable because it is used in the vmult method that is declared as const.

  template <class AInvOperator,
  class SInvOperator,
  class BTOperator,
  class VectorType>
  class BlockSchurPreconditioner : public EnableObserverPointer
  {
  public:
  BlockSchurPreconditioner(const AInvOperator &A_inverse_operator,
  const SInvOperator &S_inverse_operator,
  const BTOperator &BT_operator);
  void vmult(VectorType &dst, const VectorType &src) const;
  private:
  mutable VectorType tmp;
  const AInvOperator &A_inverse_operator;
  const SInvOperator &S_inverse_operator;
  const BTOperator &BT_operator;
  };
  template <class AInvOperator,
  class SInvOperator,
  class BTOperator,
  class VectorType>
  BlockSchurPreconditioner<AInvOperator, SInvOperator, BTOperator, VectorType>::
  BlockSchurPreconditioner(const AInvOperator &A_inverse_operator,
  const SInvOperator &S_inverse_operator,
  const BTOperator &BT_operator)
  : A_inverse_operator(A_inverse_operator)
  , S_inverse_operator(S_inverse_operator)
  , BT_operator(BT_operator)
  {}
  template <class AInvOperator,
  class SInvOperator,
  class BTOperator,
  class VectorType>
  void
  BlockSchurPreconditioner<AInvOperator, SInvOperator, BTOperator, VectorType>::
  vmult(VectorType &dst, const VectorType &src) const
  {

Allocate the temporary vector on first use. Its content doesn't matter, as it will be overwritten in the vmult() call.

  if (tmp.size() == 0)
  tmp.reinit(src);

First apply the Schur Complement inverse operator: dst.p = S^-1 * src.p

  {
  S_inverse_operator.vmult(dst.block(1), src.block(1));
  dst.block(1) *= -1.0;
  }

Apply the top right block: tmp.u = -B^T * dst.p + src.u

  {
  BT_operator.vmult(tmp, dst);
  tmp.block(0).sadd(-1.0, 1.0, src.block(0));
  }

Finally the velocity block:

  A_inverse_operator.vmult(dst.block(0), tmp.block(0));
  }

The main class StokesProblem

The remaining part of this tutorial is the StokesProblem class that puts everything together.

  template <int dim, int degree_p, typename Number = double>
  class StokesProblem
  {
  public:
  static constexpr unsigned int degree_u = degree_p + 1;
  StokesProblem();
  void run();
  using VectorType =
  using BlockVectorType =
  private:
  void setup_dofs();
  void solve();
  void postprocess();
  MappingQ<dim> mapping;
  std::shared_ptr<Portable::MatrixFree<dim, Number>> mf_data;
  BlockVectorType solution;
  BlockVectorType rhs;
  };
  template <int dim, int degree_p, typename Number>
  StokesProblem<dim, degree_p, Number>::StokesProblem()
  : tria(MPI_COMM_WORLD)
  , mapping(1)
  , fe_u(FE_Q<dim>(degree_p + 1), dim)
  , fe_p(degree_p)
  , dof_u(tria)
  , dof_p(tria)
  , pcout(std::cout, Utilities::MPI::this_mpi_process(MPI_COMM_WORLD) == 0)
  {}
Definition fe_q.h:552

The setup_dofs() function distributes the two separate DoFHandlers for the velocity and pressure before initializing the MatrixFree object with an std::vector of both of them. We can later refer to the velocity using DoFHandler index 0 and the pressure using DoFHandler index 1.

  template <int dim, int degree_p, typename Number>
  void StokesProblem<dim, degree_p, Number>::setup_dofs()
  {
  dof_u.distribute_dofs(fe_u);
  dof_p.distribute_dofs(fe_p);
  const IndexSet &owned_set_u = dof_u.locally_owned_dofs();
  const IndexSet relevant_set_u =
  constraints_u.reinit(owned_set_u, relevant_set_u);
  dof_u, 0, Functions::ZeroFunction<dim, Number>(dim), constraints_u);
  constraints_u.close();
  const IndexSet &owned_set_p = dof_p.locally_owned_dofs();
  const IndexSet relevant_set_p =
  constraints_p.reinit(owned_set_p, relevant_set_p);
  constraints_p.close();
  std::vector<const DoFHandler<dim> *> dof_handlers = {&dof_u, &dof_p};
  std::vector<const AffineConstraints<Number> *> constraints = {
  &constraints_u, &constraints_p};
  mf_data = std::make_shared<Portable::MatrixFree<dim, Number>>();
  const QGauss<1> quad(degree_p + 2);
  mf_data->reinit(mapping, dof_handlers, constraints, quad, additional_data);
  {
void make_hanging_node_constraints(const DoFHandler< dim, spacedim > &dof_handler, AffineConstraints< number > &constraints)
@ update_values
Shape function values.
@ update_gradients
Shape function gradients.
IndexSet extract_locally_relevant_dofs(const DoFHandler< dim, spacedim > &dof_handler)
void interpolate_boundary_values(const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof, const std::map< types::boundary_id, const Function< spacedim, number > * > &function_map, std::map< types::global_dof_index, number > &boundary_values, const ComponentMask &component_mask={})

create the right hand side on the host and move to device:

  rhs_host;
  mf_data->initialize_dof_vector(rhs_host);
  dof_u,
  QGauss<dim>(degree_u + 2),
  VelocityRightHandSide<dim, Number>(),
  rhs_host.block(0),
  constraints_u);
  mf_data->initialize_dof_vector(rhs);
  rhs.block(0).import_elements(rhs_host.block(0), VectorOperation::insert);
  rhs.block(1).import_elements(rhs_host.block(1), VectorOperation::insert);
  }
  }
void create_right_hand_side(const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof, const Quadrature< dim > &q, const Function< spacedim, typename VectorType::value_type > &rhs, VectorType &rhs_vector, const AffineConstraints< typename VectorType::value_type > &constraints=AffineConstraints< typename VectorType::value_type >())

In the solve() function we set up the preconditioner and run the GMRES solver. For this, we construct the multigrid hierarchy for the GMG v-cycle with a Chebyshev iteration around the point-Jacobi scheme, i.e., the inverse of the diagonal of \(A\), to approximate the action of \(A^{-1}\). We approximate the Schur Complement with a Chebyshev iteration applied to the pressure mass matrix (without multigrid).

  template <int dim, int degree_p, typename Number>
  void StokesProblem<dim, degree_p, Number>::solve()
  {
  PortableMFStokesOperator<dim, degree_u, degree_p, Number> stokes_operator(
  mf_data);
  mf_data->initialize_dof_vector(solution);
  {
  ::Timer t(tria.get_mpi_communicator());
  stokes_operator.vmult(solution, rhs);
  const double time = t.wall_time();
  const double mdofs_p_second =
  1e-6 * static_cast<double>(solution.size()) / time;
  pcout << "Stokes operator: " << time << " s, MDoFs/s: " << mdofs_p_second
  << std::endl;
  solution = 0.0;
  }
  SolverControl solver_control(1000, 1e-8 * rhs.l2_norm());
  solver_control,
  using LevelMatrixType =
  PortableMFVelocityOperator<dim, degree_u, degree_p, Number>;
  using SmootherPreconditionerType = DiagonalMatrix<VectorType>;
  using SmootherType = PreconditionChebyshev<LevelMatrixType,
  VectorType,
  SmootherPreconditionerType>;
  using MGTransferType =
  const auto coarse_grid_triangulations =
  tria);
  const unsigned int max_level = coarse_grid_triangulations.size() - 1;
std::vector< std::shared_ptr< const Triangulation< dim, spacedim > > > create_geometric_coarsening_sequence(const Triangulation< dim, spacedim > &tria)

Do not go down to level 0, because this will lead to slower runtime as the problem becomes very small:

  const unsigned int min_level = std::min(3U, max_level - 1);
  MGLevelObject<DoFHandler<dim>> mg_dof_handlers(min_level, max_level);
  MGLevelObject<AffineConstraints<Number>> mg_constraints(min_level,
  max_level);
  MGLevelObject<LevelMatrixType> mg_matrices(min_level, max_level);
  min_level, max_level);
  std::vector<std::shared_ptr<Portable::MatrixFree<dim, Number>>>
  mf_data_levels;
::VectorizedArray< Number, width > min(const ::VectorizedArray< Number, width > &, const ::VectorizedArray< Number, width > &)

Prepare the operators and data structures on all levels of the multigrid hierarchy

  for (unsigned int level = min_level; level <= max_level; ++level)
  {
  auto &dof_handler = mg_dof_handlers[level];
  auto &constraint = mg_constraints[level];
  dof_handler.reinit(*coarse_grid_triangulations[level]);
  dof_handler.distribute_dofs(fe_u);
  constraint.reinit(dof_handler.locally_owned_dofs(),
  constraint.close();
  additional_data;
  additional_data.mapping_update_flags =
  if (level == max_level)
unsigned int level
Definition grid_out.cc:4642
void make_zero_boundary_constraints(const DoFHandler< dim, spacedim > &dof, const types::boundary_id boundary_id, AffineConstraints< number > &zero_boundary_constraints, const ComponentMask &component_mask={})
@ update_JxW_values
Transformed quadrature weights.

On the finest level we can reuse the MatrixFree object from the Stokes operator. This way we can solve significantly larger problems before we run out of device memory.

  mf_data_levels.emplace_back(mf_data);
  else
  {
  const QGauss<1> quad(degree_p + 2);
  mf_data_levels.emplace_back(
  mf_data_levels.back()->reinit(
  mapping, dof_handler, constraint, quad, additional_data);
  }
  mg_matrices[level].reinit(mf_data_levels.back());
  }
  mg::Matrix<VectorType> mg_matrix(mg_matrices);

transfer operator

  for (unsigned int level = min_level; level < max_level; ++level)
  mg_transfers[level + 1].reinit_geometric_transfer(
  mg_dof_handlers[level + 1],
  mg_dof_handlers[level],
  mg_constraints[level + 1],
  mg_constraints[level]);
  MGTransferType mg_transfer(mg_transfers, [&](const auto l, auto &vec) {
  mg_matrices[l].initialize_dof_vector(vec);
  });

smoother

  min_level, max_level);
  for (unsigned int level = min_level; level <= max_level; ++level)
  {
  mg_matrices[level].compute_diagonal();
  smoother_data[level].preconditioner =
  std::make_shared<SmootherPreconditionerType>(
  *mg_matrices[level].get_matrix_diagonal_inverse());
  smoother_data[level].constraints.copy_from(mg_constraints[level]);
  if (level == min_level)
  {

Use the Chebyshev iteration as an (approximate) solver on the coarsest level. In this mode smoothing_range is a relative target tolerance and must be strictly less than one; the number of iterations is then chosen automatically by setting degree to numbers::invalid_unsigned_int. We also use more CG iterations for the eigenvalue estimate because when min_level > 0, the coarse problem can still be reasonably large and badly conditioned.

  smoother_data[level].smoothing_range = 1e-3;
  smoother_data[level].degree = numbers::invalid_unsigned_int;
  smoother_data[level].eig_cg_n_iterations = 40;
  }
  else
  {
constexpr unsigned int invalid_unsigned_int
Definition types.h:228

These values are chosen by experimentation for the problem at hand. We chose the smoothing range first. A good value will allow the smoother to effectively separate large and small scale oscillations in the residual and as such improve the convergence of the Chebyshev iteration and the multigrid method. Finally, the degree is chosen to minimize total runtime (a larger value increases the cost but improves the outer number of GMRES iterations).

  smoother_data[level].smoothing_range = 5;
  smoother_data[level].degree = 4;
  smoother_data[level].eig_cg_n_iterations = 20;
  }
  }
  mg_smoother.initialize(mg_matrices, smoother_data);
void initialize(const MGLevelObject< MatrixType2 > &matrices, const typename RelaxationType::AdditionalData &additional_data=typename RelaxationType::AdditionalData())

Estimate and print the eigenvalue spectrum of the velocity block on each level. This spectrum is later used by the Chebyshev iteration.

  pcout << "GMG velocity block smoothers:" << std::endl;
  for (unsigned int level = min_level; level <= max_level; ++level)
  {
  VectorType vec;
  mg_matrices[level].initialize_dof_vector(vec);
  auto eigenvalue_info =
  mg_smoother.smoothers[level].estimate_eigenvalues(vec);
  pcout << " level: " << level << " n_dofs: " << vec.size()
  << ", eigenvalue spectrum: [ "
  << eigenvalue_info.min_eigenvalue_estimate << ", "
  << eigenvalue_info.max_eigenvalue_estimate << " ]" << std::endl;
  }

coarse-grid solver

  mg_coarse.initialize(mg_smoother);
void initialize(const MGSmootherBase< VectorType > &coarse_smooth)

put everything together

  mg_coarse,
  mg_transfer,
  mg_smoother,
  mg_smoother,
  min_level,
  max_level);
  ::Timer timer_smoother;
  ::Timer timer_transfer;
  ::Timer timer_coarse;
  ::Timer timer_residual;
  {
  timer_smoother.reset();
  timer_transfer.reset();
  timer_coarse.reset();
  timer_residual.reset();
  auto make_timer_lambda = [&](::Timer &timer) {
  return [&](const bool before, const unsigned int /*level*/) {
  if (before)
  timer.start();
  else
  timer.stop();
  };
  };
  mg.connect_pre_smoother_step(make_timer_lambda(timer_smoother));
  mg.connect_post_smoother_step(make_timer_lambda(timer_smoother));
  mg.connect_residual_step(make_timer_lambda(timer_residual));
  mg.connect_restriction(make_timer_lambda(timer_transfer));
  mg.connect_prolongation(make_timer_lambda(timer_transfer));
  mg.connect_coarse_solve(make_timer_lambda(timer_coarse));
  }
  APreconditionerType preconditioner_A(dof_u, mg, mg_transfer);
  PortableMFMassOperator<dim, degree_u, degree_p, Number> mass_operator(
  mf_data);
  mass_operator.compute_diagonal();
  using SPreconditionerType = PreconditionChebyshev<
  PortableMFMassOperator<dim, degree_u, degree_p, Number>,
  SPreconditionerType preconditioner_schur;
  {
  typename SPreconditionerType::AdditionalData additional_data;
  additional_data.smoothing_range = 15.;
  additional_data.degree = 3;
  additional_data.eig_cg_n_iterations = 10;
  additional_data.constraints.copy_from(constraints_p);
  additional_data.preconditioner =
  mass_operator.get_matrix_diagonal_inverse();
  preconditioner_schur.initialize(mass_operator, additional_data);
  }
  using BTOperatorType =
  PortableMFBTOperator<dim, degree_u, degree_p, Number>;
  BTOperatorType BT_operator(mf_data);
  BlockSchurPreconditioner<APreconditionerType,
  SPreconditionerType,
  BTOperatorType,
  BlockVectorType>
  preconditioner(preconditioner_A, preconditioner_schur, BT_operator);
  ::Timer t(tria.get_mpi_communicator());
  solver.solve(stokes_operator, solution, rhs, preconditioner);
  t.stop();
  pcout << "Solver converged in " << solver_control.last_step()
  << " iterations in " << t.wall_time() << " seconds" << std::endl;
  pcout << "Velocity block GMG timings:"
  << "\n smoother: " << timer_smoother.wall_time()
  << " s\n transfer: " << timer_transfer.wall_time()
  << " s\n coarse : " << timer_coarse.wall_time()
  << " s\n residual: " << timer_residual.wall_time() << " s"
  << std::endl;
  }
Definition timer.h:128
Definition mg.h:79

The postprocess() function moves the solution to host memory and integrates the difference to the manufactured solution to compute errors.

  template <int dim, int degree_p, typename Number>
  void StokesProblem<dim, degree_p, Number>::postprocess()
  {
  solution_host;
  mf_data->initialize_dof_vector(solution_host);
  solution_host.block(0).import_elements(solution.block(0),
  solution_host.block(1).import_elements(solution.block(1),
  constraints_u.distribute(solution_host.block(0));
  constraints_p.distribute(solution_host.block(1));
  solution_host.update_ghost_values();
  const double mean_pressure = VectorTools::compute_mean_value(
  dof_p, QGauss<dim>(degree_p + 2), solution_host.block(1), 0);
  solution_host.block(1).add(-mean_pressure);
  const QGauss<dim> quadrature_formula(degree_u + 1);
  Vector<double> cellwise_errors_ul2(tria.n_active_cells());
  Vector<double> cellwise_errors_pl2(tria.n_active_cells());
  solution_host.block(0),
  VelocitySolution<dim, Number>(),
  cellwise_errors_ul2,
  quadrature_formula,
  solution_host.block(1),
  PressureSolution<dim, Number>(),
  cellwise_errors_pl2,
  quadrature_formula,
  const double u_l2 = VectorTools::compute_global_error(tria,
  cellwise_errors_ul2,
  const double p_l2 = VectorTools::compute_global_error(tria,
  cellwise_errors_pl2,
  pcout << "velocity error: " << u_l2 << " pressure error: " << p_l2
  << std::endl;
  }
BlockType & block(const unsigned int i)
double compute_global_error(const Triangulation< dim, spacedim > &tria, const InVector &cellwise_error, const NormType &norm, const double exponent=2.)
void integrate_difference(const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof, const ReadVector< Number > &fe_function, const Function< spacedim, Number > &exact_solution, OutVector &difference, const Quadrature< dim > &q, const NormType &norm, const Function< spacedim, double > *weight=nullptr, const double exponent=2.)
Number compute_mean_value(const hp::MappingCollection< dim, spacedim > &mapping_collection, const DoFHandler< dim, spacedim > &dof, const hp::QCollection< dim > &q_collection, const ReadVector< Number > &v, const unsigned int component)

The run() function prints some statistics and then performs a familiar refinement loop.

  template <int dim, int degree_p, typename Number>
  void StokesProblem<dim, degree_p, Number>::run()
  {
  pcout << std::setprecision(10);
  pcout << "Running on " << Utilities::MPI::n_mpi_processes(MPI_COMM_WORLD)
  << " MPI ranks (with " << MultithreadInfo::n_threads()
  << " threads each) in ";
  if constexpr (running_in_debug_mode())
  pcout << "DEBUG mode";
  else
  pcout << "RELEASE mode";
  pcout << "\nKokkos execution space: "
  << Kokkos::DefaultExecutionSpace::name();
  pcout << '\n'
  << "dim: " << dim << '\n'
  << "Element: Q" << degree_u << "-Q" << degree_p << std::endl;
  unsigned int n_refinements = 10;
  for (unsigned int i = 0; i < n_refinements; ++i)
  {
  if (i == 0)
  {
  tria.refine_global(2);
  }
  else
  {
  tria.refine_global(1);
  }
  setup_dofs();
  pcout << "\nrefinement: " << i
  << ", n_dofs: " << dof_u.n_dofs() + dof_p.n_dofs() << " = "
  << dof_u.n_dofs() << " + " << dof_p.n_dofs() << std::endl;
  solve();
  postprocess();
  }
  }
  } // namespace Step104
*  *  for(const auto &cell :triangulation.active_cell_iterators())
static unsigned int n_threads()
constexpr bool running_in_debug_mode()
Definition config.h:76
void hyper_cube(Triangulation< dim, spacedim > &tria, const double left=0., const double right=1., const bool colorize=false)
unsigned int n_mpi_processes(const MPI_Comm mpi_communicator)
Definition mpi.cc:103

The main() function

The only interesting bits here are the template arguments that specify dimension and polynomial degree to be used.

  int main(int argc, char **argv)
  {
  using namespace Step104;
  Utilities::MPI::MPI_InitFinalize mpi_initialization(argc, argv);
  const unsigned int dim = 3;
  const unsigned int degree_p = 1;
  StokesProblem<dim, degree_p, double> problem;
  problem.run();
  }
*  *  int main(int argc, char **argv)

Results

Validation

When running the program on a single GPU (here an H100) and computing the L2 error norms between the finite element solution and the analytical solution discussed above, we can produce the following table of results:

Refinement DoFs Iterations Time (s) Velocity Error Pressure Error
0 2,312 10 2.4e-02 4.5e-02 1.5e-01
1 15,468 20 6.0e-02 6.0e-03 1.2e-02
2 112,724 22 1.1e-01 7.6e-04 1.2e-03
3 859,812 23 2.5e-01 9.6e-05 2.3e-04
4 6,714,692 23 1.2e+00 1.2e-05 5.6e-05
5 53,070,468 24 8.8e+00 1.5e-06 1.4e-05

We see optimal convergence rates for L2 errors of velocity and pressure (3 and 2, respectively, which corresponds to best-approximation of the Q2 and Q1 spaces) and decent linear scaling with problem size: A factor 8 in increase in unknowns leads to a factor of 8 in solve time, at least on the finest refinement level.

Higher Order Throughput

GPUs profit from fine-grained parallelism. In fact, a modern GPU like an H100 requires a significant number of small parallel work items to saturate the device. An H100 has 132 SMs, each needing about 10-20 thread blocks to be occupied, with 256 threads per block. This would require at least 300,000 parallel threads to make use of the whole GPU. This is only a simplified estimate and only holds in the case the parallel task is compute-bound, while here, in fact, most kernels are typically memory-bound. Regardless, the matrix-free operator and smoother application is parallelized by a loop over cells and inner loops over 1d or 3d quadrature point loops, depending on the operation. This means that higher order methods offer more parallelism and they also need to stream in less memory per DoF.

Based on this argument, we would expect better performance for higher order methods. This is clearly visible when we take a look at the following results (run on an RTX 6000 using the finest mesh that fits into memory):

Element DoFs Operator MDoFs/s
Q2Q1 53,070,468 1,746
Q3Q2 188,174,468 1,627
Q4Q3 58,112,836 2,895
Q5Q4 116,203,076 3,160

The number of DoFs processed per second for a single application of the Stokes operator clearly increases significantly with polynomial degree. The whole solver is a bit harder to objectively compare as the higher order discretization requires more iterations and adjustments to GMG parameters like the Chebychev degree of the smoother. To keep things simple, we omit these results here.

Performance Portability

The advantage of using Kokkos instead of writing a code in one of the vendor languages like CUDA, is that we can run on AMD, NVIDIA, Intel or other future GPU devices without any porting required. Furthermore, there are multiple host-parallel backends available in Kokkos and thus this tutorial also runs with the CPU-only configurations. This is perfect for code development and debugging, but of course significantly slower. By default, the Kokkos Serial backend does not use multiple cores or explicit SIMD instructions. As such, the CPU-based matrix-free deal.II code as in step-37 will be significantly faster.

As an example, here is a quick comparison between different workstation and server GPUs and the time to solution for the 53 million DoF problem size:

Name FP64 [TFLOPS] VRAM [GB] Bandwidth [TB/s] Operator [MDoFs/s] Solve Time [s]
AMD W7800 1.41 32 0.6 482 32
NVIDIA RTX 6000 1.97 96 1.8 1,746 11
NVIDIA H100 25.6 80 3.4 1,457 9
32 core 5975WX CPU 1.4 - 0.05 72 220

The last line highlights the advantage of building on Kokkos over implementing CUDA or HIP code directly: Even without further optimization for CPUs (for example no SIMD vectorization is currently used) we can achieve decent performance even when running without a GPU. Here we are using the serial Kokkos backend and 32 MPI ranks. This is certainly fast enough for debugging and code development.

Multiple GPUs

This example program can also be run on more than one GPU, which allows us to solve larger problems before running out of VRAM and to, at least ideally, achieve a faster time to solution.

The way one can use more than one GPU involves running with more than one MPI rank, like in step-40. This can be done by allocating one MPI rank per GPU. As an example, if you have single node or a workstation with 4 GPUs, you would run

mpirun -n 4 ./step-104

This is typically enough for Kokkos to automatically pick a different GPU for each MPI rank. Kokkos has an API and command line options to control job placement manually, but this should not be necessary. Note that you can also oversubscribe a GPU cluster by running more MPI ranks than GPUs on a system. This will result in more than one rank sharing a GPU, which will typically degrade performance due to scheduling overhead and workload imbalance.

Note that because of this performance degradation the typical rule of using one MPI rank per physical CPU core is not applicable for a GPU enabled code. Part of the code that runs on the CPU (matrix-free setup, error estimation, etc.) can make use of multiple threads. This means for optimal performance you will need to allocate several threads per MPI rank if you are running on a GPU cluster. For example, on the Clemson Palmetto Cluster a job submission of

salloc --gpus h100:8 --ntasks 8 --cpus-per-task 10 --mem-per-cpu 2gb --time 4:00:00

would allocate a node with 8 H100 GPUs, 8 MPI ranks, and 10 threads per rank.

As an example, here is a table with solve times running one of these nodes with up to 8 H100 GPUs:

Refinement DoFs 1 GPU (s) 2 GPU (s) 4 GPU (s) 8 GPU (s)
0 2,312 .065 .074 .078 .077
1 15,468 .16 .21 .21 .21
2 112,724 .49 1.10 .98 .81
3 859,812 2.9 4.1 3.4 3.3
4 6,714,692 25 30 18 14
5 53,070,468 210 280 160 100
6 421,991,684 - - 1500 970

Notice that we can solve larger problems with 4 or more GPUs compared to a single GPU. On the one hand, we also have some reduction in time to solution, at least for the larger problem sizes. On the other hand, notice that time to solution does not scale linearly. This is likely due to additional communication overhead, which we will investigate and improve in the future.

Possibilities for extensions

You might have noticed that we used Number instead of double throughout the tutorial program. This is on purpose. Try changing the template argument to float in main(). With this change, all computations are now done in single instead of double precision. Especially on workstation GPUs, single precision computations are significantly faster (up to 64x). Even when the computations are memory-bound and not compute-bound, which is the case for most of the kernels in this example, memory usage is reduced by a factor of 2. Check what speedup you can observe and see whether you can solve bigger problems with the same amount of GPU memory.

Solving with lower precision comes with a trade-off, though. The integration of the operator and right-hand side is less accurate, which can lead to a degradation in accuracy of the solution, especially on finer meshes. By comparing the errors, determine the refinement level where floating-point error is larger than the error introduced by the discretization and linear solver tolerance.

Furthermore, be aware that our error computation using VectorTools::integrate_difference is also less precise when performed in single precision. It would be better to convert the solution to double precision and compute the error in double precision. The function LinearAlgebra::distributed::Vector::copy_locally_owned_data_from can do this conversion for you. Does that make a difference?

Finally, a more advanced strategy involves solving the linear system in double precision while running the multigrid preconditioner in single precision. This approach is called "mixed precision preconditioning". The method is very attractive on GPUs as it combines the accuracy of the double precision approach with the performance of the single precision approach. Considering we spent most of the computational effort inside the velocity block multigrid preconditioner, which does not need to be very accurate, this seems promising. The implementation is not difficult apart from having to carefully choose the correct number type in all GMG-related objects. For clarity, the mixed precision approach is not included in this tutorial, but it makes for a great exercise!

The plain program

/* -----------------------------------------------------------------------------
*
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception OR LGPL-2.1-or-later
* Copyright (C) 2026 by the deal.II authors
*
* This file is part of the deal.II library.
*
* Detailed license information governing the source code and contributions
* can be found in LICENSE.md and CONTRIBUTING.md at the top level directory.
*
* -----------------------------------------------------------------------------
*/
namespace Step104
{
using namespace dealii;
constexpr unsigned int velocity_dof_handler_index = 0;
constexpr unsigned int pressure_dof_handler_index = 1;
template <int dim, typename Number>
class VelocityRightHandSide : public Function<dim, Number>
{
public:
VelocityRightHandSide()
: Function<dim, Number>(dim)
{}
virtual Number value(const Point<dim> &p,
const unsigned int component = 0) const override;
};
template <int dim, typename Number>
VelocityRightHandSide<dim, Number>::value(const Point<dim> &p,
const unsigned int component) const
{
AssertIndexRange(component, dim);
const double x = p[0];
const double y = p[1];
const double pi = numbers::PI;
const double pi2 = pi * pi;
const double sx = std::sin(pi * x);
const double cx = std::cos(pi * x);
const double sy = std::sin(pi * y);
const double cy = std::cos(pi * y);
if constexpr (dim == 2)
{
if (component == 0)
return pi * cy * (16.0 * pi2 * sx * sx * sy - 4.0 * pi2 * sy - sx);
else
return pi * cx * (-16.0 * pi2 * sx * sy * sy + 4.0 * pi2 * sx - sy);
}
else
{
const double z = p[2];
const double sz = std::sin(pi * z);
const double cz = std::cos(pi * z);
const double sx2 = sx * sx;
const double sy2 = sy * sy;
const double sz2 = sz * sz;
const double cx2 = cx * cx;
const double cy2 = cy * cy;
const double cz2 = cz * cz;
const double pi3 = pi * pi2;
if (component == 0)
return -16.0 * pi3 * sx2 * sy2 * sz * cz +
16.0 * pi3 * sx2 * sy * sz2 * cy -
4.0 * pi3 * sx2 * sy * cy * cz2 +
4.0 * pi3 * sx2 * sz * cy2 * cz - pi * sx * cy * cz +
4.0 * pi3 * sy2 * sz * cx2 * cz -
4.0 * pi3 * sy * sz2 * cx2 * cy;
else if (component == 1)
return 16.0 * pi3 * sx2 * sy2 * sz * cz -
4.0 * pi3 * sx2 * sz * cy2 * cz -
16.0 * pi3 * sx * sy2 * sz2 * cx +
4.0 * pi3 * sx * sy2 * cx * cz2 +
4.0 * pi3 * sx * sz2 * cx * cy2 -
4.0 * pi3 * sy2 * sz * cx2 * cz - pi * sy * cx * cz;
else
return -16.0 * pi3 * sx2 * sy * sz2 * cy +
4.0 * pi3 * sx2 * sy * cy * cz2 +
16.0 * pi3 * sx * sy2 * sz2 * cx -
4.0 * pi3 * sx * sy2 * cx * cz2 -
4.0 * pi3 * sx * sz2 * cx * cy2 +
4.0 * pi3 * sy * sz2 * cx2 * cy - pi * sz * cx * cy;
}
}
template <int dim, typename Number>
class VelocitySolution : public Function<dim, Number>
{
public:
VelocitySolution()
: Function<dim, Number>(dim)
{}
virtual Number value(const Point<dim> &p,
const unsigned int component = 0) const override;
};
template <int dim, typename Number>
VelocitySolution<dim, Number>::value(const Point<dim> &p,
const unsigned int component) const
{
AssertIndexRange(component, dim);
const double x = p[0];
const double y = p[1];
const double pi = numbers::PI;
const double sx = std::sin(pi * x);
const double sy = std::sin(pi * y);
const double s2x = std::sin(2.0 * pi * x);
const double s2y = std::sin(2.0 * pi * y);
if constexpr (dim == 2)
{
if (component == 0)
return pi * sx * sx * s2y;
else
return -pi * s2x * sy * sy;
}
else
{
const double z = p[2];
const double sz = std::sin(pi * z);
const double s2z = std::sin(2.0 * pi * z);
const double sx2 = sx * sx;
const double sy2 = sy * sy;
const double sz2 = sz * sz;
const double dphidx = pi * s2x * sy2 * sz2;
const double dphidy = pi * s2y * sx2 * sz2;
const double dphidz = pi * s2z * sx2 * sy2;
if (component == 0)
return dphidy - dphidz;
else if (component == 1)
return dphidz - dphidx;
else
return dphidx - dphidy;
}
}
template <int dim, typename Number>
class PressureSolution : public Function<dim, Number>
{
public:
PressureSolution()
: Function<dim, Number>()
{}
virtual Number value(const Point<dim> &p,
const unsigned int /*component*/ = 0) const override
{
const double pi = numbers::PI;
if constexpr (dim == 2)
return std::cos(pi * p[0]) * std::cos(pi * p[1]);
else
return std::cos(pi * p[0]) * std::cos(pi * p[1]) * std::cos(pi * p[2]);
}
};
template <int dim, int fe_degree, typename Number>
class VelocityOperatorQuad
{
public:
*fe_eval,
const int q_point) const
{
const auto gradient_u = fe_eval->get_gradient(q_point);
fe_eval->submit_gradient(gradient_u, q_point);
}
};
template <int dim,
int degree_u,
int degree_p,
typename Number,
int n_q_points_1d>
class VelocityCellOperator
{
public:
static const unsigned int n_q_points =
::Utilities::pow(n_q_points_1d, dim);
{
data, velocity_dof_handler_index);
fe_u.read_dof_values(src);
VelocityOperatorQuad<dim, degree_u, Number> quad_operation;
data->for_each_quad_point(
[&](const int q_point) { quad_operation(&fe_u, q_point); });
fe_u.integrate(EvaluationFlags::gradients);
fe_u.distribute_local_to_global(dst);
}
};
template <int dim,
int degree_u,
int degree_p,
typename Number = double,
typename VectorType =
int n_q_points_1d = degree_u + 1>
class PortableMFVelocityOperator : public EnableObserverPointer
{
public:
PortableMFVelocityOperator() = default;
PortableMFVelocityOperator(
std::shared_ptr<Portable::MatrixFree<dim, Number>> data_in)
: data(data_in)
{}
void reinit(std::shared_ptr<Portable::MatrixFree<dim, Number>> data_in)
{
data = data_in;
}
void initialize_dof_vector(VectorType &vec) const
{
data->initialize_dof_vector(vec, velocity_dof_handler_index);
}
{
return data->get_vector_partitioner(velocity_dof_handler_index)->size();
}
std::shared_ptr<DiagonalMatrix<
get_matrix_diagonal_inverse() const
{
return inverse_diagonal_entries;
}
double el(const types::global_dof_index row,
const types::global_dof_index col) const
{
(void)col;
Assert(row == col, ExcNotImplemented());
Assert(inverse_diagonal_entries.get() != nullptr &&
inverse_diagonal_entries->m() > 0,
ExcNotInitialized());
return 1.0 / (*inverse_diagonal_entries)(row, row);
}
void vmult(VectorType &dst, const VectorType &src) const
{
dst = static_cast<Number>(0.);
VelocityCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
velocity_operator;
data->cell_loop(velocity_operator, src, dst);
data->copy_constrained_values(src, dst, velocity_dof_handler_index);
}
void Tvmult(VectorType & /* dst */, const VectorType & /* src */) const
{
AssertThrow(false, ExcNotImplemented());
}
{
Assert(data.get() != nullptr, ExcNotInitialized());
this->inverse_diagonal_entries =
std::make_shared<DiagonalMatrix<VectorType>>();
VectorType &inverse_diagonal =
this->inverse_diagonal_entries->get_vector();
data->initialize_dof_vector(inverse_diagonal, velocity_dof_handler_index);
VelocityOperatorQuad<dim, degree_u, Number> velocity_operator_quad;
MatrixFreeTools::
compute_diagonal<dim, degree_u, degree_u + 1, dim, Number>(
*data.get(),
inverse_diagonal,
velocity_operator_quad,
velocity_dof_handler_index);
Number *raw_diagonal = inverse_diagonal.get_values();
Kokkos::parallel_for(
"invert A diagonal",
inverse_diagonal.locally_owned_size(),
KOKKOS_LAMBDA(int i) {
Assert(raw_diagonal[i] > 0.,
ExcMessage("Diagonal entries of a positive definite operator "
"should be positive"));
raw_diagonal[i] = 1. / raw_diagonal[i];
});
}
private:
std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
std::shared_ptr<DiagonalMatrix<VectorType>> inverse_diagonal_entries;
};
template <int dim,
int degree_u,
int degree_p,
typename Number,
int n_q_points_1d>
class MassOperatorQuad
{
public:
const int q_point) const
{
fe_eval->submit_value(fe_eval->get_value(q_point), q_point);
}
};
template <int dim,
int degree_u,
int degree_p,
typename Number,
int n_q_points_1d>
class MassCellOperator
{
public:
static const unsigned int n_q_points =
::Utilities::pow(n_q_points_1d, dim);
};
template <int dim,
int degree_u,
int degree_p,
typename Number,
int n_q_points_1d>
MassCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>::operator()(
{
data, pressure_dof_handler_index);
fe_p.read_dof_values(src);
fe_p.evaluate(EvaluationFlags::values);
MassOperatorQuad<dim, degree_u, degree_p, Number, n_q_points_1d>
quad_operation;
data->for_each_quad_point(
[&](const int &q_point) { quad_operation(&fe_p, q_point); });
fe_p.integrate(EvaluationFlags::values);
fe_p.distribute_local_to_global(dst);
}
template <int dim,
int degree_u,
int degree_p,
typename Number = double,
typename VectorType =
int n_q_points_1d = degree_u + 1>
class PortableMFMassOperator : public EnableObserverPointer
{
public:
PortableMFMassOperator(
const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
: data(data_in)
{}
{
return data->get_vector_partitioner(pressure_dof_handler_index)->size();
}
const types::global_dof_index col) const
{
(void)col;
Assert(row == col, ExcNotImplemented());
Assert(inverse_diagonal_entries.get() != nullptr &&
inverse_diagonal_entries->m() > 0,
ExcNotInitialized());
return 1.0 / (*inverse_diagonal_entries)(row, row);
}
void vmult(VectorType &dst, const VectorType &src) const
{
dst = static_cast<Number>(0.);
MassCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
mass_operator;
data->cell_loop(mass_operator, src, dst);
data->copy_constrained_values(src, dst, pressure_dof_handler_index);
}
std::shared_ptr<DiagonalMatrix<VectorType>>
get_matrix_diagonal_inverse() const
{
return inverse_diagonal_entries;
}
{
this->inverse_diagonal_entries =
std::make_shared<DiagonalMatrix<VectorType>>();
VectorType &inverse_diagonal =
this->inverse_diagonal_entries->get_vector();
MassOperatorQuad<dim, degree_u, degree_p, Number, n_q_points_1d>
quad_operation;
MatrixFreeTools::
compute_diagonal<dim, degree_p, n_q_points_1d, 1, Number>(
*data.get(),
inverse_diagonal,
quad_operation,
pressure_dof_handler_index);
Number *raw_diagonal = inverse_diagonal.get_values();
Kokkos::parallel_for(
"invert Mass diagonal",
inverse_diagonal.locally_owned_size(),
KOKKOS_LAMBDA(int i) {
Assert(raw_diagonal[i] > 0.,
ExcMessage("Diagonal entries of a positive definite operator "
"should be positive"));
raw_diagonal[i] = 1. / raw_diagonal[i];
});
}
private:
std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
std::shared_ptr<DiagonalMatrix<VectorType>> inverse_diagonal_entries;
};
template <int dim,
int degree_u,
int degree_p,
typename Number,
int n_q_points_1d>
class StokesCellOperator
{
public:
static const unsigned int n_q_points =
::Utilities::pow(n_q_points_1d, dim);
{
data, velocity_dof_handler_index);
data, pressure_dof_handler_index);
fe_u.read_dof_values(src.block(0));
fe_p.read_dof_values(src.block(1));
fe_p.evaluate(EvaluationFlags::values);
data->for_each_quad_point([&](const int &q_point) {
const Tensor<2, dim, Number> gradient_u = fe_u.get_gradient(q_point);
const Number pressure_value = fe_p.get_value(q_point);
Tensor<2, dim, Number> velocity_term = gradient_u;
for (unsigned int d = 0; d < dim; ++d)
velocity_term[d][d] -= pressure_value;
fe_u.submit_gradient(velocity_term, q_point);
const Number pressure_term = -trace(gradient_u);
fe_p.submit_value(pressure_term, q_point);
});
fe_u.integrate(EvaluationFlags::gradients);
fe_p.integrate(EvaluationFlags::values);
fe_u.distribute_local_to_global(dst.block(0));
fe_p.distribute_local_to_global(dst.block(1));
}
};
template <
int dim,
int degree_u,
int degree_p,
typename Number = double,
typename VectorType =
int n_q_points_1d = degree_u + 1>
class PortableMFStokesOperator
{
public:
PortableMFStokesOperator(
const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
: data(data_in)
{}
void vmult(VectorType &dst, const VectorType &src) const
{
dst = static_cast<Number>(0.);
StokesCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
stokes_operator;
data->cell_loop(stokes_operator, src, dst);
data->copy_constrained_values(src, dst);
}
private:
std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
};
template <int dim,
int degree_u,
int degree_p,
typename Number,
int n_q_points_1d>
class BTCellOperator
{
public:
static const unsigned int n_q_points =
::Utilities::pow(n_q_points_1d, dim);
{
data, velocity_dof_handler_index);
data, pressure_dof_handler_index);
fe_p.read_dof_values(src.block(1));
fe_p.evaluate(EvaluationFlags::values);
data->for_each_quad_point([&](const int &q_point) {
const Number pressure_value = fe_p.get_value(q_point);
fe_u.submit_divergence(-pressure_value, q_point);
});
fe_u.integrate(EvaluationFlags::gradients);
fe_u.distribute_local_to_global(dst.block(0));
}
};
template <
int dim,
int degree_u,
int degree_p,
typename Number = double,
typename VectorType =
int n_q_points_1d = degree_u + 1>
class PortableMFBTOperator
{
public:
PortableMFBTOperator(
const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
: data(data_in)
{}
void vmult(VectorType &dst, const VectorType &src) const
{
dst = static_cast<Number>(0.);
BTCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
cell_operator;
data->cell_loop(cell_operator, src, dst);
data->set_constrained_values(0.0,
dst.block(0),
velocity_dof_handler_index);
}
private:
std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
};
template <class AInvOperator,
class SInvOperator,
class BTOperator,
class VectorType>
class BlockSchurPreconditioner : public EnableObserverPointer
{
public:
BlockSchurPreconditioner(const AInvOperator &A_inverse_operator,
const SInvOperator &S_inverse_operator,
const BTOperator &BT_operator);
void vmult(VectorType &dst, const VectorType &src) const;
private:
mutable VectorType tmp;
const AInvOperator &A_inverse_operator;
const SInvOperator &S_inverse_operator;
const BTOperator &BT_operator;
};
template <class AInvOperator,
class SInvOperator,
class BTOperator,
class VectorType>
BlockSchurPreconditioner<AInvOperator, SInvOperator, BTOperator, VectorType>::
BlockSchurPreconditioner(const AInvOperator &A_inverse_operator,
const SInvOperator &S_inverse_operator,
const BTOperator &BT_operator)
: A_inverse_operator(A_inverse_operator)
, S_inverse_operator(S_inverse_operator)
, BT_operator(BT_operator)
{}
template <class AInvOperator,
class SInvOperator,
class BTOperator,
class VectorType>
void
BlockSchurPreconditioner<AInvOperator, SInvOperator, BTOperator, VectorType>::
vmult(VectorType &dst, const VectorType &src) const
{
if (tmp.size() == 0)
tmp.reinit(src);
{
S_inverse_operator.vmult(dst.block(1), src.block(1));
dst.block(1) *= -1.0;
}
{
BT_operator.vmult(tmp, dst);
tmp.block(0).sadd(-1.0, 1.0, src.block(0));
}
A_inverse_operator.vmult(dst.block(0), tmp.block(0));
}
template <int dim, int degree_p, typename Number = double>
class StokesProblem
{
public:
static constexpr unsigned int degree_u = degree_p + 1;
StokesProblem();
void run();
using VectorType =
using BlockVectorType =
private:
void setup_dofs();
void solve();
void postprocess();
MappingQ<dim> mapping;
FE_Q<dim> fe_p;
std::shared_ptr<Portable::MatrixFree<dim, Number>> mf_data;
BlockVectorType solution;
BlockVectorType rhs;
};
template <int dim, int degree_p, typename Number>
StokesProblem<dim, degree_p, Number>::StokesProblem()
: tria(MPI_COMM_WORLD)
, mapping(1)
, fe_u(FE_Q<dim>(degree_p + 1), dim)
, fe_p(degree_p)
, dof_u(tria)
, dof_p(tria)
, pcout(std::cout, Utilities::MPI::this_mpi_process(MPI_COMM_WORLD) == 0)
{}
template <int dim, int degree_p, typename Number>
void StokesProblem<dim, degree_p, Number>::setup_dofs()
{
dof_u.distribute_dofs(fe_u);
dof_p.distribute_dofs(fe_p);
const IndexSet &owned_set_u = dof_u.locally_owned_dofs();
const IndexSet relevant_set_u =
constraints_u.reinit(owned_set_u, relevant_set_u);
dof_u, 0, Functions::ZeroFunction<dim, Number>(dim), constraints_u);
constraints_u.close();
const IndexSet &owned_set_p = dof_p.locally_owned_dofs();
const IndexSet relevant_set_p =
constraints_p.reinit(owned_set_p, relevant_set_p);
constraints_p.close();
std::vector<const DoFHandler<dim> *> dof_handlers = {&dof_u, &dof_p};
std::vector<const AffineConstraints<Number> *> constraints = {
&constraints_u, &constraints_p};
mf_data = std::make_shared<Portable::MatrixFree<dim, Number>>();
const QGauss<1> quad(degree_p + 2);
mf_data->reinit(mapping, dof_handlers, constraints, quad, additional_data);
{
rhs_host;
mf_data->initialize_dof_vector(rhs_host);
dof_u,
QGauss<dim>(degree_u + 2),
VelocityRightHandSide<dim, Number>(),
rhs_host.block(0),
constraints_u);
mf_data->initialize_dof_vector(rhs);
rhs.block(0).import_elements(rhs_host.block(0), VectorOperation::insert);
rhs.block(1).import_elements(rhs_host.block(1), VectorOperation::insert);
}
}
template <int dim, int degree_p, typename Number>
void StokesProblem<dim, degree_p, Number>::solve()
{
PortableMFStokesOperator<dim, degree_u, degree_p, Number> stokes_operator(
mf_data);
mf_data->initialize_dof_vector(solution);
{
::Timer t(tria.get_mpi_communicator());
stokes_operator.vmult(solution, rhs);
const double time = t.wall_time();
const double mdofs_p_second =
1e-6 * static_cast<double>(solution.size()) / time;
pcout << "Stokes operator: " << time << " s, MDoFs/s: " << mdofs_p_second
<< std::endl;
solution = 0.0;
}
SolverControl solver_control(1000, 1e-8 * rhs.l2_norm());
solver_control,
using LevelMatrixType =
PortableMFVelocityOperator<dim, degree_u, degree_p, Number>;
using SmootherPreconditionerType = DiagonalMatrix<VectorType>;
using SmootherType = PreconditionChebyshev<LevelMatrixType,
SmootherPreconditionerType>;
using MGTransferType =
const auto coarse_grid_triangulations =
tria);
const unsigned int max_level = coarse_grid_triangulations.size() - 1;
const unsigned int min_level = std::min(3U, max_level - 1);
MGLevelObject<DoFHandler<dim>> mg_dof_handlers(min_level, max_level);
MGLevelObject<AffineConstraints<Number>> mg_constraints(min_level,
max_level);
MGLevelObject<LevelMatrixType> mg_matrices(min_level, max_level);
min_level, max_level);
std::vector<std::shared_ptr<Portable::MatrixFree<dim, Number>>>
mf_data_levels;
for (unsigned int level = min_level; level <= max_level; ++level)
{
auto &dof_handler = mg_dof_handlers[level];
auto &constraint = mg_constraints[level];
dof_handler.reinit(*coarse_grid_triangulations[level]);
dof_handler.distribute_dofs(fe_u);
constraint.reinit(dof_handler.locally_owned_dofs(),
DoFTools::make_hanging_node_constraints(dof_handler, constraint);
DoFTools::make_zero_boundary_constraints(dof_handler, constraint);
constraint.close();
additional_data;
additional_data.mapping_update_flags =
if (level == max_level)
mf_data_levels.emplace_back(mf_data);
else
{
const QGauss<1> quad(degree_p + 2);
mf_data_levels.emplace_back(
std::make_shared<Portable::MatrixFree<dim, Number>>());
mf_data_levels.back()->reinit(
mapping, dof_handler, constraint, quad, additional_data);
}
mg_matrices[level].reinit(mf_data_levels.back());
}
mg::Matrix<VectorType> mg_matrix(mg_matrices);
for (unsigned int level = min_level; level < max_level; ++level)
mg_transfers[level + 1].reinit_geometric_transfer(
mg_dof_handlers[level + 1],
mg_dof_handlers[level],
mg_constraints[level + 1],
mg_constraints[level]);
MGTransferType mg_transfer(mg_transfers, [&](const auto l, auto &vec) {
mg_matrices[l].initialize_dof_vector(vec);
});
min_level, max_level);
for (unsigned int level = min_level; level <= max_level; ++level)
{
mg_matrices[level].compute_diagonal();
smoother_data[level].preconditioner =
std::make_shared<SmootherPreconditionerType>(
*mg_matrices[level].get_matrix_diagonal_inverse());
smoother_data[level].constraints.copy_from(mg_constraints[level]);
if (level == min_level)
{
smoother_data[level].smoothing_range = 1e-3;
smoother_data[level].degree = numbers::invalid_unsigned_int;
smoother_data[level].eig_cg_n_iterations = 40;
}
else
{
smoother_data[level].smoothing_range = 5;
smoother_data[level].degree = 4;
smoother_data[level].eig_cg_n_iterations = 20;
}
}
mg_smoother.initialize(mg_matrices, smoother_data);
pcout << "GMG velocity block smoothers:" << std::endl;
for (unsigned int level = min_level; level <= max_level; ++level)
{
mg_matrices[level].initialize_dof_vector(vec);
auto eigenvalue_info =
mg_smoother.smoothers[level].estimate_eigenvalues(vec);
pcout << " level: " << level << " n_dofs: " << vec.size()
<< ", eigenvalue spectrum: [ "
<< eigenvalue_info.min_eigenvalue_estimate << ", "
<< eigenvalue_info.max_eigenvalue_estimate << " ]" << std::endl;
}
mg_coarse.initialize(mg_smoother);
mg_coarse,
mg_transfer,
mg_smoother,
mg_smoother,
min_level,
max_level);
::Timer timer_smoother;
::Timer timer_transfer;
::Timer timer_coarse;
::Timer timer_residual;
{
timer_smoother.reset();
timer_transfer.reset();
timer_coarse.reset();
timer_residual.reset();
auto make_timer_lambda = [&](::Timer &timer) {
return [&](const bool before, const unsigned int /*level*/) {
if (before)
timer.start();
else
timer.stop();
};
};
mg.connect_pre_smoother_step(make_timer_lambda(timer_smoother));
mg.connect_post_smoother_step(make_timer_lambda(timer_smoother));
mg.connect_residual_step(make_timer_lambda(timer_residual));
mg.connect_restriction(make_timer_lambda(timer_transfer));
mg.connect_prolongation(make_timer_lambda(timer_transfer));
mg.connect_coarse_solve(make_timer_lambda(timer_coarse));
}
APreconditionerType preconditioner_A(dof_u, mg, mg_transfer);
PortableMFMassOperator<dim, degree_u, degree_p, Number> mass_operator(
mf_data);
mass_operator.compute_diagonal();
using SPreconditionerType = PreconditionChebyshev<
PortableMFMassOperator<dim, degree_u, degree_p, Number>,
SPreconditionerType preconditioner_schur;
{
typename SPreconditionerType::AdditionalData additional_data;
additional_data.smoothing_range = 15.;
additional_data.degree = 3;
additional_data.eig_cg_n_iterations = 10;
additional_data.constraints.copy_from(constraints_p);
additional_data.preconditioner =
mass_operator.get_matrix_diagonal_inverse();
preconditioner_schur.initialize(mass_operator, additional_data);
}
using BTOperatorType =
PortableMFBTOperator<dim, degree_u, degree_p, Number>;
BTOperatorType BT_operator(mf_data);
BlockSchurPreconditioner<APreconditionerType,
SPreconditionerType,
BTOperatorType,
BlockVectorType>
preconditioner(preconditioner_A, preconditioner_schur, BT_operator);
::Timer t(tria.get_mpi_communicator());
solver.solve(stokes_operator, solution, rhs, preconditioner);
t.stop();
pcout << "Solver converged in " << solver_control.last_step()
<< " iterations in " << t.wall_time() << " seconds" << std::endl;
pcout << "Velocity block GMG timings:"
<< "\n smoother: " << timer_smoother.wall_time()
<< " s\n transfer: " << timer_transfer.wall_time()
<< " s\n coarse : " << timer_coarse.wall_time()
<< " s\n residual: " << timer_residual.wall_time() << " s"
<< std::endl;
}
template <int dim, int degree_p, typename Number>
void StokesProblem<dim, degree_p, Number>::postprocess()
{
solution_host;
mf_data->initialize_dof_vector(solution_host);
solution_host.block(0).import_elements(solution.block(0),
solution_host.block(1).import_elements(solution.block(1),
constraints_u.distribute(solution_host.block(0));
constraints_p.distribute(solution_host.block(1));
solution_host.update_ghost_values();
const double mean_pressure = VectorTools::compute_mean_value(
dof_p, QGauss<dim>(degree_p + 2), solution_host.block(1), 0);
solution_host.block(1).add(-mean_pressure);
const QGauss<dim> quadrature_formula(degree_u + 1);
Vector<double> cellwise_errors_ul2(tria.n_active_cells());
Vector<double> cellwise_errors_pl2(tria.n_active_cells());
solution_host.block(0),
VelocitySolution<dim, Number>(),
cellwise_errors_ul2,
quadrature_formula,
solution_host.block(1),
PressureSolution<dim, Number>(),
cellwise_errors_pl2,
quadrature_formula,
const double u_l2 = VectorTools::compute_global_error(tria,
cellwise_errors_ul2,
const double p_l2 = VectorTools::compute_global_error(tria,
cellwise_errors_pl2,
pcout << "velocity error: " << u_l2 << " pressure error: " << p_l2
<< std::endl;
}
template <int dim, int degree_p, typename Number>
void StokesProblem<dim, degree_p, Number>::run()
{
pcout << std::setprecision(10);
pcout << "Running on " << Utilities::MPI::n_mpi_processes(MPI_COMM_WORLD)
<< " MPI ranks (with " << MultithreadInfo::n_threads()
<< " threads each) in ";
if constexpr (running_in_debug_mode())
pcout << "DEBUG mode";
else
pcout << "RELEASE mode";
pcout << "\nKokkos execution space: "
<< Kokkos::DefaultExecutionSpace::name();
pcout << '\n'
<< "dim: " << dim << '\n'
<< "Element: Q" << degree_u << "-Q" << degree_p << std::endl;
unsigned int n_refinements = 10;
for (unsigned int i = 0; i < n_refinements; ++i)
{
if (i == 0)
{
tria.refine_global(2);
}
else
{
tria.refine_global(1);
}
setup_dofs();
pcout << "\nrefinement: " << i
<< ", n_dofs: " << dof_u.n_dofs() + dof_p.n_dofs() << " = "
<< dof_u.n_dofs() << " + " << dof_p.n_dofs() << std::endl;
solve();
postprocess();
}
}
} // namespace Step104
int main(int argc, char **argv)
{
using namespace Step104;
Utilities::MPI::MPI_InitFinalize mpi_initialization(argc, argv);
const unsigned int dim = 3;
const unsigned int degree_p = 1;
StokesProblem<dim, degree_p, double> problem;
problem.run();
}
MGLevelObject< RelaxationType > smoothers
DeviceVector< Number > & block(unsigned int index)
void submit_gradient(const gradient_type &gradient, const int q_point)
void submit_value(const value_type &val_in, const int q_point)
value_type get_value(const int q_point) const
gradient_type get_gradient(const int q_point) const
double wall_time() const
Definition timer.cc:274
void reset()
Definition timer.cc:360
SymmetricTensor< 2, dim, Number > e(const Tensor< 2, dim, Number > &F)
Tensor< 2, dim, Number > l(const Tensor< 2, dim, Number > &F, const Tensor< 2, dim, Number > &dF_dt)
SymmetricTensor< 2, dim, Number > d(const Tensor< 2, dim, Number > &F, const Tensor< 2, dim, Number > &dF_dt)
unsigned int this_mpi_process(const MPI_Comm mpi_communicator)
Definition mpi.cc:118
constexpr T pow(const T base, const int iexp)
Definition utilities.h:966
void run(const Iterator &begin, const std_cxx20::type_identity_t< Iterator > &end, Worker worker, Copier copier, const ScratchData &sample_scratch_data, const CopyData &sample_copy_data, const unsigned int queue_length, const unsigned int chunk_size)
void reinit(MatrixBlock< MatrixType > &v, const BlockSparsityPattern &p)