---
# System prepended metadata

title: Iterator IR without partial offsets
tags: [functional cycle 13]

---

# Iterator IR without partial offsets

###### tags: `functional cycle 13`

- Developers: Hannes (after ICON dycore GT4Py transition), Enrique (after merge), Rico (after CI/CD-ext)
- Appetite: full cycle

## Motivation

The partial offsets in Iterator IR complicate the IR in several ways:
1. **No simple lowering of nested reductions from field view.** This is related to the problem that full shifts in field view go in the opposite direction than in Iterator IR, the `lift` operation makes this simple. However the Tag-Offset-Pair (full offset) needs to be kept in that order. With a whole program lowering this could be fixed, however operator-by-operator lowering is not possible (or very complicated).
2. **Reductions cannot be applied to sparse-fields.** Reductions work by detecting the last partial shift, however for spare-fields, there is no shift within Iterator IR. They can be seen as being passed pre-shifted to the Iterator IR entrypoint. 
3. Related, it is **complicated to detect the last shift for reduction** (see current `UnrollReduce` pass).
4. **Indexing a field with neighbor dimensions cannot be indexed.** Currently, we would have to represent indexing as a `shift(index)` at Iterator IR level. In field view we don't know if a field has the neighbor dimension or if it was broadcasted (broadcast could have happened outside of the current operator). Therefore, we should not always generate `shift(index)`. In operator-by-operator lowering this cannot be resolved without changing Iterator IR.

## Goal

Make Iterator IR work with the above use-cases by exploring the following solution. If we find the solution not viable, we will fall back to alternative solutions, briefly outlined later.

The ideal outcome is a working implementation. If not feasible, detailed project for next cycle need to be shaped, where all exploration tasks have an answer.

## Solution

We propose to replace partial shifts by full shifts to a list of iterators.


We introduce 

```python
neighbor_shift(o: OffsetTag)(it: Iterator) -> Iterator[list]
```
or

```python
derefed_neighbor_shift(o: OffsetTag)(it: Iterator) -> list[Value]
```
(name to be improved),

with the semantics

```python!
derefed_neighbor_shift = lambda o:
    (lambda it: 
         make_list(
             deref(shift(o, 0)(it)),
             deref(shift(o, 1)(it)),
             ...,
             deref(shift(o, max_neighbors - 1)(it))
         )
    )
```

and similarly `neighbor_shift` with the inner lambda lifted return iterator of list.

The `reduce` builtin will be changed to work on list or iterator of list respectively.

Open questions:
- How much of the list type will be exposed: Can the user `make_list(values,...)`? Can the user get the size of the list, i.e. `list_size` builtin.
- Should we use the current Tuple instead of a separate list type with uniform elements?

### Are the problems solved?

1. Explore nesting of `neighbor_shift`s. Especially, answer the question of `neighbor_shift` (which is better for composability) and `derefed_neighbor_shift`.
2. Sparsefields would be represented with the list-type as values. Their length need to be accessible to the implementation of reduce (not necessarily the user).
3. The unroll factor would be accessible, see previous point.
4. `broadcast` in field view would create semantically a list of size of the dimension with all elements being the same (the broadcasted value).

**Other problems to be explored**

- Do we need changes to the backend?
- Can we represent the fvm nabla sign field?
- What do we do with 
    ```python
    @field_operator
    def number_of_neighbors():
        return neighbor_sum(broadcast(1, (C, C2V)), C2V)
    ```
  Here now iterator is available to know where we are in the mesh and we cannot count the number of neighbors. Solution might be similar to the solution for an `index()`-builtin: somehow we need to inject an iterator for location tracking.
- How do we improve offset representation? For normal shifts, we only want to accept full shifts, i.e. a (tag,offset)-pair, however for `neighbor_shift` we need just `tag`. Alternatively, we can represent the `offset` in `neighbor_shift`s as a list of (tag,offset)-pair. Consider shaping this properly for a later cycle.
- How do we deal with optional values (do we replace `can_deref` with `has_value`?)

## Implementation

- Test the solution in embedded execution (main focus)
- If, we we are confident the problems are solved (and time allows)
    - update transformations to work with the new representation
    - update type inference
    - implement lowering from fieldview
- Otherwise, explore the problem space with the alternative solutions.

## Rabbit holes

- It might be tempting to extend frontend/language capabilities, e.g. with the possibility to index sparse fields. This is not the goal of this project.


## Alternative Solutions

### `translate_shift`-builtin

See proof of concept in https://github.com/GridTools/gt4py/pull/965

### full dimension list for `reduce`

Instead of passing just iterators, provide the offset information explicitly in the `reduce`.

## Optional: Static offset provider information to IR

Represent the static offset-provider information in Iterator IR:
- NeighborTableOffset: has_skip_values, max_neighbors
- CartesianOffset


## Discussions during implementation

- how to order list and tuple for an external tuple sparse field?

### Static offset provider information

Add an extra node in FencilDefinition:

```python!
class FencilDefinition: 
    offset_definitions: list[OffsetDefinition]
    ...

class OffsetDefinition:
    ...

class ConnectivityDefinition(OffsetDefinition):
    has_skip_values: bool
    max_neighbors: int

class SingleOffsetDefinition(OffsetDefinition):
    ...
    
```


```python!
SList[has_skip_values, size, dtype]
```


```python!
# now
def stencil(inp):
    return deref(shift(koff, 1)(inp))

# new
def stencil(inp):
    return deref(shift(koff(1))(inp))

def fencil(inp: Iterator[], koff: Callable[[int], Callable[[KDim], KDim]]):
    stencil()
```
```python!
def stencil(inp):
    return reduce(...)(nbshift(e2v)(inp))

def fencil_unstructured(e2v: Callable[[E], SList[V,...]]):
    ...
```

```python!
nbshift ::  (a -> [b]) -> It a b d -> It a a [d]

shift :: (a -> b) -> It a c d -> It b c d

derefed_nbshift(c, it) = [deref(shift(nb(c, i))(it)) for i in range(size)]
```

### Which builtins do we implement

required:
(d)nbshift
map (list[values], ...)
reduce(op, init)(list[values], ...) (maybe variadic or via map)


optional: 
list_get(0, dnbshift(E2V)(it)) == deref(shift(E2V(0))(it))
make_list?

```python!
# def foo(it: It[Cell, Vertex]):
#     return deref(shift(E2V)(shift(C2E)(it)))

def bar_lifted(it: It[Cell, Edge]) -> It[Cell, Cell]:
    return lift(lambda x: deref(shift(C2E)(x)))(it)

def bar_it(it: It[Cell, Edge]) -> It[Edge, Edge]:
    return shift(C2E)(it)

def foo(it: It[Vertex, Edge])-> It[Cell, Cell]:
    shifted: It[Cell, Edge] = shift(V2C)(it)
    return barlifted(shifted)

def bar(it: It[Cell, Edge]) -> List:
    return neighbors(C2E)(it)

def nbar_lifted(it: It[Cell, Edge]) -> Iterator[Cell, Cell, List]:
    return lift(lambda x: neighbors(C2E)(x))(it)

def nbar_it(it: It[Cell, Edge]) -> List[List[Iterator[Edge, Edge, V]]]:
    return hannes_nshift(V2E)(hannes_nshift(C2V)(it)) # ???

def reduced(it):
    reduce(lambda acc, x: acc + reduce(lambda y, z: y+ deref(z), init0)(x), init1)(nbar_it(it))

def foo(it: It[Vertex, Edge]) -> It[Cell, Cell, List]:
    shifted = barlifted()
    return bar(shifted)

# def foo(it: It[Cell, Vertex]):
#     return neighbors(C2E)(lift(lambda x: neighbors(E2V)(x))(it))


```

### 2023-02-21

- we want to use `map` (-> map fusion pass)
- make_list(args...) takes values (scalars have to be explicitly promoted)

#### Strategy for using C++ backend

a)
Implement all new builtins (map, list_get, reduce, make_list, \_list_size) in C++ with a tuple-like that has size (in case of has_skip_values)

a*) encode mask in the values (nan-boxing, int)

b)
- merge all maps
- combine reduce_op and the map op -> reduce only has calls to neighbors or derefed sparse fields
- translate neighbors(i,it) -> shift(off, i)(it) and deref(it) -> shift(i)(it)
- old unroll_reduce works


tuple_get(1, neighbors(off, a))
reduce(...)(neighbors(Off, foo)) -> reduce(...)(shift(Off)(foo), sparse_field)


```python!
(lambda x: reduce(plus, 0)(map(mult, x,neighbors(off,b), deref(c))))(neighbors(off, a))
```

### 2023-03-01

#### Topics

- `make_const_list`, no `make_list` for now
- want to simplify unroll_reduce: need easy access to max_neighbors, has_skip_values **and** the argument which can be checked if `can_deref`
- TODOs:
    - add nested reduction test
    - map fuse: take care of symbol clashes (we should collect symbol utils)

Options:
a) replace `can_deref` by `has_value(i, list)`
 `can_deref(shift(off, i)(it))` -> `has_value(i, neighbors(off, it))`
b) replace `can_deref` by `length`
 `can_deref(shift(off, i)(it))` -> `if_(length(neighbors(off, it)) > i, true, false)`
 
reduce(add_1, 0)(neighbors(off, it))

Connectivity(length_kind=)


```
neighbors(V2E, it): List[Loc]
neighbors(V2C, it2): List[Loc]
shift(V2E, 1)(it: Iterator[V, E])-> Iterator[E, E]

V2E: V -> LocalField[V2EDim, E]
shift(V2E)(it: Iterator[V, E]) -> LocalField[V2EDim, Iterator[E, E]]
```

```
o1 = FieldOffset(source=a, target=(c, dim))
o2 = FieldOffset(source=b, target=(c, dim))
sum(f(o1)*g(o2), axis=dim)
```

```
it1: Iterator[V, E]
it2: Iterator[V, C]
neighbors(it1, V2E): List[V2E]
it2_mod = lift(lambda it: deref(shift(E2C_one_to_one, 0))(it2): Iterator[V, E]
neighbors(it2_mod, V2E): Value[V2E]
```

#### Conclusions

- We go with current unroll_reduce (ideally) as a temporary solution, then implement lists in C++ backend
- We remove `can_deref` when we have lists in C++ (length can be implemented in itir with `reduce(add_1, 0)(neihbors(off, it))`)
- Can we reduce sparse fields? Not now, but we could if we don't need unroll_reduce, i.e. once we have list support in C++.
- Type inference for sparse field works if we pass the constraint from outside.
- Imperative problem:
    - use `reduce` (from normal gtfn once we have it)
    - how does itir unroll reduce look like in generated code? why is it not good enough?
- TODOs:
    - add nested reduction test
    - map fuse: take care of symbol clashes (we should collect symbol utils)