The ResourceSet Class
While a multi-process program operates within a potentially massive runtime
environment, the reality is that each process only has direct access to the
resources which are physically local to that process. Using other resources
requires sending data and/or tasks to another process. The ResourceSet
class provides an abstraction for interacting with the resources local to a
given process.
Getting a ResourceSet
Each RuntimeView
functions as a container of ResourceSet
objects. The
number of ResourceSet
objects will be equal to the number of processes
being managed by the RuntimeView
. Assuming rv
is a RuntimeView
object then we can get the ResourceSet
for process 0 via:
// Get the ResourceSet for rank 0
const auto& rank_0_rs = rv.at(0);
rv = pz.runtime.RuntimeView()
It is important to understand that rank_0_rs
is the set of resources local
to process 0. So trying to use the resources in rank_0_rs
from any process
other than rank 0 will usually involve communication. To avoid communication
one should work with their local ResourceSet
which can be obtained by:
// Get the resource set containing resources local to the current process
const auto& my_rs = rv.my_resource_set();
rank_0_rs = rv.at(0)
You can quickly figure out if a ResourceObject
describes local or remote
resources by:
// Determine if the rank_0_rs is local or not
const bool is_local = rank_0_rs.is_mine();
my_rs = rv.my_resource_set()
Accessing Hardware
ResourceSet
objects are containers of resources. As an example, let’s say
we wanted to access the local RAM or the RAM local to rank 0. Then one could do:
// Get the local RAM
const auto& my_ram = my_rs.ram();
// Get a view of the RAM owned by rank 0
const auto& ram_0 = rank_0_rs.ram();
is_local = rank_0_rs.is_mine()
# Get the local RAM
my_ram = my_rs.ram()
Using the RAM is described The RAM Class. Similar methods exist for accessing other hardware such as the CPUs and GPUs. It is worth noting that not all computers have all types of hardware. While we expect that just about every computer has RAM, we can verify this by:
// Do I locally have RAM?
const bool do_i_have_ram = my_rs.has_ram();
// Does rank 0 have RAM?
const bool does_0_have_ram = rank_0_rs.has_ram();
ram_0 = rank_0_rs.ram()
# Do I locally have RAM?
do_i_have_ram = my_rs.has_ram()
Again similar methods are available for determining if other hardware is present or not. These methods can be a convenient mechanism for dispatching based on whether certain hardware is available or not.
Process-Local Logging
Program-wide logging is done through the RuntimeView
, process-local logging
is done through the ResourceSet
class. Process-local logging works nearly
identical to program-wide logging. For example, if we want each process to
log whether its value of is_local
this is done by:
// Have each resource set print whether it's rank 0 or not
my_rs.logger().debug("Am I rank 0: " + std::to_string(is_local));
does_0_have_ram = rank_0_rs.has_ram()
Compared to program-wide logging the main difference is that with process-local logging each process can (but doesn’t necessarily) log to a different sink. This makes it much easier to figure out what each process did when going back over the logs.
Warning
Care has been taken to ensure that replicated data can always be written to the process-local logger. The reverse is not true. In particular, deadlock can occur if the program-wide logger is called from a block of source code that is not executed by all processes. Sinks in general try to avoid implementations which can deadlock, but it is not guaranteed.
Typically process local-logging uses file sinks, with one file per process.
Especially if there’s more than one process per filesystem this can have notable
performance degradation. For this reason, the process local sinks are by default
directed to null sinks (they don’t print). Generally speaking, log messages
for process-local logs should have a severity of trace
or debug
as
most users will only turn them on when debugging is needed.