Usage#

Initializing the Legate runtime#

Before using Legate.STL, you must initialize the Legate runtime. Legate.STL provides a convenience class called legate::experimental::stl::initialize_library that initializes the runtime when it is constructed and finalizes the runtime when it is destroyed. The runtime must be initialized before any other Legate.STL functions are called.

#include <legate/core/experimental/stl.hpp>

namespace stl = legate::experimental::stl;

int main(int argc, char* argv[]) {
  // Initialize the Legate runtime:
  stl::initialize_library init{argc, argv};

  // Your code here...

  // Finalize the Legate runtime:
  return 0;
}

Declaring and initializing a store#

There are several ways to declare and initialize a store. The simplest is to construct an legate::experimental::stl::logical_store object, specifying the element type and and the shape of the store:

// Declare an uninitialized 2-D store:
stl::logical_store<int, 2> store{{100, 100}};

The code above declares an uninitialized store of int’s with 100 rows and 100 columns. The second template parameter (2) specifies the number of dimensions for the store.

To initialize the store with a value, pass the value as the second argument to the constructor:

// Declare a 2-D store that is zero-initialized:
stl::logical_store<int, 2> store{{100, 100}, 0};

With this form of initialization, the template parameters can be deduced. This definition is equivalent to the one above:

// Declare a 2-D store that is zero-initialized:
stl::logical_store store{{100, 100}, 0};

Above, the dimensionality of the store is deduced from the number of elements in the shape vector ({100, 100}). The element type is deduced from the second argument (0).

Note

Declaring a store with an initial value is equivalent to declaring an uninitialized store and then calling the legate::experimental::stl::fill() algorithm.

Finally, you can create a store in situ using the legate::experimental::stl::create_store() function:

// Declare a 2-D store that is zero-initialized:
auto store = stl::create_store<int, 2>({100, 100}, 0);

// Same:
auto store = stl::create_store({100, 100}, 0);

Scalar stores#

There is a special case of a store called a “scalar store.” A scalar store is a store with zero dimensions. It is a store that holds a single element. You can declare a scalar store like this:

// Declare a scalar store that is zero-initialized:
stl::logical_store<int, 0> store{{}, 0};

// Same:
auto store = stl::create_store<int, 0>({}, 0);

Just as with higher-dimensional stores, the element of the store can be accessed by asking the store for an mdspan view:

// Get a view of the store:
auto view = stl::as_mdspan(store);

// Access the element of the store. This is a 0-D indexing operation:
int i = view();

Due to a limitation in the current implementation of Legate.STL, scalar stores are immutable. Attempts to modify the value of a scalar store will result in a runtime error.

There is a convenience function called legate::experimental::stl::scalar() for creating scalar stores:

// Declare a scalar store holding the value 0:
auto store = stl::scalar(0);

// Same:
auto store = stl::scalar<int>(0);

// Same:
auto store = stl::create_store<int, 0>({}, 0);

Element types#

The permissible element types for logical_store objects are:

  • __half

  • float

  • double

  • std::int8_t

  • std::int16_t

  • std::int32_t

  • std::int64_t

  • std::uint8_t

  • std::uint16_t

  • std::uint32_t

  • std::uint64_t

  • bool

  • std::complex<float>

  • std::complex<double>

  • std::string

Accessing the elements of a store#

A store is a logical entity that may span many disjoint address spaces, possibly distributed across multiple nodes. Unlike an ordinary STL container, the individual elements cannot be directly referenced.

To access the elements of a store, you must first obtain a view into the store. There are several ways to do this, but the easiest is to use the legate::experimental::stl::as_mdspan() function. This function returns a std::mdspan object that gives direct access to the physical elements of the store.

// Declare a 2-D store that is zero-initialized:
stl::logical_store<int, 2> store{{100, 100}, 0};

// Get a view of the whole the store:
auto view = stl::as_mdspan(store);

// Access the elements of the store:
const auto [imax, jmax] = store.extents();
for (int i = 0; i < imax; ++i)
  for (int j = 0; j < jmax; ++j)
    view(i, j) = i + j;

Warning

The as_mdspan function must pull all of the store’s backing data onto a single node and reify it in physical memory. This can be a very expensive operation. The call to as_mdspan blocks until all the data is available.

There are other ways to obtain a view into a store. See the section on Creating Views for more information.

Using algorithms#

Legate.STL provides a number of algorithms that operate on stores. These algorithms are similar to the range-based algorithms in the C++20 Standard Library, but they are designed to operate on stores and views of stores rather than STL ranges.

// Declare a 2-D store that is zero-initialized:
stl::logical_store<int, 2> store{{100, 100}, 0};

// Assign a value to all elements of the store
// with the `fill` algorithm:
stl::fill(store, 42);

// Get a view into the store:
auto view = stl::as_mdspan(store);

// Access the elements of the store:
const auto [imax, jmax] = store.extents();
for (int i = 0; i < imax; ++i)
  for (int j = 0; j < jmax; ++j)
    assert(view(i, j) == 42);

Reductions#

Reductions are algorithms on stores that reduces the stores’ dimensionality by combining elements along a chosen axis via repeated application of a binary operation. In the Standard Template Library, the std::reduce and std::accumulate algorithms are both examples of reductions.

In Legate.Core, reductions are a complicated topic. Legate.STL makes working with reductions easier by providing a familiar interface and hiding the bookkeeping details. The legate::experimental::stl::reduce() and legate::experimental::stl::transform_reduce() algorithms, together with the views described below, let you apply reductions along any axis of a store using the familiar function objects from the Standard library like std::plus and std::minus.

Below is an example that does a row-wise reduction of a 2-D store using using std::plus. It is described after the break.

auto store  = stl::create_store<std::int64_t>({3,4});

// fill the store with data so it looks like this:
//     [[0 0 0 0]
//      [1 1 1 1]
//      [2 2 2 2]]

auto init   = stl::create_store({4}, std::int64_t{0});
auto result = stl::reduce(stl::rows_of(store), init, stl::elementwise(std::plus<>()));

// result is a 1D logical store with the following values:
// [3 3 3 3]

Like std::reduce, the legate::experimental::stl::reduce algorithm takes a range of elements, an initial value, and a binary operation. The code above used the legate::experimental::stl::rows_of() view (described below) to get a range of rows from the store. The result is a range of 1-D mdspan objects, each representing a row.

Element-wise operations#

The code above wants to fold a store’s rows together using std::plus. Here we have a problem: std::plus works on elements, but we want it to work on rows, where each row is an mdspan. You can’t add two mdspan objects together, and even if you could, assigning the result to another mdspan is not going to assign the elements of the mdspan. This is where the legate::experimental::stl::elementwise() function comes in. It adapts a function that works on elements to one that works on mdspan’s of elements.

Passing two mdspan objects to the result of stl::elementwise(std::plus<>) will return a new, special “element-wise” mdspan. When you access an element of the element-wise mdspan, it will call std::plus on the corresponding elements of the two input mdspan’s. Legate.STL also knows that assigning an “element-wise” mdspan to a regular mdspan should assign the elements of the span rather than the span itself.

Custom reduction operations#

Legate.Core does not recognize the STL functional objects like std::plus as reductions. Legate.STL transparently replaces these objects with reduction operations that Legate.Core understands, like legate::SumReduction. But Legate.STL’s reduction operations are not discriminating; you are free to use Legate.Core’s reduction operations directly if you prefer.

For more algorithm information#

The current set of Legate.STL algorithms is small, but it will grow over time. It currently includes:

  • legate::experimental::stl::fill()

  • legate::experimental::stl::for_each()

  • legate::experimental::stl::for_each_zip()

  • legate::experimental::stl::reduce()

  • legate::experimental::stl::transform()

  • legate::experimental::stl::transform_reduce()

Creating views#

Legate.STL provides sequence algorithms like transform and reduce that, like their counterparts in the C++20 STL, operate on ranges of elements. There are many ways to view a store as a range of elements. For example, you may want to operate to operate on a flattened view of a store, or on a slice of a store. You may want the elements of the range to be rows or columns or some other subdimension of the store. For each of these cases, Legate.STL provides a range adaptor that presents the store as a range of elements.

elements_of#

The elements_of adaptor presents the store as a flattened range of elements. Iterating over the range visits each element of the store in row-major order.

// Declare a 2-D store that is zero-initialized:
stl::logical_store<int, 2> store{{2, 2}, 0};

// Fill the store with data.
auto view = stl::as_mdspan(store);
const auto [imax, jmax] = store.extents();
for (int i = 0; i < imax; ++i)
  for (int j = 0; j < jmax; ++j)
    view(i, j) = i * imax + j;

// Use 'elements_of' to get a flattened view of the store:
auto elements = stl::elements_of(store);
for (auto& e : elements) {
  std::cout << e << ", ";
}

The code above prints:

0, 1, 2, 3,

rows_of#

The rows_of adaptor presents a two-dimensional store as a range of rows, where each row is represented as a 1-dimensional stl::logical_store object.

// Declare a 2-D store that is zero-initialized:
stl::logical_store<int, 2> store{{2, 2}, 0};

// Fill the store with data.
auto view = stl::as_mdspan(store);
const auto [imax, jmax] = store.extents();
for (int i = 0; i < imax; ++i)
  for (int j = 0; j < jmax; ++j)
    view(i, j) = i * imax + j;

// Use 'rows_of' to get a view of the store as a range of rows:
auto rows = stl::rows_of(store);
for (stl::logical_store<int, 1> row : rows) {
  auto row_view = stl::as_mdspan(row);
  for (auto i = 0; i < row_view.extent(0); ++i) {
    std::cout << row_view(i) << ", ";
  }
  std::cout << std::endl;
}

The above code prints:

0, 1,
2, 3,

columns_of#

The columns_of adaptor presents a two-dimensional store as a range of columns, where each column is represented as a 1-dimensional stl::logical_store object.

// Declare a 2-D store that is zero-initialized:
stl::logical_store<int, 2> store{{2, 2}, 0};

// Fill the store with data.
auto view = stl::as_mdspan(store);
const auto [imax, jmax] = store.extents();
for (int i = 0; i < imax; ++i)
  for (int j = 0; j < jmax; ++j)
    view(i, j) = i * imax + j;

// Use 'columns_of' to get a view of the store as a range of columns:
auto cols = stl::columns_of(store);
for (stl::logical_store<int, 1> col : cols) {
  auto col_view = stl::as_mdspan(col);
  for (auto i = 0; i < col_view.extent(0); ++i) {
    std::cout << col_view(i) << ", ";
  }
  std::cout << std::endl;
}

The above code prints:

0, 2,
1, 3,

projections_of#

The projections_of adaptor is a generalization of rows_of and columns_of. It presents a store as a range of slices along several specified dimensions. As such, it can be used with stores of any dimensionality.

rows_of(store)) is equivalent to projections_of<0>(store).

columns_of(store)) is equivalent to projections_of<1>(store).

// Declare a 3-D store that is zero-initialized:
stl::logical_store<int, 3> store{{2, 2, 2}, 0};

// Fill the store with data.
auto view = stl::as_mdspan(store);
const auto [imax, jmax, kmax] = store.extents();
for (int i = 0; i < imax; ++i)
  for (int j = 0; j < jmax; ++j)
    for (int k = 0; k < kmax; ++k)
      view(i, j, k) = i * imax * jmax + j * jmax + k;

// Use 'projections_of' to get a view of the store as a range of slices along
// the first and second dimension:
auto slices = stl::projections_of<0,1>(store);
for (stl::logical_store<int, 1> slice : slices) {
  auto slice_view = stl::as_mdspan(slice);
  for (auto i = 0; i < slice_view.extent(0); ++i) {
    std::cout << slice_view(i) << ", ";
  }
  std::cout << std::endl;
}

The code above prints:

0, 1,
4, 5,
2, 3,
6, 7,