Conversation
A Map dimension `i = c:c+1:1` can only ever take the value `c`, but the Memlets inside its scope keep referring to `i` symbolically. `MapFusionVertical` compares producer and consumer subsets with `Range.covers()`, which does not know the Map range, so a producer writing `a[c]` is not recognized as covering a consumer reading `a[i - o]` and a legal fusion is rejected. The new transformation folds the value into the scope and runs before the top level fusion rounds. The dimension itself is kept, unlike DaCe's `TrivialMapElimination` which removes it and thereby prevents the Map from being scheduled on the GPU. On `compute_perturbed_quantities_and_interpolation` this collapses the three surface level kernels into one, 10 -> 9 kernels. It also removes a non-determinism: the same program compiled to 10 or 11 kernels depending on which other variants were compiled in the same session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e1061f3 to
84c5978
Compare
iomaganaris
left a comment
There was a problem hiding this comment.
I'm not sure how it can help in the large stencil (didn't run it locally to check) but the transformation looks reasonable
| return { | ||
| param: rng[0] | ||
| for param, rng in zip(map_.params, map_.range.ranges) | ||
| if (rng[0] == rng[1]) == True and (rng[2] == 1) == True # noqa: E712 [true-false-comparison] # SymPy comparison |
There was a problem hiding this comment.
Probably it is not needed to check (rng[2] == 1) == True. This is the step between two elements to read/write, but we just need to check that we access a single element ((rng[0] == rng[1]) == True).
There was a problem hiding this comment.
I agree the rng[2] == 1 test is unneeded.
There was a problem hiding this comment.
Agreed, dropped in bda6175. With start == end the dimension has a single iteration for any step, so the step carries no information here.
|
The transformation is interesting. I am surprised that dace cannot do this on its own. |
Maybe because we are skipping |
This pull request includes updates to dependencies, configuration handling for CUDA architecture detection, and a CI job setting. The most significant changes are an upgrade to the required DaCe version, an improved approach to configuring CUDA architecture detection, and a minor CI configuration adjustment. **Dependency and configuration updates:** * Upgraded the required `dace` package version from `2.0.0a5` to `2.0.0a6` in `pyproject.toml`, ensuring compatibility with recent DaCe features and fixes. * Changed how CUDA architecture detection is handled in `set_dace_config` in `common.py`: now sets `compiler.cuda.cuda_arch` directly instead of passing a CMake argument, aligning with best practices and recent DaCe updates. **CI/CD configuration:** * Removed `allow_failure: true` from the `build_cscs_amd_rocm` CI job in `ci/cscs-ci-dace-determinism.yml`, making failures in this job block the pipeline.
philip-paul-mueller
left a comment
There was a problem hiding this comment.
The transformation looks okay, I just have some small things.
| return { | ||
| param: rng[0] | ||
| for param, rng in zip(map_.params, map_.range.ranges) | ||
| if (rng[0] == rng[1]) == True and (rng[2] == 1) == True # noqa: E712 [true-false-comparison] # SymPy comparison |
There was a problem hiding this comment.
I agree the rng[2] == 1 test is unneeded.
| ): | ||
| return True | ||
| for node in scope.nodes(): | ||
| if node is not map_entry and any(str(sym) in replacements for sym in node.free_symbols): |
There was a problem hiding this comment.
What is about the associated map exit?
There was a problem hiding this comment.
It is covered, on both counts.
scope_subgraph(map_entry, include_entry=True, include_exit=True) contains the MapExit, so its edges are checked in can_be_applied and rewritten in apply. Its own free_symbols turns out to be empty (so is the MapEntry's), so the node is not map_entry guard is about the general case rather than this one.
The restore reaches it too, because MapEntry and MapExit share one Map object:
MapEntry.map is MapExit.map: True
after mutating entry.map.params, exit sees: ['X', 'Y']
so restoring through map_entry.map restores what the exit sees.
| The dimension itself is kept, in contrast to DaCe's `TrivialMapElimination` | ||
| which removes it. Removing it changes the shape of the Map and prevents it | ||
| from being scheduled on the GPU, whereas folding only rewrites the uses of | ||
| the parameter and leaves the iteration space, the schedule and any blocking |
There was a problem hiding this comment.
The problem with the GPU scheduling is, that you can access data that resides on the GPU only inside a Map, whereas data on CPU can be accessed everywhere except a GPU kernel (with the exception of scalar data, which should be possible to access there as well).
This note, while interesting should be shortened to something like:
Unlike the DaCe native TrivialMapElimination transformation the dimension is not removed only the single value is folded into the body of the Map.
The rest are external consequences.
There was a problem hiding this comment.
Thanks, shortened in bda6175 to:
Note:
Unlike DaCe's native `TrivialMapElimination` the dimension is not removed,
only the single value is folded into the body of the Map.
| for param, value in replacements.items(): | ||
| scope.replace(param, value) |
There was a problem hiding this comment.
There is also replace_dict() which takes a dict and operates on it as a whole.
Should be more efficient.
There was a problem hiding this comment.
Done in bda6175, one call instead of one per parameter.
| replacements = self._single_iteration_parameters(map_entry.map) | ||
| scope = graph.scope_subgraph(map_entry, include_entry=True, include_exit=True) | ||
|
|
||
| # `replace()` would also rewrite the Map's own parameters and range, which |
There was a problem hiding this comment.
This is more my curiosity.
Before the parameters and the ranges of the Map were not subject to replacement, this is why MapFusion does it manually.
So I am wondering if the replace function does it now or not?
Don't get me wrong, you should not remove the code that does it, as this "not replacement" is a bug in my opinion.
There was a problem hiding this comment.
Not yet, same as when MapFusion started doing it by hand. Measured on this DaCe version:
after replace: params ['__i', '__j'] -> ['__i', '__j']
after replace: range 0:20, 7 -> 0:20, 7
So the save and restore is currently dead code. Keeping it as you suggest, since it is the behaviour we want to be robust against if that ever changes.
| assert map_entry.map.params == ["__i", "__j"] | ||
| assert str(map_entry.map.range[1][0]) == "7" | ||
| assert all( | ||
| "__j" not in str(edge.data.subset) |
There was a problem hiding this comment.
To be complete there is also other_subset that should be tested.
However, I expect that all of them are none anyway, but technically testing only subset is not enough.
There was a problem hiding this comment.
Added in bda6175, the assertion now iterates over both subset and other_subset of every Memlet. As you expected they are all None here, so it passes either way.
| assert all(np.allclose(ref[name], res[name]) for name in ref.keys()) | ||
|
|
||
|
|
||
| def test_trivial_map_dimension_folding_is_a_fixpoint(): |
There was a problem hiding this comment.
What is the difference between this test and the test above.
Both run the transformation using apply_transformations_repeated() and check if it returns 1, so both are a fix point, the test above, just also tests if the result is correct.
Either delete this test or remove the _reapeat above.
There was a problem hiding this comment.
You are right, they overlap. Deleted the separate fixpoint test and kept apply_transformations_repeated in the main one, with a comment saying that a single application under repeated application is what makes it a fixpoint.
…rfaced by the Python 3.12 floor (GridTools#2755) Audit of the codebase after the floor moved to 3.12 (GridTools#2326). Bugs fixed: - `cartesian`: `...` index detection matched the deprecated `ast.Ellipsis` on 3.12/3.13 and nothing at all on 3.14. Now matched via `_is_ellipsis_node`, which also fixes the regression from GridTools#2772 (`types.EllipsisType` is the type of the `...` object, not of its AST node). - `next`: `_type_conversion_helper` raised on PEP 604 unions and the builtin `tuple` spelling; it now dispatches on `get_origin()`. - `eve`: `frozen="strict"` called a non-existent `xtyping.is_hashable_type` and raised at class definition. The check now decomposes composite annotations, resolves PEP 695 aliases, and names the offending fields. - `next`: `test_typing_exports` picked up the project `[tool.mypy]` table, so the 3.13/3.14 runs duplicated 3.12. Adds `typing_tests/mypy.ini`. - `next`: `wait_for_compilation` flattened multiple failures into one `RuntimeError`; they are now raised as a group, each keeping its traceback. Deprecated and dead code removed: - `eve`: the PEP 585 `typing` aliases (`Dict`, `FrozenSet`, `List`, `Set`, `Tuple`, `Type`) are dropped from the namespace and rejected explicitly, and the use sites move to the builtin generics. Forward-reference resolution in user annotations is unchanged. - Dead `sys.version_info` / `typing_extensions.Any` guards, and the `add_note` override in `next/errors/exceptions.py`, dead since PEP 678 landed in 3.11. Diagnostics: `try*` is named as a construct, and constructs newer than the floor (`ast.TemplateStr`) are registered by name and skipped where absent. Build and docs: `.gitpod.Dockerfile` Python version, the noxfile PEP 723 `requires-python`, the floor stated in `AGENTS.md`, an explicit `[tool.mypy] python_version`, and stale mypy / isort overrides.
… folding - drop the `rng[2] == 1` step check: with `start == end` the dimension has a single iteration for any step, so the step carries no information here. - fold with a single `replace_dict()` instead of one `replace()` per parameter. - shorten the note contrasting the transformation with `TrivialMapElimination` to what distinguishes them, the dimension being kept rather than removed. - test the `other_subset` of every Memlet as well, not only the `subset`. - drop the separate fixpoint test: the main test already applies the transformation repeatedly and asserts a single application. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Good question — I checked, and It propagates symbols that were assigned on interstate edges; its It early exits before doing any work. Note we also do not avoid it entirely: it is in What DaCe does have for this is It removes the dimension, which is what we cannot use here, hence folding instead. So DaCe can do it, just not in a form that survives the GPU schedule. |
- point the property description at the class docstring - drop the note on `include_entry`, it holds for `include_exit` just as much Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Staged on the fork — not yet proposed upstream.
A Map dimension
i = c:c+1:1leaves Memlets referring toisymbolically.Range.covers()does not know the Map range, so a producer writinga[c]is not seen to cover a consumer readinga[i-o], and a legal fusion is rejected.Folding the constant into the scope subgraph removes the blind spot. The dimension is kept: dace's own
TrivialMapEliminationdrops it and then refuses GPU schedules.In the SDFG
From
compute_perturbed_quantities_and_interpolationwith the static bounds the model run bakes in.i_K_gtx_vertical = 8is the surface level, so the Map has a single iteration in K while keeping the dimension.map_18_fieldop, range1:17, 8before and after — only the Memlets change:The folded value need not be
0, and symbols that are not Map parameters are left alone.map_14_fieldop, same range:i_Cellis untouched throughout: its dimension has more than one iteration.What it changes in the final SDFG
At the point in
_gt_auto_process_top_level_mapswhere the pass first matches, the program has 48 top level Maps, 11 of them single iteration in K over the surface level (1:17, 80with 80 levels). Those 11 are what the pass unblocks.Top level Maps of the whole program after
gt_auto_optimize, by range, with and without the pass. Identical at 8 and at 80 levels:The only difference is the surface: the two Maps over
K = 8become one. Nothing else in the program moves, which is what the pass is meant to do — it removes a comparison failure, it does not restructure anything.Measurements
icon4py
compute_perturbed_quantities_and_interpolation, GH200,dace_gpu. Measured before PR GridTools#2763 and GridTools#2764 landed, where it collapsed the three surface kernels into one, 11 → 9 kernels. On the current pipeline the remaining effect is the one above, two surface Maps into one.It also removes a real non-determinism: the same program compiled to 10 kernels alone and 11 in a 3-variant session.
The wall-clock contribution measured on its own was small (~0.01 s on the target program), which is why this was deprioritised relative to the other changes. The determinism argument is the stronger one now, given GridTools#2779, GridTools#2780 and GridTools#2786.
Testing
test_trivial_map_dimension_folding.py, 3 passed. Merged tomainat0d8fb34d3; the fulldace_testsunit suite passes on the merge, with one pre-existing environment failure (test_dace_fastcall[exec_alloc_descriptor1], missingcupy) that reproduces on unmodifiedmain.Status
Independent of everything else in the stack. Not reviewed by anyone yet.