espressopp.pmi

Parallel Method Invocation (PMI) allows users to write serial Python scripts that use functions and classes that are executed in parallel.

PMI is intended to be used in data-parallel environments, where several threads run in parallel and can communicate via MPI.

In PMI mode, a single thread of control (a python script that runs on the controller, i.e. the MPI root task) can invoke arbitrary functions on all other threads (the workers) in parallel via call(), invoke() and reduce(). When the function on the workers return, the control is returned to the controller.

This model is equivalent to the “Fork-Join execution model” used e.g. in OpenMP.

PMI also allows to create parallel instances of object classes via create(), i.e. instances that have a corresponding object instance on all workers. call(), invoke() and reduce() can be used to call arbitrary methods of these instances.

to execute arbitrary code on all workers, exec_() can be used, and to import python modules to all workers, use ‘import_()’.

Main program

On the workers, the main program of a PMI script usually consists of a single call to the function startWorkerLoop(). On the workers, this will start an infinite loop on the workers that waits to receive the next PMI call, while it will immediately return on the controller. On the workers, the loop ends only, when one of the commands finalizeWorkers() or stopWorkerLoop() is issued on the controller. A typical PMI main program looks like this:

>>> # compute 2*factorial(42) in parallel
>>> import pmi
>>>
>>> # start the worker loop
>>> # on the controller, this function returns immediately
>>> pmi.startWorkerLoop()
>>>
>>> # Do the parallel computation
>>> pmi.import_('math')
>>> pmi.reduce('lambda a,b: a+b', 'math.factorial', 42)
>>>
>>> # exit all workers
>>> pmi.finalizeWorkers()

Instead of using finalizeWorkers() at the end of the script, you can call registerAtExit() anywhere else, which will cause finalizeWorkers() to be called when the python interpreter exits.

Alternatively, it is possible to use PMI in an SPMD-like fashion, where each call to a PMI command on the controller must be accompanied by a corresponding call on the worker. This can be either a simple call to receive() that accepts any PMI command, or a call to the identical PMI command. In that case, the arguments of the call to the PMI command on the workers are ignored. In this way, it is possible to write SPMD scripts that profit from the PMI communication patterns.

>>> # compute 2*factorial(42) in parallel
>>> import pmi
>>>
>>> pmi.exec_('import math')
>>> pmi.reduce('lambda a,b: a+b', 'math.factorial', 42)

To start the worker loop, the command startWorkerLoop() can be issued on the workers. To stop the worker loop, stopWorkerLoop() can be issued on the controller, which will end the worker loop without exiting the workers.

Controller commands

These commands can be called in the controller script. When any of these commands is issued on a worker during the worker loop, a UserError is raised.

  • call(), invoke(), reduce() to call functions and methods in parallel
  • create() to create parallel object instances
  • exec_() and import_() to execute arbitrary python code in parallel and to import classes and functions into the global namespace of pmi.
  • sync() to make sure that all deleted PMI objects have been deleted.
  • finalizeWorkers() to stop and exit all workers
  • registerAtExit() to make sure that finalizeWorkers() is called when python exits on the controller
  • stopWorkerLoop() to interrupt the worker loop an all workers and to return control to the single workers

Worker commands

These commands can be called on a worker.

  • startWorkerLoop() to start the worker loop
  • receive() to receive a single PMI command
  • call(), invoke(), reduce(), create() and exec_() to receive a single corresponding PMI command. Note that these commands will ignore any arguments when called on a worker.

PMI Proxy metaclass

The Proxy metaclass can be used to easily generate front-end classes to distributed PMI classes. . . .

Useful constants and variables

The pmi module defines the following useful constants and variables:

  • isController is True when used on the controller, False otherwise
  • isWorker = not isController
  • ID is the rank of the MPI task
  • CONTROLLER is the rank of the Controller (normally the MPI root)
  • workerStr is a string describing the thread (‘Worker #’ or ‘Controller’)
  • inWorkerLoop is True, if PMI currently executes the worker loop on the workers.
espressopp.pmi.exec_(*args)

Controller command that executes arbitrary python code on all (active) workers.

exec_() allows to execute arbitrary Python code on all workers. It can be used to define classes and functions on all workers. Modules should not be imported via exec_(), instead import_() should be used.

Each element of args should be string that is executed on all workers.

Example:

>>> pmi.exec_('import hello')
>>> hw = pmi.create('hello.HelloWorld')
espressopp.pmi.import_(*args)

Controller command that imports python modules on all (active) workers.

Each element of args should be a module name that is imported to all workers.

Example:

>>> pmi.import_('hello')
>>> hw = pmi.create('hello.HelloWorld')
espressopp.pmi.create(cls=None, *args, **kwds)

Controller command that creates an object on all workers.

cls describes the (new-style) class that should be instantiated. args are the arguments to the constructor of the class. Only classes that are known to PMI can be used, that is, classes that have been imported to pmi via exec_() or import_().

Example:

>>> pmi.exec_('import hello')
>>> hw = pmi.create('hello.HelloWorld')
>>> print(hw)
MPI process #0: Hello World!
MPI process #1: Hello World!
...

Alternative: Note that in this case the class has to be imported to the calling module and via PMI.

>>> import hello
>>> pmi.exec_('import hello')
>>> hw = pmi.create(hello.HelloWorld)
>>> print(hw)
MPI process #0: Hello World!
MPI process #1: Hello World!
...
espressopp.pmi.call(*args, **kwds)

Call a function on all workers, returning only the return value on the controller.

function denotes the function that is to be called, args and kwds are the arguments to the function. If kwds contains keys that start with with the prefix ‘__pmictr_’, they are stripped of the prefix and are passed only to the controller. If the function should return any results, it will be locally returned. Only functions that are known to PMI can be used, that is functions that have been imported to pmi via exec_() or import_().

Example:

>>> pmi.exec_('import hello')
>>> hw = pmi.create('hello.HelloWorld')
>>> pmi.call(hw.hello)
>>> # equivalent:
>>> pmi.call('hello.HelloWorld', hw)

Note, that you can use only functions that are know to PMI when call() is called, i.e. functions in modules that have been imported via exec_().

espressopp.pmi.invoke(*args, **kwds)

Invoke a function on all workers, gathering the return values into a list.

function denotes the function that is to be called, args and kwds are the arguments to the function. If kwds contains keys that start with with the prefix ‘__pmictr_’, they are stripped of the prefix and are passed only to the controller.

On the controller, invoke() returns the results of the different workers as a list. On the workers, invoke returns None. Only functions that are known to PMI can be used, that is functions that have been imported to pmi via exec_() or import_().

Example:

>>> pmi.exec_('import hello')
>>> hw = pmi.create('hello.HelloWorld')
>>> messages = pmi.invoke(hw.hello())
>>> # alternative:
>>> messages = pmi.invoke('hello.HelloWorld.hello', hw)
espressopp.pmi.reduce(*args, **kwds)

Invoke a function on all workers, reducing the return values to a single value.

reduceOp is the (associative) operator that is used to process the return values, function denotes the function that is to be called, args and kwds are the arguments to the function. If kwds contains keys that start with with the prefix ‘__pmictr_’, they are stripped of the prefix and are passed only to the controller.

reduce() reduces the results of the different workers into a single value via the operation reduceOp. reduceOp is assumed to be associative. Both reduceOp and function have to be known to PMI, that is they must have been imported to pmi via exec_() or import_().

Example:

>>> pmi.exec_('import hello')
>>> pmi.exec_('joinstr=lambda a,b: "\n".join(a,b)')
>>> hw = pmi.create('hello.HelloWorld')
>>> print(pmi.reduce('joinstr', hw.hello()))
>>> # equivalent:
>>> print(
...   pmi.reduce('lambda a,b: "\n".join(a,b)',
...             'hello.HelloWorld.hello', hw)
...             )
espressopp.pmi.sync()

Controller command that deletes the PMI objects on the workers that have already been deleted on the controller.

espressopp.pmi.receive(expected=None)

Worker command that receives and handles the next PMI command.

This function waits to receive and handle a single PMI command. If expected is not None and the received command does not equal expected, raise a UserError.

espressopp.pmi.startWorkerLoop()

Worker command that starts the main worker loop.

This function starts a loop that expects to receive PMI commands until stopWorkerLoop() or finalizeWorkers() is called on the controller.

espressopp.pmi.finalizeWorkers()

Controller command that stops and exits all workers.

espressopp.pmi.stopWorkerLoop(doExit=False)

Controller command that stops all workers.

If doExit is set, the workers exit afterwards.

espressopp.pmi.registerAtExit()

Controller command that registers the function finalizeWorkers() via atexit.

class espressopp.pmi.Proxy(name, bases, dict)

A metaclass to be used to create frontend serial objects.

exception espressopp.pmi.UserError(msg)

Raised when PMI has encountered a user error.