Bootstrap and Communicator

Note

This tutorial follows the Basic Concepts tutorial.

In this tutorial, we introduce Bootstrap and Communicator interfaces, which provide a convenient way to set up connections across multiple processes. The example code implements the same ping-pong example as in the Basic Concepts tutorial, but using one process per GPU and the Bootstrap and Communicator interfaces to establish connections.

Build and Run the Example

The code of this tutorial is under examples/tutorials/02-bootstrap-and-communicator.

Build the example with make:

$ cd examples/tutorials/02-bootstrap-and-communicator
$ make

Run the example with ./gpu_ping_pong_mp. If you are in a container, you may need to run with root privileges. You should see output similar to the following:

# ./gpu_ping_pong_mp
GPU 1: Initializing a bootstrap ...
GPU 0: Initializing a bootstrap ...
GPU 0: Creating a connection ...
GPU 1: Creating a connection ...
GPU 0: Creating a semaphore ...
GPU 1: Creating a semaphore ...
GPU 1: Creating a channel ...
GPU 0: Creating a channel ...
GPU 1: Launching a GPU kernel ...
GPU 0: Launching a GPU kernel ...
Elapsed 4.78082 ms per iteration (100)
Succeed!

The example code uses localhost port 50505 by default. If the port is already in use, you can change it by modifying the PORT_NUMER macro in the code.

If you see error messages like “At least two GPUs are required” or “GPU 0 cannot access GPU 1”, it means that your system does not meet the requirements for running the example. Make sure you have at least two GPUs installed and that they are connected peer-to-peer (through NVLink or under the same PCIe switch). See the Prerequisites for more details.

Code Overview

The example code is similar to that in the Basic Concepts tutorial, but uses Bootstrap and Communicator interfaces to establish connections between GPUs. The code spawns one child process per GPU. The parent process waits for the child processes to finish before exiting.

Bootstrap

Bootstrap is an abstract class that defines common inter-process communication (IPC) interfaces such as send(), recv(), allGather(), and barrier(). Bootstrap is used to exchange serialized MSCCL++ objects between host processes, or to synchronize the processes. TcpBootstrap is a concrete implementation of the Bootstrap interface that uses TCP sockets for communication.

In the example code, two processes create and initialize a TcpBootstrap instance as follows:

auto bootstrap = std::make_shared<mscclpp::TcpBootstrap>(myRank, nRanks);
bootstrap->initialize("lo:127.0.0.1:" PORT_NUMER);

myRank is the rank of the current process, and nRanks is the total number of processes. The initialize() method sets up the bootstrap connection between all processes. In this example, we pass an ifIpPortTrio string, which has the format if:ip:port, where if is the network interface (e.g., lo for localhost), ip is the IP address, and port is the port number. The TcpBootstrap will listen on the specified port and accept connections from other processes.

Note

Alternatively, TcpBootstrap can be initialized with a UniqueId, which is a unique identifier for the bootstrap connection. This is similar to what NCCL does with its ncclGetUniqueId() and ncclCommInitRank() functions. The UniqueId should be shared between processes using an external mechanism, such as using MPI like the following:

auto bootstrap = std::make_shared<mscclpp::TcpBootstrap>(myRank, nRanks);
mscclpp::UniqueId id;
if (bootstrap->getRank() == 0) id = bootstrap->createUniqueId();
MPI_Bcast(&id, sizeof(id), MPI_BYTE, 0, MPI_COMM_WORLD);
bootstrap->initialize(id);

Communicator

While Bootstrap provides general IPC interfaces, Communicator is a wrapper around Bootstrap that provides more specific methods for building channels between GPUs.

In the example code, Communicator is constructed as follows:

mscclpp::Communicator comm(bootstrap);

Then it creates a GPU endpoint that connects to the remote rank:

auto connFuture = comm.connect({transport, {mscclpp::DeviceType::GPU, gpuId}}, remoteRank);
auto conn = connFuture.get();

The connect() method builds a connection asynchronously; it returns a future of a connection object. The get() method is called later on the future to retrieve the connection object. In this example, we call get() immediately since we don’t have other tasks in between.

After the connection is established, we create a semaphore for synchronization:

auto semaFuture = comm.buildSemaphore(conn, remoteRank);
auto sema = semaFuture.get();

Like connect(), buildSemaphore() is an asynchronous method that returns a future of a semaphore object.

We omit explaining the rest of the code, as it is similar to that in the Basic Concepts tutorial.

Summary and Next Steps

In this tutorial, you have learned how to use Bootstrap and Communicator interfaces to establish connections between multiple processes. Note that Bootstrap and Communicator are still optional interfaces for convenience. As noted in the Basic Concepts tutorial, you can still use your own IPC mechanisms to build connections and semaphores. For advanced examples that use Redis or torch.distributed for IPC, see the Advanced Connections guide.

In the next tutorial, we will introduce more comprehensive usage of MemoryChannel including how to use it for efficient data transfer between GPUs. It will also cover how to create communication buffers and how to use them with channels.