legate::LogicalStore#

class LogicalStore#

A multi-dimensional data container.

LogicalStore is a multi-dimensional data container for fixed-size elements. Stores are internally partitioned and distributed across the system. By default, Legate clients need not create nor maintain the partitions explicitly, and the Legate runtime is responsible for managing them. Legate clients can control how stores should be partitioned for a given task by attaching partitioning constraints to the task (see the constraint module for partitioning constraint APIs).

Each LogicalStore object is a logical handle to the data and is not immediately associated with a physical allocation. To access the data, a client must “map” the store to a physical store (PhysicalStore). A client can map a store by passing it to a task, in which case the task body can see the allocation, or calling LogicalStore::get_physical_store(), which gives the client a handle to the physical allocation (see PhysicalStore for details about physical stores).

Normally, a LogicalStore gets a fixed Shape upon creation. However, there is a special type of logical stores called “unbound” stores whose shapes are unknown at creation time. (see Runtime for the logical store creation API.) The shape of an unbound store is determined by a task that first updates the store; upon the submission of the task, the LogicalStore becomes a normal store. Passing an unbound store as a read-only argument or requesting a PhysicalStore of an unbound store are invalid.

One consequence due to the nature of unbound stores is that querying the shape of a previously unbound store can block the client’s control flow for an obvious reason; to know the shape of the LogicalStore whose Shape was unknown at creation time, the client must wait until the updater task to finish. However, passing a previously unbound store to a downstream operation can be non-blocking, as long as the operation requires no changes in the partitioning and mapping for the LogicalStore.

Public Functions

std::uint32_t dim() const#

Returns the number of dimensions of the store.

Returns:

The number of dimensions

bool has_scalar_storage() const#

Indicates whether the store’s storage is optimized for scalars.

Returns:

true The store is backed by a scalar storage

Returns:

false The store is a backed by a normal region storage

bool overlaps(const LogicalStore &other) const#

Indicates whether this store overlaps with a given store.

Returns:

true The stores overlap

Returns:

false The stores are disjoint

Type type() const#

Returns the element type of the store.

Returns:

Type of elements in the store

Shape shape() const#

Returns the shape of the array.

Returns:

The store’s Shape

const tuple<std::uint64_t> &extents() const#

Returns the extents of the store.

The call can block if the store is unbound

Returns:

The store’s extents

std::size_t volume() const#

Returns the number of elements in the store.

The call can block if the store is unbound

Returns:

The number of elements in the store

bool unbound() const#

Indicates whether the store is unbound.

Returns:

true if the store is unbound, false otherwise

bool transformed() const#

Indicates whether the store is transformed.

Returns:

true if the store is transformed, false otherwise

LogicalStore reinterpret_as(const Type &type) const#

Reinterpret the underlying data of a LogicalStore byte-for-byte as another type.

The size and alignment of the new type must match that of the existing type.

The reinterpreted store will share the same underlying storage as the original, and therefore any writes to one will also be reflected in the other. No type conversions of any kind are performed across the stores, the bytes are interpreted as-is. In effect, if one were to model a LogicalStore as a pointer to an array, then this routine is equivalent to reinterpret_cast-ing the pointer.

Example:

  // Create a store of some shape filled with int32 data.
  constexpr std::int32_t minus_one = -1;
  const auto store                 = runtime->create_store(shape, legate::int32());

  runtime->issue_fill(store, legate::Scalar{minus_one});
  // Reinterpret the underlying data as unsigned 32-bit integers.
  auto reinterp_store = store.reinterpret_as(legate::uint32());
  // Our new store should have the same type as it was reinterpreted to.
  ASSERT_EQ(reinterp_store.type(), legate::uint32());
  // Our old store still has the same type though.
  ASSERT_EQ(store.type(), legate::int32());
  // Both stores should refer to the same underlying storage.
  ASSERT_TRUE(store.equal_storage(reinterp_store));

  const auto phys_store = reinterp_store.get_physical_store();
  const auto acc        = phys_store.read_accessor<std::uint32_t, 1>();

  std::uint32_t interp_value;
  // Need to memcpy here in order to do a "true" bitcast. A reinterpret_cast() may or may not
  // result in the compilers generating the conversion, since type-punning with
  // reinterpret_cast is UB.
  std::memcpy(&interp_value, &minus_one, sizeof(minus_one));
  for (auto it = legate::PointInRectIterator<1>{phys_store.shape<1>()}; it.valid(); ++it) {
    ASSERT_EQ(acc[*it], interp_value);
  }

Parameters:

type – The new type to interpret the data as.

Returns:

The reinterpreted store.

Throws:
  • std::invalid_argument – If the size (in bytes) of the new type does not match that of the old type.

  • std::invalid_argument – If the alignment of the new type does not match that of the old type.

LogicalStore promote(
std::int32_t extra_dim,
std::size_t dim_size
) const#

Adds an extra dimension to the store.

Value of extra_dim decides where a new dimension should be added, and each dimension \(i\), where \(i\) >= extra_dim, is mapped to dimension \(i+1\) in a returned store. A returned store provides a view to the input store where the values are broadcasted along the new dimension.

For example, for a 1D store A contains [1, 2, 3], A.promote(0, 2) yields a store equivalent to:

[[1, 2, 3],
 [1, 2, 3]]

whereas A.promote(1, 2) yields:

[[1, 1],
 [2, 2],
 [3, 3]]

The call can block if the store is unbound

Parameters:
  • extra_dim – Position for a new dimension

  • dim_size – Extent of the new dimension

Throws:

std::invalid_argument – When extra_dim is not a valid dimension name

Returns:

A new store with an extra dimension

LogicalStore project(std::int32_t dim, std::int64_t index) const#

Projects out a dimension of the store.

Each dimension \(i\), where \(i\) > dim, is mapped to dimension \(i-1\) in a returned store. A returned store provides a view to the input store where the values are on hyperplane \(x_\mathtt{dim} = \mathtt{index}\).

For example, if a 2D store A contains [[1, 2], [3, 4]], A.project(0, 1) yields a store equivalent to [3, 4], whereas A.project(1, 0) yields [1, 3].

The call can block if the store is unbound

Parameters:
  • dim – Dimension to project out

  • index – Index on the chosen dimension

Throws:

std::invalid_argument – If dim is not a valid dimension name or index is out of bounds

Returns:

A new store with one fewer dimension

LogicalStore slice(std::int32_t dim, Slice sl) const#

Slices a contiguous sub-section of the store.

For example, consider a 2D store A:

[[1, 2, 3],
 [4, 5, 6],
 [7, 8, 9]]

A slicing A.slice(0, legate::Slice{1}) yields

[[4, 5, 6],
 [7, 8, 9]]

The result store will look like this on a different slicing call A.slice(1, legate::Slice{legate::Slice::OPEN, 2}):

[[1, 2],
 [4, 5],
 [7, 8]]

Finally, chained slicing calls

A.slice(0, legate::Slice{1})
 .slice(1, legate::Slice{legate::Slice::OPEN, 2})

results in:

[[4, 5],
 [7, 8]]

The call can block if the store is unbound

Parameters:
  • dim – Dimension to slice

  • slSlice descriptor

Throws:

std::invalid_argument – If dim is not a valid dimension name

Returns:

A new store that corresponds to the sliced section

LogicalStore transpose(std::vector<std::int32_t> &&axes) const#

Reorders dimensions of the store.

Dimension \(i\)i of the resulting store is mapped to dimension axes[i] of the input store.

For example, for a 3D store A

[[[1, 2],
  [3, 4]],
 [[5, 6],
  [7, 8]]]

transpose calls A.transpose({1, 2, 0}) and A.transpose({2, 1, 0}) yield the following stores, respectively:

[[[1, 5],
  [2, 6]],
 [[3, 7],
  [4, 8]]]
[[[1, 5],
 [3, 7]],

 [[2, 6],
  [4, 8]]]

The call can block if the store is unbound

Parameters:

axes – Mapping from dimensions of the resulting store to those of the input

Throws:

std::invalid_argument – If any of the following happens: 1) The length of axes doesn’t match the store’s dimension; 2) axes has duplicates; 3) Any axis in axes is an invalid axis name.

Returns:

A new store with the dimensions transposed

LogicalStore delinearize(
std::int32_t dim,
std::vector<std::uint64_t> sizes
) const#

Delinearizes a dimension into multiple dimensions.

Each dimension \(i\) of the store, where \(i >\) dim, will be mapped to dimension \(i+N\) of the resulting store, where \(N\) is the length of sizes. A delinearization that does not preserve the size of the store is invalid.

For example, consider a 2D store A

[[1, 2, 3, 4],
 [5, 6, 7, 8]]

A delinearizing call A.delinearize(1, {2, 2})) yields:

[[[1, 2],
  [3, 4]],

 [[5, 6],
  [7, 8]]]

Unlike other transformations, delinearization is not an affine transformation. Due to this nature, delinearized stores can raise legate::NonInvertibleTransformation in places where they cannot be used.

The call can block if the store is unbound

Parameters:
  • dim – Dimension to delinearize

  • sizes – Extents for the resulting dimensions

Throws:

std::invalid_argument – If dim is invalid for the store or sizes does not preserve the extent of the chosen dimension

Returns:

A new store with the chosen dimension delinearized

LogicalStorePartition partition_by_tiling(
std::vector<std::uint64_t> tile_shape
) const#

Creates a tiled partition of the store.

The call can block if the store is unbound

Parameters:

tile_shapeShape of tiles

Returns:

A store partition

PhysicalStore get_physical_store(
std::optional<mapping::StoreTarget> target = std::nullopt
) const#

Creates a PhysicalStore for this LogicalStore

This call blocks the client’s control flow and fetches the data for the whole store to the current node.

When the target is StoreTarget::FBMEM, the data will be consolidated in the framebuffer of the first GPU available in the scope.

If no target is given, the runtime uses StoreTarget::SOCKETMEM if it exists and StoreTarget::SYSMEM otherwise.

If there already exists a physical store for a different memory target, that physical store will be unmapped from memory and become invalid to access.

Parameters:

target – The type of memory in which the physical store would be created.

Throws:

std::invalid_argument – If no memory of the chosen type is available

Returns:

A PhysicalStore of the LogicalStore

void detach()#

Detach a store from its attached memory.

This call will wait for all operations that use the store (or any sub-store) to complete.

After this call returns, it is safe to deallocate the attached external allocation. If the allocation was mutable, the contents would be up-to-date upon the return. The contents of the store are invalid after that point.

void offload_to(mapping::StoreTarget target_mem)#

Offload store to specified target memory.

Parameters:

target_mem – The target memory.

bool equal_storage(const LogicalStore &other) const#

Determine whether two stores refer to the same memory.

This routine can be used to determine whether two seemingly unrelated stores refer to the same logical memory region, including through possible transformations in either this or other.

The user should note that some transformations do modify the underlying storage. For example, the store produced by slicing will not share the same storage as its parent, and this routine will return false for it:

  const auto store       = runtime->create_store(legate::Shape{4, 3}, legate::int64());
  const auto transformed = store.slice(1, legate::Slice{-2, -1});

  // Slices partition a store into a parent and sub-store which both cover distinct regions,
  // and hence don't share storage.
  ASSERT_FALSE(store.equal_storage(transformed));

Transposed stores, on the other hand, still share the same storage, and hence this routine will return true for them:

  const auto store       = runtime->create_store(legate::Shape{4, 3}, legate::int64());
  const auto transformed = store.transpose({1, 0});

  // Transposing a store doesn't modify the storage
  ASSERT_TRUE(store.equal_storage(transformed));
Parameters:

other – The LogicalStore to compare with.

Returns:

true if two stores cover the same underlying memory region, false otherwise.

class Impl#