# GT4Py Execution Model discussions (leading to new functional model) ###### tags: `archive` `discussion` ## Discussion session 1 (10.03.2021) ### Core definitions - Grid: set of connected points. - Field: mapping from grid points to quantities. Inside a stencil can also be viewed as a function that can be called with an offset and returns a value(s). - Stencil: computation using neighborhood shapes to compute output value(s) from a set of input fields. + Each input field may be read with a different neighborhood shape. - Computation area: set of points (or subset of a grid) where a stencil is applied. - Fencil: sequence of _(stencil, computation area)_ pairs, usually computed together for optimization. + In the current DSL, the computation area for each stencil is indirectly defined as a function of a rectangle _domain_ and the stencil itself: * ComputationArea = Function(Stencil, Domain) + Instead, we could define the ComputationArea explictly as the subset of grid points where this stencil will be applied. ``` - Grid: Set[Items] e.g. grid = {p0, p1, p2, ... p_n, e0, ... e_n ...} - ComputationArea: Set[Items] # subset of Grid - Field: Mapping[[Grid], Quantity] - Accessor: Callable[[Optional[Offset], Quantity] # Field bound to an application point inside a stencil - Stencil: Callable[[Accessor, ...]], Quantity] e.g. a(x, y, z) => x[1] + y[0] + 2* z[3] - Fencil: List[Tuple[Stencil, ComputationArea]] e.g. [(a(x), comp_area_1), (b(lift(a)(x)), comp_area_2)] ``` ### Model Assumptions in the internal representation of a stencil to guarantee a well-defined interpretation: - Fields used in a stencil are either input or output (not both). - Symbols are only assigned once (SSA). - Assignment to temporaries can be considered as _lifting_ a stencil (RHS expression) to a field. - The computation area of a stencil is computed explicitly as a function of the domain rectangle passed at run-time. A fencil is implemented as the sequential application of its stencils in the corresponding computation areas. A _call_ from a fencil (_caller_) to another fencil (_callee_) represents the concatenation of the list of stencil-computation pairs of the _callee_ in the call site of the _caller_ fencil. ## Possible issues - Definition of computation areas for each stencil might be more complicated than simply slicing a rectangular domain: + how to define a disjoint computation area for a stencil? + how to define non-rectangular computation areas? + how to use named computation areas defined outside of the stencil? - Grid concept should be explicitly specified as an argument in the stencil definition. - _Halo/ghost cells_ and _domain_ specification should be explicitly defined for any object implementing the _Field_ concept (e.g. gt4py storages) - If a single domain (rectangle?) is passed to the fencil at runtime, what happens when output fields have different domain sizes (e.g. staggered fields)? - What should be the scope of symbol names in the DSL: global (currently), per computation or per region? - If assignment to temporaries is considered as just a "lifting", doesn't it complicate the optimization of assigning to a temporary field and reusing the result from several places? In other words, isn't it easier to demote temporaries to scalars than promote computations to temporaries? - A grid is not just a set of points but several (related) sets of points: vertices, faces, edges (in cartesian grids, even two sets of edges: I-edges and J-edges). In principle, stencils applied on a computation area defined on one of the sets, should only write to output fields defined on that grid item. - How this model can be extended to solvers? - How does the execution model relate to the current GridTools C++ execution model? ## Discussion session 2 Preliminar: So far it is been discussed how to represent the internal model in a way that makes sense for a non-ambiguos implementation. The internal model is lower-level than the current DSL (SSA, lifting, etc.), under the assumption of that the current DSL can be regarded as syntatic sugar on top of the model. What I think is still missing in the discussion is the full description of the transforamation to go from the DSL to our model (the _de-sugarization_ of the DSL). It could also be important to re-examine if the current DSL syntax represents sufficiently well the NWP domain concepts. ```mermaid flowchart LR MWP[NWP equations] --> DSL DSL --> Model[Internal model] Model --> Implemenation ``` ------------------------------ ## Scratchpad ```python= @gt.stencil def stencil_1(in_field_1: InputField, out_field_2: OutputField): with computation(), interval(...): with horizontal_region([3:3, :]): out_field_1 = 0.5 * (in_field_1[I+1] + in_field_1[I-1]) def foo(in_field_1: InputField, in_field_2: InputField): tmp <<= in_field_1[J+1] out_field_1 = in_field_1[I+1] + in_field_1[I-1] + in_field_2[I+2] + in_field_2[I-2], out_field_2 = out_field_1[I+1] + out_field_1[I-1] + in_field_2[I+2] + in_field_2[I-2] return out_field_1, out_field_2 def bar(x): return x[I + 0] + 1 def baz(x: acc) -> value: tmp : acc = bar(x) # bar' = lift(bar) return x[I + 0] + tmp[I - 1] @true_stencil def bazz(x: acc) -> value: tmp = bar(x) # return x[I + 0] + tmp # x[I + 0] + bar(x) vs. x[I + 0] + lift(bar)(x)[I + 0] def lap(x): return ... def foo(x): tmp <= lap(x) return lap(tmp) @gt.stencil def stencil_2(domain, in_field_1: InputField, in_field_2: InputField, out_field_1: OutputField, out_field_2: OutputField): with computation(), interval(...): with horizontal_region(domain[:, :]) as ff: with computation(), interval(...): with horizontal_region([:, :]) as ff: a, b = foo(in_field_1, in_field_2) tmp = baz(in) with horizontal_region([2:-2, :]): out = bar(in) + baz(in) out2 = bar(tmp) #stencil_1(domain[2:-2], in_field_1, out_field_1) stencil(if1, if2, of1, of2, domain=[100, 100]) # origin=(2,0)) #dict(in_field_1=(1,0), in_field_2=(2,0)) def s(in): return in[1, 1, 1] + in[0, 0, 0] stencil is a function from accessor(s) to value(s) fencil: [ {a(x), comp_area}, {b(lift(a)(x) ), comp_area_2 }] y = a(x) z = b(y) return y, z @fencil def aa(x) : return [(a(x), a0), (b(x), a1)] bb = [(c(x), a0}, (d(x), a1)] aa(bb(x)) => [(a(c(x)), a0}, (b(d(x), a1)] ``` ```python @stencil def laplacian(psi: Accessor) -> double: return -4 * delta_psi + delta_psi[I+1] + delta_psi[I-1] + delta_psi[J+1] + delta_psi[J-1] @fencil def laplace_operator(psi: InField) -> OutField: with computation(PARALLEL), interval(...): with horizontal_region([1:-1, 1:-1]) lap_psi = laplacian(psi) return lap_psi @fencil def average_laplacian_operator(psi: InField) -> OutField: """Compute the average of the laplacian of the I+-1, J+-1 neighbors""" with computation(PARALLEL), interval(...): with horizontal_region([1:-1, 1:-1]) # reuse of stencils avg_lap_psi = laplacian(psi[I+1]) + laplacian(psi[I-1]) + laplacian(psi[J+1]) + laplacian(psi[J-1]) return delta_psi @fencil def laplap(psi: InField) -> OutField: # composition of fencils return laplacian(laplacian(psi)) --- @stencil def nabla(psi: Accessor) -> Tuple[double, double]: return [ psi[I+1]-psi[I-1], psi[J+1]-psi[J-1] ] @stencil def divergence(vec: Tuple[Accessor, Accessor]): # arg return vec[0][I+1]-vec[0][I-1] + vec[0][J+1]-vec[0][J-1] @fencil def laplacian_2(psi: InField): with computation(PARALLEL), interval(...): with horizontal_region([1:-1, 1:-1]): nab = nabla(psi) lap = divergence(nab) return lap ``` ## List of samples - gtbench without periodic boundary conditions - ??