deal.II version GIT relicensing-6809-ge913b9bb34 2026-09-25 17: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
step-104.h
Go to the documentation of this file.
1 = 0) const override
454 *   {
455 *   const double pi = numbers::PI;
456 *   if constexpr (dim == 2)
457 *   return std::cos(pi * p[0]) * std::cos(pi * p[1]);
458 *   else
459 *   return std::cos(pi * p[0]) * std::cos(pi * p[1]) * std::cos(pi * p[2]);
460 *   }
461 *   };
462 *  
463 *  
464 * @endcode
465 *
466 *
467 * <a name="step_104-Thevelocityoperator"></a>
468 * <h3>The velocity operator</h3>
469 *
470
471 *
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
478 * with operator().
479 *
480
481 *
482 *
483 * @code
484 *   template <int dim, int fe_degree, typename Number>
485 *   class VelocityOperatorQuad
486 *   {
487 *   public:
490 *   *fe_eval,
491 *   const int q_point) const
492 *   {
493 *   const auto gradient_u = fe_eval->get_gradient(q_point);
494 *   fe_eval->submit_gradient(gradient_u, q_point);
495 *   }
496 *   };
497 *  
498 *  
499 *  
500 *   template <int dim,
501 *   int degree_u,
502 *   int degree_p,
503 *   typename Number,
504 *   int n_q_points_1d>
505 *   class VelocityCellOperator
506 *   {
507 *   public:
508 *   static const unsigned int n_q_points =
509 *   ::Utilities::pow(n_q_points_1d, dim);
510 *  
515 *   {
517 *   data, velocity_dof_handler_index);
518 *  
519 *   fe_u.read_dof_values(src);
520 *   fe_u.evaluate(EvaluationFlags::gradients);
521 *  
522 *   VelocityOperatorQuad<dim, degree_u, Number> quad_operation;
523 *  
524 *   data->for_each_quad_point(
525 *   [&](const int q_point) { quad_operation(&fe_u, q_point); });
526 *  
527 *   fe_u.integrate(EvaluationFlags::gradients);
528 *   fe_u.distribute_local_to_global(dst);
529 *   }
530 *   };
531 *  
532 *  
533 * @endcode
534 *
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.
540 *
541 * @code
542 *   template <int dim,
543 *   int degree_u,
544 *   int degree_p,
545 *   typename Number = double,
546 *   typename VectorType =
547 *   LinearAlgebra::distributed::Vector<Number, MemorySpace::Default>,
548 *   int n_q_points_1d = degree_u + 1>
549 *   class PortableMFVelocityOperator : public EnableObserverPointer
550 *   {
551 *   public:
552 *   PortableMFVelocityOperator() = default;
553 *  
554 *   PortableMFVelocityOperator(
555 *   std::shared_ptr<Portable::MatrixFree<dim, Number>> data_in)
556 *   : data(data_in)
557 *   {}
558 *  
559 *   void reinit(std::shared_ptr<Portable::MatrixFree<dim, Number>> data_in)
560 *   {
561 *   data = data_in;
562 *   }
563 *  
564 *   void initialize_dof_vector(VectorType &vec) const
565 *   {
566 *   data->initialize_dof_vector(vec, velocity_dof_handler_index);
567 *   }
568 *  
569 *   types::global_dof_index m() const
570 *   {
571 *   return data->get_vector_partitioner(velocity_dof_handler_index)->size();
572 *   }
573 *  
574 *   std::shared_ptr<DiagonalMatrix<
576 *   get_matrix_diagonal_inverse() const
577 *   {
578 *   return inverse_diagonal_entries;
579 *   }
580 *  
581 *   double el(const types::global_dof_index row,
582 *   const types::global_dof_index col) const
583 *   {
584 *   (void)col;
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);
590 *   }
591 *  
592 *   void vmult(VectorType &dst, const VectorType &src) const
593 *   {
594 *   dst = static_cast<Number>(0.);
595 *   VelocityCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
596 *   velocity_operator;
597 *   data->cell_loop(velocity_operator, src, dst);
598 *  
599 *   data->copy_constrained_values(src, dst, velocity_dof_handler_index);
600 *   }
601 *  
602 *   void Tvmult(VectorType & /* dst */, const VectorType & /* src */) const
603 *   {
604 *   AssertThrow(false, ExcNotImplemented());
605 *   }
606 *  
607 *   void compute_diagonal()
608 *   {
609 *   Assert(data.get() != nullptr, ExcNotInitialized());
610 *  
611 *   this->inverse_diagonal_entries =
612 *   std::make_shared<DiagonalMatrix<VectorType>>();
613 *   VectorType &inverse_diagonal =
614 *   this->inverse_diagonal_entries->get_vector();
615 *   data->initialize_dof_vector(inverse_diagonal, velocity_dof_handler_index);
616 *  
617 *   VelocityOperatorQuad<dim, degree_u, Number> velocity_operator_quad;
618 *  
620 *   compute_diagonal<dim, degree_u, degree_u + 1, dim, Number>(
621 *   *data.get(),
622 *   inverse_diagonal,
623 *   velocity_operator_quad,
626 *   velocity_dof_handler_index);
627 *  
628 *   Number *raw_diagonal = inverse_diagonal.get_values();
629 *  
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];
638 *   });
639 *   }
640 *  
641 *   private:
642 *   std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
643 *   std::shared_ptr<DiagonalMatrix<VectorType>> inverse_diagonal_entries;
644 *   };
645 *  
646 *  
647 *  
648 * @endcode
649 *
650 *
651 * <a name="step_104-TheSchurcomplementoperator"></a>
652 * <h3>The Schur complement operator</h3>
653 *
654
655 *
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
661 * instead of 0 to the FEEvaluation class.
662 *
663 * @code
664 *   template <int dim,
665 *   int degree_u,
666 *   int degree_p,
667 *   typename Number,
668 *   int n_q_points_1d>
669 *   class MassOperatorQuad
670 *   {
671 *   public:
674 *   const int q_point) const
675 *   {
676 *   fe_eval->submit_value(fe_eval->get_value(q_point), q_point);
677 *   }
678 *   };
679 *  
680 *  
681 *  
682 *   template <int dim,
683 *   int degree_u,
684 *   int degree_p,
685 *   typename Number,
686 *   int n_q_points_1d>
687 *   class MassCellOperator
688 *   {
689 *   public:
690 *   static const unsigned int n_q_points =
691 *   ::Utilities::pow(n_q_points_1d, dim);
692 *  
697 *   };
698 *  
699 *  
700 *  
701 *   template <int dim,
702 *   int degree_u,
703 *   int degree_p,
704 *   typename Number,
705 *   int n_q_points_1d>
707 *   MassCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>::operator()(
711 *   {
713 *   data, pressure_dof_handler_index);
714 *  
715 *   fe_p.read_dof_values(src);
716 *   fe_p.evaluate(EvaluationFlags::values);
717 *  
718 *   MassOperatorQuad<dim, degree_u, degree_p, Number, n_q_points_1d>
719 *   quad_operation;
720 *   data->for_each_quad_point(
721 *   [&](const int &q_point) { quad_operation(&fe_p, q_point); });
722 *  
723 *   fe_p.integrate(EvaluationFlags::values);
724 *   fe_p.distribute_local_to_global(dst);
725 *   }
726 *  
727 *  
728 *  
729 *   template <int dim,
730 *   int degree_u,
731 *   int degree_p,
732 *   typename Number = double,
733 *   typename VectorType =
735 *   int n_q_points_1d = degree_u + 1>
736 *   class PortableMFMassOperator : public EnableObserverPointer
737 *   {
738 *   public:
739 *   PortableMFMassOperator(
740 *   const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
741 *   : data(data_in)
742 *   {}
743 *  
744 *   types::global_dof_index m() const
745 *   {
746 *   return data->get_vector_partitioner(pressure_dof_handler_index)->size();
747 *   }
748 *  
749 *   Number el(const types::global_dof_index row,
750 *   const types::global_dof_index col) const
751 *   {
752 *   (void)col;
753 *   Assert(row == col, ExcNotImplemented());
754 *   Assert(inverse_diagonal_entries.get() != nullptr &&
755 *   inverse_diagonal_entries->m() > 0,
756 *   ExcNotInitialized());
757 *   return 1.0 / (*inverse_diagonal_entries)(row, row);
758 *   }
759 *  
760 *   void vmult(VectorType &dst, const VectorType &src) const
761 *   {
762 *   dst = static_cast<Number>(0.);
763 *   MassCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
764 *   mass_operator;
765 *   data->cell_loop(mass_operator, src, dst);
766 *  
767 *   data->copy_constrained_values(src, dst, pressure_dof_handler_index);
768 *   }
769 *  
770 *   std::shared_ptr<DiagonalMatrix<VectorType>>
771 *   get_matrix_diagonal_inverse() const
772 *   {
773 *   return inverse_diagonal_entries;
774 *   }
775 *  
776 *   void compute_diagonal()
777 *   {
778 *   this->inverse_diagonal_entries =
779 *   std::make_shared<DiagonalMatrix<VectorType>>();
780 *   VectorType &inverse_diagonal =
781 *   this->inverse_diagonal_entries->get_vector();
782 *  
783 *   MassOperatorQuad<dim, degree_u, degree_p, Number, n_q_points_1d>
784 *   quad_operation;
785 *  
787 *   compute_diagonal<dim, degree_p, n_q_points_1d, 1, Number>(
788 *   *data.get(),
789 *   inverse_diagonal,
790 *   quad_operation,
793 *   pressure_dof_handler_index);
794 *  
795 *   Number *raw_diagonal = inverse_diagonal.get_values();
796 *  
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];
805 *   });
806 *   }
807 *  
808 *   private:
809 *   std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
810 *   std::shared_ptr<DiagonalMatrix<VectorType>> inverse_diagonal_entries;
811 *   };
812 *  
813 *  
814 *  
815 * @endcode
816 *
817 *
818 * <a name="step_104-TheStokesoperator"></a>
819 * <h3>The Stokes operator</h3>
820 *
821
822 *
823 * The following set of classes provides the whole Stokes operator
824 * @f{eqnarray*}{
825 * \begin{bmatrix} A & B^T \\ B & 0 \end{bmatrix}.
826 * @f}
827 * While structured in a similar way (class PortableMFStokesOperator uses
828 * StokesCellOperator and a lambda function for the action at each
829 * quadrature point), we now operate on Portable::DeviceBlockVector
830 * and use two Portable::FEEvaluation objects, one for the velocity
831 * and one for the pressure.
832 *
833
834 *
835 * Note that we don't need support for computing the diagonal, as this
836 * is not needed in the block preconditioner.
837 *
838 * @code
839 *   template <int dim,
840 *   int degree_u,
841 *   int degree_p,
842 *   typename Number,
843 *   int n_q_points_1d>
844 *   class StokesCellOperator
845 *   {
846 *   public:
847 *   static const unsigned int n_q_points =
848 *   ::Utilities::pow(n_q_points_1d, dim);
849 *  
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
854 *   {
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);
859 *  
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);
864 *  
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);
868 *  
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);
873 *  
874 *   const Number pressure_term = -trace(gradient_u);
875 *   fe_p.submit_value(pressure_term, q_point);
876 *   });
877 *  
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));
882 *   }
883 *   };
884 *  
885 *  
886 *   template <
887 *   int dim,
888 *   int degree_u,
889 *   int degree_p,
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
895 *   {
896 *   public:
897 *   PortableMFStokesOperator(
898 *   const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
899 *   : data(data_in)
900 *   {}
901 *  
902 *   void vmult(VectorType &dst, const VectorType &src) const
903 *   {
904 *   dst = static_cast<Number>(0.);
905 *   StokesCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
906 *   stokes_operator;
907 *   data->cell_loop(stokes_operator, src, dst);
908 *  
909 *   data->copy_constrained_values(src, dst);
910 *   }
911 *  
912 *   private:
913 *   std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
914 *   };
915 *  
916 *  
917 *  
918 * @endcode
919 *
920 *
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.
930 *
931 * @code
932 *   template <int dim,
933 *   int degree_u,
934 *   int degree_p,
935 *   typename Number,
936 *   int n_q_points_1d>
937 *   class BTCellOperator
938 *   {
939 *   public:
940 *   static const unsigned int n_q_points =
941 *   ::Utilities::pow(n_q_points_1d, dim);
942 *  
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
947 *   {
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);
952 *  
953 *   fe_p.read_dof_values(src.block(1));
954 *   fe_p.evaluate(EvaluationFlags::values);
955 *  
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);
959 *   });
960 *  
961 *   fe_u.integrate(EvaluationFlags::gradients);
962 *   fe_u.distribute_local_to_global(dst.block(0));
963 *   }
964 *   };
965 *  
966 *  
967 *  
968 *   template <
969 *   int dim,
970 *   int degree_u,
971 *   int degree_p,
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
977 *   {
978 *   public:
979 *   PortableMFBTOperator(
980 *   const std::shared_ptr<Portable::MatrixFree<dim, Number>> &data_in)
981 *   : data(data_in)
982 *   {}
983 *  
984 *  
985 *  
986 *   void vmult(VectorType &dst, const VectorType &src) const
987 *   {
988 *   dst = static_cast<Number>(0.);
989 *   BTCellOperator<dim, degree_u, degree_p, Number, n_q_points_1d>
990 *   cell_operator;
991 *   data->cell_loop(cell_operator, src, dst);
992 *  
993 * @endcode
994 *
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
998 * of the velocity:
999 *
1000 * @code
1001 *   data->set_constrained_values(0.0,
1002 *   dst.block(0),
1003 *   velocity_dof_handler_index);
1004 *   }
1005 *  
1006 *   private:
1007 *   std::shared_ptr<Portable::MatrixFree<dim, Number>> data;
1008 *   };
1009 *  
1010 *  
1011 *  
1012 * @endcode
1013 *
1014 *
1015 * <a name="step_104-ThePreconditionercodeBlockSchurPreconditionercode"></a>
1016 * <h3>The Preconditioner <code>BlockSchurPreconditioner</code></h3>
1017 *
1018
1019 *
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".
1024 *
1025
1026 *
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.
1030 *
1031 * @code
1032 *   template <class AInvOperator,
1033 *   class SInvOperator,
1034 *   class BTOperator,
1035 *   class VectorType>
1036 *   class BlockSchurPreconditioner : public EnableObserverPointer
1037 *   {
1038 *   public:
1039 *   /**
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.
1046 *   */
1047 *   BlockSchurPreconditioner(const AInvOperator &A_inverse_operator,
1048 *   const SInvOperator &S_inverse_operator,
1049 *   const BTOperator &BT_operator);
1050 *  
1051 *   /**
1052 *   * Matrix-vector product with this preconditioner object.
1053 *   */
1054 *   void vmult(VectorType &dst, const VectorType &src) const;
1055 *  
1056 *   private:
1057 *   mutable VectorType tmp;
1058 *   const AInvOperator &A_inverse_operator;
1059 *   const SInvOperator &S_inverse_operator;
1060 *   const BTOperator &BT_operator;
1061 *   };
1062 *  
1063 *  
1064 *  
1065 *   template <class AInvOperator,
1066 *   class SInvOperator,
1067 *   class BTOperator,
1068 *   class VectorType>
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)
1076 *   {}
1077 *  
1078 *  
1079 *  
1080 *   template <class AInvOperator,
1081 *   class SInvOperator,
1082 *   class BTOperator,
1083 *   class VectorType>
1084 *   void
1085 *   BlockSchurPreconditioner<AInvOperator, SInvOperator, BTOperator, VectorType>::
1086 *   vmult(VectorType &dst, const VectorType &src) const
1087 *   {
1088 * @endcode
1089 *
1090 * Allocate the temporary vector on first use. Its content doesn't matter,
1091 * as it will be overwritten in the vmult() call.
1092 *
1093 * @code
1094 *   if (tmp.size() == 0)
1095 *   tmp.reinit(src);
1096 *  
1097 * @endcode
1098 *
1099 * First apply the Schur Complement inverse operator: dst.p = S^-1 * src.p
1100 *
1101 * @code
1102 *   {
1103 *   S_inverse_operator.vmult(dst.block(1), src.block(1));
1104 *   dst.block(1) *= -1.0;
1105 *   }
1106 *  
1107 * @endcode
1108 *
1109 * Apply the top right block: tmp.u = -B^T * dst.p + src.u
1110 *
1111 * @code
1112 *   {
1113 *   BT_operator.vmult(tmp, dst);
1114 *   tmp.block(0).sadd(-1.0, 1.0, src.block(0));
1115 *   }
1116 *  
1117 * @endcode
1118 *
1119 * Finally the velocity block:
1120 *
1121 * @code
1122 *   A_inverse_operator.vmult(dst.block(0), tmp.block(0));
1123 *   }
1124 *  
1125 *  
1126 *  
1127 * @endcode
1128 *
1129 *
1130 * <a name="step_104-ThemainclasscodeStokesProblemcode"></a>
1131 * <h3>The main class <code>StokesProblem</code></h3>
1132 *
1133
1134 *
1135 * The remaining part of this tutorial is the StokesProblem class
1136 * that puts everything together.
1137 *
1138 * @code
1139 *   template <int dim, int degree_p, typename Number = double>
1140 *   class StokesProblem
1141 *   {
1142 *   public:
1143 *   static constexpr unsigned int degree_u = degree_p + 1;
1144 *  
1145 *   StokesProblem();
1146 *  
1147 *   void run();
1148 *  
1149 *   using VectorType =
1151 *   using BlockVectorType =
1153 *  
1154 *   private:
1155 *   void setup_dofs();
1156 *  
1157 *   void solve();
1158 *  
1159 *   void postprocess();
1160 *  
1162 *  
1163 *   MappingQ<dim> mapping;
1164 *  
1165 *   FESystem<dim> fe_u;
1166 *   FE_Q<dim> fe_p;
1167 *  
1168 *   DoFHandler<dim> dof_u;
1169 *   DoFHandler<dim> dof_p;
1170 *  
1171 *   AffineConstraints<Number> constraints_u;
1172 *   AffineConstraints<Number> constraints_p;
1173 *  
1174 *   std::shared_ptr<Portable::MatrixFree<dim, Number>> mf_data;
1175 *   BlockVectorType solution;
1176 *   BlockVectorType rhs;
1177 *   ConditionalOStream pcout;
1178 *   };
1179 *  
1180 *  
1181 *   template <int dim, int degree_p, typename Number>
1182 *   StokesProblem<dim, degree_p, Number>::StokesProblem()
1183 *   : tria(MPI_COMM_WORLD)
1184 *   , mapping(1)
1185 *   , fe_u(FE_Q<dim>(degree_p + 1), dim)
1186 *   , fe_p(degree_p)
1187 *   , dof_u(tria)
1188 *   , dof_p(tria)
1189 *   , pcout(std::cout, Utilities::MPI::this_mpi_process(MPI_COMM_WORLD) == 0)
1190 *   {}
1191 *  
1192 * @endcode
1193 *
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
1197 * DoFHandler index 0 and the pressure using DoFHandler index 1.
1198 *
1199 * @code
1200 *   template <int dim, int degree_p, typename Number>
1201 *   void StokesProblem<dim, degree_p, Number>::setup_dofs()
1202 *   {
1203 *   dof_u.distribute_dofs(fe_u);
1204 *   dof_p.distribute_dofs(fe_p);
1205 *  
1206 *   const IndexSet &owned_set_u = dof_u.locally_owned_dofs();
1207 *   const IndexSet relevant_set_u =
1209 *   constraints_u.reinit(owned_set_u, relevant_set_u);
1210 *   DoFTools::make_hanging_node_constraints(dof_u, constraints_u);
1212 *   dof_u, 0, Functions::ZeroFunction<dim, Number>(dim), constraints_u);
1213 *   constraints_u.close();
1214 *  
1215 *   const IndexSet &owned_set_p = dof_p.locally_owned_dofs();
1216 *   const IndexSet relevant_set_p =
1218 *  
1219 *   constraints_p.reinit(owned_set_p, relevant_set_p);
1220 *   DoFTools::make_hanging_node_constraints(dof_p, constraints_p);
1221 *   constraints_p.close();
1222 *  
1223 *   std::vector<const DoFHandler<dim> *> dof_handlers = {&dof_u, &dof_p};
1224 *   std::vector<const AffineConstraints<Number> *> constraints = {
1225 *   &constraints_u, &constraints_p};
1226 *  
1227 *   mf_data = std::make_shared<Portable::MatrixFree<dim, Number>>();
1228 *  
1229 *   const QGauss<1> quad(degree_p + 2);
1230 *   typename Portable::MatrixFree<dim, Number>::AdditionalData additional_data;
1232 *   mf_data->reinit(mapping, dof_handlers, constraints, quad, additional_data);
1233 *  
1234 *   {
1235 * @endcode
1236 *
1237 * create the right hand side on the host and move to device:
1238 *
1239
1240 *
1241 *
1242 * @code
1244 *   rhs_host;
1245 *   mf_data->initialize_dof_vector(rhs_host);
1246 *  
1248 *   dof_u,
1249 *   QGauss<dim>(degree_u + 2),
1250 *   VelocityRightHandSide<dim, Number>(),
1251 *   rhs_host.block(0),
1252 *   constraints_u);
1253 *  
1254 *   mf_data->initialize_dof_vector(rhs);
1255 *   rhs.block(0).import_elements(rhs_host.block(0), VectorOperation::insert);
1256 *   rhs.block(1).import_elements(rhs_host.block(1), VectorOperation::insert);
1257 *   }
1258 *   }
1259 *  
1260 *  
1261 * @endcode
1262 *
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).
1270 *
1271 * @code
1272 *   template <int dim, int degree_p, typename Number>
1273 *   void StokesProblem<dim, degree_p, Number>::solve()
1274 *   {
1275 *   PortableMFStokesOperator<dim, degree_u, degree_p, Number> stokes_operator(
1276 *   mf_data);
1277 *  
1278 *   mf_data->initialize_dof_vector(solution);
1279 *  
1280 *   {
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 *   1e-6 * static_cast<double>(solution.size()) / time;
1286 *   pcout << "Stokes operator: " << time << " s, MDoFs/s: " << mdofs_p_second
1287 *   << std::endl;
1288 *   solution = 0.0;
1289 *   }
1290 *  
1291 *   SolverControl solver_control(1000, 1e-8 * rhs.l2_norm());
1292 *  
1294 *   solver_control,
1296 *  
1297 *   using LevelMatrixType =
1298 *   PortableMFVelocityOperator<dim, degree_u, degree_p, Number>;
1299 *   using SmootherPreconditionerType = DiagonalMatrix<VectorType>;
1300 *   using SmootherType = PreconditionChebyshev<LevelMatrixType,
1301 *   VectorType,
1302 *   SmootherPreconditionerType>;
1303 *   using MGTransferType =
1305 *  
1306 *   const auto coarse_grid_triangulations =
1308 *   tria);
1309 *  
1310 *   const unsigned int max_level = coarse_grid_triangulations.size() - 1;
1311 * @endcode
1312 *
1313 * Do not go down to level 0, because this will lead to slower runtime as
1314 * the problem becomes very small:
1315 *
1316 * @code
1317 *   const unsigned int min_level = std::min(3U, max_level - 1);
1318 *  
1319 *   MGLevelObject<DoFHandler<dim>> mg_dof_handlers(min_level, max_level);
1320 *   MGLevelObject<AffineConstraints<Number>> mg_constraints(min_level,
1321 *   max_level);
1322 *   MGLevelObject<LevelMatrixType> mg_matrices(min_level, max_level);
1323 *  
1325 *   min_level, max_level);
1326 *  
1327 *   std::vector<std::shared_ptr<Portable::MatrixFree<dim, Number>>>
1328 *   mf_data_levels;
1329 *  
1330 * @endcode
1331 *
1332 * Prepare the operators and data structures on all levels of the multigrid
1333 * hierarchy
1334 *
1335 * @code
1336 *   for (unsigned int level = min_level; level <= max_level; ++level)
1337 *   {
1338 *   auto &dof_handler = mg_dof_handlers[level];
1339 *   auto &constraint = mg_constraints[level];
1340 *  
1341 *   dof_handler.reinit(*coarse_grid_triangulations[level]);
1342 *   dof_handler.distribute_dofs(fe_u);
1343 *  
1344 *   constraint.reinit(dof_handler.locally_owned_dofs(),
1346 *  
1347 *   DoFTools::make_hanging_node_constraints(dof_handler, constraint);
1348 *   DoFTools::make_zero_boundary_constraints(dof_handler, constraint);
1349 *   constraint.close();
1350 *  
1352 *   additional_data;
1353 *   additional_data.mapping_update_flags =
1355 *  
1356 *   if (level == max_level)
1357 * @endcode
1358 *
1359 * On the finest level we can reuse the MatrixFree object from the
1360 * Stokes operator. This way we can solve significantly larger
1361 * problems before we run out of device memory.
1362 *
1363 * @code
1364 *   mf_data_levels.emplace_back(mf_data);
1365 *   else
1366 *   {
1367 *   const QGauss<1> quad(degree_p + 2);
1368 *   mf_data_levels.emplace_back(
1369 *   std::make_shared<Portable::MatrixFree<dim, Number>>());
1370 *  
1371 *   mf_data_levels.back()->reinit(
1372 *   mapping, dof_handler, constraint, quad, additional_data);
1373 *   }
1374 *  
1375 *   mg_matrices[level].reinit(mf_data_levels.back());
1376 *   }
1377 *  
1378 *   mg::Matrix<VectorType> mg_matrix(mg_matrices);
1379 *  
1380 * @endcode
1381 *
1382 * transfer operator
1383 *
1384 * @code
1385 *   for (unsigned int level = min_level; level < max_level; ++level)
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]);
1391 *  
1392 *   MGTransferType mg_transfer(mg_transfers, [&](const auto l, auto &vec) {
1393 *   mg_matrices[l].initialize_dof_vector(vec);
1394 *   });
1395 *  
1396 * @endcode
1397 *
1398 * smoother
1399 *
1400 * @code
1402 *   min_level, max_level);
1403 *  
1404 *   for (unsigned int level = min_level; level <= max_level; ++level)
1405 *   {
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]);
1411 *  
1412 *   if (level == min_level)
1413 *   {
1414 * @endcode
1415 *
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
1420 * @p degree to numbers::invalid_unsigned_int. We also use more
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.
1424 *
1425 * @code
1426 *   smoother_data[level].smoothing_range = 1e-3;
1427 *   smoother_data[level].degree = numbers::invalid_unsigned_int;
1428 *   smoother_data[level].eig_cg_n_iterations = 40;
1429 *   }
1430 *   else
1431 *   {
1432 * @endcode
1433 *
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
1441 * iterations).
1442 *
1443 * @code
1444 *   smoother_data[level].smoothing_range = 5;
1445 *   smoother_data[level].degree = 4;
1446 *   smoother_data[level].eig_cg_n_iterations = 20;
1447 *   }
1448 *   }
1449 *  
1451 *   mg_smoother.initialize(mg_matrices, smoother_data);
1452 *  
1453 * @endcode
1454 *
1455 * Estimate and print the eigenvalue spectrum of the velocity block on each
1456 * level. This spectrum is later used by the Chebyshev iteration.
1457 *
1458 * @code
1459 *   pcout << "GMG velocity block smoothers:" << std::endl;
1460 *   for (unsigned int level = min_level; level <= max_level; ++level)
1461 *   {
1462 *   VectorType vec;
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;
1470 *   }
1471 *  
1472 * @endcode
1473 *
1474 * coarse-grid solver
1475 *
1476 * @code
1478 *   mg_coarse.initialize(mg_smoother);
1479 *  
1480 * @endcode
1481 *
1482 * put everything together
1483 *
1484 * @code
1485 *   Multigrid<VectorType> mg(mg_matrix,
1486 *   mg_coarse,
1487 *   mg_transfer,
1488 *   mg_smoother,
1489 *   mg_smoother,
1490 *   min_level,
1491 *   max_level);
1492 *  
1493 *  
1494 *   ::Timer timer_smoother;
1495 *   ::Timer timer_transfer;
1496 *   ::Timer timer_coarse;
1497 *   ::Timer timer_residual;
1498 *   {
1499 *   timer_smoother.reset();
1500 *   timer_transfer.reset();
1501 *   timer_coarse.reset();
1502 *   timer_residual.reset();
1503 *  
1504 *   auto make_timer_lambda = [&](::Timer &timer) {
1505 *   return [&](const bool before, const unsigned int /*level*/) {
1506 *   if (before)
1507 *   timer.start();
1508 *   else
1509 *   timer.stop();
1510 *   };
1511 *   };
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));
1518 *   }
1519 *  
1520 *   using APreconditionerType = PreconditionMG<dim, VectorType, MGTransferType>;
1521 *   APreconditionerType preconditioner_A(dof_u, mg, mg_transfer);
1522 *  
1523 *  
1524 *   PortableMFMassOperator<dim, degree_u, degree_p, Number> mass_operator(
1525 *   mf_data);
1526 *   mass_operator.compute_diagonal();
1527 *  
1528 *   using SPreconditionerType = PreconditionChebyshev<
1529 *   PortableMFMassOperator<dim, degree_u, degree_p, Number>,
1530 *   VectorType>;
1531 *  
1532 *   SPreconditionerType preconditioner_schur;
1533 *   {
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();
1541 *  
1542 *   preconditioner_schur.initialize(mass_operator, additional_data);
1543 *   }
1544 *  
1545 *   using BTOperatorType =
1546 *   PortableMFBTOperator<dim, degree_u, degree_p, Number>;
1547 *   BTOperatorType BT_operator(mf_data);
1548 *  
1549 *   BlockSchurPreconditioner<APreconditionerType,
1550 *   SPreconditionerType,
1551 *   BTOperatorType,
1552 *   BlockVectorType>
1553 *   preconditioner(preconditioner_A, preconditioner_schur, BT_operator);
1554 *  
1555 *   ::Timer t(tria.get_mpi_communicator());
1556 *   solver.solve(stokes_operator, solution, rhs, preconditioner);
1557 *   t.stop();
1558 *  
1559 *   pcout << "Solver converged in " << solver_control.last_step()
1560 *   << " iterations in " << t.wall_time() << " seconds" << std::endl;
1561 *  
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"
1567 *   << std::endl;
1568 *   }
1569 *  
1570 *  
1571 *  
1572 * @endcode
1573 *
1574 * The postprocess() function moves the solution to host memory
1575 * and integrates the difference to the manufactured solution to
1576 * compute errors.
1577 *
1578 * @code
1579 *   template <int dim, int degree_p, typename Number>
1580 *   void StokesProblem<dim, degree_p, Number>::postprocess()
1581 *   {
1583 *   solution_host;
1584 *   mf_data->initialize_dof_vector(solution_host);
1585 *  
1586 *   solution_host.block(0).import_elements(solution.block(0),
1588 *   solution_host.block(1).import_elements(solution.block(1),
1590 *  
1591 *   constraints_u.distribute(solution_host.block(0));
1592 *   constraints_p.distribute(solution_host.block(1));
1593 *   solution_host.update_ghost_values();
1594 *   const double mean_pressure = VectorTools::compute_mean_value(
1595 *   dof_p, QGauss<dim>(degree_p + 2), solution_host.block(1), 0);
1596 *   solution_host.block(1).add(-mean_pressure);
1597 *  
1598 *   const QGauss<dim> quadrature_formula(degree_u + 1);
1599 *  
1600 *   Vector<double> cellwise_errors_ul2(tria.n_active_cells());
1601 *   Vector<double> cellwise_errors_pl2(tria.n_active_cells());
1602 *  
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,
1615 *  
1616 *   const double u_l2 = VectorTools::compute_global_error(tria,
1617 *   cellwise_errors_ul2,
1619 *   const double p_l2 = VectorTools::compute_global_error(tria,
1620 *   cellwise_errors_pl2,
1622 *  
1623 *   pcout << "velocity error: " << u_l2 << " pressure error: " << p_l2
1624 *   << std::endl;
1625 *   }
1626 *  
1627 *  
1628 *  
1629 * @endcode
1630 *
1631 * The run() function prints some statistics and then performs a familiar
1632 * refinement loop.
1633 *
1634 * @code
1635 *   template <int dim, int degree_p, typename Number>
1636 *   void StokesProblem<dim, degree_p, Number>::run()
1637 *   {
1638 *   pcout << std::setprecision(10);
1639 *   pcout << "Running on " << Utilities::MPI::n_mpi_processes(MPI_COMM_WORLD)
1640 *   << " MPI ranks (with " << MultithreadInfo::n_threads()
1641 *   << " threads each) in ";
1642 *   if constexpr (running_in_debug_mode())
1643 *   pcout << "DEBUG mode";
1644 *   else
1645 *   pcout << "RELEASE mode";
1646 *  
1647 *   pcout << "\nKokkos execution space: "
1648 *   << Kokkos::DefaultExecutionSpace::name();
1649 *   pcout << '\n'
1650 *   << "dim: " << dim << '\n'
1651 *   << "Element: Q" << degree_u << "-Q" << degree_p << std::endl;
1652 *  
1653 *   unsigned int n_refinements = 10;
1654 *  
1655 *   for (unsigned int i = 0; i < n_refinements; ++i)
1656 *   {
1657 *   if (i == 0)
1658 *   {
1660 *   tria.refine_global(2);
1661 *   }
1662 *   else
1663 *   {
1664 *   tria.refine_global(1);
1665 *   }
1666 *   setup_dofs();
1667 *  
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;
1671 *  
1672 *   solve();
1673 *   postprocess();
1674 *   }
1675 *   }
1676 *   } // namespace Step104
1677 *  
1678 *  
1679 *  
1680 * @endcode
1681 *
1682 *
1683 * <a name="step_104-Thecodemaincodefunction"></a>
1684 * <h3>The <code>main()</code> function</h3>
1685 *
1686
1687 *
1688 * The only interesting bits here are the template arguments that
1689 * specify dimension and polynomial degree to be used.
1690 *
1691 * @code
1692 *   int main(int argc, char **argv)
1693 *   {
1694 *   using namespace Step104;
1695 *   Utilities::MPI::MPI_InitFinalize mpi_initialization(argc, argv);
1696 *  
1697 *   const unsigned int dim = 3;
1698 *   const unsigned int degree_p = 1;
1699 *   StokesProblem<dim, degree_p, double> problem;
1700 *   problem.run();
1701 *   }
1702 * @endcode
1703<a name="step_104-Results"></a><h1>Results</h1>
1704
1705
1706<a name="step_104-Validation"></a><h3>Validation</h3>
1707
1708
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:
1712
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 |
1721
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.
1726
1727<a name="step_104-HigherOrderThroughput"></a><h3>Higher Order Throughput</h3>
1728
1729
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 1d or 3d 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.
1740
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):
1744
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 |
1751
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.
1756
1757
1758
1759<a name="step_104-PerformancePortability"></a><h3>Performance Portability</h3>
1760
1761
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.
1770
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:
1773
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 |
1780
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.
1785
1786<a name="step_104-MultipleGPUs"></a><h3>Multiple GPUs</h3>
1787
1788
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.
1792
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
1796run
1797@code
1798 mpirun -n 4 ./step-104
1799@endcode
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.
1806
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
1814job submission of
1815@code
1816 salloc --gpus h100:8 --ntasks 8 --cpus-per-task 10 --mem-per-cpu 2gb --time 4:00:00
1817@endcode
1818would allocate a node with 8 H100 GPUs, 8 MPI ranks, and 10 threads per rank.
1819
1820As an example, here is a table with solve times running one of these nodes
1821with up to 8 H100 GPUs:
1822
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 |
1832
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
1837in the future.
1838
1839<a name="step_104-Possibilitiesforextensions"></a><h3> Possibilities for extensions </h3>
1840
1841
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.
1849
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.
1854
1855Furthermore, be aware that our error computation using VectorTools::integrate_difference is also less precise
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?
1859
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!
1868 *
1869 *
1870<a name="step_104-PlainProg"></a>
1871<h1> The plain program</h1>
1872@include "step-104.cc"
1873*/
*  iterator end()
*  *  for(const auto &cell :triangulation.active_cell_iterators())
*  *  int main(int argc, char **argv)
*  *  iterator begin()
*  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)
Definition fe_q.h:552
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()
Definition timer.h:128
constexpr bool running_in_debug_mode()
Definition config.h:76
#define DEAL_II_HOST_DEVICE
Definition config.h:171
Point< 2 > second
Definition grid_out.cc:4640
Point< 2 > first
Definition grid_out.cc:4639
unsigned int level
Definition grid_out.cc:4642
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())
Definition loop.h:562
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
Definition mpi.cc:734
std::size_t size
Definition mpi.cc:733
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)
IndexSet extract_locally_relevant_dofs(const DoFHandler< dim, spacedim > &dof_handler)
void hyper_cube(Triangulation< dim, spacedim > &tria, const double left=0., const double right=1., const bool colorize=false)
void scale(const double scaling_factor, Triangulation< dim, spacedim > &triangulation)
@ matrix
Contents is actually a matrix.
@ diagonal
Matrix is diagonal.
constexpr char T
constexpr char A
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.)
Definition l2.h:157
std::vector< std::shared_ptr< const Triangulation< dim, spacedim > > > create_geometric_coarsening_sequence(const Triangulation< dim, spacedim > &tria)
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)
Point< spacedim > point(const gp_Pnt &p, const double tolerance=1e-10)
Definition utilities.cc:210
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)
Definition mpi.cc:103
unsigned int this_mpi_process(const MPI_Comm mpi_communicator)
Definition mpi.cc:118
double compute_global_error(const Triangulation< dim, spacedim > &tria, const InVector &cellwise_error, const NormType &norm, const double exponent=2.)
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={})
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.)
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 >())
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)
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)
Definition mg.h:79
constexpr double PI
Definition numbers.h:240
constexpr unsigned int invalid_unsigned_int
Definition types.h:228
STL namespace.
::VectorizedArray< Number, width > min(const ::VectorizedArray< Number, width > &, const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > cos(const ::VectorizedArray< Number, width > &)