Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New and Improved MapFusion #1629

Open
wants to merge 125 commits into
base: main
Choose a base branch
from

Conversation

philip-paul-mueller
Copy link
Collaborator

@philip-paul-mueller philip-paul-mueller commented Aug 22, 2024

This PR introduces a new and improved version of MapFusion.

The PR fixes several bugs and several limitations of the previous versions.
This is a summary of all the changes:

  • The subsets (not the .subset member of the Memlet; I mean the concept) of the new intermediate data descriptor were not computed correctly in some cases, especially in presence of offsets. See the test_offset_correction_range_read(), test_offset_correction_scalar_read() and the test_offset_correction_empty() tests.
  • Upon the propagation of the subsets, due to the changed intermediate, was not handled properly. Essentially, the transformation only updated .subset and ignored .other_subset. Which is correct in most cases but not always. See the test_fusion_intrinsic_memlet_direction() for more.
  • The check if an intermediate could be fully removed or had to be recreated, as a new Map output, was not done properly. For this it is needed to scan the entire SDFG to determine if it is used somewhere else. To speed up this, a cache was introduced that scans the SDFG once and then reuses this information. Not a perfect solution as there is no way to check if the cache as to rebuild. The use in auto_optimizer() is such that it takes advantage of it. See also the comment about assume_always_shared flag.
  • During the check if two maps could be fused the .dynamic property of the Memelts were fully ignored leading to wrong code.
  • The read-write conflict checks were refined, before all arrays needed to be accessed the wrong way, i.e. before a fusion was rejected when one map accessed A[i, j] and the other map was accessing B[i + 1, j]. Now this is possible as long as every access is point wise. See the test_fusion_different_global_accesses() test for an example.
  • The shape of the reduced intermediate is cleaned, i.e. unnecessary dimensions of size 1, are removed, except they were present in the original shape. To make an example, the intermediate array, T, had shape (10, 1, 20) and inside the map was accessed T[__i, 0, __j], then the old transformation would have created an reduced intermediate of shape (1, 1, 1), new its shape is (1). Note if the intermediate has shape (10, 20) instead and would be accessed as T[__i, __j] then a Scalar would have been created. See also the struct_dataflow flag below.

In addition some new flags were introduced:

  • only_toplevel_maps: If True the transformation will only fuse maps that are located at the top level, i.e. maps inside maps will not be merged.
  • only_inner_maps: If True then the transformation will only fuse maps that are inside other maps.
  • assume_always_shared: If True` then the transformation will assume that every intermediate is shared, i.e. the referenced data is used somewhere else in the SDFG and has to become an output of the fused maps. This will create dead data flow, but avoids a scan of the full SDFG.
  • strict_dataflow: This flag is enabled by default. It has two effects, first it will disable the cleaning of reduced intermediate storage. The second effect is more important as it will preserve a much stricter data flow. Most importantly, if the intermediate array is used downstream (this is not limited to the case that the array is the output of the second map) then the maps will not be fused together. This is mostly to work around some other bugs in DaCe, where other transformations failed to pink up the dependency. Note that the fused map would be correct, the problem are other transformations.

Collection of known issues in other transformation:

@philip-paul-mueller philip-paul-mueller changed the title Started with a first version of the map fusion stuff. New and Improved MapFusion Aug 22, 2024
@philip-paul-mueller philip-paul-mueller marked this pull request as draft August 22, 2024 13:55
Now using the 3.9 type hints.
When the function was fixing the innteriour of the second map, it did not remove the readiong.
It almost passes all fuction.
However, the one that needs renaming are not yet done.
…t in the input and output set.

However, it is very simple.
Before it was going to look for the memlet of the consumer or producer.
However, one should actually only look at the memlets that are adjacent to the scope node.
At least this is how the original worked.

I noticed this because of the `buffer_tiling_test.py::test_basic()` test.
I was not yet focused on maps that were nested and not multidimensional.
It seems that the transformation has some problems there.
Whet it now cheks for covering (i.e. if the information to exchange is enough) it will now no longer decend into the maps, but only inspect the first outgoing/incomming edges of the map entrie and exit.
I noticed that the other way was to restrictive, especially for map tiling.
Otherwise we can end up in recursion.
Before it was replacing the elimated variables by zero.
Which actually worked pretty good, but I have now changed that such that `offset()` is used.
I am not sure why I used `replace` in the first place, but I think that there was an issue.
However, I am not sure.
…ured.

Before the output edges were before set to dynamic.
However, this was not true as it was always set, thus the new map fusion did not fuse them.
My first attempt was to just disable the `dynamic` property, but now the SDFG is generated manually.
It is almost the same, but uses lesss symbol, as it was simpler to implement it this way, and we are now using float.
For such edges we are sure that the data exists, so it is just a conditional read, which is fine.
Using `nodes()` on an SDFG will only give us the control flow regions, but using `state` will give us also the nested states.
I looked through my code and this seems to be the only places where they appear.
This fixes the correlaton test, but the heat test still fails.
The issue was similar as before.
When I computed the name of the intermediate transient then I used `sdfg.node_id(state)` to get the state ID.
However, now if the state is part of these recursive control flow regions then this may not work, because the state is not a direct node of the SDFG.
However, if I use `self.state_id` then it works, this is what the old MapFusion was doing.
This tests dynamic Memlets inside producers; the original transformation fails on it.
@philip-paul-mueller
Copy link
Collaborator Author

Thanks for reviewing and the wall of text.

To give you some context.
Initially I started with JaCe (JAX frontend for DaCe), I applied it to stencils from ICON4Py.
However, for some of them DaCe's auto_optimizer, was unable to handle them, either the fusion was not performed, the resulting SDFG was invalid or the computation was wrong.
I traced them down to MapFusion, at first I was trying to fix the original implementation, but I had trouble understanding the code at all, so I started to rewrite the transformation.

The main issues I found (not limited to ICON4Py) were:

  • The subsets (not the .subset member of the Memlet; I mean the concept of where we write to it and from where we read) of the new intermediate array were not computed correctly.
  • The transformation did not make a difference between .subset and .other_subset of a Memlet and in most cases just accessed .subset which might be wrong. In fact this is a general impression I had that a lot of code simply accesses .subset (which happens to be the right choice in most but not all cases) and does not care about the intrinsic direction of the Memlet.
  • The check if an intermediate can be removed or must be recreated afterwards was wrong. For this the whole SDFG has to be scanned, there is no way around it, but it was not done.
  • The .dynamic property of the Memelts where fully ignored.
  • As a side note, the check for WCR is on line 427
  • The code that propagates the change (removed intermediate) into the scope was wrong; again .subset was not handled correctly. (Although, I have to say that the current code should also be improved, but just a little.)

I want to point out that this PR adds a lot of tests for MapFusion (approximately 40% of the edits) and the previous version is not able to pass them; roughly 1/3 of them fails.

Regarding the description, I agree the doc string of the class is not that good, however, the code itself is in my view better documented than before, but I have updated the description of the transformation to give a better high level overview, which points to the functions that performs the tests.

I do not know OTFMapFusion and SubgraphFusion very well, however, I have seen that SubgraphFusion is much more general, for example, instead of reducing the intermediate it will move the intermediate data access inside the Map.
The only capability I know SubgraphFusion has is that it is able to handle Maps that are parallel.
This is a capability that my MapFusion currently lacks (it was originally included in the PR, but removed afterwards).

I think the best way to see my MapFusion is not as something new but just as a new iteration of what was already there, it just performs more analysis to handle more cases than before. This allows it to handle more cases. However, there are still some todo's that are open.

I have to admit that I have not performed any testing of the runtime, but I do not have the impression that it takes much more time than before. The reason is that MapFusion is, beside two exceptions, a very local operation.
The first exception is, the check if an intermediate can be removed or not. However, this information is more or less static, so the transformation computes this set at the beginning and then caches it. The downside is that it is hard to tell if the cache should be renewed. However, the cache remains valid as long as no AccessNodes are added. I checked that the use in auto_optimizer is fine. Further, to avoid this I added the assume_always_shared flag. This tells the transformation that every intermediate is shared. Thus no scan is ever needed, however, it will lead to dead dataflow.
The second exception is where we have to ensure that no cycles are created, however, this will only explore the dataflow graph locally (everything downstream).
Furthermore, when I wrote the thing I tried to order the checks in such a way that the ones that are either cheap or very likely to fail come first.

Copy link
Collaborator

@phschaad phschaad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for addressing the questions and concerns. This LGTM now in general. Please update the PR description to be in-line with the actual changes after revisions (ideally also with some of the details from the docstring of the transformation itself). After that I am happy to approve the PR.

The transformation checks if the first map satisifes the data dependencies of the second map.
For this is looks at the writes and reads of the intermediate.
It also checks if, a data container is used as input of the first and as output of the second map, if the access is pointwise and can be fused.

Furthermore, it was allowed that the intermediate is also used as input to the first map.
However, in that particular case, it was not checked if the the reads and writes of the first map alone to the intermediate are valid.
I.e. it could read read `A[i]` but write `A[i+1]` which would cause problems (note that this usage is botherline legal anyway.
This commit adds a check to make sure that this is not the case by enforcing if a data container is used as input and output of the first map and also as intermediate node then its read must be pointwise.
Note that if it is not an intermediate node, i.e. not also read by the second map, then this rule does not apply.

NOTE: It is forbidden that the intermediate is used as intermediate and output of the second map.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants