Repository navigation
Conversation
…ursors) Add `Memlet.schedule`, a `MemletSchedule` record describing how a leaf memlet's addressing is realized in generated code. Schedules are descriptive only and are lowered to ordinary SDFG constructs in the code-generation window; code generation has no schedule-specific paths. Two kinds: - `CopyOnAccess` (default, not serialized): the behavior every SDFG had so far, full offset at every access, movement at the access / via CopyLibraryNode. - `LoopCursor`: the offset is affine in an enclosing LoopRegion's induction variable (step, invariant base, lane part, cursor type, optional share_key). Passes (`dace/transformation/passes/memlet_schedules.py`): - `ScheduleLoopCursors` (analysis): attaches `LoopCursor` records to affine leaf memlets (scope gpu|all, arrays, cursor_type, overwrite). - `LowerMemletSchedules` (codegen window, run automatically by `generate_code` after InsertExplicitCopies and before expansion): dispatches to each kind's `lower`. Loop cursors become one loop-carried integer symbol per cursor class (assigned in the loop's init statement, advanced in its update statement), a flat `Reference` per array set once at SDFG entry, and the memlets are rewritten to `flat[cursor + immediate]`; non-contiguous reads use a window `Reference` set per iteration with the memlet's original shape. Classes are anchored at the lowest member offset (or a stencil's centre), nest-invariant symbolic terms are immediates, loop nests chain cursors (one add per level), successive inner nests share the outer loop's cursors, names are deterministic, stale schedules are dropped with a warning, idempotent. Generic infrastructure changes: LoopRegion init/update code blocks may hold several assignments (emitted in the for header, counted as defined symbols, declared by the frame; `loop_analysis.get_assignments`); Reference `set` casts to the reference's pointer type so read-only (const) nested-SDFG inputs can be referenced; InsertExplicitCopies carries a schedule onto the lifted operand. Docs: IR paragraph on memlet schedules, API index entries. Tests: default schedule, analysis/lowering/serialization/staleness/idempotence, GPU code generation, loop-nest chaining, window references, and two CPU stress programs (3-D stencil on a padded window with a nonzero offset; K loop with two successive I/J nests), compiled and checked numerically. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…generic leaves) - MemletSchedule kinds are plain dataclasses; JSON (de)serialization is generic in the base class, driven by field metadata (kind='symbolic' / 'dtype'). - `cursor_type` is an Optional[dtypes.typeclass] (None = automatic int32/int64) on LoopCursor and a TypeClassProperty on ScheduleLoopCursors; the cursor symbol is registered with that type, so any integer typeclass works. - Leaf memlets are found structurally as the innermost edge of a memlet path (the endpoint away from the data's access node is not a map entry/exit), instead of a list of node types; this also covers access nodes inside a map fed through the map entry (new test). Path rewriting adds the new memlet path before removing the old one, so scope nodes and connectors are never orphaned in between. - GPU scope filter uses the existing dace.sdfg.scope.is_devicelevel_gpu. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ization round trip) Rewritten memlets used a bare sympy Symbol for the cursor, which serializes as "$name" on the first save and as "symbol(name, dtype=...)" after a round trip, so the save/load/save check of DACE_testing_serialization (the CI autoopt job) failed. The cursor table now creates the typed DaCe symbol once, when it registers the cursor, and returns the object; memlet subsets and chained init expressions use it directly instead of re-creating symbols from the name. Two tests compared subsets textually and depended on term order; they now compare symbolically. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Ongoing testing on GEOS' FV3 Dynamics core routine of D_SW. Testing the In the case of single K (vertical) loop and many J I (horizontal) dual-loop where code within the kernel is heavily dependent on
Overall this looks like it's clearly superior for serial CPU code |
After more thorough look, |
"Access policy" better describes what the record is: how a leaf memlet's addressing is realized, not an execution schedule. Renames the module (dace.sdfg.memlet_access_policy), the base class (MemletAccessPolicy), the Memlet property (Memlet.access_policy), the passes module and passes (AssignLoopCursors, LowerMemletAccessPolicies), tests and documentation. The access_policy and wcr_nonatomic properties are now in the "Code Generation" category. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
cscs-ci run |
The test rebuilt the cursor symbol from its name, which yields a default-typed symbol, while the lowering declares the cursor with its actual dtype (int64 when the extent is not provably 31-bit). The comparison only held while a symbol's dtype was not part of its identity; compare against the declared symbols instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove the yapf pre-commit hook, .style.yapf and the yapf pin in the linting group.
Add the ruff-format hook next to ruff-check. line-length and quote-style carry over the yapf settings; notebooks stay unformatted as before.
ruff honours fmt: off/on at statement level only, so the parametrize decorator uses a trailing fmt: skip.
Files the branch only reformatted take main's version. The branch's changes to control_flow.py, cpu.py, framecode.py, memlet.py, state.py and insert_explicit_copies.py are reapplied on top of main's versions. No-view nested SDFG contract: a nested SDFG connector now receives the whole container (an equivalent descriptor, passed without an offset), so its binding memlet is no longer a cursor leaf. Rerouting it to a window reference broke descriptor equivalence. AssignLoopCursors/LowerMemletAccessPolicies now skip nested SDFG bindings; the memlets inside the nested SDFG remain leaves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A nested SDFG receives the whole container, so a memlet path continues through the chain of nested SDFGs to the leaf memlets inside them. Loop cursors are now assigned and lowered on these global paths: a leaf memlet in a nested SDFG may be addressed relative to a loop of an enclosing SDFG. Its offset is restated in the loop SDFG's symbols through the symbol mappings (symbols defined inside a nested SDFG become per-iteration atoms), the cursor is created on the loop and passed down as a symbol, and the immediate is restated in the leaf SDFG's symbols, adding symbol mapping entries where a nested SDFG does not receive a symbol yet. Fixes the tests that failed with simplification disabled, where map bodies stay nested SDFGs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the squashed ruff lint migration (#2671) and later fixes. Conflicts were resolved by normalizing base and branch with main's pinned ruff (0.15.5, check --fix + format) before a three-way merge. In the CPU codegen for setting a reference, the branch's cast to the reference's pointer type is kept over main's f-string rewrite of the uncast line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applies the pinned ruff 0.15.5 safe fixes and hand-fixes a Union type alias (UP007) and a set comprehension (C416). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Just as an FYI: As part of my master's thesis I'm working on TMA support and I will be adding the corresponding access policy :) |
| return expr | ||
|
|
||
|
|
||
| def _statements(block) -> list: |
There was a problem hiding this comment.
We should keep type hints always, block: CodeBlock and return type would be list[CodeBlock] ?
| for (i = 0, __dace_cur_A_for_1 = 3, __dace_cur_B_for_1 = 0; i < N; | ||
| i = i + 1, __dace_cur_A_for_1 = __dace_cur_A_for_1 + 16, __dace_cur_B_for_1 = __dace_cur_B_for_1 + 1) { | ||
| __dace_flat_B[__dace_cur_B_for_1] = __dace_flat_A[__dace_cur_A_for_1] + 2 * __dace_flat_A[(__dace_cur_A_for_1 + 4)]; | ||
| } |
There was a problem hiding this comment.
I think having both versions' generated code (copy-on-access and ptr-increment) would nice for documentation I think
|
|
||
| _SCOPE_NODES = (nodes.EntryNode, nodes.ExitNode) | ||
| _GPU_KERNEL_SCHEDULES = (dtypes.ScheduleType.GPU_Device, dtypes.ScheduleType.GPU_Persistent) | ||
| _LANE_SCHEDULES = (dtypes.ScheduleType.GPU_ThreadBlock, dtypes.ScheduleType.GPU_ThreadBlock_Dynamic) |
There was a problem hiding this comment.
I think this is the 3rd or 4th PR where we need to move around the horrible default GPU_SCHEDULES type which is just GPU_ThreadBlock, which is massively confusing. (Including new gpu codegen and new gpu offloading pass).
I think we should update the dtypes to have GPU_KERNEL_SCHEDULES in dtypes.py, and to replace GPU_SCHEDULES with a better name, such as GPU_LANE_SCHEDULES.
To avoid an accidental conflict I propose not using GPU_SCHEDULES but GPU_KERNEL_SCHEDULES and GPU_LANE_SCHEDULES
| from dace.transformation import transformation | ||
| from dace.transformation.passes.analysis import loop_analysis | ||
|
|
||
| _SCOPE_NODES = (nodes.EntryNode, nodes.ExitNode) |
There was a problem hiding this comment.
Would be nice addition to dtypes.py or a similar file.
| """Number of elements of ``subset`` if it is a contiguous run in memory (at most one dimension of size > 1, | ||
| with unit step and unit stride), otherwise ``None``.""" | ||
| if not isinstance(subset, Range): | ||
| return None |
There was a problem hiding this comment.
Since we removed Indices, this function should only receive Subsets at this stage?
| d = wide[0] | ||
| if sp.sympify(subset.ranges[d][2]) != 1 or sp.sympify(desc.strides[d]) != 1: | ||
| return None | ||
| return sp.sympify(sizes[d]) |
There was a problem hiding this comment.
Could we achieve the same thing by querying Subset.is_contiguous_subset(array) in a cond check, and if true return subset.num_elements()?
Seem equivalent to me
| *within* an iteration and therefore belong to the per-access immediate, never to the cursor.""" | ||
| result: set[str] = set() | ||
| for region in loop.all_control_flow_regions(recursive=False): | ||
| if isinstance(region, LoopRegion) and region is not loop and region.loop_variable: |
There was a problem hiding this comment.
Should we include maps that are sequential here?
| def lane_symbols(state: SDFGState, node: nodes.Node) -> set[str]: | ||
| """Parameters of enclosing maps that code generation assigns to threads/lanes (GPU_ThreadBlock/GPU_Warp | ||
| maps, or a map directly nested in a GPU kernel map -- the ``[0:W, 0:64]`` wave/lane convention).""" | ||
| sdict = state.scope_dict() |
There was a problem hiding this comment.
How does this handle the nested GPU device maps?
Because from what I have seen something like this happens:
for i in [0:N:1]: at GPU Device
for j1 in [0:M1:1]: at GPU Device
body1
for j2 in [0:M2:1]: at GPU Device
body2
Then codegen implicitly generates a map that is like:
for i, j in [0:N:1, 0:max(M1,M2):1]: at GPU Device
if j <= M1:
body1
if j <= M2:
body2
So I think while this check works for cases having tblock map and not, it will have issues when we have nested gpu device maps?
To mention: #2534 (#2534 ), this pass always fixes nested GPU device maps and makes them into a single gpu device map that is easier to reason about, so maybe we merge that PR, and then ensure there are no nested gpu device maps when we run this?
Also if we have > 3 dimensions then then all upper dimensions are flattened into a z index, and then this loop should be ok.
| @@ -0,0 +1,1060 @@ | |||
| # Copyright 2019-2026 ETH Zurich and the DaCe authors. All rights reserved. | |||
There was a problem hiding this comment.
Although I do not see an issue in the implementation (and well let's be honest, if there is a bug here it will come out really fast and we can then fix it), I think the organization of the files is very hard for me.
I would propose something like this, especially because we expect to have more access policies implemented over time:
dace/
├── sdfg/
│ ├── memlet_access_policy.py # MemletAccessPolicy base class
│ └── memlet_access_policies/
│ ├── __init__.py
│ ├── copy_on_access.py # CopyOnAccess
│ └── loop_cursor.py # LoopCursor
│
└── transformation/
└── passes/
├── memlet_schedules/
│ ├── __init__.py
│ ├── helpers.py # Global memlet-path utilities
│ └── assign_loop_cursors.py # AssignLoopCursors
│
└── memlet_access_policy_lowering.py # LowerMemletAccessPolicies
There was a problem hiding this comment.
As a note current structure is like this:
dace/
├── memlet.py
│ └── Memlet.access_policy
│
├── sdfg/
│ └── memlet_access_policy.py (can be split)
│ ├── MemletAccessPolicy
│ ├── CopyOnAccess
│ └── LoopCursor
│
│
└── transformation/
└── passes/
├── memlet_access_policies.py (Imho feels too long)
│ ├── AssignLoopCursors
│ ├── LowerMemletAccessPolicies
│ └── Loop-cursor lowering helpers
│
│
└── __init__.py
└── Exports both access-policy passes
A memlet serves two purposes at the same time. It produces an address, and it moves bytes. Currently, the default is copy-on-access, recomputing the offset at every access. This PR adds
Memlet.access_policy, which allows to modify that. At code generation, the access policy is lowered to LibraryNodes (just like the copy node today) and can now represent concepts such as pointer incrementation (cursors), buffer resource descriptors, TMA-generated reads, or general address-generation/iterators/accessors.