How to distribute a calculation over MPI ranks?

How to distribute a calculation over MPI ranks?#

When every MPI rank holds the same structure, each rank would normally repeat the complete dispersion calculation. A work partition assigns a disjoint share of the pairwise and ATM interaction loops to each rank instead.

Parts are zero based and every unit of work belongs to exactly one part, so summing the energy, gradient and virial over all parts reproduces the complete result. DFT-D4 itself performs no communication; the reduction is left to the caller.

Note

Structure-dependent quantities such as coordination numbers, charges and \(C_6\) coefficients are evaluated for the full system on every part. The speedup is therefore bounded by the interaction loops, which dominate for larger systems and when the ATM contribution is enabled.

Partition the interaction loops#

The Fortran API takes the partition as an optional argument; omitting it selects the complete work. The C API stores it on the dispersion model, next to the cutoffs.

mpi.f90#
program mpi_partition
   use, intrinsic :: iso_fortran_env, only : r8 => real64
   use dftd4, only : d4_model, damping_param, get_dispersion, &
      & get_rational_damping, new_d4_model, new_work_partition, &
      & realspace_cutoff, work_partition
   use mctc_env, only : error_type
   use mctc_io, only : structure_type, new
   use mpi_f08, only : MPI_COMM_WORLD, MPI_DOUBLE_PRECISION, MPI_IN_PLACE, &
      & MPI_SUM, MPI_Abort, MPI_Allreduce, MPI_Comm_rank, MPI_Comm_size, &
      & MPI_Finalize, MPI_Init
   implicit none

   type(structure_type) :: mol
   type(error_type), allocatable :: error
   integer, allocatable :: num(:)
   real(r8), allocatable :: xyz(:, :), gradient(:, :)
   real(r8) :: energy
   type(d4_model) :: disp
   class(damping_param), allocatable :: param
   type(work_partition) :: partition
   integer :: rank, nranks

   call MPI_Init()
   call MPI_Comm_rank(MPI_COMM_WORLD, rank)
   call MPI_Comm_size(MPI_COMM_WORLD, nranks)

   num = [6, 1, 1, 1, 1]
   xyz = reshape([ &  ! coordinates in Bohr
     &  0.0000000_r8, -0.0000000_r8,  0.0000000_r8, &
     & -1.1922080_r8,  1.1922080_r8,  1.1922080_r8, &
     &  1.1922080_r8, -1.1922080_r8,  1.1922080_r8, &
     & -1.1922080_r8, -1.1922080_r8, -1.1922080_r8, &
     &  1.1922080_r8,  1.1922080_r8, -1.1922080_r8], &
     & [3, size(num)])
   call new(mol, num, xyz, charge=0.0_r8, uhf=0)

   call get_rational_damping("pbe0", param, s9=1.0_r8)
   if (.not.allocated(param)) call fatal("No parameters for PBE0 available")
   call new_d4_model(error, disp, mol)
   if (allocated(error)) call fatal(error%message)

   call new_work_partition(error, partition, rank, nranks)
   if (allocated(error)) call fatal(error%message)

   allocate(gradient(3, mol%nat))
   call get_dispersion(mol, disp, param, realspace_cutoff(), energy, gradient, &
      & partition=partition)

   call MPI_Allreduce(MPI_IN_PLACE, energy, 1, MPI_DOUBLE_PRECISION, MPI_SUM, &
      & MPI_COMM_WORLD)
   call MPI_Allreduce(MPI_IN_PLACE, gradient, size(gradient), &
      & MPI_DOUBLE_PRECISION, MPI_SUM, MPI_COMM_WORLD)

   if (rank == 0) then
      print "(a, f13.10, a)", "PBE0-D4 dispersion energy: ", energy, " Hartree"
   end if

   call MPI_Finalize()

contains

   subroutine fatal(message)
      character(len=*), intent(in) :: message
      print "(2a)", "Error: ", message
      call MPI_Abort(MPI_COMM_WORLD, 1)
   end subroutine fatal

end program mpi_partition
mpi.c#
#include <stdbool.h>
#include <stdio.h>

#include <mpi.h>

#include "dftd4.h"

int main(int argc, char **argv)
{
  dftd4_error error = dftd4_new_error();
  dftd4_structure mol = NULL;
  dftd4_model d4 = NULL;
  dftd4_param param = NULL;
  int rank, nranks;

  MPI_Init(&argc, &argv);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);
  MPI_Comm_size(MPI_COMM_WORLD, &nranks);

  int nat = 5;
  int num[5] = {6, 1, 1, 1, 1};
  double xyz[15] = {
     0.00000000, -0.00000000,  0.00000000,
    -1.19220800,  1.19220800,  1.19220800,
     1.19220800, -1.19220800,  1.19220800,
    -1.19220800, -1.19220800, -1.19220800,
     1.19220800,  1.19220800, -1.19220800};

  mol = dftd4_new_structure(error, nat, num, xyz, NULL, NULL, NULL);
  if (dftd4_check_error(error)) goto handle_error;

  param = dftd4_load_rational_damping(error, "pbe0", true);
  if (dftd4_check_error(error)) goto handle_error;

  d4 = dftd4_new_d4_model(error, mol);
  if (dftd4_check_error(error)) goto handle_error;

  dftd4_set_model_work_partition(error, d4, rank, nranks);
  if (dftd4_check_error(error)) goto handle_error;

  double energy;
  double gradient[15];
  dftd4_get_dispersion(error, mol, d4, param, &energy, gradient, NULL);
  if (dftd4_check_error(error)) goto handle_error;

  MPI_Allreduce(MPI_IN_PLACE, &energy, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
  MPI_Allreduce(MPI_IN_PLACE, gradient, 3 * nat, MPI_DOUBLE, MPI_SUM,
                MPI_COMM_WORLD);

  if (rank == 0) {
    printf("PBE0-D4 dispersion energy: %13.10lf Hartree\n", energy);
  }

  dftd4_delete(error);
  dftd4_delete(mol);
  dftd4_delete(d4);
  dftd4_delete(param);
  MPI_Finalize();
  return 0;

handle_error: {
  char msg[512];
  dftd4_get_error(error, msg, NULL);
  printf("Error: %s\n", msg);

  dftd4_delete(error);
  dftd4_delete(mol);
  dftd4_delete(d4);
  dftd4_delete(param);
  MPI_Abort(MPI_COMM_WORLD, 1);
  return 1;
}
}

The examples can be compiled using mpifort or mpicc together with the flags reported by pkg-config dftd4 mctc-lib --cflags --libs. The result is independent of the number of ranks up to the summation order.

Running with one part is identical to omitting the partition. In Fortran this is also available as serial_work_partition; in C and Python the model can be reset with part=0 and nparts=1.

The numerical Hessian uses the same partition. The pairwise decomposition and the model properties do not.