Skip to content

codegen: decide the OpenMP simd clause in a preprocessing pass - #2503

Draft
ThrudPrimrose wants to merge 7 commits into
mainfrom
omp-simd-pass
Draft

codegen: decide the OpenMP simd clause in a preprocessing pass#2503
ThrudPrimrose wants to merge 7 commits into
mainfrom
omp-simd-pass

Conversation

@ThrudPrimrose

Copy link
Copy Markdown
Collaborator

MarkSIMDMaps marks the innermost Sequential and CPU_Multicore maps whose body holds no further loop or map, splitting a multidimensional map with MapExpansion first since the clause vectorizes the loop it precedes, and withholding it from min/max reductions and Sequential scatters. Code generation only renders what the pass decided.

#pragma omp parallel for      // outer dimension
    #pragma omp simd          // innermost, split off by the pass

MarkSIMDMaps sets Map.omp_simd on the innermost Sequential and CPU_Multicore maps
whose body holds no further loop or map, splitting a multidimensional map's
innermost dimension off with MapExpansion first, since the clause vectorizes the
loop it precedes. min/max reductions and Sequential scatters keep the clause off.
Code generation only renders what the pass decided.
… leaf map now carries it

The Fortran frontend outlines every map body into a NestedSDFG, so the pass has to open one
rather than refuse it on sight; the parametrized case pins that, including the loop and map
payloads that withhold the clause. openmp_test asserts the emitted pragma text, which now
reads 'parallel for simd' on the leaf map it builds.
A Sequential map lowers WCR to wcr_fixed::reduce, a read-modify-write in the loop body: an
accumulation into a fixed location carries across iterations and a scatter can alias across
them, neither of which simd permits. CPU_Multicore keeps the clause, since its conflicted WCR
goes through reduce_atomic - pinned by running the sum.
…x what it uncovered

An unexpanded library node lowers to a Map after this pass runs, and the OpenMP Reduce expansion
writes "#pragma omp parallel for" into a tasklet body; either lands inside the simd region, which
gcc rejects. MapExpansion left a stale collapse count on the map it reuses, and omp_simd did not
serialize for Sequential maps - the schedule this pass marks most often.
PatternTransformation.apply_to propagates memlets across the entire SDFG after every
application unless told otherwise, so one whole-SDFG propagation rode along with each
candidate map: quadratic in the map count, and code generation for a LLaMA-sized model
never finished. MapExpansion already propagates the scope it rewrote, and hands back
the new nest, so the pass turns the annotation off and takes the innermost entry from
the return value instead of walking the scope tree again.
…tion guard for what it is

Nothing checked the nest MapExpansion leaves behind when the pass splits a marked
multidimensional map, so a permuted or mis-nested expansion would have gone through with the
clause on it. The new parametrized test pins that structure for a 2-D and a 3-D map: one
single-dimension scope per dimension in the original order and range, entries nesting and
exits re-joining in mirror order, each scope handing the next its slice, one vectorized loop
in the nest, and the same numbers as numpy. The whole-SDFG propagation guard says in its name
that it guards cost, and swaps a hand-rolled global swap for the monkeypatch fixture.
ThrudPrimrose added a commit that referenced this pull request Aug 18, 2026
The merge of main into extended took upstream's version of several files whole, dropping
extended-side definitions their own callers still use, and left two pragma emitters both
firing. Every hunk here is one of those, resolved toward upstream's implementation:

* ProgramVisitor.defined went back to a dict-building property while DefinedNames -- the view
  every caller reaches through .materialize() -- stayed. Restores the view, and finishes the
  PR #2501 port inside it (SDFG arrays already carry process grids).
* cpu.py defined _use_aligned_operator_new but calls it, and experimental_cpu imports it,
  under the public name.
* codegen.py calls RegionBoundaryStates without importing it.
* SDFG grew a second UsedNames class while find_new_name_avoiding_connectors, which
  sdfg_nesting calls, disappeared. Keeps upstream's _UsedNames, teaches it the connector
  walk, restores the method.
* Autodiff lost get_symbol_upper_bound_from_loop, init_grad and a typing import, and gained
  a duplicate connector_symbol and TYPE_CHECKING. connector_dict goes rather than comes back:
  no caller, and the call sites already spell sorted() over an OrderedSet.
* MarkSIMDMaps (PR #2503) and this target's own simd_sequential_maps both wrote
  "#pragma omp simd" before the same loop, so the second one preceded no loop at all and
  every map_expansion case failed to compile ("loop nest expected"). One emission site now,
  fed by either decision.

Two GPU-specialization defects the spmv sample hits under the transformation tester, both
edges wired across a scope boundary: state_fission asserted a boundary node has NO in-edges
when an empty ORDERING edge between two boundary nodes legitimately stays, and stream
propagation wired gpu_streams straight into a NestedSDFG sitting inside a host map, leaving
scope_dict with an undequeued MapExit. samples/simple/spmv.py goes 27/29 -> 29/29.

The SMT oracle (z3, optional -- absent it returns None and every caller keeps its existing
refusal) answers the two questions LoopToMap cannot: is a non-affine write injective over the
iteration domain, and does a read/write pair carry a RAW. Only a proven 'none' admits a pair:
BreakAntiDependence runs earlier and has no solver, so a WAR still standing here was never
snapshot-renamed. tests/canonicalize/smt_required_parallel_test.py's quadratic scatter
XPASSes and becomes a plain assertion; the other two cases stay xfail.

CI: the disabled whole-suite rustworkx job becomes a graphlib-only one on 3.14 with
simplify=1, so General Tests keeps the networkx backend and nothing runs twice.
ThrudPrimrose added a commit that referenced this pull request Aug 18, 2026
The Code Quality lane was red on four files ``pre-commit run --all-files`` reformats; running it
over the tree is the fix (two of them also lose an import ruff proves unused).

The same merge that dropped those had dropped the block emitting ``reduction(op:target)`` on a
CPU_Multicore ``parallel for`` -- the clause targets are what
``_omp_reduction_scope_stack`` feeds to ``write_and_resolve_expr``, so with nothing pushing to
it every reduction fell back to ``wcr_fixed::reduce_atomic``. Restored, and
``normalize_wcr_test``'s indirect-read reduction goes back to a clause instead of an atomic.

The simd emission then had two owners. ``MarkSIMDMaps`` (PR #2503) decides whether a map
vectorizes and records it in ``Map.omp_simd``; this target carried its own leaf-body /
WCR-coverage rules behind two config knobs and stamped the clause independently, so the two
disagreed on min/max WCR maps and on tasklets carrying their own OpenMP directive -- and on any
leaf map they both accepted, the clause was written twice, leaving the second ``#pragma omp
simd`` facing no loop ("loop nest expected", every map_expansion case). The pass owns the
analysis now: both CPU targets read ``omp_simd`` and render, ``simd_sequential_maps`` /
``simd_innermost_multicore_maps`` are gone with the rules they gated (``simd_maps`` is the
switch), and the tests that pinned the old rules pin the pass's contract instead -- a
multidimensional map reaches the clause through expansion rather than ``collapse``, and a
Sequential map with any WCR keeps none.
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.

1 participant