From 87832eddc7ee0156ab97290b168e2499d7ab541b Mon Sep 17 00:00:00 2001 From: nfarabullini Date: Thu, 16 Nov 2023 13:29:38 +0100 Subject: [PATCH] presentation slides --- docs/user/next/presentation_slides.md | 411 ++++++++++++++++++++++++++ docs/user/next/scan_operator.png | Bin 0 -> 8760 bytes docs/user/next/simple_offset.png | Bin 0 -> 10292 bytes 3 files changed, 411 insertions(+) create mode 100644 docs/user/next/presentation_slides.md create mode 100644 docs/user/next/scan_operator.png create mode 100644 docs/user/next/simple_offset.png diff --git a/docs/user/next/presentation_slides.md b/docs/user/next/presentation_slides.md new file mode 100644 index 0000000000..87cd2b7787 --- /dev/null +++ b/docs/user/next/presentation_slides.md @@ -0,0 +1,411 @@ +--- +jupytext: + formats: ipynb,md:myst + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.15.2 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# GT4Py workshop + ++++ + +## GT4Py: GridTools for Python + +GT4Py is a Python library for generating high performance implementations of stencil kernels from a high-level definition using regular Python functions. + +GT4Py is part of the GridTools framework: a set of libraries and utilities to develop performance portable applications in the area of weather and climate modeling. + +**NOTE:** The `gt4py.next` subpackage contains a new and currently experimental version of GT4Py. + +## Description + +GT4Py is a Python library for expressing computational motifs as found in weather and climate applications. + +These computations are expressed in a domain specific language (GTScript) which is translated to high-performance implementations for CPUs and GPUs. + +The DSL expresses computations on a 3-dimensional Cartesian grid. The horizontal axes are always computed in parallel, while the vertical can be iterated in sequential, forward or backward, order. + +In addition, GT4Py provides functions to allocate arrays with memory layout suited for a particular backend. + +The following backends are supported: + +- `numpy`: Pure-Python backend +- `gt:cpu_ifirst`: GridTools C++ CPU backend using `I`-first data ordering +- `gt:cpu_kfirst`: GridTools C++ CPU backend using `K`-first data ordering +- `gt:gpu`: GridTools backend for CUDA +- `cuda`: CUDA backend minimally using utilities from GridTools +- `dace:cpu`: Dace code-generated CPU backend +- `dace:gpu`: Dace code-generated GPU backend + ++++ + +## Installation + +You can install the library directly from GitHub using pip: + +```{raw-cell} +pip install --upgrade git+https://github.com/gridtools/gt4py.git +``` + +```{code-cell} ipython3 +import warnings +warnings.filterwarnings('ignore') +``` + +```{code-cell} ipython3 +import numpy as np +import gt4py.next as gtx +from gt4py.next import float64, neighbor_sum, where +from gt4py.next.common import DimensionKind +``` + +## Key concepts and application structure + +- [Fields](#Fields), +- [Field operators](#Field-operators), and +- [Programs](#Programs). + ++++ + +### Fields +Fields are **multi-dimensional array** defined over a set of dimensions and a dtype: `gtx.Field[[dimensions], dtype]` + +The `as_field` builtin is used to define fields + +```{code-cell} ipython3 +CellDim = gtx.Dimension("Cell") +KDim = gtx.Dimension("K", kind=DimensionKind.VERTICAL) +grid_shape = (5, 6) +a = gtx.as_field([CellDim, KDim], np.full(shape=grid_shape, fill_value=2.0, dtype=np.float64)) +b = gtx.as_field([CellDim, KDim], np.full(shape=grid_shape, fill_value=3.0, dtype=np.float64)) + +print("a definition: \n {}".format(a)) +print("a array: \n {}".format(np.asarray(a))) +print("b array: \n {}".format(np.asarray(b))) +``` + +### Field operators + +Field operators perform operations on a set of fields, i.e. elementwise addition or reduction along a dimension. + +They are written as Python functions by using the `@field_operator` decorator. + +```{code-cell} ipython3 +@gtx.field_operator +def add(a: gtx.Field[[CellDim, KDim], float64], + b: gtx.Field[[CellDim, KDim], float64]) -> gtx.Field[[CellDim, KDim], float64]: + return a + b +``` + +Direct calls to field operators require two additional arguments: +- `out`: a field to write the return value to +- `offset_provider`: empty dict for now, explanation will follow + +```{code-cell} ipython3 +result = gtx.as_field([CellDim, KDim], np.zeros(shape=grid_shape)) +add(a, b, out=result, offset_provider={}) + +print("result array \n {}".format(np.asarray(result))) +``` + +### Programs + ++++ + +Programs are used to call field operators to mutate their arguments. + +They are written as Python functions by using the `@program` decorator. + +This example below calls the `add` field operator twice: + +```{code-cell} ipython3 +# @gtx.field_operator +# def add(a, b): +# return a + b + +@gtx.program +def run_add(a : gtx.Field[[CellDim, KDim], float64], + b : gtx.Field[[CellDim, KDim], float64], + result : gtx.Field[[CellDim, KDim], float64]): + add(a, b, out=result) # 2.0 + 3.0 = 5.0 + add(b, result, out=result) # 5.0 + 3.0 = 8.0 +``` + +```{code-cell} ipython3 +result = gtx.as_field([CellDim, KDim], np.zeros(shape=grid_shape)) +run_add(a, b, result, offset_provider={}) + +print("result array: \n {}".format(np.asarray(result))) +``` + +The fields in the subsequent code snippets are 1-dimensional, either over the cells or over the edges. The corresponding named dimensions are thus the following: + ++++ + +### Offsets +Fields can be offset by a predefined number of indices. + +Take an array with values ranging from 0 to 5: + +```{code-cell} ipython3 +a_off = gtx.as_field([CellDim], np.array([1.0, 1.0, 2.0, 3.0, 5.0, 8.0])) + +print("a_off array: \n {}".format(np.asarray(a_off))) +``` + +Visually, offsetting this field by 1 would result in the following: + +| ![Coff](simple_offset.png) | +| :------------------------: | +| _CellDim Offset (Coff)_ | + ++++ + +Fields can be offeset by a predefined number of indices. + +Take an array with values ranging from 0 to 5: + +```{code-cell} ipython3 +Coff = gtx.FieldOffset("Coff", source=CellDim, target=(CellDim,)) + +@gtx.field_operator +def a_offset(a_off: gtx.Field[[CellDim], float64]) -> gtx.Field[[CellDim], float64]: + return a_off(Coff[1]) + +a_offset(a_off, out=a_off, offset_provider={"Coff": CellDim}) +print("result array: \n {}".format(np.asarray(a_off))) +``` + +## Defining the mesh and its connectivities +Take an unstructured mesh with numbered cells (in red) and edges (in blue). + +| ![grid_topo](connectivity_numbered_grid.svg) | +| :------------------------------------------: | +| _The mesh with the indices_ | + +```{code-cell} ipython3 +CellDim = gtx.Dimension("Cell") +EdgeDim = gtx.Dimension("Edge") +``` + +Connectivityy among mesh elements is expressed through connectivity tables. + +For example, `e2c_table` lists for each edge its adjacent rows. + +Similarly, `c2e_table` lists the edges that are neighbors to a particular cell. + +Note that if an edge is lying at the border, one entry will be filled with -1. + +```{code-cell} ipython3 +e2c_table = np.array([ + [0, -1], # edge 0 (neighbours: cell 0) + [2, -1], # edge 1 + [2, -1], # edge 2 + [3, -1], # edge 3 + [4, -1], # edge 4 + [5, -1], # edge 5 + [0, 5], # edge 6 (neighbours: cell 0, cell 5) + [0, 1], # edge 7 + [1, 2], # edge 8 + [1, 3], # edge 9 + [3, 4], # edge 10 + [4, 5] # edge 11 +]) + +c2e_table = np.array([ + [0, 6, 7], # cell 0 (neighbors: edge 0, edge 6, edge 7) + [7, 8, 9], # cell 1 + [1, 2, 8], # cell 2 + [3, 9, 10], # cell 3 + [4, 10, 11], # cell 4 + [5, 6, 11], # cell 5 +]) +``` + +#### Using connectivities in field operators + +Let's start by defining two fields: one over the cells and another one over the edges. The field over cells serves input for subsequent calculations and is therefore filled up with values, whereas the field over the edges stores the output of the calculations and is therefore left blank. + +```{code-cell} ipython3 +cell_field = gtx.as_field([CellDim], np.array([1.0, 1.0, 2.0, 3.0, 5.0, 8.0])) +edge_field = gtx.as_field([EdgeDim], np.zeros((12,))) +``` + +| ![cell_values](connectivity_cell_field.svg) | +| :-----------------------------------------: | +| _Cell values_ | + ++++ + +`field_offset` is used as an argument to transform fields over one domain to another domain. + +For example, `E2C` can be used to shift a field over cells to edges with the following dimension transformation: + +[CellDim] -> CellDim(E2C) -> [EdgeDim, E2CDim] + +A field with an offset dimension is called a sparse field + +```{code-cell} ipython3 +E2CDim = gtx.Dimension("E2C", kind=gtx.DimensionKind.LOCAL) +E2C = gtx.FieldOffset("E2C", source=CellDim, target=(EdgeDim, E2CDim)) +``` + +```{code-cell} ipython3 +E2C_offset_provider = gtx.NeighborTableOffsetProvider(e2c_table, EdgeDim, CellDim, 2) +``` + +```{code-cell} ipython3 +@gtx.field_operator +def nearest_cell_to_edge(cell_field: gtx.Field[[CellDim], float64]) -> gtx.Field[[EdgeDim], float64]: + return cell_field(E2C[0]) # 0th index to isolate edge dimension + +@gtx.program +def run_nearest_cell_to_edge(cell_field: gtx.Field[[CellDim], float64], edge_field: gtx.Field[[EdgeDim], float64]): + nearest_cell_to_edge(cell_field, out=edge_field) + +run_nearest_cell_to_edge(cell_field, edge_field, offset_provider={"E2C": E2C_offset_provider}) + +print("0th adjacent cell's value: {}".format(np.asarray(edge_field))) +``` + +Running the above snippet results in the following edge field: + +| ![nearest_cell_values](connectivity_numbered_grid.svg) | $\mapsto$ | ![grid_topo](connectivity_edge_0th_cell.svg) | +| :----------------------------------------------------: | :-------: | :------------------------------------------: | +| _Domain (edges)_ | | _Edge values_ | + ++++ + +### Using reductions on connected mesh elements + +To sum up all the cells adjacent to an edge the `neighbor_sum` builtin function can be called to operate along the `E2CDim` dimension. + +```{code-cell} ipython3 +@gtx.field_operator +def sum_adjacent_cells(cell_field : gtx.Field[[CellDim], float64]) -> gtx.Field[[EdgeDim], float64]: + return neighbor_sum(cell_field(E2C), axis=E2CDim) + +@gtx.program +def run_sum_adjacent_cells(cell_field : gtx.Field[[CellDim], float64], edge_field: gtx.Field[[EdgeDim], float64]): + sum_adjacent_cells(cell_field, out=edge_field) + +run_sum_adjacent_cells(cell_field, edge_field, offset_provider={"E2C": E2C_offset_provider}) + +print("sum of adjacent cells: {}".format(np.asarray(edge_field))) +``` + +For the border edges, the results are unchanged compared to the previous example, but the inner edges now contain the sum of the two adjacent cells: + +| ![nearest_cell_values](connectivity_numbered_grid.svg) | $\mapsto$ | ![cell_values](connectivity_edge_cell_sum.svg) | +| :----------------------------------------------------: | :-------: | :--------------------------------------------: | +| _Domain (edges)_ | | _Edge values_ | + ++++ + +#### Using conditionals on fields + +To filter operations such that they are performed on only certain cells instead of the whole field, the `where` builtin was developed. + +This function takes 3 input arguments: +- mask: a field of booleans or an expression evaluating to this type +- true branch: a tuple, a field, or a scalar +- false branch: a tuple, a field, of a scalar + +```{code-cell} ipython3 +mask = gtx.as_field([CellDim], np.zeros(shape=grid_shape[0], dtype=bool)) +result = gtx.as_field([CellDim], np.zeros(shape=grid_shape[0])) +b = 6.0 + +@gtx.field_operator +def conditional(mask: gtx.Field[[CellDim], bool], cell_field: gtx.Field[[CellDim], float64], b: float +) -> gtx.Field[[CellDim], float64]: + return where(mask, cell_field, b) + +conditional(mask, cell_field, b, out=result, offset_provider={}) +print("where return: {}".format(np.asarray(result))) +``` + +#### Using domain on fields + +Another way to filter parts of a field where to perform operations, is to use the `domain` keyword argument when calling the field operator. + +Note: domain needs both dimensions to be included with integer tuple values. + +```{code-cell} ipython3 +# @gtx.field_operator +# def add(a, b): +# return a + b + +@gtx.program +def run_add_domain(a : gtx.Field[[CellDim, KDim], float64], + b : gtx.Field[[CellDim, KDim], float64], + result : gtx.Field[[CellDim, KDim], float64]): + add(a, b, out=result, domain={CellDim: (1, 3), KDim: (1, 4)}) +``` + +```{code-cell} ipython3 +a = gtx.as_field([CellDim, KDim], np.full(shape=grid_shape, fill_value=2.0, dtype=np.float64)) +b = gtx.as_field([CellDim, KDim], np.full(shape=grid_shape, fill_value=3.0, dtype=np.float64)) +result = gtx.as_field([CellDim, KDim], np.zeros(shape=grid_shape)) +run_add_domain(a, b, result, offset_provider={}) + +print("result array: \n {}".format(np.asarray(result))) +``` + +#### Scan operators + +Scan operators work in a similar fashion to iterations in Python. + +```{code-cell} ipython3 +x = np.asarray([1.0, 2.0, 4.0, 6.0, 0.0, 2.0, 5.0]) +def x_iteration(x): + for i, x_i in enumerate(x): + if i > 0: + x[i] = x[i-1] + x[i] + return x + +print("result array: \n {}".format(x_iteration(x))) +``` + +Visually, this is what `x_iteration` is doing: + +| ![scan_operator](scan_operator.png) | +| :---------------------------------: | +| _Iterative sum over K_ | + ++++ + +`scan_operators` allow for the same computations and only require a return statement for the operation, for loops and indexing are handled in the background. The return state of the previous iteration is provided as its first argument. + +This decorator takes 3 input arguments: +- `axis`: vertical axis over which operations have to be performed +- `forward`: True if order of operations is from bottom to top, False if from top to bottom +- `init`: initialized decorator value with type float or tuple thereof + +```{code-cell} ipython3 +@gtx.scan_operator(axis=KDim, forward=True, init=0.0) +def add_scan(state: float, k: float) -> float: + return state + k +``` + +```{code-cell} ipython3 +k_field = gtx.as_field([KDim], np.asarray([1.0, 2.0, 4.0, 6.0, 0.0, 2.0, 5.0])) +result = gtx.as_field([KDim], np.zeros(shape=(7,))) + +add_scan(k_field, out=result, offset_provider={}) # Note: `state` is not an input here + +print("result array: \n {}".format(np.asarray(result))) +``` + +Note: `scan_operators` can be called from `field_operators` and `programs`. Likewise, `field_operators` can be called from `scan_operators` + +```{code-cell} ipython3 + +``` diff --git a/docs/user/next/scan_operator.png b/docs/user/next/scan_operator.png new file mode 100644 index 0000000000000000000000000000000000000000..f0c1d03636b2758296da39a29251c2adc5b321d3 GIT binary patch literal 8760 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 Tz9PZDf+)DVdg2-|ds6=g`@|Ku literal 0 HcmV?d00001 diff --git a/docs/user/next/simple_offset.png b/docs/user/next/simple_offset.png new file mode 100644 index 0000000000000000000000000000000000000000..660abe87642151d390abb723a0c40e5ee9c22e00 GIT binary patch literal 10292 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 Ubnz(o8z98g$ilD)<9_1*0a&l~Y5)KL literal 0 HcmV?d00001