Skip to content

Commit a0fdccf

Browse files
authored
Merge pull request #1238 from parthenon-hpc-lab/lroberts36/refactor-pack-code
[Trivial] Split up `SparsePack` related stuff and document `SparsePack`s
2 parents 4c891b0 + cec4b29 commit a0fdccf

15 files changed

+538
-276
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
- [[PR 1173]](https://github.com/parthenon-hpc-lab/parthenon/pull/1173) Make debugging easier by making parthenon throw an error if ParameterInput is different on multiple MPI ranks.
4242

4343
### Infrastructure (changes irrelevant to downstream codes)
44-
- [[PR1218]](https://github.com/parthenon-hpc-lab/parthenon/pull/1219) Add ability to run the tasking without threading
44+
- [[PR 1238]](https://github.com/parthenon-hpc-lab/parthenon/pull/1238) Refactor pack related files and add SparsePack docs
45+
- [[PR 1218]](https://github.com/parthenon-hpc-lab/parthenon/pull/1219) Add ability to run the tasking without threading
4546
- [[PR 1176]](https://github.com/parthenon-hpc-lab/parthenon/pull/1176) Move some code from header to implementation files
4647

4748
### Removed (removing behavior/API/varaibles/...)

doc/sphinx/src/particles.rst

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,16 @@ Similarly to grid variables, particle swarms support
151151
This also supports ``FlatIdx`` for indexing; see the
152152
``particle_leapfrog`` example for usage.
153153

154-
``SwarmPack``s
155-
----------------
154+
. _swarm_packs:
155+
156+
``SwarmPack``\ s
157+
--------------
156158

157-
Similar to grid variables, swarms can be packed over ``MeshBlock``s via ``SwarmPack``s.
158-
``SwarmPack``s are the particle analog to ``SparsePack``s for field variables. A single
159+
Similar to grid variables, swarms can be packed over ``MeshBlock``\ s via ``SwarmPack``\ s.
160+
``SwarmPack``\ s are the particle analog to ``SparsePack``\ s for field variables. A single
159161
``SwarmPack`` can contain either ``int`` or ``Real`` entries, but not both. One can pack
160162
a ``SwarmPack`` via a ``std::vector<std::string>`` or the type-based variable prescription
161-
previously used by ``SparsePack``s.
163+
previously used by ``SparsePack``\ s (see :ref:`sparse_packs`).
162164

163165
For packing via string (wherein below, ``swarm_position::x::name()`` returns a string),
164166
one must specify the data type by template argument:

doc/sphinx/src/sparse_packs.rst

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
.. _sparse_packs:
2+
3+
Packing Variables
4+
=================
5+
6+
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.
7+
8+
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
9+
10+
.. code:: c++
11+
12+
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)
13+
ParArray3D<Real> var1 = pack(b, var1_t()); // Pull out a reference to the 3D array containing var1 on block b
14+
15+
etc. within a kernel.
16+
17+
``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.*
18+
19+
*If you want to deal with particle fields, you will need to use* ``SwarmPack``\ *s, which are described at :ref:`swarm_packs`.
20+
Type-based Packing
21+
------------------
22+
23+
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
24+
25+
.. code:: c++
26+
27+
struct var1_t : public parthenon::variable_names::base_t<false> {
28+
template <class... Ts>
29+
KOKKOS_INLINE_FUNCTION varname(Ts &&...args)
30+
: parthenon::variable_names::base_t<false>(std::forward<Ts>(args)...) {}
31+
static std::string name() { return "var1"; }
32+
}
33+
34+
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
35+
36+
.. code:: c++
37+
38+
StateDescriptor pkg;
39+
Metadata metadata;
40+
pkg->AddField<var1_t>(metadata);
41+
42+
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.
43+
44+
Building and Using a ``SparsePack``
45+
-----------------------------------
46+
47+
``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
48+
49+
.. code:: c++
50+
51+
parthenon::TaskStatus my_task(MeshData *md) {
52+
// Pull out indices, etc.
53+
std:::vector<MetadataFlags> md_flags; // Optional argument below
54+
std::set<PDOpt> options{PDOpt::WithFluxes}; // Optional argument below
55+
auto desc = parthenon::MakePackDescriptor<var1_t, var2_t>(pmd, md_flags, options);
56+
auto pack = desc.GetPack(md);
57+
parthenon::par_for(/*index ranges, etc. go here */,
58+
KOKKOS_LAMBDA(int b, int k, int j, int i) {
59+
for (int c = 0; c < ncomponents_2; ++c)
60+
pack(b, var1_t(), k, j, i) += 2.0 * pack(b, var2_t(c), k, j, i);
61+
pack.flux(b, X1DIR, var1_t(), k, j, i) = pack(b, var1_t(), k, j, i) * pack(b, var2_t(), k, j, i);
62+
});
63+
return TaskStatus::complete;
64+
}
65+
66+
``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.*
67+
68+
``PackDescriptor`` takes a ``std::set`` of ``PDOpt`` options to determine what to include in the pack:
69+
70+
.. list-table:: Pack Descriptor Options
71+
:widths: 8 20
72+
:header-rows: 0
73+
74+
* - ``PDOpt::WithFluxes``
75+
- Fluxes associated with variables in the pack are included in the pack and accessible through ``pack.flux(...)``
76+
* - ``PDOpt::Coarse``
77+
- Pack the coarse buffers for the fields rather than the normal resolution buffers.
78+
* - ``PDOpt::Flatten``
79+
- Packs all blocks across all fields into the variable index so that the pack looks like it has a single block.
80+
81+
``SparsePack::operator()``
82+
^^^^^^^^^^^^^^^^^^^^^^^^^^
83+
There are a number of different overloads for ``operator()`` in sparse packs that allow accessing field data:
84+
85+
* ``template <class var_t> Real &operator()(int b, TopologicalElement te, const var_t &t, int k, int j, int i)`` and ``template <class var_t> ParArray3D operator()(int b, TopologicalElement te, const var_t &t)`` : Returns the value on block ``b`` for topological element ``te`` of ``var_t``. The first call returns a reference to the value at position ``(k, j, i)`` while the second returns a ``ParArray3D`` (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.
86+
* ``template <class var_t> Real &operator()(int b, const var_t &t, int k, int j, int i)`` and ``template <class var_t> Real &operator()(int b, const var_t &t)``: Same as above, but with the topological element defaulted to cell-centered.
87+
* ``Real &operator()(int b, TopologicalElement te, int idx, int k, int j, int i)`` and ``Real &operator()(int b, int idx, int k, int j, int i)`` and ``Real &operator()(int b, TopologicalElement te, int idx)`` and ``Real &operator()(int b, int idx)``: Same as above, but directly accesses the field at position ``idx`` in the pack. This should be used with the bounds returned from ``SparsePack::GetLowerBound(...)`` and ``SparsePack::GetUpperBound(...)``.
88+
* ``Real &operator()(int b, TopologicalElement te, PackIdx idx, int k, int j, int i)`` and ``Real &operator()(int b, PackIdx idx, int k, int j, int i)`` and ``Real &operator()(int b, TopologicalElement te, PackIdx idx)`` and ``Real &operator()(int b, PackIdx idx)``: Same as above, but access the field using ``PackIdx idx``. This only works for packs that were built using a list of names (as opposed to a list of types), see [4]_.
89+
90+
Other ``SparsePack`` Methods
91+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
92+
* ``Coordinates_t &GetCoordinates(const int b = 0)``: Returns a reference to the coordinates object associated with block ``b``.
93+
* ``template <class var_t> int GetLowerBound(int b, var_t)``: Returns the first index in the pack where a field corresponding to ``var_t`` is stored. Returns ``-1`` if ``var_t`` is not allocated on block ``b``. A similar functions exist for ``PackIdx``.
94+
* ``template <class var_t> int GetUpperBound(int b, var_t)``: Returns the last index in the pack where a field corresponding to ``var_t`` is stored. Returns ``-2`` if ``var_t`` is not allocated on block ``b``. A similar functions exist for ``PackIdx``.
95+
* ``int GetLevel(int b, int off3, int off2, int off1)``: Returns the logical level of neighbor block(s) of block ``b`` offset in direction ``(off3, off2, off1)``.
96+
* ``bool IsPhysicalBoundary(int b, int off3, int off2, int off1)``: Returns if block ``b`` has a physical boundary in offset direction ``(off3, off2, off1)``.
97+
* ``template <class var_t> bool Contains(const int b, const var_t t)``: Returns if ``var_t`` is allocated on block ``b``.
98+
* ``template <class var_t> Real &flux(int b, int dir, const var_t &t, int k, int j, int i)``: Gets the flux in direction ``dir`` associated with variable ``var_t``.
99+
100+
``SparsePack``\ s with Sparse Fields
101+
------------------------------------
102+
103+
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
104+
105+
.. code:: c++
106+
107+
parthenon::TaskStatus my_task(MeshData *md) {
108+
// Pull out indices, etc.
109+
auto desc = parthenon::MakePackDescriptor<sparse_var_t>(pmd);
110+
auto pack = desc.GetPack(md);
111+
parthenon::par_for(/*index ranges, etc. go here */,
112+
KOKKOS_LAMBDA(int b, int k, int j, int i) {
113+
// For a single sparse field
114+
if (pack.Contains(b, sparse_var_t())) {
115+
// The sparse field is allocated on block b, and so is safe to access
116+
}
117+
// This loop will go over all allocated sparse fields in the sparse_var sparse pool
118+
for (int idx = pack.GetLowerBound(b, sparse_var_t()); idx <= pack.GetUpperBound(b, sparse_var_t()); ++idx) {
119+
pack(b, idx, k, j, i) = 0.0;
120+
}
121+
});
122+
return TaskStatus::complete;
123+
}
124+
125+
.. [1] In practice, there are ways of selecting subsets of fields for inclusion in a given ``MeshBlockData`` instance.
126+
.. [2] ``ParArray``\ s are lite wrappers around ``Kokkos::View``\ s and therefore obey reference semantics.
127+
.. [3] Additionally, because many ``std::`` library containers don't work on device, the chain from field name through ``MeshBlockData`` to ``Variable`` to the underlying ``ParArray`` cannot be legally followed within a kernel.
128+
.. [4] It is also possible to use ``SparsePack``\ s created with a vector of string field names. In this case, the ``PackDescriptor`` can return a map from variable name strings to ``PackIdx``\ s using the function ``PackDescriptor::GetMap()``. These indices need to be pulled out from the map outside of the kernel, since ``std::unordered_map`` is unavailable on device. Then inside the kernel, packs are accessed using ``PackIdx var_idx`` in calls to the pack ``pack(b, var_idx, k, j, i)``, etc.
129+
.. [5] The template arguments for ``parthenon::variable_names::base_t`` are ``<bool REGEX, int... NCOMP>`` where the ``REGEX`` boolean determines if the returned name sure be interpreted as a simple string or as a regex that possibly selects multiple variable names. The variadic integer pack ``NCOMP...`` defines the tensor shape of the field, so that when the ``var1_t(m, l)`` constructor is called the correct component of the field is returned from a call to ``SparsePack::operator()`` (assuming we have a two component field).

src/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,15 @@ add_library(parthenon
212212
pack/block_selector.hpp
213213
pack/make_pack_descriptor.hpp
214214
pack/make_swarm_pack_descriptor.hpp
215+
pack/pack_descriptor.cpp
216+
pack/pack_descriptor.hpp
215217
pack/pack_utils.hpp
216218
pack/sparse_pack.hpp
219+
pack/sparse_pack.cpp
217220
pack/sparse_pack_base.cpp
218221
pack/sparse_pack_base.hpp
222+
pack/sparse_pack_cache.hpp
223+
pack/sparse_pack_cache.cpp
219224
pack/swarm_default_names.hpp
220225
pack/swarm_pack.hpp
221226
pack/swarm_pack_base.hpp

src/interface/mesh_data.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "mesh/meshblock.hpp"
3030
#include "mesh/meshblock_pack.hpp"
3131
#include "pack/sparse_pack_base.hpp"
32+
#include "pack/sparse_pack_cache.hpp"
3233
#include "pack/swarm_pack_base.hpp"
3334
#include "utils/communication_buffer.hpp"
3435
#include "utils/error_checking.hpp"

src/interface/meshblock_data.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "interface/variable_pack.hpp"
3131
#include "mesh/domain.hpp"
3232
#include "pack/sparse_pack_base.hpp"
33+
#include "pack/sparse_pack_cache.hpp"
3334
#include "pack/swarm_pack_base.hpp"
3435
#include "utils/concepts_lite.hpp"
3536
#include "utils/error_checking.hpp"

src/pack/pack_descriptor.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//========================================================================================
2+
// (C) (or copyright) 2020-2025. Triad National Security, LLC. All rights reserved.
3+
//
4+
// This program was produced under U.S. Government contract 89233218CNA000001 for Los
5+
// Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC
6+
// for the U.S. Department of Energy/National Nuclear Security Administration. All rights
7+
// in the program are reserved by Triad National Security, LLC, and the U.S. Department
8+
// of Energy/National Nuclear Security Administration. The Government is granted for
9+
// itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide
10+
// license in this material to reproduce, prepare derivative works, distribute copies to
11+
// the public, perform publicly and display publicly, and to permit others to do so.
12+
//========================================================================================
13+
14+
#include <cstdio>
15+
#include <string>
16+
17+
#include "pack/pack_descriptor.hpp"
18+
19+
namespace parthenon {
20+
namespace impl {
21+
22+
void PackDescriptor::Print() const {
23+
printf("--------------------\n");
24+
for (int i = 0; i < var_group_names.size(); ++i) {
25+
printf("group name: %s\n", var_group_names[i].c_str());
26+
printf("--------------------\n");
27+
for (const auto &[var_name, uid] : var_groups[i]) {
28+
printf("%s\n", var_name.label().c_str());
29+
}
30+
}
31+
printf("--------------------\n");
32+
}
33+
} // namespace impl
34+
} // namespace parthenon

0 commit comments

Comments
 (0)