5

Is it possible in Pybind11 to use mpi4py on the Python side and then to hand over the communicator to the C++ side?

If so, how would it work?

If not, is it possible for example with Boost? And if so, how would it be done?

I searched the web literally for hours but didn't find anything.

Quasar
  • 295
  • 3
  • 13
  • Here is at least a discussion about the same question you ask, maybe it helps: https://github.com/pybind/pybind11/issues/23 – NOhs Apr 25 '18 at 10:33

2 Answers2

8

Passing an mpi4py communicator to C++ using pybind11 can be done using the mpi4py C-API. The corresponding header files can be located using the following Python code:

import mpi4py
print(mpi4py.get_include())

To convenietly pass communicators between Python and C++, a custom pybind11 type caster can be implemented. For this purpose, we start with the typical preamble.

// native.cpp
#include <pybind11/pybind11.h>
#include <mpi.h>
#include <mpi4py/mpi4py.h>

namespace py = pybind11;

In order for pybind11 to automatically convert a Python type to a C++ type, we need a distinct type that the C++ compiler can recognise. Unfortunately, the MPI standard does not specify the type for MPI_comm. Worse, in common MPI implementations MPI_comm can be defined as int or void* which the C++ compiler cannot distinguish from regular use of these types. To create a distinct type, we define a wrapper class for MPI_Comm which implicitly converts to and from MPI_Comm.

struct mpi4py_comm {
  mpi4py_comm() = default;
  mpi4py_comm(MPI_Comm value) : value(value) {}
  operator MPI_Comm () { return value; }

  MPI_Comm value;
};

The type caster is then implemented as follows:

namespace pybind11 { namespace detail {
  template <> struct type_caster<mpi4py_comm> {
    public:
      PYBIND11_TYPE_CASTER(mpi4py_comm, _("mpi4py_comm"));

      // Python -> C++
      bool load(handle src, bool) {
        PyObject *py_src = src.ptr();

        // Check that we have been passed an mpi4py communicator
        if (PyObject_TypeCheck(py_src, &PyMPIComm_Type)) {
          // Convert to regular MPI communicator
          value.value = *PyMPIComm_Get(py_src);
        } else {
          return false;
        }

        return !PyErr_Occurred();
      }

      // C++ -> Python
      static handle cast(mpi4py_comm src,
                         return_value_policy /* policy */,
                         handle /* parent */)
      {
        // Create an mpi4py handle
        return PyMPIComm_New(src.value);
      }
  };
}} // namespace pybind11::detail

Below is the code of an example module which uses the type caster. Note, that we use mpi4py_comm instead of MPI_Comm in the function definitions exposed to pybind11. However, due to the implicit conversion, we can use these variables as regular MPI_Comm variables. Especially, they can be passed to any function expecting an argument of type MPI_Comm.

// recieve a communicator and check if it equals MPI_COMM_WORLD
void print_comm(mpi4py_comm comm)
{
  if (comm == MPI_COMM_WORLD) {
    std::cout << "Received the world." << std::endl;
  } else {
    std::cout << "Received something else." << std::endl;
  }
}

mpi4py_comm get_comm()
{
  return MPI_COMM_WORLD; // Just return MPI_COMM_WORLD for demonstration
}

PYBIND11_MODULE(native, m)
{
  // import the mpi4py API
  if (import_mpi4py() < 0) {
    throw std::runtime_error("Could not load mpi4py API.");
  }

  // register the test functions
  m.def("print_comm", &print_comm, "Do something with the mpi4py communicator.");
  m.def("get_comm", &get_comm, "Return some communicator.");
}

The module can be compiled, e.g., using

mpicxx -O3 -Wall -shared -std=c++14 -fPIC \
  $(python3 -m pybind11 --includes) \
  -I$(python3 -c 'import mpi4py; print(mpi4py.get_include())') \
  native.cpp -o native$(python3-config --extension-suffix)

and tested using

import native
from mpi4py import MPI
import math

native.print_comm(MPI.COMM_WORLD)

# Create a cart communicator for testing
# (MPI_COMM_WORLD.size has to be a square number)
d = math.sqrt(MPI.COMM_WORLD.size)
cart_comm = MPI.COMM_WORLD.Create_cart([d,d], [1,1], False)
native.print_comm(cart_comm)

print(f'native.get_comm() == MPI.COMM_WORLD '
      f'-> {native.get_comm() == MPI.COMM_WORLD}')

The output should be:

Received the world.
Received something else.
native.get_comm() == MPI.COMM_WORLD -> True
H. Rittich
  • 814
  • 7
  • 15
7

This is indeed possible. As was pointed out in the comments by John Zwinck, MPI_COMM_WORLD will automatically point to the correct communicator, so nothing has to be passed from python to the C++ side.

Example

First we have a simple pybind11 module that does expose a single function which simple prints some MPI information (taken from one of the many online tutorials). To compile the module see here pybind11 cmake example.

#include <pybind11/pybind11.h>
#include <mpi.h>
#include <stdio.h>

void say_hi()
{
    int world_size;
    MPI_Comm_size(MPI_COMM_WORLD, &world_size);
    int world_rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
    char processor_name[MPI_MAX_PROCESSOR_NAME];
    int name_len;
    MPI_Get_processor_name(processor_name, &name_len);
    printf("Hello world from processor %s, rank %d out of %d processors\n",
        processor_name,
        world_rank,
        world_size);
}

PYBIND11_MODULE(mpi_lib, pybind_module)
{
    constexpr auto MODULE_DESCRIPTION = "Just testing out mpi with python.";
    pybind_module.doc() = MODULE_DESCRIPTION;

    pybind_module.def("say_hi", &say_hi, "Each process is allowed to say hi");
}

Next the python side. Here I reuse the example from this post: Hiding MPI in Python and simply put in the pybind11 library. So first the python script that will call the MPI python script:

import sys
import numpy as np

from mpi4py import MPI

def parallel_fun():
    comm = MPI.COMM_SELF.Spawn(
        sys.executable,
        args = ['child.py'],
        maxprocs=4)

    N = np.array(0, dtype='i')

    comm.Reduce(None, [N, MPI.INT], op=MPI.SUM, root=MPI.ROOT)

    print(f'We got the magic number {N}')

And the child process file. Here we simply call the library function and it just works.

from mpi4py import MPI
import numpy as np

from mpi_lib import say_hi


comm = MPI.Comm.Get_parent()

N = np.array(comm.Get_rank(), dtype='i')

say_hi()

comm.Reduce([N, MPI.INT], None, op=MPI.SUM, root=0)

The end result is:

from prog import parallel_fun
parallel_fun()
# Hello world from processor arch_zero, rank 1 out of 4 processors
# Hello world from processor arch_zero, rank 2 out of 4 processors
# Hello world from processor arch_zero, rank 0 out of 4 processors
# Hello world from processor arch_zero, rank 3 out of 4 processors
# We got the magic number 6
NOhs
  • 2,780
  • 3
  • 25
  • 59
  • You are passing `MPI._addressof(comm)` to C++, then dereferencing it over there. But you could just use the constant `MPI_COMM_WORLD` in C++, as is tradition in single-communicator programs. Given that MPI is a process-wide thing, you do not really need to pass the default communicator from Python to C++, because it is already accessible by both. Put another way: I think you'll find `comm_world` in C++ always has the same value--so there is no need to pass it. – John Zwinck Apr 25 '18 at 13:06
  • You are absolutely correct. I will simplify the example. – NOhs Apr 25 '18 at 13:19
  • 1
    @JohnZwinck now that I think about it, the question was about passing communicators to the C++ side without any mentioning if it is a one communicator scenario. So I think my more general answer might have been better at answering the question. But I cannot roll-back. Can you do that? Maybe then I can just add a side-note about how in the easy case of one communicator things simplify. – NOhs Apr 25 '18 at 13:45
  • OP asked to use "the communicator" rather than "a communicator." Given that the vast majority of MPI programs only use the one "world" communicator, I think it's good how it is. The original code was overly complex, but if someone comes here wanting to pass a communicator handle from Python to C++, all they need to know is that it is an `int` handle, so they can just pass that `int` from Python to C++ and use it instead of `MPI_COMM_WORLD`. That part is trivial. – John Zwinck Apr 26 '18 at 07:57
  • 1
    Int only for mpich right? I think openmpi uses void* – NOhs Apr 26 '18 at 09:20
  • Yeah, you can just cast whatever it is to `uintptr_t` and pass it. – John Zwinck Apr 26 '18 at 09:33
  • I see. The difficulty was mainly that pybind11 didn't like the void* but I guess this doesn't matter then. – NOhs Apr 26 '18 at 09:36
  • 1
    Using MPI_COMM_WORLD everywhere is a bad practice. It is essentially as bad as using global variables everywhere. You do not know in advance, whether at some point you need some MPI ranks to work on something else while doing your main computation, e.g., checkpointing, data post-processing, data analysis, or live visualization. At some point you might also want to include your code into a bigger computation where some other computation can be done in parallel to your computation. Hence, you should always pass a user-defined communicator to your computation routines. – H. Rittich Jun 18 '20 at 10:29