456 *
if constexpr (dim == 2)
467 * <a name=
"step_104-Thevelocityoperator"></a>
468 * <h3>The velocity
operator</h3>
472 * The
matrix-free
operator for the velocity block @f$A@f$
473 * given by @f$(\nabla u,\nabla v)@f$ is defined by the
class
474 * PortableMFVelocityOperator. It uses
475 * the
class VelocityCellOperator, which is evaluated in
parallel
476 * on each cell. On each cell, we define the action at each
477 * quadrature
point with the small helper
class VelocityOperatorQuad
484 *
template <
int dim,
int fe_degree,
typename Number>
485 *
class VelocityOperatorQuad
491 *
const int q_point)
const
493 *
const auto gradient_u = fe_eval->get_gradient(q_point);
494 *
fe_eval->submit_gradient(gradient_u, q_point);
505 *
class VelocityCellOperator
508 *
static const unsigned int n_q_points =
509 * ::Utilities::pow(n_q_points_1d, dim);
517 *
data, velocity_dof_handler_index);
519 *
fe_u.read_dof_values(src);
522 *
VelocityOperatorQuad<dim, degree_u, Number> quad_operation;
524 *
data->for_each_quad_point(
525 *
[&](
const int q_point) { quad_operation(&fe_u, q_point); });
528 *
fe_u.distribute_local_to_global(dst);
535 * This
class finally provides the
matrix-free operator
for the velocity
536 * block. Note that we also compute the inverse
diagonal of the
operator,
537 * which is used in the Chebyshev smoother when we
approximate A^{-1} with a
538 * GMG v-cycle. We note that Tvmult() is not implemented because it is not
539 * required
for the smoother and @f$A@f$ is symmetric anyway.
545 *
typename Number =
double,
546 *
typename VectorType =
548 *
int n_q_points_1d = degree_u + 1>
552 *
PortableMFVelocityOperator() =
default;
554 *
PortableMFVelocityOperator(
564 *
void initialize_dof_vector(VectorType &vec)
const
566 *
data->initialize_dof_vector(vec, velocity_dof_handler_index);
571 *
return data->get_vector_partitioner(velocity_dof_handler_index)->size();
576 *
get_matrix_diagonal_inverse() const
578 *
return inverse_diagonal_entries;
585 *
Assert(row == col, ExcNotImplemented());
586 *
Assert(inverse_diagonal_entries.get() !=
nullptr &&
587 *
inverse_diagonal_entries->m() > 0,
588 *
ExcNotInitialized());
589 *
return 1.0 / (*inverse_diagonal_entries)(row, row);
592 *
void vmult(VectorType &dst,
const VectorType &src)
const
594 *
dst =
static_cast<Number>(0.);
595 *
VelocityCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
597 *
data->cell_loop(velocity_operator, src, dst);
599 *
data->copy_constrained_values(src, dst, velocity_dof_handler_index);
602 *
void Tvmult(VectorType & ,
const VectorType & )
const
609 *
Assert(
data.get() !=
nullptr, ExcNotInitialized());
611 *
this->inverse_diagonal_entries =
612 *
std::make_shared<DiagonalMatrix<VectorType>>();
614 *
this->inverse_diagonal_entries->get_vector();
615 *
data->initialize_dof_vector(inverse_diagonal, velocity_dof_handler_index);
617 *
VelocityOperatorQuad<dim, degree_u, Number> velocity_operator_quad;
620 *
compute_diagonal<dim, degree_u, degree_u + 1, dim, Number>(
623 *
velocity_operator_quad,
626 *
velocity_dof_handler_index);
628 *
Number *raw_diagonal = inverse_diagonal.get_values();
630 *
Kokkos::parallel_for(
631 *
"invert A diagonal",
632 *
inverse_diagonal.locally_owned_size(),
633 *
KOKKOS_LAMBDA(
int i) {
634 *
Assert(raw_diagonal[i] > 0.,
635 *
ExcMessage(
"Diagonal entries of a positive definite operator "
636 *
"should be positive"));
637 *
raw_diagonal[i] = 1. / raw_diagonal[i];
642 *
std::shared_ptr<Portable::MatrixFree<dim, Number>>
data;
643 *
std::shared_ptr<DiagonalMatrix<VectorType>> inverse_diagonal_entries;
651 * <a name=
"step_104-TheSchurcomplementoperator"></a>
652 * <h3>The Schur complement
operator</h3>
656 * The preconditioner
requires a Schur complement
657 * approximation, which is here given by a mass
matrix in the
658 * pressure space @f$(p,q)@f$. This is implemented in a very similar way
659 * to the velocity block above.
A notable difference is that
660 * we select the pressure by passing a dof_handler_index of 1
669 *
class MassOperatorQuad
674 *
const int q_point)
const
676 *
fe_eval->submit_value(fe_eval->get_value(q_point), q_point);
687 *
class MassCellOperator
690 *
static const unsigned int n_q_points =
691 * ::Utilities::pow(n_q_points_1d, dim);
707 *
MassCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>::operator()(
713 *
data, pressure_dof_handler_index);
715 *
fe_p.read_dof_values(src);
718 *
MassOperatorQuad<dim, degree_u, degree_p, Number, n_q_points_1d>
720 *
data->for_each_quad_point(
721 *
[&](
const int &q_point) { quad_operation(&fe_p, q_point); });
724 *
fe_p.distribute_local_to_global(dst);
732 *
typename Number = double,
735 *
int n_q_points_1d = degree_u + 1>
739 *
PortableMFMassOperator(
746 *
return data->get_vector_partitioner(pressure_dof_handler_index)->size();
754 *
Assert(inverse_diagonal_entries.get() !=
nullptr &&
755 *
inverse_diagonal_entries->m() > 0,
756 *
ExcNotInitialized());
757 *
return 1.0 / (*inverse_diagonal_entries)(row, row);
760 *
void vmult(VectorType &dst,
const VectorType &src)
const
762 *
dst =
static_cast<Number>(0.);
763 *
MassCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
765 *
data->cell_loop(mass_operator, src, dst);
767 *
data->copy_constrained_values(src, dst, pressure_dof_handler_index);
770 *
std::shared_ptr<DiagonalMatrix<VectorType>>
771 *
get_matrix_diagonal_inverse() const
773 *
return inverse_diagonal_entries;
778 *
this->inverse_diagonal_entries =
779 *
std::make_shared<DiagonalMatrix<VectorType>>();
781 *
this->inverse_diagonal_entries->get_vector();
783 *
MassOperatorQuad<dim, degree_u, degree_p, Number, n_q_points_1d>
787 *
compute_diagonal<dim, degree_p, n_q_points_1d, 1, Number>(
793 *
pressure_dof_handler_index);
795 *
Number *raw_diagonal = inverse_diagonal.get_values();
797 *
Kokkos::parallel_for(
798 *
"invert Mass diagonal",
799 *
inverse_diagonal.locally_owned_size(),
800 *
KOKKOS_LAMBDA(
int i) {
801 *
Assert(raw_diagonal[i] > 0.,
802 *
ExcMessage(
"Diagonal entries of a positive definite operator "
803 *
"should be positive"));
804 *
raw_diagonal[i] = 1. / raw_diagonal[i];
809 *
std::shared_ptr<Portable::MatrixFree<dim, Number>>
data;
810 *
std::shared_ptr<DiagonalMatrix<VectorType>> inverse_diagonal_entries;
818 * <a name=
"step_104-TheStokesoperator"></a>
819 * <h3>The Stokes
operator</h3>
823 * The following
set of classes provides the whole Stokes
operator
825 * \
begin{bmatrix}
A & B^
T \\ B & 0 \
end{bmatrix}.
827 * While structured in a similar way (
class PortableMFStokesOperator uses
828 * StokesCellOperator and a lambda function
for the action at each
831 * and
one for the pressure.
835 * Note that we don
't need support for computing the diagonal, as this
836 * is not needed in the block preconditioner.
844 * class StokesCellOperator
847 * static const unsigned int n_q_points =
848 * ::Utilities::pow(n_q_points_1d, dim);
850 * DEAL_II_HOST_DEVICE void
851 * operator()(const typename Portable::MatrixFree<dim, Number>::Data *data,
852 * const Portable::DeviceBlockVector<Number> &src,
853 * Portable::DeviceBlockVector<Number> &dst) const
855 * Portable::FEEvaluation<dim, degree_u, n_q_points_1d, dim, Number> fe_u(
856 * data, velocity_dof_handler_index);
857 * Portable::FEEvaluation<dim, degree_p, n_q_points_1d, 1, Number> fe_p(
858 * data, pressure_dof_handler_index);
860 * fe_u.read_dof_values(src.block(0));
861 * fe_p.read_dof_values(src.block(1));
862 * fe_u.evaluate(EvaluationFlags::gradients);
863 * fe_p.evaluate(EvaluationFlags::values);
865 * data->for_each_quad_point([&](const int &q_point) {
866 * const Tensor<2, dim, Number> gradient_u = fe_u.get_gradient(q_point);
867 * const Number pressure_value = fe_p.get_value(q_point);
869 * Tensor<2, dim, Number> velocity_term = gradient_u;
870 * for (unsigned int d = 0; d < dim; ++d)
871 * velocity_term[d][d] -= pressure_value;
872 * fe_u.submit_gradient(velocity_term, q_point);
874 * const Number pressure_term = -trace(gradient_u);
875 * fe_p.submit_value(pressure_term, q_point);
878 * fe_u.integrate(EvaluationFlags::gradients);
879 * fe_p.integrate(EvaluationFlags::values);
880 * fe_u.distribute_local_to_global(dst.block(0));
881 * fe_p.distribute_local_to_global(dst.block(1));
890 * typename Number = double,
891 * typename VectorType =
892 * LinearAlgebra::distributed::BlockVector<Number, MemorySpace::Default>,
893 * int n_q_points_1d = degree_u + 1>
894 * class PortableMFStokesOperator
897 * PortableMFStokesOperator(
898 * const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
902 * void vmult(VectorType &dst, const VectorType &src) const
904 * dst = static_cast<Number>(0.);
905 * StokesCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
907 * data->cell_loop(stokes_operator, src, dst);
909 * data->copy_constrained_values(src, dst);
913 * std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
921 * <a name="step_104-TheBToperator"></a>
922 * <h3>The BT operator</h3>
923 * The last ingredient of the block preconditioner is the action
924 * of the block @f$B^T@f$ given by @f$-(p,\nabla \cdot v)@f$. The operator reads
925 * pressure values @f$p@f$ and produces
926 * a velocity (you can think of it as a rectangular matrix block). Therefore,
927 * the implementation is similar to the Stokes operator in that we work with
928 * two Portable::FEEvaluation objects and operate on
929 * Portable::DeviceBlockVector.
937 * class BTCellOperator
940 * static const unsigned int n_q_points =
941 * ::Utilities::pow(n_q_points_1d, dim);
943 * DEAL_II_HOST_DEVICE void
944 * operator()(const typename Portable::MatrixFree<dim, Number>::Data *data,
945 * const Portable::DeviceBlockVector<Number> &src,
946 * Portable::DeviceBlockVector<Number> &dst) const
948 * Portable::FEEvaluation<dim, degree_u, n_q_points_1d, dim, Number> fe_u(
949 * data, velocity_dof_handler_index);
950 * Portable::FEEvaluation<dim, degree_p, n_q_points_1d, 1, Number> fe_p(
951 * data, pressure_dof_handler_index);
953 * fe_p.read_dof_values(src.block(1));
954 * fe_p.evaluate(EvaluationFlags::values);
956 * data->for_each_quad_point([&](const int &q_point) {
957 * const Number pressure_value = fe_p.get_value(q_point);
958 * fe_u.submit_divergence(-pressure_value, q_point);
961 * fe_u.integrate(EvaluationFlags::gradients);
962 * fe_u.distribute_local_to_global(dst.block(0));
972 * typename Number = double,
973 * typename VectorType =
974 * LinearAlgebra::distributed::BlockVector<Number, MemorySpace::Default>,
975 * int n_q_points_1d = degree_u + 1>
976 * class PortableMFBTOperator
979 * PortableMFBTOperator(
980 * const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
986 * void vmult(VectorType &dst, const VectorType &src) const
988 * dst = static_cast<Number>(0.);
989 * BTCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
991 * data->cell_loop(cell_operator, src, dst);
995 * Instead of copying constrained values, zero them out. The BT operator
996 * does not receive an input velocity to copy values from and zeroing out
997 * is the correct operation for boundary and hanging nodes for an update
1001 * data->set_constrained_values(0.0,
1003 * velocity_dof_handler_index);
1007 * std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
1015 * <a name="step_104-ThePreconditionercodeBlockSchurPreconditionercode"></a>
1016 * <h3>The Preconditioner <code>BlockSchurPreconditioner</code></h3>
1020 * The following class implements the block preconditioner. The class
1021 * takes the types of the operators for @f$A^{-1}@f$, @f$S^{-1}@f$, and
1022 * @f$B^T@f$ as template arguments. This is the same preconditioner used
1023 * in @ref step_32 "step-32" and @ref step_56 "step-56".
1027 * We keep a temporary vector `tmp` inside this class to avoid reallocating
1028 * memory in every vmult call. It has to be mutable because it is used in the
1029 * vmult method that is declared as const.
1032 * template <class AInvOperator,
1033 * class SInvOperator,
1036 * class BlockSchurPreconditioner : public EnableObserverPointer
1040 * * @brief Constructor
1041 * * @param A_inverse_operator Approximation of the inverse of the velocity block.
1042 * * @param S_inverse_operator Approximation of the inverse Schur complement.
1043 * * @param BT_operator Operator for the B^T block of the Stokes system. Note
1044 * * that this operator is exactly applied while @p A_inverse_operator and @p S_inverse_operator
1045 * * are approximations for the purpose of the block preconditioner.
1047 * BlockSchurPreconditioner(const AInvOperator &A_inverse_operator,
1048 * const SInvOperator &S_inverse_operator,
1049 * const BTOperator &BT_operator);
1052 * * Matrix-vector product with this preconditioner object.
1054 * void vmult(VectorType &dst, const VectorType &src) const;
1057 * mutable VectorType tmp;
1058 * const AInvOperator &A_inverse_operator;
1059 * const SInvOperator &S_inverse_operator;
1060 * const BTOperator &BT_operator;
1065 * template <class AInvOperator,
1066 * class SInvOperator,
1069 * BlockSchurPreconditioner<AInvOperator, SInvOperator, BTOperator, VectorType>::
1070 * BlockSchurPreconditioner(const AInvOperator &A_inverse_operator,
1071 * const SInvOperator &S_inverse_operator,
1072 * const BTOperator &BT_operator)
1073 * : A_inverse_operator(A_inverse_operator)
1074 * , S_inverse_operator(S_inverse_operator)
1075 * , BT_operator(BT_operator)
1080 * template <class AInvOperator,
1081 * class SInvOperator,
1085 * BlockSchurPreconditioner<AInvOperator, SInvOperator, BTOperator, VectorType>::
1086 * vmult(VectorType &dst, const VectorType &src) const
1090 * Allocate the temporary vector on first use. Its content doesn't matter,
1091 * as it will be overwritten in the vmult() call.
1094 *
if (tmp.
size() == 0)
1099 * First apply the Schur Complement inverse operator: dst.p = S^-1 * src.p
1103 *
S_inverse_operator.vmult(dst.block(1), src.block(1));
1104 *
dst.block(1) *= -1.0;
1109 * Apply the top right block: tmp.u = -B^
T * dst.p + src.u
1113 *
BT_operator.vmult(tmp, dst);
1114 *
tmp.block(0).sadd(-1.0, 1.0, src.block(0));
1119 * Finally the velocity block:
1122 *
A_inverse_operator.vmult(dst.block(0), tmp.block(0));
1130 * <a name=
"step_104-ThemainclasscodeStokesProblemcode"></a>
1131 * <h3>The
main class <code>StokesProblem</code></h3>
1135 * The remaining part of
this tutorial is the StokesProblem
class
1136 * that puts everything together.
1139 *
template <
int dim,
int degree_p,
typename Number =
double>
1140 *
class StokesProblem
1143 *
static constexpr unsigned int degree_u = degree_p + 1;
1151 *
using BlockVectorType =
1155 *
void setup_dofs();
1159 *
void postprocess();
1174 *
std::shared_ptr<Portable::MatrixFree<dim, Number>> mf_data;
1175 *
BlockVectorType solution;
1176 *
BlockVectorType rhs;
1181 *
template <
int dim,
int degree_p,
typename Number>
1182 *
StokesProblem<dim, degree_p, Number>::StokesProblem()
1183 *
: tria(MPI_COMM_WORLD)
1194 * The setup_dofs() function distributes the two separate DoFHandlers
for the
1195 * velocity and pressure before initializing the
MatrixFree object with an
1196 *
std::vector of both of them. We can later refer to the velocity using
1200 *
template <
int dim,
int degree_p, typename Number>
1201 *
void StokesProblem<dim, degree_p, Number>::setup_dofs()
1204 *
dof_p.distribute_dofs(fe_p);
1206 *
const IndexSet &owned_set_u = dof_u.locally_owned_dofs();
1209 *
constraints_u.reinit(owned_set_u, relevant_set_u);
1213 *
constraints_u.close();
1215 *
const IndexSet &owned_set_p = dof_p.locally_owned_dofs();
1219 *
constraints_p.reinit(owned_set_p, relevant_set_p);
1221 *
constraints_p.close();
1223 *
std::vector<const DoFHandler<dim> *> dof_handlers = {&dof_u, &dof_p};
1224 *
std::vector<const AffineConstraints<Number> *> constraints = {
1225 *
&constraints_u, &constraints_p};
1227 *
mf_data = std::make_shared<Portable::MatrixFree<dim, Number>>();
1232 *
mf_data->reinit(mapping, dof_handlers, constraints, quad, additional_data);
1237 * create the right hand side on the host and move to device:
1245 *
mf_data->initialize_dof_vector(rhs_host);
1250 *
VelocityRightHandSide<dim, Number>(),
1251 *
rhs_host.block(0),
1254 *
mf_data->initialize_dof_vector(rhs);
1263 * In the solve() function we
set up the preconditioner and run the GMRES
1264 * solver. For this, we construct the multigrid
1265 * hierarchy
for the GMG v-cycle with a Chebyshev iteration around the
1266 * point-Jacobi scheme, i.e., the inverse of the diagonal of @f$A@f$, to
1267 * approximate the action of @f$A^{-1}@f$.
1268 * We
approximate the Schur Complement with a Chebyshev iteration
1269 * applied to the pressure mass
matrix (without multigrid).
1272 *
template <
int dim,
int degree_p,
typename Number>
1273 *
void StokesProblem<dim, degree_p, Number>::solve()
1275 *
PortableMFStokesOperator<dim, degree_u, degree_p, Number> stokes_operator(
1278 *
mf_data->initialize_dof_vector(solution);
1281 * ::Timer t(tria.get_mpi_communicator());
1282 *
stokes_operator.vmult(solution, rhs);
1283 *
const double time = t.wall_time();
1284 *
const double mdofs_p_second =
1285 *
1
e-6 *
static_cast<double>(solution.size()) / time;
1286 *
pcout <<
"Stokes operator: " << time <<
" s, MDoFs/s: " << mdofs_p_second
1297 *
using LevelMatrixType =
1298 *
PortableMFVelocityOperator<dim, degree_u, degree_p, Number>;
1302 *
SmootherPreconditionerType>;
1303 *
using MGTransferType =
1306 *
const auto coarse_grid_triangulations =
1310 *
const unsigned int max_level = coarse_grid_triangulations.size() - 1;
1313 * Do not go down to
level 0, because
this will lead to slower runtime as
1314 * the problem becomes very small:
1317 *
const unsigned int min_level =
std::min(3U, max_level - 1);
1325 *
min_level, max_level);
1327 *
std::vector<std::shared_ptr<Portable::MatrixFree<dim, Number>>>
1332 * Prepare the operators and
data structures on all levels of the multigrid
1338 *
auto &dof_handler = mg_dof_handlers[
level];
1339 *
auto &constraint = mg_constraints[
level];
1341 *
dof_handler.reinit(*coarse_grid_triangulations[
level]);
1342 *
dof_handler.distribute_dofs(fe_u);
1344 *
constraint.reinit(dof_handler.locally_owned_dofs(),
1349 *
constraint.close();
1356 *
if (
level == max_level)
1360 * Stokes
operator. This way we can solve significantly larger
1361 * problems before we
run out of device memory.
1364 *
mf_data_levels.emplace_back(mf_data);
1368 *
mf_data_levels.emplace_back(
1371 *
mf_data_levels.back()->reinit(
1372 *
mapping, dof_handler, constraint, quad, additional_data);
1375 *
mg_matrices[
level].reinit(mf_data_levels.back());
1386 *
mg_transfers[
level + 1].reinit_geometric_transfer(
1387 *
mg_dof_handlers[
level + 1],
1388 *
mg_dof_handlers[
level],
1389 *
mg_constraints[
level + 1],
1390 *
mg_constraints[
level]);
1392 *
MGTransferType mg_transfer(mg_transfers, [&](
const auto l,
auto &vec) {
1393 *
mg_matrices[
l].initialize_dof_vector(vec);
1402 *
min_level, max_level);
1406 *
mg_matrices[
level].compute_diagonal();
1407 *
smoother_data[
level].preconditioner =
1408 *
std::make_shared<SmootherPreconditionerType>(
1409 *
*mg_matrices[
level].get_matrix_diagonal_inverse());
1410 *
smoother_data[
level].constraints.copy_from(mg_constraints[
level]);
1412 *
if (
level == min_level)
1416 * Use the Chebyshev iteration as an (approximate) solver on the
1417 * coarsest
level. In
this mode @p smoothing_range is a relative
1418 * target tolerance and must be strictly less than
one; the number
1419 * of iterations is then chosen automatically by setting
1421 * CG iterations
for the eigenvalue estimate because when
1422 * @p min_level > 0, the coarse problem can still be reasonably
1423 * large and badly conditioned.
1426 *
smoother_data[
level].smoothing_range = 1
e-3;
1428 *
smoother_data[
level].eig_cg_n_iterations = 40;
1434 * These
values are chosen by experimentation
for the problem at
1435 * hand. We chose the smoothing range
first.
A good
value will allow
1436 * the smoother to effectively separate large and small
scale
1437 * oscillations in the residual and as such improve the convergence
1438 * of the Chebyshev iteration and the multigrid method. Finally, the
1439 * degree is chosen to minimize total runtime (a larger value
1440 * increases the cost but improves the outer number of GMRES
1444 *
smoother_data[
level].smoothing_range = 5;
1445 *
smoother_data[
level].degree = 4;
1446 *
smoother_data[
level].eig_cg_n_iterations = 20;
1451 *
mg_smoother.
initialize(mg_matrices, smoother_data);
1455 * Estimate and print the eigenvalue spectrum of the velocity block on each
1456 *
level. This spectrum is later used by the Chebyshev iteration.
1459 *
pcout <<
"GMG velocity block smoothers:" << std::endl;
1463 *
mg_matrices[
level].initialize_dof_vector(vec);
1464 *
auto eigenvalue_info =
1465 *
mg_smoother.smoothers[
level].estimate_eigenvalues(vec);
1466 *
pcout <<
" level: " <<
level <<
" n_dofs: " << vec.size()
1467 *
<<
", eigenvalue spectrum: [ "
1468 *
<< eigenvalue_info.min_eigenvalue_estimate <<
", "
1469 *
<< eigenvalue_info.max_eigenvalue_estimate <<
" ]" << std::endl;
1474 * coarse-grid solver
1482 * put everything together
1494 * ::Timer timer_smoother;
1495 * ::Timer timer_transfer;
1496 * ::Timer timer_coarse;
1497 * ::Timer timer_residual;
1499 *
timer_smoother.reset();
1500 *
timer_transfer.reset();
1501 *
timer_coarse.reset();
1502 *
timer_residual.reset();
1504 *
auto make_timer_lambda = [&](
::Timer &timer) {
1505 *
return [&](
const bool before,
const unsigned int ) {
1512 *
mg.connect_pre_smoother_step(make_timer_lambda(timer_smoother));
1513 *
mg.connect_post_smoother_step(make_timer_lambda(timer_smoother));
1514 *
mg.connect_residual_step(make_timer_lambda(timer_residual));
1515 *
mg.connect_restriction(make_timer_lambda(timer_transfer));
1516 *
mg.connect_prolongation(make_timer_lambda(timer_transfer));
1517 *
mg.connect_coarse_solve(make_timer_lambda(timer_coarse));
1521 *
APreconditionerType preconditioner_A(dof_u,
mg, mg_transfer);
1524 *
PortableMFMassOperator<dim, degree_u, degree_p, Number> mass_operator(
1526 *
mass_operator.compute_diagonal();
1529 *
PortableMFMassOperator<dim, degree_u, degree_p, Number>,
1532 *
SPreconditionerType preconditioner_schur;
1534 *
typename SPreconditionerType::AdditionalData additional_data;
1535 *
additional_data.smoothing_range = 15.;
1536 *
additional_data.degree = 3;
1537 *
additional_data.eig_cg_n_iterations = 10;
1538 *
additional_data.constraints.copy_from(constraints_p);
1539 *
additional_data.preconditioner =
1540 *
mass_operator.get_matrix_diagonal_inverse();
1542 *
preconditioner_schur.initialize(mass_operator, additional_data);
1545 *
using BTOperatorType =
1546 *
PortableMFBTOperator<dim, degree_u, degree_p, Number>;
1547 *
BTOperatorType BT_operator(mf_data);
1549 *
BlockSchurPreconditioner<APreconditionerType,
1550 *
SPreconditionerType,
1553 *
preconditioner(preconditioner_A, preconditioner_schur, BT_operator);
1555 * ::Timer t(tria.get_mpi_communicator());
1556 *
solver.solve(stokes_operator, solution, rhs, preconditioner);
1559 *
pcout <<
"Solver converged in " << solver_control.last_step()
1560 *
<<
" iterations in " << t.wall_time() <<
" seconds" << std::endl;
1562 *
pcout <<
"Velocity block GMG timings:"
1563 *
<<
"\n smoother: " << timer_smoother.wall_time()
1564 *
<<
" s\n transfer: " << timer_transfer.wall_time()
1565 *
<<
" s\n coarse : " << timer_coarse.wall_time()
1566 *
<<
" s\n residual: " << timer_residual.wall_time() <<
" s"
1574 * The postprocess() function moves the solution to host memory
1575 * and integrates the difference to the manufactured solution to
1579 *
template <
int dim,
int degree_p, typename Number>
1580 *
void StokesProblem<dim, degree_p, Number>::postprocess()
1584 *
mf_data->initialize_dof_vector(solution_host);
1586 *
solution_host.
block(0).import_elements(solution.block(0),
1588 *
solution_host.block(1).import_elements(solution.block(1),
1591 *
constraints_u.distribute(solution_host.block(0));
1592 *
constraints_p.distribute(solution_host.block(1));
1593 *
solution_host.update_ghost_values();
1595 *
dof_p,
QGauss<dim>(degree_p + 2), solution_host.block(1), 0);
1596 *
solution_host.block(1).add(-mean_pressure);
1598 *
const QGauss<dim> quadrature_formula(degree_u + 1);
1604 *
solution_host.block(0),
1605 *
VelocitySolution<dim, Number>(),
1606 *
cellwise_errors_ul2,
1607 *
quadrature_formula,
1610 *
solution_host.block(1),
1611 *
PressureSolution<dim, Number>(),
1612 *
cellwise_errors_pl2,
1613 *
quadrature_formula,
1617 *
cellwise_errors_ul2,
1620 *
cellwise_errors_pl2,
1623 *
pcout <<
"velocity error: " << u_l2 <<
" pressure error: " << p_l2
1631 * The
run() function prints some statistics and then performs a familiar
1635 *
template <
int dim,
int degree_p, typename Number>
1636 *
void StokesProblem<dim, degree_p, Number>::run()
1638 *
pcout << std::setprecision(10);
1641 *
<<
" threads each) in ";
1643 *
pcout <<
"DEBUG mode";
1645 *
pcout <<
"RELEASE mode";
1647 *
pcout <<
"\nKokkos execution space: "
1648 *
<< Kokkos::DefaultExecutionSpace::name();
1650 *
<<
"dim: " << dim <<
'\n'
1651 *
<<
"Element: Q" << degree_u <<
"-Q" << degree_p << std::endl;
1653 *
unsigned int n_refinements = 10;
1655 *
for (
unsigned int i = 0; i < n_refinements; ++i)
1660 *
tria.refine_global(2);
1664 *
tria.refine_global(1);
1668 *
pcout <<
"\nrefinement: " << i
1669 *
<<
", n_dofs: " << dof_u.n_dofs() + dof_p.n_dofs() <<
" = "
1670 *
<< dof_u.n_dofs() <<
" + " << dof_p.n_dofs() << std::endl;
1683 * <a name=
"step_104-Thecodemaincodefunction"></a>
1684 * <h3>The <code>
main()</code> function</h3>
1688 * The only interesting bits here are the
template arguments that
1689 * specify dimension and polynomial degree to be used.
1692 *
int main(
int argc,
char **argv)
1694 *
using namespace Step104;
1697 *
const unsigned int dim = 3;
1698 *
const unsigned int degree_p = 1;
1699 *
StokesProblem<dim, degree_p, double> problem;
1703<a name=
"step_104-Results"></a><h1>Results</h1>
1706<a name=
"step_104-Validation"></a><h3>Validation</h3>
1709When running the program on a single GPU (here an H100) and computing the
L2 error norms
1710between the finite element solution and the analytical solution discussed above,
1711we can produce the following table of results:
1713| Refinement | DoFs | Iterations | Time (s) | Velocity Error | Pressure Error |
1714|-----------:|-----------:|-----------:|------------:|---------------:|---------------:|
1715| 0 | 2,312 | 10 | 2.4e-02 | 4.5e-02 | 1.5e-01 |
1716| 1 | 15,468 | 20 | 6.0e-02 | 6.0e-03 | 1.2e-02 |
1717| 2 | 112,724 | 22 | 1.1e-01 | 7.6e-04 | 1.2e-03 |
1718| 3 | 859,812 | 23 | 2.5e-01 | 9.6e-05 | 2.3e-04 |
1719| 4 | 6,714,692 | 23 | 1.2e+00 | 1.2e-05 | 5.6e-05 |
1720| 5 | 53,070,468 | 24 | 8.8e+00 | 1.5e-06 | 1.4e-05 |
1722We see optimal convergence rates
for L2 errors of velocity and pressure (3 and 2, respectively,
1723which corresponds to best-approximation of the Q2 and Q1 spaces)
1724and decent linear scaling with problem
size:
A factor 8 in increase in unknowns leads to
1725a factor of 8 in solve time, at least on the finest refinement
level.
1727<a name=
"step_104-HigherOrderThroughput"></a><h3>Higher Order Throughput</h3>
1730GPUs profit from fine-grained parallelism. In fact, a modern GPU like an H100
requires
1731a significant number of small
parallel work items to saturate the device. An H100 has
1732132 SMs, each needing about 10-20 thread blocks to be occupied, with 256 threads per
1733block. This would require at least 300,000
parallel threads to make use of the whole
1734GPU. This is only a simplified estimate and only holds in the
case the
parallel task
1735is compute-bound,
while here, in fact, most kernels are typically memory-bound.
1736Regardless, the
matrix-free
operator and smoother application is parallelized by
1737a
loop over cells and inner loops over 1
d or 3
d quadrature
point loops, depending on the
1738operation. This means that higher order methods offer more parallelism and
1739they also need to stream in less memory per DoF.
1741Based on
this argument, we would expect better performance
for higher order methods.
1742This is clearly visible when we take a look at the following results (run on an RTX
17436000
using the finest mesh that fits into memory):
1745| Element | DoFs | Operator MDoFs/s |
1746|---------|------------:|-----------------:|
1747| Q2Q1 | 53,070,468 | 1,746 |
1748| Q3Q2 | 188,174,468 | 1,627 |
1749| Q4Q3 | 58,112,836 | 2,895 |
1750| Q5Q4 | 116,203,076 | 3,160 |
1752The number of DoFs processed per
second for a single application of the Stokes operator clearly increases
1753significantly with polynomial degree. The whole solver is a bit harder to objectively compare as
1754the higher order discretization requires more iterations and adjustments to GMG parameters like the
1755Chebychev degree of the smoother. To keep things simple, we omit these results here.
1759<a name=
"step_104-PerformancePortability"></a><h3>Performance Portability</h3>
1762The advantage of using
Kokkos instead of writing a code in
one of the vendor
1763languages like CUDA, is that we can
run on AMD, NVIDIA, Intel or other
1764future GPU devices without any porting required. Furthermore, there are multiple
1765host-
parallel backends available in
Kokkos and thus this tutorial also
1766runs with the CPU-only configurations. This is perfect
for
1767code development and debugging, but of course significantly slower. By default,
1768the
Kokkos Serial backend does not use multiple cores or explicit SIMD instructions. As such,
1769the CPU-based
matrix-free deal.II code as in @ref step_37
"step-37" will be significantly faster.
1771As an example, here is a quick comparison between different workstation and
1772server GPUs and the time to solution
for the 53 million DoF problem
size:
1774| Name | FP64 [TFLOPS] | VRAM [GB] | Bandwidth [TB/s] | Operator [MDoFs/s] | Solve Time [s] |
1775| ------------------ | ------------: | ---------: | ---------------: | -----------------: | -------------: |
1776| AMD W7800 | 1.41 | 32 | 0.6 | 482 | 32 |
1777| NVIDIA RTX 6000 | 1.97 | 96 | 1.8 | 1,746 | 11 |
1778| NVIDIA H100 | 25.6 | 80 | 3.4 | 1,457 | 9 |
1779| 32 core 5975WX CPU | 1.4 | - | 0.05 | 72 | 220 |
1781The last line highlights the advantage of building on
Kokkos over implementing CUDA or HIP code directly:
1782Even without further optimization
for CPUs (
for example no SIMD vectorization is currently used) we can
1783achieve decent performance even when running without a GPU. Here we are using the
serial Kokkos backend and
178432
MPI ranks. This is certainly fast enough
for debugging and code development.
1786<a name=
"step_104-MultipleGPUs"></a><h3>Multiple GPUs</h3>
1789This example program can also be
run on more than
one GPU, which allows us to
1790solve larger problems before running out of VRAM and to, at least ideally,
1791achieve a faster time to solution.
1793The way
one can use more than
one GPU involves running with more than
one MPI
1794rank, like in @ref step_40
"step-40". This can be done by allocating
one MPI rank per GPU.
1795As an example,
if you have single node or a workstation with 4 GPUs, you would
1798 mpirun -n 4 ./step-104
1800This is typically enough
for Kokkos to automatically pick a different GPU
for
1801each
MPI rank.
Kokkos has an API and command line options to control job
1802placement manually, but this should not be necessary. Note that you can
1803also oversubscribe a GPU cluster by running more
MPI ranks than GPUs on a
1804system. This will result in more than
one rank sharing a GPU, which will typically
1805degrade performance due to scheduling overhead and workload imbalance.
1807Note that because of this performance degradation
1808the typical rule of using
one MPI rank per physical CPU core is
1809not applicable
for a GPU enabled code. Part of the code that runs on the CPU
1810(
matrix-free setup, error estimation, etc.) can make use of multiple threads.
1811This means
for optimal performance you will need to allocate several threads per
MPI rank
if you are
1812running on a GPU cluster. For example, on the
1813<a href=
"https://docs.rcd.clemson.edu/palmetto/">Clemson Palmetto Cluster</a> a
1816 salloc --gpus h100:8 --ntasks 8 --cpus-per-task 10 --mem-per-cpu 2gb --time 4:00:00
1818would allocate a node with 8 H100 GPUs, 8
MPI ranks, and 10 threads per rank.
1820As an example, here is a table with solve times running
one of these nodes
1821with up to 8 H100 GPUs:
1823| Refinement | DoFs | 1 GPU (s) | 2 GPU (s) | 4 GPU (s) | 8 GPU (s) |
1824|----------: |-----------: |---------: |---------: |---------: |---------: |
1825| 0 | 2,312 | .065 | .074 | .078 | .077 |
1826| 1 | 15,468 | .16 | .21 | .21 | .21 |
1827| 2 | 112,724 | .49 | 1.10 | .98 | .81 |
1828| 3 | 859,812 | 2.9 | 4.1 | 3.4 | 3.3 |
1829| 4 | 6,714,692 | 25 | 30 | 18 | 14 |
1830| 5 | 53,070,468 | 210 | 280 | 160 | 100 |
1831| 6 | 421,991,684 | - | - | 1500 | 970 |
1833Notice that we can solve larger problems with 4 or more GPUs compared to a single GPU. On
1834the
one hand, we also have some reduction in time to solution, at least
for the larger problem sizes.
1835On the other hand, notice that time to solution does not
scale linearly. This is likely
1836due to additional communication overhead, which we will investigate and improve
1839<a name=
"step_104-Possibilitiesforextensions"></a><h3> Possibilities
for extensions </h3>
1842You might have noticed that we used `
Number` instead of `double` throughout the tutorial
1843program. This is on purpose. Try changing the template argument to `float` in `
main()`. With this change,
1844all computations are now done in single instead of double precision. Especially on workstation GPUs, single
1845precision computations are significantly faster (up to 64x). Even when the computations
1846are memory-bound and not compute-bound, which is the case
for most of the kernels in this
1847example, memory usage is reduced by a factor of 2. Check what speedup you can observe and
1848see whether you can solve bigger problems with the same amount of GPU memory.
1850Solving with lower precision comes with a trade-off, though. The integration of the operator and
1851right-hand side is less accurate, which can lead to a degradation in accuracy of the solution,
1852especially on finer meshes. By comparing the errors, determine the refinement
level where floating-
point
1853error is larger than the error introduced by the discretization and linear solver tolerance.
1856when performed in single precision. It would be better to convert the solution to double precision
1857and compute the error in double precision. The function
LinearAlgebra::distributed::
Vector::copy_locally_owned_data_from
1858can do this conversion
for you. Does that make a difference?
1860Finally, a more advanced strategy involves solving the linear system in double precision while running
1861the multigrid preconditioner in single precision. This approach is called
"mixed precision preconditioning".
1862The method is very attractive on GPUs as it combines the accuracy of the double precision approach with
1863the performance of the single precision approach. Considering we spent most of the computational effort
1864inside the velocity block multigrid preconditioner, which does not need to be very accurate, this
1865seems promising. The implementation is not difficult apart from having to carefully choose the correct number
1866type in all GMG-related objects. For clarity, the mixed precision approach is not included in this tutorial,
1867but it makes
for a great exercise!
1870<a name=
"step_104-PlainProg"></a>
1871<h1> The plain program</h1>
1872@include
"step-104.cc"
* * for(const auto &cell :triangulation.active_cell_iterators())
* * int main(int argc, char **argv)
* x_component_mask set(0, true)
* * * struct InterferenceTaperTransform *
* * Point< dim > operator()(const Point< dim > &p) const *
BlockType & block(const unsigned int i)
void distribute_dofs(const FiniteElement< dim, spacedim > &fe)
void initialize(const MGSmootherBase< VectorType > &coarse_smooth)
void initialize(const MGLevelObject< MatrixType2 > &matrices, const typename RelaxationType::AdditionalData &additional_data=typename RelaxationType::AdditionalData())
static unsigned int n_threads()
constexpr bool running_in_debug_mode()
#define DEAL_II_HOST_DEVICE
static ::ExceptionBase & ExcNotImplemented()
#define Assert(cond, exc)
static ::ExceptionBase & ExcMessage(std::string arg1)
#define AssertThrow(cond, exc)
void loop(IteratorType begin, std_cxx20::type_identity_t< IteratorType > end, DOFINFO &dinfo, INFOBOX &info, const std::function< void(std_cxx20::type_identity_t< DOFINFO > &, typename INFOBOX::CellInfo &)> &cell_worker, const std::function< void(std_cxx20::type_identity_t< DOFINFO > &, typename INFOBOX::CellInfo &)> &boundary_worker, const std::function< void(std_cxx20::type_identity_t< DOFINFO > &, std_cxx20::type_identity_t< DOFINFO > &, typename INFOBOX::CellInfo &, typename INFOBOX::CellInfo &)> &face_worker, AssemblerType &assembler, const LoopControl &lctrl=LoopControl())
void make_hanging_node_constraints(const DoFHandler< dim, spacedim > &dof_handler, AffineConstraints< number > &constraints)
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_values
Shape function values.
@ update_JxW_values
Transformed quadrature weights.
@ update_gradients
Shape function gradients.
std::vector< index_type > data
void approximate(const SynchronousIterators< std::tuple< typename DoFHandler< dim, spacedim >::active_cell_iterator, Vector< float >::iterator > > &cell, const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof_handler, const InputVector &solution, const unsigned int component)
void hyper_cube(Triangulation< dim, spacedim > &tria, const double left=0., const double right=1., const bool colorize=false)
@ matrix
Contents is actually a matrix.
@ diagonal
Matrix is diagonal.
constexpr types::blas_int one
Tpetra::Vector< Number, LO, GO, NodeType< MemorySpace > > VectorType
void L2(Vector< number > &result, const FEValuesBase< dim > &fe, const std::vector< double > &input, const double factor=1.)
Point< spacedim > point(const gp_Pnt &p, const double tolerance=1e-10)
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)
* * if(update_pressure &update_flags) * compute_pressure(constitutive_request
* * * RotationFunction< dim, Number >::RotationFunction Number(dim)
Kokkos::View< Number *, MemorySpace::Default::kokkos_space > DeviceVector
std::vector< unsigned int > serial(const std::vector< unsigned int > &targets, const std::function< RequestType(const unsigned int)> &create_request, const std::function< AnswerType(const unsigned int, const RequestType &)> &answer_request, const std::function< void(const unsigned int, const AnswerType &)> &process_answer, const MPI_Comm comm)
unsigned int n_mpi_processes(const MPI_Comm mpi_communicator)
unsigned int this_mpi_process(const MPI_Comm mpi_communicator)
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)
constexpr unsigned int invalid_unsigned_int
::VectorizedArray< Number, width > min(const ::VectorizedArray< Number, width > &, const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > cos(const ::VectorizedArray< Number, width > &)
UpdateFlags mapping_update_flags