template<typename T>
class ryujin::Mirrored< T >
A convenience wrapper around MirroredStorage that maintains a (trivially copyable) payload mirrored between the host and default (device) memory spaces.
This allows to encapsulate a "POD style" payload (such as the runtime parameters of a class) in a single structure that is moved into device memory once - instead of copying individual parameters field by field into a "view" object that is then captured by value in a computation loop.
The class operates in one of two modes that are selected by the template argument:
- Mirrored<T> maintains a single object of type
T.
- Mirrored<T *> maintains a one dimensional array of objects of type
T. The length is set in the constructor, or with reinit() and queried with size().
Intended usage of the scalar mode, here with the TransferPolicy::implicit_transfers_host_resident policy so that the payload is transferred on demand and the host storage - into which the runtime parameters are bound - is never deallocated:
class HyperbolicSystem {
public:
struct Parameters { double gamma; double gamma_inverse; };
HyperbolicSystem()
: parameters_("hyperbolic system parameters",
{
auto *parameters = parameters_.view();
parameters->gamma = 1.4;
add_parameter("gamma", parameters->gamma, "...");
}
void update_parameters()
{
auto *parameters = parameters_.view();
parameters->gamma_inverse = 1. / parameters->gamma;
}
template <typename MemorySpace>
class View {
public:
View(const HyperbolicSystem &hyperbolic_system)
: parameters_(
hyperbolic_system.parameters_.template view<MemorySpace>())
{
}
DEAL_II_HOST_DEVICE double gamma() const { return parameters_->gamma; }
private:
const Parameters *parameters_;
};
template <typename MemorySpace>
View<MemorySpace> view() const { return View<MemorySpace>(*this); }
private:
Mirrored<Parameters> parameters_;
};
@ implicit_transfers_host_resident
The array mode uses the same raw pointer interface, but the storage is sized explicitly with reinit():
class Stencil {
public:
struct Entry { unsigned int index; double weight; };
Stencil() : entries_("stencil entries") {}
void prepare(const std::size_t n_entries)
{
entries_.reinit(n_entries, TransferPolicy::implicit_transfers);
auto *entries = entries_.view();
for (std::size_t i = 0; i < entries_.size(); ++i)
entries[i] = {static_cast<unsigned int>(i), 1.};
}
private:
Mirrored<Entry *> entries_;
};
- Note
- The transfer policy defaults to TransferPolicy::explicit_transfers, where view() never triggers a memory transfer on its own and all transfers have to be requested manually. Keep in mind that copy_to_memory_space() is a no-op if the selected memory space is already resident: updating the payload under that policy requires a move_to_memory_space<Host>() before the write, and a copy_to_memory_space<Default>() afterwards.
-
The pointer returned by view() may only be dereferenced on the selected memory space. In the array mode the pointer references the first of size() contiguously stored objects; the length is not part of the returned pointer and has to be carried along by the caller.
-
In contrast to a copied Kokkos::View the pointer returned by view() does not pin the underlying storage: it dangles after a move_to_memory_space() away from the corresponding memory space - such as the implicit one performed by a writable view() under an implicit transfer policy. The TransferPolicy::implicit_transfers_host_resident policy used in the example above guarantees that the pointer into the host storage remains valid, but the device mirror is still dropped by every writable host view(): Recreate the View objects of the example above after every parameter update.
-
An object of this class allocates memory with Kokkos. It thus has to be created after Kokkos has been initialized, which is done by the dealii::Utilities::MPI::MPI_InitFinalize constructor.
Definition at line 459 of file gpu.h.