Stanford University logo SLAC National Accelerator Laboratory logo Los Alamos National Laboratory logo NVIDIA logo Winner of the R&D 100 Award

Legion

A Data-Centric Parallel Programming System

Github

Realm Index Spaces

Introduction

Index space is a core data structure offered by Realm to applications as part of it’s public interface. This tutorial introduces the basic knowledge necessary to work with index spaces and discusses how to manage index spaces in the application code.

Here is a list of covered topics:

Index Space Basics

An IndexSpace (index space) is a Plain Old Data (POD) structure in Realm that defines an N-dimensional space with a set of indices (points). It is templated on two parameters: IndexSpace<N, T>, where N is the number of dimensions and T is the type of index values. Index spaces provide an interface to data copies in Realm and offer a number of set operations, such as union, intersection, difference, partitioning, and more.

In Realm, two related classes, Point and Rect, represent single points and ranges of points, respectively, in an index space. An index space contains a bounding rectangle and an optional SparsityMap (sparsity map). A sparsity map is the actual dynamically allocated object that exists on each node that is interested in accessing its data. A sparsity map is used to indicate which indices are present and which ones are missing or “sparse”.

Creating Index Spaces

An index space can be constructed from either a list of points or rectangles. In this example, all index spaces are constructed from a single dense rectangle:

  IndexSpace<2> center_is(Rect<2>(Point<2>(size / 2 - 1, size / 2 - 1),
                                  Point<2>(size / 2 + 1, size / 2 + 1)));

Set operations

An application can perform various set operations on index spaces. In this example, we demonstrate Realm’s set operations that include compute_union, compute_difference, compute_intersection, and compute_coverings. Most of these operations utilize Realm’s deferred execution model, returning an event that can be used to query the operation’s status and use it as a pre- or post-condition for subsequent calls.

The tutorial begins by creating a list of disjoint dense index spaces in two-dimensional space:

  std::vector<IndexSpace<2>> subspaces;
  for (size_t y = 0; y <= size; y++) {
    subspaces.push_back(
        IndexSpace<2>(Rect<2>(Point<2>(0, y), Point<2>(size, y))));
  }

with the following shape:

-----------------------------------
(0, 0), (0, 1), (0, 2)...(0, 15)
(1, 0), (1, 1), (1, 2)...(1, 15)
(2, 0), (2, 1), (2, 2)...(2, 15)
...
(15, 0), (15, 1), (15, 2)...(15, 15)
------------------------------------

Each index space describes a 1D line along the x-axis. These index spaces are then passed to compute_union, which constructs an index space with bounds defined by a single rectangle. This resulting index space is assigned to the variable union_is:

  IndexSpace<2> union_is;
  Event event1 =
      IndexSpace<2>::compute_union(subspaces, union_is, ProfilingRequestSet());

It may be slightly surprising that union_is has a sparsity map initially because Realm defers the computation of the union (and the grouping into a single rectangle). We can call tighten to make the sparsity map disappear, so the result is just a dense rectangle:

/**
 * Return the tightest description possible of the index space.
 * @param precise false is the sparsity map may be preserved even
 * for dense spaces.
 * @return the tightest index space possible.
 */
IndexSpace<N,T> tighten(bool precise = true) const;

Next, we continue by cutting out a small index space from the center of union_is to obtain a sparse index space containing four disjoint rectangles. This is done using compute_difference, and the resulting index space is assigned to the variable diffs_is:

  IndexSpace<2> diffs_is;
  Event event2 = IndexSpace<2>::compute_difference(
      union_is, center_is, diffs_is, ProfilingRequestSet(), event1);

We then compute the intersection of diffs_is and union_is, although this is purely for demonstrative purposes since the resulting index space, isect_is, will be the same as diffs_is.

Iterating Over Index Spaces

Since the result of an intersection is a sparse index space, the rectangles are stored inside the internal sparsity map. There are several ways to access the sparsity, such as via an IndexSpaceIterator or using the compute_covering interface. The latter is better suited for building an approximate covering of the sparsity map when making region instances.

In this example, we use IndexSpaceIterator to extract the disjoint rectangles from the sparsity maps of both isect_is and diffs_is. Since both isect_is and diffs_is should be the same, this iterator validates that this is indeed the case:

  IndexSpaceIterator<2> diffs_it(diffs_is), isect_it(isect_is);
  while (diffs_it.valid && isect_it.valid) {
    if (diffs_it.rect != isect_it.rect) {
      log_app.error() << "rects don't match: " << diffs_it.rect
                      << " != " << isect_it.rect;
    }
    diffs_it.step();
    isect_it.step();
  }
  if (diffs_it.valid || isect_it.valid) {
    log_app.error() << "At least one iterator is invalid";
  }

Managing Memory In Index Spaces

As discussed earlier, Realm dynamically allocates an underlying sparsity map on the owner node when creating sparse index spaces. The allocated sparsity maps are being held during the whole application lifetime. This will likely be fixed in one of the upcoming Realm releases but users should be aware of this fact.

References

  1. Index spaces header file