Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,57 @@ def can_be_applied(
):
return False

# NOTE: In case the node we keep is a non-transient we do not have to
# check if it is read or written to somewhere else in this state.
# The reason is that ADR18 guarantees that every access is point-wise,
# therefore the temporary we remove is never used as double buffer.
# If the container we keep is a global that is also read somewhere else in
# this state, then the temporary we remove *is* used as a double buffer:
# it holds the new value while the old one is still being read. Merging
# it away moves the write back to where the temporary was produced, which
# is no longer ordered after those reads, so the readers would observe the
# new value. This is the situation of a field that a `field_operator` call
# takes as an input and also produces as an output.
# NOTE: Transients are written only once (ADR-18), so they cannot be
# affected; only globals need this check.
if self._creates_write_after_read_hazard(sdfg, graph):
return False

return True

def _creates_write_after_read_hazard(
self,
sdfg: dace.SDFG,
graph: dace.SDFGState,
) -> bool:
"""Checks if removing the copy chain would let a write overtake a read.

The surviving container takes over the writes of the removed temporary,
which moves them to where that temporary was produced. If the surviving
container is a global that another AccessNode of this state reads, that
read is no longer guaranteed to happen before the write.

Args:
sdfg: The SDFG on which the transformation is applied.
graph: The state on which the transformation is applied.

Returns:
`True` if the merge would create a write-after-read hazard.
"""
copy_chain_mode = self._get_copy_chain_mode(sdfg, graph)
if copy_chain_mode == CopyChainRemoverMode.PULL:
surviving_node = self.node_a2
elif copy_chain_mode == CopyChainRemoverMode.PUSH:
surviving_node = self.node_a1
else:
return True

if surviving_node.desc(sdfg).transient:
return False

return any(
dnode is not surviving_node
and dnode.data == surviving_node.data
and graph.out_degree(dnode) != 0
for dnode in graph.data_nodes()
)

def is_single_use_data(
self,
sdfg: dace.SDFG,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,88 @@ def _make_copy_chain_with_reduction_node(
return sdfg, state, red0, accumulators[0], red1, accumulators[1], output_ac


def _make_copy_chain_destination_read_in_map() -> dace.SDFG:
"""Builds an SDFG where the copy chain destination is also read in the Map.

The SDFG implements the following code (`b` is a transient array):
```python
for i in range(10):
d[i] = c[i] + 1.0 # `c` read in a branch independent of the producer of `b`.
b[i] = a[i] * 2.0
c[:] = b[:] # Copy chain `(b) -> (c)`: the surviving global `c` is read in the Map.
```

`CopyChainRemover` would remove the transient double buffer `b` and take over
its writes with `c`, moving them into the Map. But `c` is still read there, in
a branch independent of the one producing `b`, so the read could observe the
newly written value (write-after-read hazard). The transformation must not
apply.
"""
sdfg = dace.SDFG(util.unique_name("copy_chain_destination_read_in_map"))
state = sdfg.add_state(is_start_block=True)

for name in ["a", "c", "d"]:
sdfg.add_array(name, shape=(10,), dtype=dace.float64, transient=False)
sdfg.add_array("b", shape=(10,), dtype=dace.float64, transient=True)

a, b, d = (state.add_access(name) for name in "abd")
# Two separate AccessNodes for `c` are needed, such that the state is
# acyclic: one for the read and one for the write back.
c_read, c_write = state.add_access("c"), state.add_access("c")

me, mx = state.add_map("comp_map", ndrange={"__i": "0:10"})
tlet_read = state.add_tasklet(
"read_c", inputs={"__in"}, outputs={"__out"}, code="__out = __in + 1.0"
)
tlet_prod = state.add_tasklet(
"produce_b", inputs={"__in"}, outputs={"__out"}, code="__out = __in * 2.0"
)

# `c` is read inside the Map, in a branch independent of the one producing `b`.
state.add_edge(c_read, None, me, "IN_c", dace.Memlet("c[0:10]"))
state.add_edge(me, "OUT_c", tlet_read, "__in", dace.Memlet("c[__i]"))
me.add_scope_connectors("c")
state.add_edge(tlet_read, "__out", mx, "IN_d", dace.Memlet("d[__i]"))
state.add_edge(mx, "OUT_d", d, None, dace.Memlet("d[0:10]"))
mx.add_scope_connectors("d")

state.add_edge(a, None, me, "IN_a", dace.Memlet("a[0:10]"))
state.add_edge(me, "OUT_a", tlet_prod, "__in", dace.Memlet("a[__i]"))
me.add_scope_connectors("a")
state.add_edge(tlet_prod, "__out", mx, "IN_b", dace.Memlet("b[__i]"))
state.add_edge(mx, "OUT_b", b, None, dace.Memlet("b[0:10]"))
mx.add_scope_connectors("b")

# The transient `b` is fully copied into the global `c`, which is read above.
state.add_nedge(b, c_write, dace.Memlet("b[0:10] -> [0:10]"))

sdfg.validate()
return sdfg


def test_copy_chain_destination_read_in_map_no_apply():
sdfg = _make_copy_chain_destination_read_in_map()

ref, res = util.make_sdfg_args(sdfg)
util.compile_and_run_sdfg(sdfg, **ref)

# The copy chain `(b) -> (c)` must not be removed: `c` survives and is still
# read inside the Map, so removing the transient double buffer `b` would
# create a write-after-read hazard on `c`.
nb_applies = gtx_transformations.gt_remove_copy_chain(sdfg, validate_all=True)
assert nb_applies is None

# Since the transformation did not apply, the transient `b` must still exist.
acnodes: list[dace_nodes.AccessNode] = util.count_nodes(
sdfg, dace_nodes.AccessNode, return_nodes=True
)
assert any(ac.data == "b" for ac in acnodes)
assert "b" in sdfg.arrays

util.compile_and_run_sdfg(sdfg, **res)
assert util.compare_sdfg_res(ref=ref, res=res)


def test_simple_linear_chain():
sdfg = _make_simple_linear_chain_sdfg()

Expand Down