Repository navigation
Conversation
1e3e29c to
eca19e5
Compare
philip-paul-mueller
left a comment
There was a problem hiding this comment.
I agree that this is a similar but different kind of bug than the other ones and it is a real defect in the transformation.
The changes looks okay, however, I think that we do not need to scan all nodes in the graph, but it is enough to perform the test on the upstream nodes (although it could even be reduced more, by recognizing that a global access node acts as a serialization point.
philip-paul-mueller
left a comment
There was a problem hiding this comment.
It looks good to me, but there is one question that I have and might trigger another round.
But if the answer is negative, i.e. this case is harmless, then we can approve it right away.
Address review comments on PR GridTools#2863 in `_creates_write_after_read_hazard`: - reword the comment describing which reads are affected, so the "downstream of" vs "pass through" distinction is clearer - add a TODO noting that empty Memlets and the order they impose are currently ignored - remove the redundant `graph.out_degree(node) != 0` guard: every node returned by `find_upstream_nodes` is reached by walking an incoming edge, so it always has at least one outgoing edge
The write-after-read hazard check in CopyChainRemover only scanned upstream of the survivor, which is correct for PULL mode ((T) -> (G), survivor G): the write to G is moved upstream, so reads upstream of the survivor can be overtaken. In PUSH mode ((G) -> (T), survivor G) the read of G is instead moved downstream, to where the transient was consumed. If G is written at another AccessNode between the survivor and that consumer, the moved read would observe the new value instead of the one frozen in the temporary. The upstream scan did not catch this. Split `_creates_write_after_read_hazard` by mode: - PULL: scan upstream for reads (unchanged) - PUSH: scan downstream for writes to the same data Add `find_downstream_nodes` as the symmetric counterpart to `find_upstream_nodes`, and a regression test mirroring the existing PULL-mode test for the PUSH case.
philip-paul-mueller
left a comment
There was a problem hiding this comment.
LGTM some modified comments.
It is important that we do a performance test to see if there are changes.
| # 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. |
There was a problem hiding this comment.
Please keep this note but reformulate it to:
NOTE: ADR18 technically only allows for point-wise dependencies for global accesses, however, to support ICON4Py this is sometimes not fully validated, see test_copy_chain_destination_read_in_map_no_apply() for an example.
The issue could be solved by adding empty Memlets, but the pipeline support of them outside of Maps is not very mature.
The simplest thing would be to forbid inout variables altogether.
Thus we simply forbid this.
There was a problem hiding this comment.
I have updated the comment., but differently.
The hazard check previously rejected the copy-chain removal whenever any AccessNode with the same data as the survivor was found upstream (PULL) or downstream (PUSH), using plain graph traversal that ignores connector names. This over-rejects the common case of a single-branch map whose tasklet reads the surviving global upstream of the copy: the read and the moved write share the same branch, so no write-after-read hazard exists. Add a connector-aware path counter 'count_paths' to the dace transformations utils. Unlike the existing find_upstream_nodes / find_downstream_nodes / is_reachable (and DaCe's own all_simple_paths, which is networkx-backed and connector-agnostic), it only continues through the scope connector that pairs with the one it arrived on (IN_<x> <-> OUT_<x>), following real data flow. A single map tasklet reading two inputs therefore reports one path to a downstream survivor, not two. Relax the check: when the removed temporary has a single producer and the survivor has no other producer/consumer besides the copy, a same-data access with a single connector-aware path to the survivor is ordered before the moved write both before and after the merge, so the removal is safe. Otherwise the conservative answer (reject) is kept, including the case where the plain-upstream match was a connector- crossing false positive (count == 0). This preserves the existing hazard detection (test_copy_chain_destination_read_in_map_no_apply, test_push_chain_source_written_in_map_no_apply) while allowing the new test_copy_chain_destination_read_single_path_apply. Also drop a test-only 'raise AssertionError' debug line in can_be_applied.
411303f to
8db5d1f
Compare
|
After analysis, it is very difficult to detect all valid cases, which do not represent a write-after-read hazard. |
Description
CopyChainRemovermerges a(A1) -> (A2)copy by removing one container andrewiring the flow onto the survivor. Depending on the mode this moves either the
writes or the reads of the removed transient:
(T) -> (G), survivorG): the transientTis removed andits writes are taken over by
G, which moves them back to whereTwasproduced.
(G) -> (T), survivorG): the transientTis removed andits consumers are rerouted to read from
Gdirectly, which moves the read ofGdownstream to whereTwas consumed.When the surviving container is a global that is also accessed elsewhere in
the same state, the removed transient was acting as a double buffer and the move
can let a write overtake a read (PULL) or a read overtake a write (PUSH),
yielding silently wrong results. This is the pattern generated by an in-place
update stencil, i.e. a field that a
field_operatorcall takes as an input andalso produces as an output.
Fix
Add
_creates_write_after_read_hazardand split it by copy-chain mode:Gis moved upstream, so a read ofGupstream ofthe survivor can be overtaken. Scan the ancestor region of the survivor for
reads of the same data.
Gis moved downstream, so a write toGbetween thesurvivor and where the transient was consumed can be overtaken. Scan the
descendant region of the survivor for writes to the same data.
Transients are written only once (ADR-18), so they are unaffected and are
skipped by the check. As part of the PUSH scan,
find_downstream_nodesis addedas the symmetric counterpart to
find_upstream_nodes.Tests
test_copy_chain_destination_read_in_map_no_applyreproduces the in-placeupdate pattern in PULL mode:
The
(b) -> (c)copy chain must not be removed, sincecsurvives and isstill read in the Map.
test_push_chain_source_written_in_map_no_applymirrors the above forPUSH mode:
The
(a) -> (b)copy chain must not be removed, sinceasurvives and iswritten again in the Map before the consumer reads.
Both tests compile and run the SDFG before and after, assert the transformation
does not apply, and that the transient double buffer is retained. Each was
verified to fail when the corresponding guard is neutralized, confirming the
tests reproduce the issue.
Notes
This is one of a family of write-after-read fixes for in-place update stencils on
the DaCe backend (cf. #2815
GT4PyMapBufferElimination, #2816DoubleWriteRemover,#2817
GT4PyStateFusion). It targets a distinct transformation (CopyChainRemover)and does not overlap with those PRs; the PULL reproducer deliberately mirrors the SDFG
shape used in #2816. The fix was extracted from the
dace_refactor_loweringdraft(#2690) into this standalone PR.