Data Model
FleCSI provides a data model that integrates with the task and kernel abstractions to provide easy registration and access to various data types with automatic dependency tracking.
Example 1: Global data
Global fields are used to store global variables/objects that can be
accessed by any task. For the common case of only one value for each field, it
is natural to use the data::single
layout.
template<typename T>
using single = field<T, data::single>;
const single<double>::definition<global> gfield;
To create a topology instance, declare a topology slot and then use allocate
with an argument appropriate to the topology called a coloring.
In general, a coloring describes the structure of a topology and its distribution among colors.
The global topology is a special case that does not actually use colors; its “coloring” is simply a count of values for each field.
Writing to a global field requires a single task launch.
void
init(double v, single<double>::accessor<wo> gv) {
gv = v;
}
void
print(single<double>::accessor<ro> gv) {
flog(trace) << "global value: " << gv << std::endl;
}
void
advance(control_policy &) {
topo::global::slot gtopo;
gtopo.allocate(1);
const auto v = gfield(gtopo);
execute<init>(42.0, v);
execute<print>(v);
} // advance()
Example 2: Index data
A field on an index
topology stores one value for each color.
using namespace flecsi;
template<typename T>
using single = field<T, data::single>;
const single<std::size_t>::definition<topo::index> ifield;
void
init(single<std::size_t>::accessor<wo> iv) {
flog(trace) << "initializing value on color " << color() << " of " << colors()
<< std::endl;
iv = color();
}
void
print(single<std::size_t>::accessor<ro> iv) {
flog(trace) << "index value: " << iv << " (color " << color() << " of "
<< colors() << ")" << std::endl;
}
void
advance(control_policy &) {
topo::index::slot custom_topology;
custom_topology.allocate(4);
execute<init>(ifield(custom_topology));
execute<print>(ifield(custom_topology));
} // advance()
Example 3: Dense data
A dense field is a field defined on a dense topology index space. In
this example we allocate a pressure
field on the cells
index space of the canonical
topology.
const field<double>::definition<canon, canon::cells> pressure;
One can access the field inside of the FleCSI task by passing
topology and field accessors with access permissions (wo/rw/ro).
The canonical
topology is a very simple specialization of the unstructured
core topology.
It illustrates the use of the mpi_coloring
type, which applies a specialization-defined rule for specifying a coloring.
Here, a file is the source of the mesh (for purposes of illustration).
The resulting coloring is used to initialize two meshes canonical
and cp
, and the copy
task operates on both of them at once using a low-level accessor.
The init
and print
tasks, by contrast, use a topology accessor as a parameter that provides access to the structure of the mesh via the entities
function.
void
init(canon::accessor<ro> t, field<double>::accessor<wo> p) {
std::size_t off{0};
for(const auto c : t.cells()) {
p[c] = (off++) * 2.0;
} // for
} // init
void
copy(field<double>::accessor<ro> src, field<double>::accessor<wo> dest) {
auto s = src.span();
std::copy(s.begin(), s.end(), dest.span().begin());
}
void
print(canon::accessor<ro> t, field<double>::accessor<ro> p) {
std::size_t off{0};
for(auto c : t.cells()) {
flog(info) << "cell " << off++ << " has pressure " << p[c] << std::endl;
} // for
} // print
void
advance(control_policy &) {
canon::slot canonical, cp;
canon::mpi_coloring c("test.txt");
canonical.allocate(c);
cp.allocate(c);
auto pf = pressure(canonical), pf2 = pressure(cp);
execute<init>(canonical, pf);
execute<copy>(pf, pf2);
execute<print>(cp, pf2);
} // advance()
Example 4: Ragged data
A ragged field stores a variable amount of data at each index point. It is defined in much the same way as a dense field, but using it involves additional steps.
using ints = field<int, data::ragged>;
const ints::definition<canon, canon::cells> rag;
Because the total storage required for a ragged field is not determined by the size of the index space, an amount of memory to use for elements must be specified.
The field reference for a ragged field has a special interface for managing it.
Here we use a simple helper task (called allocate
) with that interface to specify a fixed, known total, although typically a heuristic overallocation is required:
// This special field type is predefined for this purpose.
void
allocate(topo::resize::Field::accessor<wo> a) {
a = 6;
}
After executing a task to store the sizes, they must be applied with resize
:
const auto f = rag(mesh);
auto & elem = f.get_elements();
execute<allocate>(elem.sizes());
elem.resize();
Initializing the field, or changing the number of values stored at any point later, requires a mutator.
The interface is closely modeled on std::vector
:
// Use wo to initialize any field.
void
init(ints::mutator<wo> m) {
int i = 0;
for(auto r : m) {
r.resize(i, i);
++i;
}
}
Using the field is much like a dense field, with a span
at each index point:
// Accessors can modify but not create or destroy values.
int
total(ints::accessor<ro> a) {
int ret = 0;
for(auto r : a)
for(auto i : r)
ret += i;
return ret;
}
Tasks using either of these are launched with ordinary field references:
execute<init>(f);
if(reduce<total, exec::fold::sum>(f).get() != 14)
throw control_policy::exception{1};