The RuntimeView Class

Programs operate within a runtime environment. In ParallelZone the responsibility for modeling the runtime environment falls to the RuntimeView class. The entire program runs in a single runtime environment. RuntimeView objects can “see” the singular runtime environment and interact with it.

One of the first tasks that software built on ParallelZone will do is obtain a RuntimeView object (analogous to how MPI-based software starts MPI). The program will then pass that RuntimeView object to each sub call (similar to passing an MPI communicator around). So if you are contributing to software which already uses ParallelZone you will want to grab the RuntimeView instance via whatever mechanism your software affords, and not create a new instance. How to get the RuntimeView is software specific, so for the purposes of this tutorial we ignore it and assume you already have a RuntimeView instance rv.

First Steps

One of the most common RuntimeView operations is to get the ResourceSet (the set of resources like RAM, CPUs, GPUs, etc.) that the current process has direct access to. This is done by:

// Get the resource set containing resources local to the current process
const auto& my_rs = rv.my_resource_set();

How to use a ResourceSet is covered in the next tutorial, The ResourceSet Class.

All-to-All MPI-Like Operations

RuntimeView objects have access to the entire runtime environment, thus one can use RuntimeView objects to synchronize state across the entire runtime environment. For example, say we wanted to do an all gather operation. In an all gather operation each process computes some data, and gives that data to every other process. As a trivial example we first show how to have MPI rank \(r\) “compute” three pieces of data, the integers \(r\), \(r+1\), and \(r+2\). After computing the data, we synchronize it so that each process has a copy of all the data.

// Get the MPI rank of the current process
const auto my_rank = my_rs.mpi_rank();

// "Compute some data"
std::vector<std::size_t> local_data{my_rank, my_rank + 1, my_rank + 2};

// Synchronize the data across the program
auto results = rv.gather(local_data);

The value of results will be a \(3n\) element vector with the elements: \(0, 1, 2, 1, 2, 3, 2, 3, 4, \ldots, n, n+1, n+2\) when the program is run with \(n\) processes.

Program-Wide Logging

Logging (the fancy term for printing) in a program distributed across several processes can be a bit tricky. ParallelZone exposes two logging mechanisms, one for logging program-wide state and one for logging process-local state.

Warning

Logging program-wide state to the process-local logger is always okay. It usually just results in logging redundant information. Logging process-local information to the program-wide logger will usually not cause an error, but it may cause deadlock if every process does not call the program-wide logger.

Generally speaking, the program-wide logger should be used for all logging needs except: when one needs to log distributed data, or when a logging statement will only be executed by a subset of processes. As an example, say we want to log the result of the all gather we performed in the previous example, we can do this by:

// Log the value of result (N.B. after gather all processes have the result)
for(const auto& x : results) rv.logger() << std::to_string(x);

This snippet shows the streaming API of the Logger class. This is a convenient API for users familiar with std::cout (or more generally std::ostream) style printing, but can only be used with “info”-level log messages…which brings us to log levels.

Logging primarily differs from traditional printing in that each log statement is assigned a severity/importance. The severity levels tell you how severe the message is. In ParallelZone the severity choices are (and their suggested meanings):

  • trace - Logging that is considered verbose, even for debugging

  • debug - Logging that is likely only needed when trying to debug

  • info - Base-level logs, log messages primarily showing progress

  • warn - Something looks weird, it’s not necessarily wrong, but it’s odd

  • error - Something went wrong, but the program can recover

  • critical - Something went wrong, but recovery is not possible

Severity increases from “trace” to “critical” such that “trace” is the least important and “critical” is the most severe log statements. In practice, the value of a computed result usually falls under debug or trace. There’s at least two ways to do that:

// Log the value of result as severity::trace
for(const auto& x : results) rv.logger().trace(std::to_string(x));

// Log the value of result as severity::debug
auto debug_s = parallelzone::Logger::severity::debug;
for(const auto& x : results) rv.logger().log(debug_s, std::to_string(x));

The first example illustrates the use of the trace method. Each severity level has its own corresponding method. The second example shows how to use the more general log method. This particular overload of the log method allows you to specify (at runtime if you like) the severity of the message.

Note

Notice that we did not discuss where the log message gets printed. This is called the “sink”. By default the program-wide logger has a sink which prints to standard out. When developing code with ParallelZone you should always use the loggers provided to your code, and not mess with the sinks. This is because the sinks are set by the person running the program to their liking.