Communication Patterns

Cicada communicators provide a set of communication patterns that are inspired by the Message Passing Interface (MPI). These communication patterns are used by the other components in Cicada (such as Logger, AdditiveProtocolSuite, and ShamirProtocolSuite) to implement their functionality, but they’re also available for you to use in your application code. Using communication patterns helps make your Cicada programs easier to implement and easier to understand, because they provide well-tested implementations and explicit semantics that would otherwise be buried in for-loops.

To begin, let’s look at the broadcast pattern, where one player sends a single piece of information to the other players (including themselves); a one-to-all communication:

455a35605c76470ab0ac0c6f5faa462a

[1]:
import logging

from cicada.communicator import SocketCommunicator
from cicada.logging import Logger

logging.basicConfig(level=logging.INFO)

def main(communicator):
    log = Logger(logging.getLogger(), communicator)

    # Player 0 will broadcast a value to every player
    value = "Hello!" if communicator.rank == 0 else None
    value = communicator.broadcast(src=0, value=value)
    log.info(f"Player {communicator.rank} received {value!r}")

SocketCommunicator.run(world_size=3, fn=main);
INFO:root:Player 0 received 'Hello!'
INFO:root:Player 1 received 'Hello!'
INFO:root:Player 2 received 'Hello!'

Note that this is an example of a collective operation, which must be called by every player, with consistent arguments. The arguments to broadcast() are the rank of the player that is broadcasting, and the value to be broadcast (for every player except the one doing the broadcasting, this value is ignored).

The complement to broadcasting is gather, also a collective operation, where every player sends a single piece of information to one player; an all-to-one communication:

8153fc2677cc4ddaaa3ac514ff9d7e75

[2]:
def main(communicator):
    log = Logger(logging.getLogger(), communicator)

    # Every player will send a value to player 1
    value = f"Hello from player {communicator.rank}!"
    value = communicator.gather(value=value, dst=1)
    log.info(f"Player {communicator.rank} received {value!r}")

SocketCommunicator.run(world_size=3, fn=main);
INFO:root:Player 0 received None
INFO:root:Player 1 received ['Hello from player 0!', 'Hello from player 1!', 'Hello from player 2!']
INFO:root:Player 2 received None

In this case every player passes a value to gather(), but only the destination player returns a list of values (the other players return None). The values are returned in rank-order, so result[0] is the value sent by player 0, and-so-on.

Building on gather(), Cicada also provides all-to-all communication using allgather(), where every player simultaneously broadcasts one piece of information to every other player:

c20d05c3efcd47d0bb115ea4a5591536

[3]:
def main(communicator):
    log = Logger(logging.getLogger(), communicator)

    # Every player will broadcast a value to every other player
    value = f"Hello from player {communicator.rank}!"
    value = communicator.allgather(value=value)
    log.info(f"Player {communicator.rank} received {value!r}")

SocketCommunicator.run(world_size=3, fn=main);
INFO:root:Player 0 received ['Hello from player 0!', 'Hello from player 1!', 'Hello from player 2!']
INFO:root:Player 1 received ['Hello from player 0!', 'Hello from player 1!', 'Hello from player 2!']
INFO:root:Player 2 received ['Hello from player 0!', 'Hello from player 1!', 'Hello from player 2!']

In the allgather case, every player returns a list of values received from every player, in rank order.

An alternative form of one-to-all communication is scatter(), where one player with \(n\) pieces of information sends one piece each to the other players (\(n\) is the number of players in this case):

159c2f9a8a004dba9fd7f63c7fed77f7

[4]:
def main(communicator):
    log = Logger(logging.getLogger(), communicator)

    # Player two will send a unique piece of information to every player.
    if communicator.rank == 2:
        values = [f"Hello to player {rank}" for rank in communicator.ranks]
    else:
        values = None

    value = communicator.scatter(src=2, values=values)
    log.info(f"Player {communicator.rank} received {value!r}")

SocketCommunicator.run(world_size=3, fn=main);
INFO:root:Player 0 received 'Hello to player 0'
INFO:root:Player 1 received 'Hello to player 1'
INFO:root:Player 2 received 'Hello to player 2'

In this case the sending player provides a sequence of values in rank order, and each player returns the value that corresponds to its rank (including the sending player).

Not all communications are collective operations, of course - communicators also support point-to-point communication between individual players, using send() and recv():

910c154b7bfd46a6908aa7a3c916d01d

[5]:
def main(communicator):
    log = Logger(logging.getLogger(), communicator)

    # Player one will send information to player 2.
    if communicator.rank ==1:
        communicator.send(value="Hey, 2!", dst=2, tag=42)
    if communicator.rank == 2:
        value = communicator.recv(src=1, tag=42)
        logging.info(f"Player {communicator.rank} received {value!r}")

SocketCommunicator.run(world_size=3, fn=main);
INFO:root:Player 2 received 'Hey, 2!'

See Communicator for additional communication patterns that you can use in your programs.