Repository: GridTools/gt4py
Pull request #1782 (2024-12-17 12:32:41)

feat[dace][next]: Fixing strides in optimization
Fixing some strides.
Mostly the work of Edoardo.




Patch:
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/__init__.py b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/__init__.py
index 4f3efb19..0902bd66 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/__init__.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/__init__.py
@@ -35,7 +35,13 @@ from .simplify import (
     gt_simplify,
     gt_substitute_compiletime_symbols,
 )
-from .strides import gt_change_transient_strides
+from .strides import (
+    gt_change_transient_strides,
+    gt_map_strides_to_dst_nested_sdfg,
+    gt_map_strides_to_src_nested_sdfg,
+    gt_propagate_strides_from_access_node,
+    gt_propagate_strides_of,
+)
 from .util import gt_find_constant_arguments, gt_make_transients_persistent
 
 
@@ -59,6 +65,10 @@ __all__ = [
     "gt_gpu_transformation",
     "gt_inline_nested_sdfg",
     "gt_make_transients_persistent",
+    "gt_map_strides_to_dst_nested_sdfg",
+    "gt_map_strides_to_src_nested_sdfg",
+    "gt_propagate_strides_from_access_node",
+    "gt_propagate_strides_of",
     "gt_reduce_distributed_buffering",
     "gt_set_gpu_blocksize",
     "gt_set_iteration_order",
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/gpu_utils.py b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/gpu_utils.py
index 2cd30201..7b14144e 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/gpu_utils.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/gpu_utils.py
@@ -95,7 +95,7 @@ def gt_gpu_transformation(
 
     if try_removing_trivial_maps:
         # In DaCe a Tasklet, outside of a Map, can not write into an _array_ that is on
-        #  GPU. `sdfg.appyl_gpu_transformations()` will wrap such Tasklets in a Map. So
+        #  GPU. `sdfg.apply_gpu_transformations()` will wrap such Tasklets in a Map. So
         #  we might end up with lots of these trivial Maps, each requiring a separate
         #  kernel launch. To prevent this we will combine these trivial maps, if
         #  possible, with their downstream maps.
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/simplify.py b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/simplify.py
index 6b7bd1b6..4339a761 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/simplify.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/simplify.py
@@ -950,7 +950,7 @@ class GT4PyMapBufferElimination(dace_transformation.SingleStateTransformation):
 
     def apply(
         self,
-        graph: dace.SDFGState | dace.SDFG,
+        graph: dace.SDFGState,
         sdfg: dace.SDFG,
     ) -> None:
         # Removal
@@ -971,6 +971,9 @@ class GT4PyMapBufferElimination(dace_transformation.SingleStateTransformation):
             tmp_out_subset = dace_subsets.Range.from_array(tmp_desc)
         assert glob_in_subset is not None
 
+        # Recursively visit the nested SDFGs for mapping of strides from inner to outer array
+        gtx_transformations.gt_map_strides_to_src_nested_sdfg(sdfg, graph, map_to_tmp_edge, glob_ac)
+
         # We now remove the `tmp` node, and create a new connection between
         #  the global node and the map exit.
         new_map_to_glob_edge = graph.add_edge(
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/strides.py b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/strides.py
index 4e254f28..980b2a8f 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/strides.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/strides.py
@@ -6,14 +6,30 @@
 # Please, refer to the LICENSE file in the root directory.
 # SPDX-License-Identifier: BSD-3-Clause
 
+from typing import Optional, TypeAlias
+
 import dace
 from dace import data as dace_data
+from dace.sdfg import nodes as dace_nodes
 
 from gt4py.next.program_processors.runners.dace_fieldview import (
     transformations as gtx_transformations,
 )
 
 
+PropagatedStrideRecord: TypeAlias = tuple[str, dace_nodes.NestedSDFG]
+"""Record of a stride that has been propagated into a NestedSDFG.
+
+The type combines the NestedSDFG into which the strides were already propagated
+and the data within that NestedSDFG to which we have propagated the strides,
+which is the connector name on the NestedSDFG.
+We need the NestedSDFG because we have to know what was already processed,
+however, we also need the inner array name because of aliasing, i.e. a data
+descriptor on the outside could be mapped to multiple data descriptors
+inside the NestedSDFG.
+"""
+
+
 def gt_change_transient_strides(
     sdfg: dace.SDFG,
     gpu: bool,
@@ -24,6 +40,11 @@ def gt_change_transient_strides(
     transients in the optimal way.
     The function should run after all maps have been created.
 
+    After the strides have been adjusted the function will also propagate
+    the strides into nested SDFG. This propagation will happen with
+    `ignore_symbol_mapping` set to `True`, see `gt_propagate_strides_of()`
+    for more.
+
     Args:
         sdfg: The SDFG to process.
         gpu: If the SDFG is supposed to run on the GPU.
@@ -35,8 +56,6 @@ def gt_change_transient_strides(
 
     Todo:
         - Implement the estimation correctly.
-        - Handle the case of nested SDFGs correctly; on the outside a transient,
-            but on the inside a non transient.
     """
     # TODO(phimeull): Implement this function correctly.
 
@@ -46,54 +65,608 @@ def gt_change_transient_strides(
         return sdfg
 
     for nsdfg in sdfg.all_sdfgs_recursive():
-        # TODO(phimuell): Handle the case when transient goes into nested SDFG
-        #   on the inside it is a non transient, so it is ignored.
         _gt_change_transient_strides_non_recursive_impl(nsdfg)
 
 
 def _gt_change_transient_strides_non_recursive_impl(
     sdfg: dace.SDFG,
 ) -> None:
-    """Essentially this function just changes the stride to FORTRAN order."""
-    for top_level_transient in _find_toplevel_transients(sdfg, only_arrays=True):
+    """Set optimal strides of all transients in the SDFG.
+
+    The function will look for all top level transients, see `_gt_find_toplevel_data_accesses()`
+    and set their strides such that the access is optimal, see Note. The function
+    will also run `gt_propagate_strides_of()` to propagate the strides into nested SDFGs.
+
+    This function should never be called directly but always through
+    `gt_change_transient_strides()`!
+
+    Note:
+        Currently the function just reverses the strides of the data descriptor
+        it processes. Since DaCe generates `C` order by default this lead to
+        FORTRAN order, which is (for now) sufficient to optimize the memory
+        layout to GPU.
+
+    Todo:
+        Make this function more intelligent to analyse the access pattern and then
+        figuring out the best order.
+    """
+    # NOTE: Processing the transient here is enough. If we are inside a
+    #   NestedSDFG then they were handled before on the level above us.
+    top_level_transients_and_their_accesses = _gt_find_toplevel_data_accesses(
+        sdfg=sdfg,
+        only_transients=True,
+        only_arrays=True,
+    )
+    for top_level_transient, accesses in top_level_transients_and_their_accesses.items():
         desc: dace_data.Array = sdfg.arrays[top_level_transient]
+
+        # Setting the strides only make sense if we have more than one dimensions
         ndim = len(desc.shape)
         if ndim <= 1:
             continue
+
         # We assume that everything is in C order initially, to get FORTRAN order
         #  we simply have to reverse the order.
+        # TODO(phimuell): Improve this.
         new_stride_order = list(range(ndim))
         desc.set_strides_from_layout(*new_stride_order)
 
+        # Now we have to propagate the changed strides. Because we already have
+        #  collected all the AccessNodes we are using the
+        #  `gt_propagate_strides_from_access_node()` function, but we have to
+        #  create `processed_nsdfg` set already outside here.
+        #  Furthermore, the same comment as above applies here, we do not have to
+        #  propagate the non-transients, because they either come from outside,
+        #  or they were already handled in the levels above, where they were
+        #  defined and then propagated down.
+        # TODO(phimuell): Updated the functions such that only one scan is needed.
+        processed_nsdfgs: set[dace_nodes.NestedSDFG] = set()
+        for state, access_node in accesses:
+            gt_propagate_strides_from_access_node(
+                sdfg=sdfg,
+                state=state,
+                outer_node=access_node,
+                processed_nsdfgs=processed_nsdfgs,
+                ignore_symbol_mapping=True,
+            )
+
+
+def gt_propagate_strides_of(
+    sdfg: dace.SDFG,
+    data_name: str,
+    ignore_symbol_mapping: bool = True,
+) -> None:
+    """Propagates the strides of `data_name` within the whole SDFG.
+
+    This function will call `gt_propagate_strides_from_access_node()` for every
+    AccessNode that refers to `data_name`. It will also make sure that a descriptor
+    inside a NestedSDFG is only processed once.
+
+    Args:
+        sdfg: The SDFG on which we operate.
+        data_name: Name of the data descriptor that should be handled.
+        ignore_symbol_mapping: If `False` (default is `True`) try to modify the `symbol_mapping`
+            of NestedSDFGs instead of manipulating the data descriptor.
+    """
+
+    # Defining it here ensures that we will not enter an NestedSDFG multiple times.
+    processed_nsdfgs: set[PropagatedStrideRecord] = set()
+
+    for state in sdfg.states():
+        for dnode in state.data_nodes():
+            if dnode.data != data_name:
+                continue
+            gt_propagate_strides_from_access_node(
+                sdfg=sdfg,
+                state=state,
+                outer_node=dnode,
+                processed_nsdfgs=processed_nsdfgs,
+                ignore_symbol_mapping=ignore_symbol_mapping,
+            )
+
+
+def gt_propagate_strides_from_access_node(
+    sdfg: dace.SDFG,
+    state: dace.SDFGState,
+    outer_node: dace_nodes.AccessNode,
+    ignore_symbol_mapping: bool = True,
+    processed_nsdfgs: Optional[set[PropagatedStrideRecord]] = None,
+) -> None:
+    """Propagates the stride of `outer_node` to any adjacent NestedSDFG.
+
+    The function will propagate the strides of the data descriptor `outer_node`
+    refers to along all adjacent edges of `outer_node`. If one of these edges
+    leads to a NestedSDFG then the function will modify the strides of data
+    descriptor within to match the strides on the outside. The function will then
+    recursively process NestedSDFG.
+
+    It is important that this function will only handle the NestedSDFGs that are
+    reachable from `outer_node`. To fully propagate the strides the
+    `gt_propagate_strides_of()` should be used.
+
+    Args:
+        sdfg: The SDFG to process.
+        state: The state where the data node is used.
+        edge: The edge that reads from the data node, the nested SDFG is expected as the destination.
+        outer_node: The data node whose strides should be propagated.
+        ignore_symbol_mapping: If `False` (default is `True`), try to modify the `symbol_mapping`
+            of NestedSDFGs instead of manipulating the data descriptor.
+        processed_nsdfgs: Set of NestedSDFG that were already processed and will be ignored.
+            Only specify when you know what your are doing.
+    """
+    if processed_nsdfgs is None:
+        # For preventing the case that nested SDFGs are handled multiple time.
+        processed_nsdfgs = set()
+
+    for in_edge in state.in_edges(outer_node):
+        gt_map_strides_to_src_nested_sdfg(
+            sdfg=sdfg,
+            state=state,
+            edge=in_edge,
+            outer_node=outer_node,
+            processed_nsdfgs=processed_nsdfgs,
+            ignore_symbol_mapping=ignore_symbol_mapping,
+        )
+    for out_edge in state.out_edges(outer_node):
+        gt_map_strides_to_dst_nested_sdfg(
+            sdfg=sdfg,
+            state=state,
+            edge=out_edge,
+            outer_node=outer_node,
+            processed_nsdfgs=processed_nsdfgs,
+            ignore_symbol_mapping=ignore_symbol_mapping,
+        )
+
+
+def gt_map_strides_to_dst_nested_sdfg(
+    sdfg: dace.SDFG,
+    state: dace.SDFGState,
+    edge: dace.sdfg.graph.Edge,
+    outer_node: dace.nodes.AccessNode,
+    ignore_symbol_mapping: bool = True,
+    processed_nsdfgs: Optional[set[PropagatedStrideRecord]] = None,
+) -> None:
+    """Propagates the strides of `outer_node` along `edge` in the dataflow direction.
+
+    In this context "along the dataflow direction" means that `edge` is an outgoing
+    edge of `outer_node` and the strides are propagated into all NestedSDFGs that
+    are downstream of `outer_node`.
+
+    Except in certain cases this function should not be used directly. It is
+    instead recommended to use `gt_propagate_strides_of()`, which propagates
+    all edges in the SDFG.
+
+    Args:
+        sdfg: The SDFG to process.
+        state: The state where the data node is used.
+        edge: The edge that writes to the data node, the nested SDFG is expected as the source.
+        outer_node: The data node whose strides should be propagated.
+        ignore_symbol_mapping: If `False`, the default, try to modify the `symbol_mapping`
+            of NestedSDFGs instead of manipulating the data descriptor.
+        processed_nsdfgs: Set of NestedSDFGs that were already processed. Only specify when
+            you know what your are doing.
+    """
+    assert edge.src is outer_node
+    _gt_map_strides_to_nested_sdfg_src_dst(
+        sdfg=sdfg,
+        state=state,
+        edge=edge,
+        outer_node=outer_node,
+        processed_nsdfgs=processed_nsdfgs,
+        propagate_along_dataflow=True,
+        ignore_symbol_mapping=ignore_symbol_mapping,
+    )
+
+
+def gt_map_strides_to_src_nested_sdfg(
+    sdfg: dace.SDFG,
+    state: dace.SDFGState,
+    edge: dace.sdfg.graph.Edge,
+    outer_node: dace.nodes.AccessNode,
+    ignore_symbol_mapping: bool = False,
+    processed_nsdfgs: Optional[set[PropagatedStrideRecord]] = None,
+) -> None:
+    """Propagates the strides of `outer_node` along `edge` in the opposite direction of the dataflow
+
+    In this context "in the opposite direction of the dataflow" means that `edge`
+    is an incoming edge of `outer_node` and the strides are propagated into all
+    NestedSDFGs that are upstream of `outer_node`.
+
+    Except in certain cases this function should not be used directly. It is
+    instead recommended to use `gt_propagate_strides_of()`, which propagates
+    all edges in the SDFG.
+
+    Args:
+        sdfg: The SDFG to process.
+        state: The state where the data node is used.
+        edge: The edge that writes to the data node, the nested SDFG is expected as the source.
+        outer_node: The data node whose strides should be propagated.
+        ignore_symbol_mapping: If `False`, the default, try to modify the `symbol_mapping`
+            of NestedSDFGs instead of manipulating the data descriptor.
+        processed_nsdfgs: Set of NestedSDFGs that were already processed. Only specify when
+            you know what your are doing.
+    """
+    _gt_map_strides_to_nested_sdfg_src_dst(
+        sdfg=sdfg,
+        state=state,
+        edge=edge,
+        outer_node=outer_node,
+        processed_nsdfgs=processed_nsdfgs,
+        propagate_along_dataflow=False,
+        ignore_symbol_mapping=ignore_symbol_mapping,
+    )
+
+
+def _gt_map_strides_to_nested_sdfg_src_dst(
+    sdfg: dace.SDFG,
+    state: dace.SDFGState,
+    edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet],
+    outer_node: dace.nodes.AccessNode,
+    processed_nsdfgs: Optional[set[PropagatedStrideRecord]],
+    propagate_along_dataflow: bool,
+    ignore_symbol_mapping: bool = False,
+) -> None:
+    """Propagates the stride of `outer_node` along `edge`.
+
+    The function will follow `edge`, the direction depends on the value of
+    `propagate_along_dataflow` and propagate the strides of `outer_node`
+    into every NestedSDFG that is reachable by following `edge`.
+
+    When the function encounters a NestedSDFG it will determine what data
+    the `outer_node` is mapped to on the inside of the NestedSDFG.
+    It will then replace the stride of the inner descriptor with the ones
+    of the outside. Afterwards it will recursively propagate the strides
+    inside the NestedSDFG.
+    During this propagation the function will follow any edges.
+
+    If the function reaches a NestedSDFG that is listed inside `processed_nsdfgs`
+    then it will be skipped. NestedSDFGs that have been processed will be added
+    to the `processed_nsdfgs`.
+
+    Args:
+        sdfg: The SDFG to process.
+        state: The state where the data node is used.
+        edge: The edge that reads from the data node, the nested SDFG is expected as the destination.
+        outer_node: The data node whose strides should be propagated.
+        processed_nsdfgs: Set of Nested SDFG that were already processed and will be ignored.
+            Only specify when you know what your are doing.
+        propagate_along_dataflow: Determine the direction of propagation. If `True` the
+            function follows the dataflow.
+        ignore_symbol_mapping: If `False`, the default, try to modify the `symbol_mapping`
+            of NestedSDFGs instead of manipulating the data descriptor.
+
+    Note:
+        A user should not use this function directly, instead `gt_propagate_strides_of()`,
+        `gt_map_strides_to_src_nested_sdfg()` (`propagate_along_dataflow == `False`)
+        or `gt_map_strides_to_dst_nested_sdfg()` (`propagate_along_dataflow == `True`)
+        should be used.
+
+    Todo:
+        Try using `MemletTree` for the propagation.
+    """
+    # If `processed_nsdfg` is `None` then this is the first call. We will now
+    #  allocate the `set` and pass it as argument to all recursive calls, this
+    #  ensures that the `set` is the same everywhere.
+    if processed_nsdfgs is None:
+        processed_nsdfgs = set()
+
+    if propagate_along_dataflow:
+        # Propagate along the dataflow or forward, so we are interested at the `dst` of the edge.
+        ScopeNode = dace_nodes.MapEntry
+
+        def get_node(edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]) -> dace_nodes.Node:
+            return edge.dst
+
+        def get_inner_data(edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]) -> str:
+            return edge.dst_conn
+
+        def get_subset(
+            state: dace.SDFGState,
+            edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet],
+        ) -> dace.subsets.Subset:
+            return edge.data.get_src_subset(edge, state)
 
-def _find_toplevel_transients(
+        def next_edges_by_connector(
+            state: dace.SDFGState,
+            edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet],
+        ) -> list[dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]]:
+            if edge.dst_conn is None or not edge.dst_conn.startswith("IN_"):
+                return []
+            return list(state.out_edges_by_connector(edge.dst, "OUT_" + edge.dst_conn[3:]))
+
+    else:
+        # Propagate against the dataflow or backward, so we are interested at the `src` of the edge.
+        ScopeNode = dace_nodes.MapExit
+
+        def get_node(edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]) -> dace_nodes.Node:
+            return edge.src
+
+        def get_inner_data(edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]) -> str:
+            return edge.src_conn
+
+        def get_subset(
+            state: dace.SDFGState,
+            edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet],
+        ) -> dace.subsets.Subset:
+            return edge.data.get_dst_subset(edge, state)
+
+        def next_edges_by_connector(
+            state: dace.SDFGState,
+            edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet],
+        ) -> list[dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]]:
+            return list(state.in_edges_by_connector(edge.src, "IN_" + edge.src_conn[4:]))
+
+    if isinstance(get_node(edge), ScopeNode):
+        for next_edge in next_edges_by_connector(state, edge):
+            _gt_map_strides_to_nested_sdfg_src_dst(
+                sdfg=sdfg,
+                state=state,
+                edge=next_edge,
+                outer_node=outer_node,
+                processed_nsdfgs=processed_nsdfgs,
+                propagate_along_dataflow=propagate_along_dataflow,
+                ignore_symbol_mapping=ignore_symbol_mapping,
+            )
+
+    elif isinstance(get_node(edge), dace.nodes.NestedSDFG):
+        nsdfg_node = get_node(edge)
+        inner_data = get_inner_data(edge)
+        process_record = (inner_data, nsdfg_node)
+
+        if process_record in processed_nsdfgs:
+            # We already handled this NestedSDFG and the inner data.
+            return
+
+        # Mark this nested SDFG as processed.
+        processed_nsdfgs.add(process_record)
+
+        # Now set the stride of the data descriptor inside the nested SDFG to
+        #  the ones it has outside.
+        _gt_map_strides_into_nested_sdfg(
+            sdfg=sdfg,
+            nsdfg_node=nsdfg_node,
+            inner_data=inner_data,
+            outer_subset=get_subset(state, edge),
+            outer_desc=outer_node.desc(sdfg),
+            ignore_symbol_mapping=ignore_symbol_mapping,
+        )
+
+        # Since the function call above is not recursive we have now to propagate
+        #  the change into the NestedSDFGs. Using `_gt_find_toplevel_data_accesses()`
+        #  is a bit overkill, but allows for a more uniform processing.
+        # TODO(phimuell): Instead of scanning every level for every data we modify
+        #   we should scan the whole SDFG once and then reuse this information.
+        accesses_in_nested_sdfg = _gt_find_toplevel_data_accesses(
+            sdfg=nsdfg_node.sdfg,
+            only_transients=False,  # Because on the nested levels they are globals.
+            only_arrays=True,
+        )
+        for nested_state, nested_access in accesses_in_nested_sdfg.get(inner_data, list()):
+            # We have to use `gt_propagate_strides_from_access_node()` here because we
+            #  have to handle its entirety. We could wait until the other branch processes
+            #  the nested SDFG, but this might not work, so let's do it fully now.
+            gt_propagate_strides_from_access_node(
+                sdfg=nsdfg_node.sdfg,
+                state=nested_state,
+                outer_node=nested_access,
+                processed_nsdfgs=processed_nsdfgs,
+                ignore_symbol_mapping=ignore_symbol_mapping,
+            )
+
+
+def _gt_map_strides_into_nested_sdfg(
     sdfg: dace.SDFG,
+    nsdfg_node: dace.nodes.NestedSDFG,
+    inner_data: str,
+    outer_subset: dace.subsets.Subset,
+    outer_desc: dace_data.Data,
+    ignore_symbol_mapping: bool,
+) -> None:
+    """Modify the strides of `inner_data` inside `nsdfg_node` to match `outer_desc`.
+
+    `inner_data` is the name of a data descriptor inside the NestedSDFG.
+    The function will then modify the strides of `inner_data`, assuming this
+    is an array, to match the ones of `outer_desc`.
+
+    Args:
+        sdfg: The SDFG containing the NestedSDFG.
+        nsdfg_node: The node in the parent SDFG that contains the NestedSDFG.
+        inner_data: The name of the data descriptor that should be processed
+            inside the NestedSDFG (by construction also a connector name).
+        outer_subset: The subset that describes what part of the outer data is
+            mapped into the NestedSDFG.
+        outer_desc: The data descriptor of the data on the outside.
+        ignore_symbol_mapping: If possible the function will perform the renaming
+            through the `symbol_mapping` of the nested SDFG. If `True` then
+            the function will always perform the renaming.
+            Note that setting this value to `False` might have negative side effects.
+
+    Todo:
+        - Handle explicit dimensions of size 1.
+        - What should we do if the stride symbol is used somewhere else, creating an
+            alias is probably not the right thing?
+        - Handle the case if the outer stride symbol is already used in another
+            context inside the Neste SDFG.
+    """
+    # We need to compute the new strides. In the following we assume that the
+    #  relative order of the dimensions does not change, but we support the case
+    #  where some dimensions of the outer data descriptor are not present on the
+    #  inside. For example this happens for the Memlet `a[__i0, 0:__a_size1]`. We
+    #  detect this case by checking if the Memlet subset in that dimension has size 1.
+    # TODO(phimuell): Handle the case were some additional size 1 dimensions are added.
+    inner_desc: dace_data.Data = nsdfg_node.sdfg.arrays[inner_data]
+    inner_shape = inner_desc.shape
+    inner_strides_init = inner_desc.strides
+
+    outer_strides = outer_desc.strides
+    outer_inflow = outer_subset.size()
+
+    new_strides: list = []
+    for dim_ostride, dim_oinflow in zip(outer_strides, outer_inflow, strict=True):
+        if dim_oinflow == 1:
+            # This is the case of implicit slicing along one dimension.
+            pass
+        else:
+            # There is inflow into the SDFG, so we need the stride.
+            new_strides.append(dim_ostride)
+        assert len(new_strides) <= len(inner_shape)
+
+    # If we have a scalar on the inside, then there is nothing to adjust.
+    #  We could have performed the test above, but doing it here, gives us
+    #  the chance of validating it.
+    if isinstance(inner_desc, dace_data.Scalar):
+        if len(new_strides) != 0:
+            raise ValueError(f"Dimensional error for '{inner_data}' in '{nsdfg_node.label}'.")
+        return
+
+    if not isinstance(inner_desc, dace_data.Array):
+        raise TypeError(
+            f"Expected that '{inner_data}' is an 'Array' but it is '{type(inner_desc).__name__}'."
+        )
+
+    if len(new_strides) != len(inner_shape):
+        raise ValueError("Failed to compute the inner strides.")
+
+    # Now we actually replace the strides, there are two ways of doing it.
+    #  The first is to create an alias in the `symbol_mapping`, however,
+    #  this is only possible if the current strides are singular symbols,
+    #  like `__a_strides_1`, but not expressions such as `horizontal_end - horizontal_start`
+    #  or literal values. Furthermore, this would change the meaning of the
+    #  old stride symbol in any context and not only in the one of the stride
+    #  of a single and isolated data descriptor.
+    #  The second way would be to replace `strides` attribute of the
+    #  inner data descriptor. In case the new stride consists of expressions
+    #  such as `value1 - value2` we have to make them available inside the
+    #  NestedSDFG. However, it could be that the strides is used somewhere else.
+    # We will do the following, if `ignore_symbol_mapping` is `False` and
+    #  the strides of the inner descriptors are symbols, we will use the
+    #  symbol mapping. Otherwise, we will replace the `strides` attribute
+    #  of the inner descriptor, in addition we will install a remapping,
+    #  for those values that were a symbol.
+    if (not ignore_symbol_mapping) and all(
+        isinstance(inner_stride, dace.symbol) for inner_stride in inner_strides_init
+    ):
+        # Use the symbol
+        for inner_stride, outer_stride in zip(inner_desc.strides, new_strides, strict=True):
+            nsdfg_node.symbol_mapping[inner_stride.name] = outer_stride
+    else:
+        # We have to replace the `strides` attribute of the inner descriptor.
+        inner_desc.set_shape(inner_desc.shape, new_strides)
+
+        # Now find the free symbols that the new strides need.
+        #  Note that usually `free_symbols` returns `set[str]`, but here, because
+        #  we fall back on SymPy, we get back symbols. We will keep them, because
+        #  then we can use them to extract the type form them, which we need later.
+        new_strides_symbols: list[dace.symbol] = []
+        for new_stride_dim in new_strides:
+            if dace.symbolic.issymbolic(new_stride_dim):
+                new_strides_symbols.extend(sym for sym in new_stride_dim.free_symbols)
+            else:
+                # It is not already a symbol, so we turn it into a symbol.
+                #  However, we only add it, if it is also a symbol, for example `1`.
+                #  should not be added.
+                new_stride_symbol = dace.symbolic.pystr_to_symbolic(new_stride_dim)
+                if new_stride_symbol.is_symbol:
+                    new_strides_symbols.append(new_stride_symbol)
+
+        # Now we determine the set of symbols that should be mapped inside the NestedSDFG.
+        #  We will exclude all that are already inside the `symbol_mapping` (we do not
+        #  check if they map to the same value, we just hope it). Furthermore,
+        #  we will exclude all symbols that are listed in the `symbols` property
+        #  of the SDFG that is nested, and hope that it has the same meaning.
+        # TODO(phimuell): Add better checks to avoid overwriting.
+        missing_symbol_mappings: set[dace.symbol] = {
+            sym
+            for sym in new_strides_symbols
+            if not (sym.name in nsdfg_node.sdfg.symbols or sym.name in nsdfg_node.symbol_mapping)
+        }
+
+        # Now propagate the symbols from the parent SDFG to the NestedSDFG.
+        for sym in missing_symbol_mappings:
+            assert sym.name in sdfg.symbols, f"Expected that '{sym}' is defined in the parent SDFG."
+            nsdfg_node.sdfg.add_symbol(sym.name, sdfg.symbols[sym.name])
+            nsdfg_node.symbol_mapping[sym.name] = sym
+
+
+def _gt_find_toplevel_data_accesses(
+    sdfg: dace.SDFG,
+    only_transients: bool,
     only_arrays: bool = False,
-) -> set[str]:
-    """Find all top level transients in the SDFG.
+) -> dict[str, list[tuple[dace.SDFGState, dace_nodes.AccessNode]]]:
+    """Find all data that is accessed on the top level.
 
     The function will scan the SDFG, ignoring nested one, and return the
-    name of all transients that have an access node at the top level.
-    However, it will ignore access nodes that refers to registers.
+    name of all data that only have AccessNodes on the top level. In data
+    is found that has an AccessNode on both the top level and in a nested
+    scope and error is generated.
+    By default the function will return transient and non transient data,
+    however, if `only_transients` is `True` then only transient data will
+    be returned.
+    Furthermore, the function will ignore an access in the following cases:
+    - The AccessNode refers to data that is a register.
+    - The AccessNode refers to a View.
+
+    Args:
+        sdfg: The SDFG to process.
+        only_transients: If `True` only include transients.
+        only_arrays: If `True`, defaults to `False`, only arrays are returned.
+
+    Returns:
+        A `dict` that maps the name of a data container, to a list of tuples
+        containing the state where the AccessNode was found and the AccessNode.
     """
-    top_level_transients: set[str] = set()
+    # List of data that is accessed on the top level and all its access node.
+    top_level_data: dict[str, list[tuple[dace.SDFGState, dace_nodes.AccessNode]]] = dict()
+
+    # List of all data that were found not on top level.
+    not_top_level_data: set[str] = set()
+
     for state in sdfg.states():
         scope_dict = state.scope_dict()
         for dnode in state.data_nodes():
             data: str = dnode.data
             if scope_dict[dnode] is not None:
-                if data in top_level_transients:
-                    top_level_transients.remove(data)
+                # The node was not found on the top level. So we can ignore it.
+                # We also check if it was ever found on the top level, this should
+                #  not happen, as everything should go through Maps. But some strange
+                #  DaCe transformation might do it.
+                assert (
+                    data not in top_level_data
+                ), f"Found {data} on the top level and inside a scope."
+                not_top_level_data.add(data)
                 continue
-            elif data in top_level_transients:
+
+            elif data in top_level_data:
+                # The data is already known to be in top level data, so we must add the
+                #  AccessNode to the list of known nodes. But nothing else.
+                top_level_data[data].append((state, dnode))
                 continue
+
             elif gtx_transformations.util.is_view(dnode, sdfg):
+                # The AccessNode refers to a View so we ignore it anyway.
                 continue
+
+            # We have found a new data node that is on the top node and is unknown.
+            assert (
+                data not in not_top_level_data
+            ), f"Found {data} on the top level and inside a scope."
             desc: dace_data.Data = dnode.desc(sdfg)
 
-            if not desc.transient:
+            # Check if we only accept arrays
+            if only_arrays and not isinstance(desc, dace_data.Array):
                 continue
-            elif only_arrays and not isinstance(desc, dace_data.Array):
+
+            # For now we ignore registers.
+            #  We do this because register are allocated on the stack, so the compiler
+            #  has all information and should organize the best thing possible.
+            # TODO(phimuell): verify this.
+            elif desc.storage is dace.StorageType.Register:
                 continue
-            top_level_transients.add(data)
-    return top_level_transients
+
+            # We are only interested in transients
+            if only_transients and (not desc.transient):
+                continue
+
+            # Now create the new entry in the list and record the AccessNode.
+            top_level_data[data] = [(state, dnode)]
+    return top_level_data


Test patch:
diff --git a/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_map_buffer_elimination.py b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_map_buffer_elimination.py
index 1a4ce6d0..a98eac3c 100644
--- a/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_map_buffer_elimination.py
+++ b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_map_buffer_elimination.py
@@ -22,10 +22,6 @@ from . import util
 import dace
 
 
-def _make_test_data(names: list[str]) -> dict[str, np.ndarray]:
-    return {name: np.array(np.random.rand(10), dtype=np.float64, copy=True) for name in names}
-
-
 def _make_test_sdfg(
     output_name: str = "G",
     input_name: str = "G",
@@ -262,3 +258,92 @@ def test_map_buffer_elimination_not_apply():
         validate_all=True,
     )
     assert count == 0
+
+
+def test_map_buffer_elimination_with_nested_sdfgs():
+    """
+    After removing a transient connected to a nested SDFG node, ensure that the strides
+    are propagated to the arrays in nested SDFG.
+    """
+
+    stride1, stride2, stride3 = [dace.symbol(f"stride{i}", dace.int32) for i in range(3)]
+
+    # top-level sdfg
+    sdfg = dace.SDFG(util.unique_name("map_buffer"))
+    inp, inp_desc = sdfg.add_array("__inp", (10,), dace.float64)
+    out, out_desc = sdfg.add_array(
+        "__out", (10, 10, 10), dace.float64, strides=(stride1, stride2, stride3)
+    )
+    tmp, _ = sdfg.add_temp_transient_like(out_desc)
+    state = sdfg.add_state()
+    tmp_node = state.add_access(tmp)
+
+    nsdfg1 = dace.SDFG(util.unique_name("map_buffer"))
+    inp1, inp1_desc = nsdfg1.add_array("__inp", (10,), dace.float64)
+    out1, out1_desc = nsdfg1.add_array("__out", (10, 10), dace.float64)
+    tmp1, _ = nsdfg1.add_temp_transient_like(out1_desc)
+    state1 = nsdfg1.add_state()
+    tmp1_node = state1.add_access(tmp1)
+
+    nsdfg2 = dace.SDFG(util.unique_name("map_buffer"))
+    inp2, _ = nsdfg2.add_array("__inp", (10,), dace.float64)
+    out2, out2_desc = nsdfg2.add_array("__out", (10,), dace.float64)
+    tmp2, _ = nsdfg2.add_temp_transient_like(out2_desc)
+    state2 = nsdfg2.add_state()
+    tmp2_node = state2.add_access(tmp2)
+
+    state2.add_mapped_tasklet(
+        "broadcast2",
+        map_ranges={"__i": "0:10"},
+        code="__oval = __ival + 1.0",
+        inputs={
+            "__ival": dace.Memlet(f"{inp2}[__i]"),
+        },
+        outputs={
+            "__oval": dace.Memlet(f"{tmp2}[__i]"),
+        },
+        output_nodes={tmp2_node},
+        external_edges=True,
+    )
+    state2.add_nedge(tmp2_node, state2.add_access(out2), dace.Memlet.from_array(out2, out2_desc))
+
+    nsdfg2_node = state1.add_nested_sdfg(nsdfg2, nsdfg1, inputs={"__inp"}, outputs={"__out"})
+    me1, mx1 = state1.add_map("broadcast1", ndrange={"__i": "0:10"})
+    state1.add_memlet_path(
+        state1.add_access(inp1),
+        me1,
+        nsdfg2_node,
+        dst_conn="__inp",
+        memlet=dace.Memlet.from_array(inp1, inp1_desc),
+    )
+    state1.add_memlet_path(
+        nsdfg2_node, mx1, tmp1_node, src_conn="__out", memlet=dace.Memlet(f"{tmp1}[__i, 0:10]")
+    )
+    state1.add_nedge(tmp1_node, state1.add_access(out1), dace.Memlet.from_array(out1, out1_desc))
+
+    nsdfg1_node = state.add_nested_sdfg(nsdfg1, sdfg, inputs={"__inp"}, outputs={"__out"})
+    me, mx = state.add_map("broadcast", ndrange={"__i": "0:10"})
+    state.add_memlet_path(
+        state.add_access(inp),
+        me,
+        nsdfg1_node,
+        dst_conn="__inp",
+        memlet=dace.Memlet.from_array(inp, inp_desc),
+    )
+    state.add_memlet_path(
+        nsdfg1_node, mx, tmp_node, src_conn="__out", memlet=dace.Memlet(f"{tmp}[__i, 0:10, 0:10]")
+    )
+    state.add_nedge(tmp_node, state.add_access(out), dace.Memlet.from_array(out, out_desc))
+
+    sdfg.validate()
+
+    count = sdfg.apply_transformations_repeated(
+        gtx_transformations.GT4PyMapBufferElimination(
+            assume_pointwise=False,
+        ),
+        validate=True,
+        validate_all=True,
+    )
+    assert count == 3
+    assert out1_desc.strides == out_desc.strides[1:]
+    assert out2_desc.strides == out_desc.strides[2:]
diff --git a/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_strides.py b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_strides.py
new file mode 100644
index 00000000..5b16e41b
--- /dev/null
+++ b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_strides.py
@@ -0,0 +1,541 @@
+# GT4Py - GridTools Framework
+#
+# Copyright (c) 2014-2024, ETH Zurich
+# All rights reserved.
+#
+# Please, refer to the LICENSE file in the root directory.
+# SPDX-License-Identifier: BSD-3-Clause
+
+import pytest
+import numpy as np
+import copy
+
+dace = pytest.importorskip("dace")
+from dace import symbolic as dace_symbolic
+from dace.sdfg import nodes as dace_nodes
+
+from gt4py.next.program_processors.runners.dace_fieldview import (
+    transformations as gtx_transformations,
+)
+
+from . import util
+
+import dace
+
+
+def _make_strides_propagation_level3_sdfg() -> dace.SDFG:
+    """Generates the level 3 SDFG (nested-nested) SDFG for `test_strides_propagation()`."""
+    sdfg = dace.SDFG(util.unique_name("level3"))
+    state = sdfg.add_state(is_start_block=True)
+    names = ["a3", "c3"]
+
+    for name in names:
+        stride_name = name + "_stride"
+        stride_sym = dace_symbolic.pystr_to_symbolic(stride_name)
+        sdfg.add_array(
+            name,
+            shape=(10,),
+            dtype=dace.float64,
+            transient=False,
+            strides=(stride_sym,),
+        )
+
+    state.add_mapped_tasklet(
+        "compL3",
+        map_ranges={"__i0": "0:10"},
+        inputs={"__in1": dace.Memlet("a3[__i0]")},
+        code="__out = __in1 + 10.",
+        outputs={"__out": dace.Memlet("c3[__i0]")},
+        external_edges=True,
+    )
+    sdfg.validate()
+    return sdfg
+
+
+def _make_strides_propagation_level2_sdfg() -> tuple[dace.SDFG, dace_nodes.NestedSDFG]:
+    """Generates the level 2 SDFG (nested) SDFG for `test_strides_propagation()`.
+
+    The function returns the level 2 SDFG and the NestedSDFG node that contains
+    the level 3 SDFG.
+    """
+    sdfg = dace.SDFG(util.unique_name("level2"))
+    state = sdfg.add_state(is_start_block=True)
+    names = ["a2", "a2_alias", "b2", "c2"]
+
+    for name in names:
+        stride_name = name + "_stride"
+        stride_sym = dace_symbolic.pystr_to_symbolic(stride_name)
+        sdfg.add_symbol(stride_name, dace.int64)
+        sdfg.add_array(
+            name,
+            shape=(10,),
+            dtype=dace.float64,
+            transient=False,
+            strides=(stride_sym,),
+        )
+
+    state.add_mapped_tasklet(
+        "compL2_1",
+        map_ranges={"__i0": "0:10"},
+        inputs={"__in1": dace.Memlet("a2[__i0]")},
+        code="__out = __in1 + 10",
+        outputs={"__out": dace.Memlet("b2[__i0]")},
+        external_edges=True,
+    )
+
+    state.add_mapped_tasklet(
+        "compL2_2",
+        map_ranges={"__i0": "0:10"},
+        inputs={"__in1": dace.Memlet("c2[__i0]")},
+        code="__out = __in1",
+        outputs={"__out": dace.Memlet("a2_alias[__i0]")},
+        external_edges=True,
+    )
+
+    # This is the nested SDFG we have here.
+    sdfg_level3 = _make_strides_propagation_level3_sdfg()
+
+    nsdfg = state.add_nested_sdfg(
+        sdfg=sdfg_level3,
+        parent=sdfg,
+        inputs={"a3"},
+        outputs={"c3"},
+        symbol_mapping={s3: s3 for s3 in sdfg_level3.free_symbols},
+    )
+
+    state.add_edge(state.add_access("a2"), None, nsdfg, "a3", dace.Memlet("a2[0:10]"))
+    state.add_edge(nsdfg, "c3", state.add_access("c2"), None, dace.Memlet("c2[0:10]"))
+    sdfg.validate()
+
+    return sdfg, nsdfg
+
+
+def _make_strides_propagation_level1_sdfg() -> (
+    tuple[dace.SDFG, dace_nodes.NestedSDFG, dace_nodes.NestedSDFG]
+):
+    """Generates the level 1 SDFG (top) SDFG for `test_strides_propagation()`.
+
+    Note that the SDFG is valid, but will be indeterminate. The only point of
+    this SDFG is to have a lot of different situations that have to be handled
+    for renaming.
+
+    Returns:
+        A tuple of length three, with the following members:
+        - The top level SDFG.
+        - The NestedSDFG node that contains the level 2 SDFG (member of the top level SDFG).
+        - The NestedSDFG node that contains the lebel 3 SDFG (member of the level 2 SDFG).
+    """
+
+    sdfg = dace.SDFG(util.unique_name("level1"))
+    state = sdfg.add_state(is_start_block=True)
+    names = ["a1", "b1", "c1"]
+
+    for name in names:
+        stride_name = name + "_stride"
+        stride_sym = dace_symbolic.pystr_to_symbolic(stride_name)
+        sdfg.add_symbol(stride_name, dace.int64)
+        sdfg.add_array(
+            name,
+            shape=(10,),
+            dtype=dace.float64,
+            transient=False,
+            strides=(stride_sym,),
+        )
+
+    sdfg_level2, nsdfg_level3 = _make_strides_propagation_level2_sdfg()
+
+    nsdfg_level2: dace_nodes.NestedSDFG = state.add_nested_sdfg(
+        sdfg=sdfg_level2,
+        parent=sdfg,
+        inputs={"a2", "c2"},
+        outputs={"a2_alias", "b2", "c2"},
+        symbol_mapping={s: s for s in sdfg_level2.free_symbols},
+    )
+
+    for inner_name in nsdfg_level2.in_connectors:
+        outer_name = inner_name[0] + "1"
+        state.add_edge(
+            state.add_access(outer_name),
+            None,
+            nsdfg_level2,
+            inner_name,
+            dace.Memlet(f"{outer_name}[0:10]"),
+        )
+    for inner_name in nsdfg_level2.out_connectors:
+        outer_name = inner_name[0] + "1"
+        state.add_edge(
+            nsdfg_level2,
+            inner_name,
+            state.add_access(outer_name),
+            None,
+            dace.Memlet(f"{outer_name}[0:10]"),
+        )
+
+    sdfg.validate()
+
+    return sdfg, nsdfg_level2, nsdfg_level3
+
+
+def test_strides_propagation_use_symbol_mapping():
+    # Note that the SDFG we are building here is not really meaningful.
+    sdfg_level1, nsdfg_level2, nsdfg_level3 = _make_strides_propagation_level1_sdfg()
+
+    # Tests if all strides are distinct in the beginning and match what we expect.
+    for sdfg in [sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg]:
+        for aname, adesc in sdfg.arrays.items():
+            exp_stride = f"{aname}_stride"
+            actual_stride = adesc.strides[0]
+            assert len(adesc.strides) == 1
+            assert (
+                str(actual_stride) == exp_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+            nsdfg = sdfg.parent_nsdfg_node
+            if nsdfg is not None:
+                assert exp_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[exp_stride]) == exp_stride
+
+    # Now we propagate `a` and `b`, but not `c`.
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "a1", ignore_symbol_mapping=False)
+    sdfg_level1.validate()
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "b1", ignore_symbol_mapping=False)
+    sdfg_level1.validate()
+
+    # Because `ignore_symbol_mapping=False` the strides of the data descriptor should
+    #  not have changed. But the `symbol_mapping` has been updated for `a` and `b`.
+    #  However, the symbols will only point one level above.
+    for level, sdfg in enumerate([sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg], start=1):
+        for aname, adesc in sdfg.arrays.items():
+            nsdfg = sdfg.parent_nsdfg_node
+            original_stride = f"{aname}_stride"
+
+            if aname.startswith("c"):
+                target_symbol = f"{aname}_stride"
+            else:
+                target_symbol = f"{aname[0]}{level - 1}_stride"
+
+            if nsdfg is not None:
+                assert original_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[original_stride]) == target_symbol
+            assert len(adesc.strides) == 1
+            assert (
+                str(adesc.strides[0]) == original_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+    # Now we also propagate `c` thus now all data descriptors have the same stride
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "c1", ignore_symbol_mapping=False)
+    sdfg_level1.validate()
+    for level, sdfg in enumerate([sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg], start=1):
+        for aname, adesc in sdfg.arrays.items():
+            nsdfg = sdfg.parent_nsdfg_node
+            original_stride = f"{aname}_stride"
+            target_symbol = f"{aname[0]}{level-1}_stride"
+            if nsdfg is not None:
+                assert original_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[original_stride]) == target_symbol
+            assert len(adesc.strides) == 1
+            assert (
+                str(adesc.strides[0]) == original_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+
+def test_strides_propagation_ignore_symbol_mapping():
+    # Note that the SDFG we are building here is not really meaningful.
+    sdfg_level1, nsdfg_level2, nsdfg_level3 = _make_strides_propagation_level1_sdfg()
+
+    # Tests if all strides are distinct in the beginning and match what we expect.
+    for sdfg in [sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg]:
+        for aname, adesc in sdfg.arrays.items():
+            exp_stride = f"{aname}_stride"
+            actual_stride = adesc.strides[0]
+            assert len(adesc.strides) == 1
+            assert (
+                str(actual_stride) == exp_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+            nsdfg = sdfg.parent_nsdfg_node
+            if nsdfg is not None:
+                assert exp_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[exp_stride]) == exp_stride
+
+    # Now we propagate `a` and `b`, but not `c`.
+    # TODO(phimuell): Create a version where we can set `ignore_symbol_mapping=False`.
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "a1", ignore_symbol_mapping=True)
+    sdfg_level1.validate()
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "b1", ignore_symbol_mapping=True)
+    sdfg_level1.validate()
+
+    # After the propagation `a` and `b` should use the same stride (the one that
+    #  it has on level 1, but `c` should still be level depending.
+    for sdfg in [sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg]:
+        for aname, adesc in sdfg.arrays.items():
+            original_stride = f"{aname}_stride"
+            if aname.startswith("c"):
+                exp_stride = f"{aname}_stride"
+            else:
+                exp_stride = f"{aname[0]}1_stride"
+            assert len(adesc.strides) == 1
+            assert (
+                str(adesc.strides[0]) == exp_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+            nsdfg = sdfg.parent_nsdfg_node
+            if nsdfg is not None:
+                assert original_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[original_stride]) == original_stride
+
+    # Now we also propagate `c` thus now all data descriptors have the same stride
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "c1", ignore_symbol_mapping=True)
+    sdfg_level1.validate()
+    for sdfg in [sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg]:
+        for aname, adesc in sdfg.arrays.items():
+            exp_stride = f"{aname[0]}1_stride"
+            original_stride = f"{aname}_stride"
+            assert len(adesc.strides) == 1
+            assert (
+                str(adesc.strides[0]) == exp_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+            nsdfg = sdfg.parent_nsdfg_node
+            if nsdfg is not None:
+                # The symbol mapping must should not be updated.
+                assert original_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[original_stride]) == original_stride
+
+
+def _make_strides_propagation_dependent_symbol_nsdfg() -> dace.SDFG:
+    sdfg = dace.SDFG(util.unique_name("strides_propagation_dependent_symbol_nsdfg"))
+    state = sdfg.add_state(is_start_block=True)
+
+    array_names = ["a2", "b2"]
+    for name in array_names:
+        stride_sym = dace.symbol(f"{name}_stride", dtype=dace.uint64)
+        sdfg.add_symbol(stride_sym.name, stride_sym.dtype)
+        sdfg.add_array(
+            name,
+            shape=(10,),
+            dtype=dace.float64,
+            strides=(stride_sym,),
+            transient=False,
+        )
+
+    state.add_mapped_tasklet(
+        "nested_comp",
+        map_ranges={"__i0": "0:10"},
+        inputs={"__in1": dace.Memlet("a2[__i0]")},
+        code="__out = __in1 + 10.",
+        outputs={"__out": dace.Memlet("b2[__i0]")},
+        external_edges=True,
+    )
+    sdfg.validate()
+    return sdfg
+
+
+def _make_strides_propagation_dependent_symbol_sdfg() -> tuple[dace.SDFG, dace_nodes.NestedSDFG]:
+    sdfg_level1 = dace.SDFG(util.unique_name("strides_propagation_dependent_symbol_sdfg"))
+    state = sdfg_level1.add_state(is_start_block=True)
+
+    array_names = ["a1", "b1"]
+    for name in array_names:
+        stride_sym1 = dace.symbol(f"{name}_1stride", dtype=dace.uint64)
+        stride_sym2 = dace.symbol(f"{name}_2stride", dtype=dace.int64)
+        sdfg_level1.add_symbol(stride_sym1.name, stride_sym1.dtype)
+        sdfg_level1.add_symbol(stride_sym2.name, stride_sym2.dtype)
+        stride_sym = stride_sym1 * stride_sym2
+        sdfg_level1.add_array(
+            name,
+            shape=(10,),
+            dtype=dace.float64,
+            strides=(stride_sym,),
+            transient=False,
+        )
+
+    sdfg_level2 = _make_strides_propagation_dependent_symbol_nsdfg()
+
+    for sym, sym_dtype in sdfg_level2.symbols.items():
+        sdfg_level1.add_symbol(sym, sym_dtype)
+
+    nsdfg = state.add_nested_sdfg(
+        sdfg=sdfg_level2,
+        parent=sdfg_level1,
+        inputs={"a2"},
+        outputs={"b2"},
+        symbol_mapping={s: s for s in sdfg_level2.symbols},
+    )
+
+    state.add_edge(state.add_access("a1"), None, nsdfg, "a2", dace.Memlet("a1[0:10]"))
+    state.add_edge(nsdfg, "b2", state.add_access("b1"), None, dace.Memlet("b1[0:10]"))
+    sdfg_level1.validate()
+
+    return sdfg_level1, nsdfg
+
+
+def test_strides_propagation_dependent_symbol():
+    sdfg_level1, nsdfg_level2 = _make_strides_propagation_dependent_symbol_sdfg()
+    sym1_dtype = dace.uint64
+    sym2_dtype = dace.int64
+
+    # Ensure that the special symbols are not already present inside the nested SDFG.
+    for aname, adesc in sdfg_level1.arrays.items():
+        sym1 = f"{aname}_1stride"
+        sym2 = f"{aname}_2stride"
+        for sym, dtype in [(sym1, sym1_dtype), (sym2, sym2_dtype)]:
+            assert sym in {fs.name for fs in adesc.strides[0].free_symbols}
+            assert sym not in nsdfg_level2.symbol_mapping
+            assert sym not in nsdfg_level2.sdfg.symbols
+            assert sym in sdfg_level1.symbols
+            assert sdfg_level1.symbols[sym] == dtype
+
+    # Now propagate `a1` and `b1`.
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "a1", ignore_symbol_mapping=True)
+    sdfg_level1.validate()
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "b1", ignore_symbol_mapping=True)
+    sdfg_level1.validate()
+
+    # Now we check if the update has worked.
+    for aname, adesc in sdfg_level1.arrays.items():
+        sym1 = f"{aname}_1stride"
+        sym2 = f"{aname}_2stride"
+        adesc2 = nsdfg_level2.sdfg.arrays[aname.replace("1", "2")]
+        assert adesc2.strides == adesc.strides
+
+        for sym, dtype in [(sym1, sym1_dtype), (sym2, sym2_dtype)]:
+            assert sym in nsdfg_level2.symbol_mapping
+            assert nsdfg_level2.symbol_mapping[sym].name == sym
+            assert sym in sdfg_level1.symbols
+            assert sdfg_level1.symbols[sym] == dtype
+            assert sym in nsdfg_level2.sdfg.symbols
+            assert nsdfg_level2.sdfg.symbols[sym] == dtype
+
+
+def _make_strides_propagation_shared_symbols_nsdfg() -> dace.SDFG:
+    sdfg = dace.SDFG(util.unique_name("strides_propagation_shared_symbols_nsdfg"))
+    state = sdfg.add_state(is_start_block=True)
+
+    # NOTE: Both arrays have the same symbols used for strides.
+    array_names = ["a2", "b2"]
+    stride_sym0 = dace.symbol(f"__stride_0", dtype=dace.uint64)
+    stride_sym1 = dace.symbol(f"__stride_1", dtype=dace.uint64)
+    sdfg.add_symbol(stride_sym0.name, stride_sym0.dtype)
+    sdfg.add_symbol(stride_sym1.name, stride_sym1.dtype)
+    for name in array_names:
+        sdfg.add_array(
+            name,
+            shape=(10, 10),
+            dtype=dace.float64,
+            strides=(stride_sym0, stride_sym1),
+            transient=False,
+        )
+
+    state.add_mapped_tasklet(
+        "nested_comp",
+        map_ranges={
+            "__i0": "0:10",
+            "__i1": "0:10",
+        },
+        inputs={"__in1": dace.Memlet("a2[__i0, __i1]")},
+        code="__out = __in1 + 10.",
+        outputs={"__out": dace.Memlet("b2[__i0, __i1]")},
+        external_edges=True,
+    )
+    sdfg.validate()
+    return sdfg
+
+
+def _make_strides_propagation_shared_symbols_sdfg() -> tuple[dace.SDFG, dace_nodes.NestedSDFG]:
+    sdfg_level1 = dace.SDFG(util.unique_name("strides_propagation_shared_symbols_sdfg"))
+    state = sdfg_level1.add_state(is_start_block=True)
+
+    # NOTE: Both arrays use the same symbols as strides.
+    #   Furthermore, they are the same as in the nested SDFG, i.e. they are shared.
+    array_names = ["a1", "b1"]
+    stride_sym0 = dace.symbol(f"__stride_0", dtype=dace.uint64)
+    stride_sym1 = dace.symbol(f"__stride_1", dtype=dace.uint64)
+    sdfg_level1.add_symbol(stride_sym0.name, stride_sym0.dtype)
+    sdfg_level1.add_symbol(stride_sym1.name, stride_sym1.dtype)
+    for name in array_names:
+        sdfg_level1.add_array(
+            name,
+            shape=(10, 10),
+            dtype=dace.float64,
+            strides=(
+                stride_sym0,
+                stride_sym1,
+            ),
+            transient=False,
+        )
+
+    sdfg_level2 = _make_strides_propagation_shared_symbols_nsdfg()
+    nsdfg = state.add_nested_sdfg(
+        sdfg=sdfg_level2,
+        parent=sdfg_level1,
+        inputs={"a2"},
+        outputs={"b2"},
+        symbol_mapping={s: s for s in sdfg_level2.symbols},
+    )
+
+    state.add_edge(state.add_access("a1"), None, nsdfg, "a2", dace.Memlet("a1[0:10, 0:10]"))
+    state.add_edge(nsdfg, "b2", state.add_access("b1"), None, dace.Memlet("b1[0:10, 0:10]"))
+    sdfg_level1.validate()
+
+    return sdfg_level1, nsdfg
+
+
+def test_strides_propagation_shared_symbols_sdfg():
+    """Tests what happens if symbols are (unintentionally) shred between descriptor.
+
+    This test looks rather artificial, but it is actually quite likely. Because
+    transients will most likely have the same shape and if the strides are not
+    set explicitly, which is the case, the strides will also be related to their
+    shape. This test explores the situation, where we can, for whatever reason,
+    only propagate the strides of one such data descriptor.
+
+    Note:
+        If `ignore_symbol_mapping` is `False` then this test will fail.
+        This is because the `symbol_mapping` of the NestedSDFG will act on the
+        whole SDFG. Thus it will not only change the strides of `b` but as an
+        unintended side effect also the strides of `a`.
+    """
+
+    def ref(a1, b1):
+        for i in range(10):
+            for j in range(10):
+                b1[i, j] = a1[i, j] + 10.0
+
+    sdfg_level1, nsdfg_level2 = _make_strides_propagation_shared_symbols_sdfg()
+
+    res_args = {
+        "a1": np.array(np.random.rand(10, 10), order="C", dtype=np.float64, copy=True),
+        "b1": np.array(np.random.rand(10, 10), order="F", dtype=np.float64, copy=True),
+    }
+    ref_args = copy.deepcopy(res_args)
+
+    # Now we change the strides of `b1`, and then we propagate the new strides
+    #  into the nested SDFG. We want to keep (for whatever reasons) strides of `a1`.
+    stride_b1_sym0 = dace.symbol(f"__b1_stride_0", dtype=dace.uint64)
+    stride_b1_sym1 = dace.symbol(f"__b1_stride_1", dtype=dace.uint64)
+    sdfg_level1.add_symbol(stride_b1_sym0.name, stride_b1_sym0.dtype)
+    sdfg_level1.add_symbol(stride_b1_sym1.name, stride_b1_sym1.dtype)
+
+    desc_b1 = sdfg_level1.arrays["b1"]
+    desc_b1.set_shape((10, 10), (stride_b1_sym0, stride_b1_sym1))
+
+    # Now we propagate the data into it.
+    gtx_transformations.gt_propagate_strides_of(
+        sdfg=sdfg_level1,
+        data_name="b1",
+    )
+
+    # Now we have to prepare the call arguments, i.e. adding the strides
+    itemsize = res_args["b1"].itemsize
+    res_args.update(
+        {
+            "__b1_stride_0": res_args["b1"].strides[0] // itemsize,
+            "__b1_stride_1": res_args["b1"].strides[1] // itemsize,
+            "__stride_0": res_args["a1"].strides[0] // itemsize,
+            "__stride_1": res_args["a1"].strides[1] // itemsize,
+        }
+    )
+    ref(**ref_args)
+    sdfg_level1(**res_args)
+    assert np.allclose(ref_args["b1"], res_args["b1"])