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();
result = [0, 1, 2]
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);
Note
MPI operations are presently limited to the C++ API. Consider using mpi4py for your Python-based MPI needs.
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);
Note
The streaming syntax of the Logger is presently a C++-only feature. See the next code snippet for how to log in Python.
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));
my_rs = rv.my_resource_set()
# Log the value of result as severity::trace
for x in result:
rv.logger().trace(str(x))
# Log the value of result as severity::debug
debug_s = pz.Logger.severity.debug
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.