Packing Variables
Starting from a high level, at the beginning of a simulation a Parthenon based code defines fields on the Mesh
by adding them to a StateDescriptor
. The StateDescriptor
s for all packages are then passed to the Mesh
constructor. A constructed Mesh
includes a collection of MeshBlock
s that cover the domain of the Mesh
. The MeshBlock
s contain information about the coordinate region they cover, their relationship to neighboring MeshBlock
s, and importantly a container holding MeshBlockData
objects. These MeshBlockData
objects in turn hold Variable
objects corresponding to the fields defined on the Mesh
[1]. Each of the MeshBlockData
objects stored in a given MeshBlock
is labeled as a stage. By construction, every MeshBlock
in a Parthenon contains the same stages. Memory for storing a field on a block on a stage is only actually allocated (on device) and held within a Variable
object in a ParArray
[2]. Putting the storage for fields in a separate object from MeshBlock
, the MeshBlockData
, easily allows for multiple storage locations for a given field on a given block within the mesh, e.g. to store multiple Runge-Kutta stages. Further, for performance reasons, downstream Parthenon codes generally should work with MeshData
objects, which hold pointers to groups of MeshBlockData
objects across different blocks but on the same stage.
As a result of this somewhat complicated structure, it is impractical to access variable storage by following through these different objects in a downstream code [3]. Therefore, Parthenon defines SparsePack
s and SwarmPack
s to allow seamless access to variables within compute kernels. Essentially, packs are objects that contain a ParArray
of references to the ParArray
s stored within Variable
over a chosen set of fields on a given set of blocks. Said differently, given a SparsePack pack
built from MeshData md
and a set of fields var1
, var2
,… , a sparse pack allows one to access the field var1
on block b
of md
at position (k, j, i)
using syntax like
Real &my_val = pack(b, var1_t(), k, j, i); // Pull out a reference to the value of var1 on block b in cell (k, j, i)
ParArray3D<Real> var1 = pack(b, var1_t()); // Pull out a reference to the 3D array containing var1 on block b
etc. within a kernel.
SparsePack
s work for all types of variables (both dense and sparse). They were originally implemented to support sparse variables and to supersede the older VariablePack
s and VariableFluxPack
s and picked up the Sparse
modifier to differentiate them. The latter have not been removed from ``Parthenon`` because some downstream codes still rely on them, but they are deprecated and will be removed eventually.
If you want to deal with particle fields, you will need to use SwarmPack
*s, which are described at swarm_packs.
Type-based Packing
——————
Parthenon provides functionality for accessing fields in a pack via a type that is associated with a field name [4]. As an example, if a downstream code includes a field with the name "var1"
the code could also define a type
struct var1_t : public parthenon::variable_names::base_t<false> {
template <class... Ts>
KOKKOS_INLINE_FUNCTION varname(Ts &&...args)
: parthenon::variable_names::base_t<false>(std::forward<Ts>(args)...) {}
static std::string name() { return "var1"; }
}
which inherits from parthenon::variable_names::base_t
[5]. Rather than write this boilerplate out for every variable name in a downstream code, it is often easier to define a macro that expands to this class for a given variable name argument. Additionally, fields can be added to a StateDescriptor
using the variable name type directly using
StateDescriptor pkg;
Metadata metadata;
pkg->AddField<var1_t>(metadata);
The advantage of using types instead of just strings to denote field names is that the types are accessible within kernels on device. Under the hood, SparsePack::operator()
is overloaded on each of the type list of variable name types used to create the pack, so an instance of the variable name type can be used to access desired field within a pack.
Building and Using a SparsePack
SparsePack
s are built in two stages, first a PackDescriptor
is built that defines the set of fields to include in the pack using one of the overloaded MakePackDescriptor
functions. Then PackDescriptor::GetPack(...)
is called on a given MeshData
or MeshBlockData
object to return an actual pack. In practice, this will look like
parthenon::TaskStatus my_task(MeshData *md) {
// Pull out indices, etc.
std:::vector<MetadataFlags> md_flags; // Optional argument below
std::set<PDOpt> options{PDOpt::WithFluxes}; // Optional argument below
auto desc = parthenon::MakePackDescriptor<var1_t, var2_t>(pmd, md_flags, options);
auto pack = desc.GetPack(md);
parthenon::par_for(/*index ranges, etc. go here */,
KOKKOS_LAMBDA(int b, int k, int j, int i) {
for (int c = 0; c < ncomponents_2; ++c)
pack(b, var1_t(), k, j, i) += 2.0 * pack(b, var2_t(c), k, j, i);
pack.flux(b, X1DIR, var1_t(), k, j, i) = pack(b, var1_t(), k, j, i) * pack(b, var2_t(), k, j, i);
});
return TaskStatus::complete;
}
PackDescriptor
s are somewhat expensive to build and are not currently cached. As a result, it is often beneficial to mark them as ``static`` where possible.
PackDescriptor
takes a std::set
of PDOpt
options to determine what to include in the pack:
|
Fluxes associated with variables in the pack are included in the pack and accessible through |
|
Pack the coarse buffers for the fields rather than the normal resolution buffers. |
|
Packs all blocks across all fields into the variable index so that the pack looks like it has a single block. |
SparsePack::operator()
There are a number of different overloads for operator()
in sparse packs that allow accessing field data:
template <class var_t> Real &operator()(int b, TopologicalElement te, const var_t &t, int k, int j, int i)
andtemplate <class var_t> ParArray3D operator()(int b, TopologicalElement te, const var_t &t)
: Returns the value on blockb
for topological elementte
ofvar_t
. The first call returns a reference to the value at position(k, j, i)
while the second returns aParArray3D
(which obeys reference semantics) containing that component of the field on the block.var_t
must be in the list of types used to create the pack.template <class var_t> Real &operator()(int b, const var_t &t, int k, int j, int i)
andtemplate <class var_t> Real &operator()(int b, const var_t &t)
: Same as above, but with the topological element defaulted to cell-centered.Real &operator()(int b, TopologicalElement te, int idx, int k, int j, int i)
andReal &operator()(int b, int idx, int k, int j, int i)
andReal &operator()(int b, TopologicalElement te, int idx)
andReal &operator()(int b, int idx)
: Same as above, but directly accesses the field at positionidx
in the pack. This should be used with the bounds returned fromSparsePack::GetLowerBound(...)
andSparsePack::GetUpperBound(...)
.Real &operator()(int b, TopologicalElement te, PackIdx idx, int k, int j, int i)
andReal &operator()(int b, PackIdx idx, int k, int j, int i)
andReal &operator()(int b, TopologicalElement te, PackIdx idx)
andReal &operator()(int b, PackIdx idx)
: Same as above, but access the field usingPackIdx idx
. This only works for packs that were built using a list of names (as opposed to a list of types), see [4].
Other SparsePack
Methods
Coordinates_t &GetCoordinates(const int b = 0)
: Returns a reference to the coordinates object associated with blockb
.template <class var_t> int GetLowerBound(int b, var_t)
: Returns the first index in the pack where a field corresponding tovar_t
is stored. Returns-1
ifvar_t
is not allocated on blockb
. A similar functions exist forPackIdx
.template <class var_t> int GetUpperBound(int b, var_t)
: Returns the last index in the pack where a field corresponding tovar_t
is stored. Returns-2
ifvar_t
is not allocated on blockb
. A similar functions exist forPackIdx
.int GetLevel(int b, int off3, int off2, int off1)
: Returns the logical level of neighbor block(s) of blockb
offset in direction(off3, off2, off1)
.bool IsPhysicalBoundary(int b, int off3, int off2, int off1)
: Returns if blockb
has a physical boundary in offset direction(off3, off2, off1)
.template <class var_t> bool Contains(const int b, const var_t t)
: Returns ifvar_t
is allocated on blockb
.template <class var_t> Real &flux(int b, int dir, const var_t &t, int k, int j, int i)
: Gets the flux in directiondir
associated with variablevar_t
.
SparsePack
s with Sparse Fields
A given sparse field may or may not be allocated on each block within a pack. To safely access the fields in a given pack, SparsePack
s provide checks on whether or not a given sparse variable/pool is allocated
parthenon::TaskStatus my_task(MeshData *md) {
// Pull out indices, etc.
auto desc = parthenon::MakePackDescriptor<sparse_var_t>(pmd);
auto pack = desc.GetPack(md);
parthenon::par_for(/*index ranges, etc. go here */,
KOKKOS_LAMBDA(int b, int k, int j, int i) {
// For a single sparse field
if (pack.Contains(b, sparse_var_t())) {
// The sparse field is allocated on block b, and so is safe to access
}
// This loop will go over all allocated sparse fields in the sparse_var sparse pool
for (int idx = pack.GetLowerBound(b, sparse_var_t()); idx <= pack.GetUpperBound(b, sparse_var_t()); ++idx) {
pack(b, idx, k, j, i) = 0.0;
}
});
return TaskStatus::complete;
}