# Reductions for the Functional Model ###### tags: `IR` `Felix’s remains` ## Antons Original Implementation Signatures of `shift` and `reduce`: ```haskell= shift :: (Connectivity, Offset) → Iterator → Iterator shift :: Connectivity → Iterator → NeighborIterator shift :: Offset → NeighborIterator → NeighborIterator reduce :: (F, Init) → (NeighborIterators, …) → Value ``` ### Problem Analysis According to the definition of lift, the following two equivalences should hold: ```cpp= shift(v2v, 0)(lift(f)(it)) == lift(f)(shift(v2v, 0)(it)) ``` This is a crucial requirement for inline/on-the-fly evaluation of `lift`: ```cpp= deref(shift(v2v, 0)(lift(f)(it))) == f(shift(v2v, 0)(it)); ``` Now to reductions. For simplicity, let’s assume we have a connectivity _v2e_ with _only a single neighbor_, that is, `max_neighbors(v2e) == 1`. According to the current implementation, the evaluation of a reduction is then as follows: ```cpp= g = reduce(f, 0); // Evaluation (with only one neighbor) g(shift(v2e)(it)); shifted = shift(0)(shift(v2e)(it)); res = can_deref(shifted) ? f(res, deref(shifted)) : res; ``` Note that `shift(v2e)(it)` returns a 1D iterator that is only shiftable again with a single integer offset. In this case, this is fine as obviously the next shift is `shift(0)`. Now let’s lift `g`! We immediately introduce a problem; there’s a full shift happening after a partial shift: ```cpp= deref(shift(v2v, 0)(lift(g)(shift(v2e)(it)))) == g(shift(v2v, 0)(shift(v2e)(it))); ``` If we look at the evaluation of the reduction, the situation does not improve. Now we have a partial shift (with connectivity), followed by a full shift, followed by a partial shift (with offset). ```cpp= // Evaluation would be shifted = shift(0)(shift(v2v, 0)(shift(v2e)(it))); res = can_deref(shifted) ? f(res, deref(shifted)) : res; ``` When looking at nested reductions, the situation gets even worse. Now, there are additionally multiple partial shifts applied one after another: ```cpp= h = reduce(f1, 0) // Does not work: normal shift after partial shift! deref(shift(v2v, 0)(lift(g)(shift(v2v)(lift(h)(shift(v2e)(it)))))) == g(shift(v2v, 0)(shift(v2v)(lift(h)(shift(v2e)(it))))) == g(shift(v2v, 0)(lift(h)(shift(v2v)(shift(v2e)(it))))) == g(lift(h)(shift(v2v, 0)(shift(v2v)(shift(v2e)(it))))); // Evaluation would be shifted_g = shift(0)(lift(h)(shift(v2v, 0)(shift(v2v)(shift(v2e)(it))))); // Which is shifted_g = lift(h)(shift(0)(shift(v2v, 0)(shift(v2v)(shift(v2e)(it))))); // And evaluating h shifted_h = shift(0)(shift(0)(shift(v2v, 0)(shift(v2v)(shift(v2e)(it))))); res_h = can_deref(shifted_h) ? f(res_h, deref(shifted_h)) : res_h; // Now (ignoring can_deref) res_g = f(res_g, res_h); ``` ### Just for Reference: `reduce` & `shift` Pseudocode ```cpp= auto reduce(auto f, auto init) { return [=](auto it, auto... its) { auto res = init; for_each_neighbor([&](auto i) { if (can_deref(shift(i)(it))) res = f(res, deref(shift(i)(it)), deref(shift(i)(its))...); }); return res; }; } auto shift(auto connectivity, auto offset) { return [=](auto iter) { if constexpr (is_lifted_stencil_iter(iter)) { return lifted_stencil_iter( iter.func, transform(shift(connectivity, offset), iter.args) ); } else { ... } }; } ``` ## Alternative 1: Trivializing `reduce` If `reduce` is not a stencil, the requirement of a liftable reduce can be dropped. Instead, some new tuple-like type concept—here called `LazyOptionalTuple`—must be introduced to keep efficient lazy shifting and dereferencing capabilities. The newly introduced concept has the following API requirements: ```haskell= neighbors :: It → LazyOptionalTuple reduce :: (F, Init) → LazyOptionalTuples… → Value has_value :: (LazyOptionalTuple, Int) → Bool get_value :: (LazyOptionalTuple, Int) → Value ``` A possible implementation of reduce on the backend side (note that the shifting and dereferencing happens lazily in `get_value`): ```cpp= g = reduce(f, 0); g(neighbors(it1), neighbors(it2), deref(sparse)); auto reduce(auto f, auto init) { return [=](auto t, auto ts...) { auto res = init; for_each_index(t)([&](auto i) { if (has_value(t, i)) res = f(res, get_value(t, i), get_value(ts, i)...); }); return res; }; } ``` ## Alternative 2: Fixing the 1D Iterators Let’s call the fixed iterator type _partial iterator_. Most importantly, we need to be able to recover a normal iterator from a partial iterator if we apply a offset-only shift to a partial iterator. That is, we need the following functionality: ```haskell= shift :: (Connectivity, Offset) → Iterator → Iterator shift :: Connectivity → Iterator → PartialIterator shift :: Offset → PartialIterator → Iterator reduce :: (F, Init) → (PartialIterators, …) → Value ``` Second, to allow for lifted reductions, we need to be able to apply normal shifts interleaved with partial shifts. That is, the expression `shift(0)(shift(v2v, 0)(shift(v2e)(it)))` from the examples above has to be valid. Third, multiple partial shifts must be applicable to enable nested (lifted) evaluation of reductions. For example, the expression `shift(0)(shift(0)(shift(v2v, 0)(shift(v2v)(shift(v2e)(it)))))` from the examples above must be valid. All these requirements can be met in a straightforward way by parametrizing iterators by a stack of partially shifted connectivities. That is, we have the following API: ```haskell= shift :: (Conn, Offset) → Iterator[Conns…] → Iterator[Conns…] shift :: Conn → Iterator[Conns…] → Iterator[Conn, Conns…] shift :: Offset → Iterator[Conn, Conns…] → Iterator[Conns…] reduce :: (F, Init) → (Iterator[Conn1, Conns1…], Iterator[Conn2, Conns2…], …) → Value ``` Note that `can_deref` returns false for any partial iterator, that is for any iterator with at least one partial shift applied. Example type evaluations for the previous expressions: `shift(0)(shift(v2v, 0)(shift(v2e)(it)))`: ```cpp= it; // Iterator[]: normal iterator shift(v2e)(it); // Iterator[v2e], one partial shift applied shift(v2v, 0)(shift(v2e)(it)); // Iterator[v2e], full shift does not change the type shift(0)(shift(v2v, 0)(shift(v2e)(it))); // normal Iterator[] again ``` `shift(0)(shift(0)(shift(v2v, 0)(shift(v2v)(shift(v2e)(it)))))`: ```cpp= it; // Iterator[]: normal iterator shift(v2e)(it); // Iterator[v2e], one partial shift applied shift(v2v)(shift(v2e)(it)); // Iterator[v2v, v2e], second partial shift shift(v2v, 0)(shift(v2v)(shift(v2e)(it))); // Iterator[v2v, v2e] shift(0)(shift(v2v, 0)(shift(v2v)(shift(v2e)(it)))); // Iterator[v2e] shift(0)(shift(0)(shift(v2v, 0)(shift(v2v)(shift(v2e)(it))))); // Iterator[] ``` Note on sparse fields: the previous strategy of making all tuple-like types shiftable does not work anymore, as shifting along any other but the partial dimension is not possible. Instead, we require a function to obtain a partial iterator from an iterator with tuple value type. The function, here called `sparse`, has the following signature: ```haskell= sparse :: Iterator[] → Iterator[TupleDim] ``` An experimental (but tested!) implementation on top of Anton’s C++20 CPU-only code is available at https://github.com/GridTools/gridtools/pull/1703.