Skip to content

[Only for CI] Extensions - #2475

Draft
ThrudPrimrose wants to merge 3824 commits into
mainfrom
extended
Draft

[Only for CI] Extensions#2475
ThrudPrimrose wants to merge 3824 commits into
mainfrom
extended

Conversation

@ThrudPrimrose

Copy link
Copy Markdown
Collaborator

No description provided.

@ThrudPrimrose
ThrudPrimrose changed the base branch from ci-fix to main August 3, 2026 11:37
ThrudPrimrose and others added 29 commits August 3, 2026 14:48
__builtin_trap is a GCC/Clang extension lowering to ud2, so a fired guard
surfaced as SIGILL and read as a bad-instruction/ISA fault -- it sent a real
investigation down an AVX-512 path. std::abort is standard C++20, survives
NDEBUG unlike assert(), needs no exceptions, and SIGABRT reads as deliberate.
Cost is irrelevant: every site is on a failure branch.

The assumption-guard dedup in assume_symbols_nonnegative recognises its own
guards by searching tasklet code for the trap string, so it moves with the
emission -- otherwise it would find none and re-emit every guard per run.

Also drops a hardcoded interpreter path in a committed test (sys.executable).
…opagation

Constant collection is a fixpoint over each control flow region, and it recursed into nested regions
from inside the fixpoint loop. So every sweep of every region re-ran blockorder_topological_sort,
which computes immediate dominators and branch merges, and a region nested d levels deep was
re-analysed on every sweep at every enclosing level.

Neither is needed. The CFG does not change while constants are being collected, so the block order
is the same on every sweep. What a nested region computes is a function of its 'in' constants alone,
so re-collecting it with unchanged inputs recomputes the same fixpoint.

Cache the block order per region, and record the 'in' constants each nested region was last
collected with so it is re-collected only when those change. The transfer and meet rules are
untouched.
command_cache and precompiled_header already default true, but these two
workflows are dominated by build time, so pin them rather than inherit a
default that can drift.
The recipe's last `SimplifyPass` runs BEFORE the terminal `LoopToMap` and the
terminal vertical/horizontal fuse, so from that point on nothing reclaims
arrays. `ArrayElimination` -- Simplify's array reclaimer -- would not have taken
this shape anyway: its `_is_war_carrier` guard skips a candidate whenever the
DESTINATION is read and written in the same state, which is every in-place
stencil sweep. That guard is left alone. `RedundantArray` only ever redirects a
transient's WRITERS into the destination, so it cannot expose a read to a later
in-place write; the mirrored `RedundantSecondArray` fold -- redirecting a copy's
READERS onto a WAR carrier, which is the TSVC s212 shape the guard exists for --
is deliberately not run here. It matched nothing on heat3d, jacobi_2d, seidel_2d,
vadv, cavity_flow or channel_flow, and refusing costs a warning.

heat3d is the case: its second sweep filled an (N-2)^3 transient that was then
copied wholesale into `A[1:-1, 1:-1, 1:-1]`, and the buffer reached codegen as a
heap allocation plus a full-size copy loop per timestep. `RedundantArray` (main,
unchanged) removes it and lets the map write `A` directly -- transients 30 -> 29,
heap buffers 1 -> 0, container-to-container copies 1 -> 0, one `new[]` and one of
three `omp parallel for` regions gone from the generated code. Bit-exact against
the same pipeline without the stage and against a numpy oracle.

Wiring only, no new module. The slot is right after the terminal fuse and before
the remat stage: fusion is the last thing that can strand a transient, the map
shapes are final so the "no other reader" test is on the final graph, and a
deleted buffer shortens the chains remat then walks.

Four tests: the recipe carries the stage exactly once; the heat3d buffer and
copy disappear (A/B against the same recipe with the stage skipped, so the test
cannot go vacuous); re-entering the stage on the canonicalized SDFG is a no-op,
which canonicalize needs since it is not yet a fixed point; and heat3d stays
bit-exact. Three of the four fail with this commit reverted, four pass with it.
The --all-files ruff sweep deleted HAVE_ISL + is_domain_empty from
wavefront_polyhedron and edmonds_karp from the graphlib flow shim. All three are
re-exports reached as poly.<name> / edmondskarp.<name>, which ruff cannot see, so
F401 fired; noqa marks them. The graphlib file was left as a docstring and
nothing else.

The two subprocess guard tests asserted only "child failed" / "marker absent",
so an ImportError satisfied them -- both now require a BUILT marker first. Also
drops two hardcoded developer paths from those subprocess scripts.
branch_mode changes the multidim pipeline by exactly three passes:
LowerITEToFpFactor (integer ITE -> c*t + (1-c)*e), EliminateBranches
(matches a ConditionalBlock) and LowerInterstateConditionalAssignments
(consumes only what EliminateBranches mints). None can fire without a
conditional, and neither the frontend nor base_pipeline makes one from
straight-line source -- so on a branchless kernel both modes emit the
same code and the fpfac phase recompiles and reruns identical C.

exercises_branch_lowering reads the kernel source (recursing into the
@dace.program helpers npbench pushes its conditionals into, decorators
stripped), never a name list, so a kernel that gains an if/else picks
the phase back up on its own. Over-broad on purpose: Compare / BoolOp /
min / max / where= all count.

Cross-check: the three kernels measured to emit DIFFERENT code under the
two modes (s332, s481, s482) all keep their fpfac phases.

3644 -> 3238 collected (-406), 0 ids added, every removed id a *_fpfac
one in the four *_simplify_multidim files. Measured A/B on 6 polybench
kernels, serial, same box: 30 items/452.0s -> 22 items/389.4s (-13.9%).

Also here:
* Phase label is hostsimd_*, not the detected ISA. The label reaches the
  test ID, so an ISA-derived one renamed every SIMD case per runner and
  a before/after ID diff showed a rename instead of the real delta.
  Renames 351 ids once; count unchanged.
* np/poly canonicalize corpora now uniquify sdfg.name per phase, like
  their simplify siblings -- the two phases shared a .dacecache dir.
* xdist_group(kernel) so one kernel's phases can share the memoized
  _base on one worker. Needs --dist loadgroup in CI to take effect.
Both specializers cost a full recursive walk per call -- every state, every
nested SDFG, every interstate edge -- almost regardless of how many names they
carry. Callers loop, so N names cost N walks.

Add plural entry points that take a {name: value} map and walk once; the
singular forms become the one-entry case. Batching substitutes simultaneously,
which differs from the sequential loop only when a value names another key, so
_interacting detects that and falls back to the loop.

Switch the three looping callers (ConstantPropagation, canonicalize pipeline,
cloudsc benches) to one batched call.

Gate: guid-stripped serialized CloudSC SDFG is byte-identical to the
one-call-per-scalar loop at N=8.
{a: 'a'} named its own key, so the interaction guard fired and specialize_symbols
recursed on the same one-entry dict forever. A value naming its OWN key is a
degenerate substitution, identical batched or looped -- only another entry's key
counts.

Add the two tests the plural entry points were missing: batched == looped on a
three-scalar program (serialized graph and numerics), and an interacting mapping
still taking the sequential path.
…raphlib

Five call sites still reached networkx directly instead of dace.graphlib:
promote_constant_index_access and split_tasklets (both in the canonicalize /
vectorization pipelines), vector_inference, experimental_cuda, and cuda.

Add graphlib.weakly_connected_component(G, node): the weak component of a
DIRECTED graph containing a node, i.e. what
node_connected_component(G.to_undirected(as_view=True), node) computed at the two
call sites that asked an undirected question. Exposing it this way keeps
undirected graph types out of the backend protocol entirely -- rustworkx would
otherwise need a whole parallel PyGraph handle class for two callers. Native on
both backends, no networkx fallback. The rustworkx side deliberately scans
weakly_connected_components on the PyDiGraph rather than to_undirected(), which
renumbers node indices once the graph has holes from a remove_node.

Result is returned in graph node order rather than as a bare set: the cuda.py
caller's iteration reaches codegen and set order over id()-hashed DaCe nodes is
not reproducible across processes.

Give the rustworkx handle's in_edges/out_edges networkx's data= keyword. That
also repairs dace/sdfg/graph.py's DiGraph.in_edges/out_edges, which pass it
positionally and so raised TypeError under backend='rustworkx'.

Convert VectorInferenceGraph from "is-a networkx.DiGraph" to "has-a graphlib
graph" -- graphlib.DiGraph() is a backend-selecting factory, so there is no
stable class left to subclass.

cuda.py line 989 was calling nx.node_connected_component through the graphlib
alias, which graphlib never exported -- a live AttributeError on extended that
main (real networkx) does not have. Repointing it at the new wrapper restores
parity rather than changing main's behaviour.
One canonicalize call on the CloudSC dwarf, compared against the un-transformed SDFG
(simplify=False) on identical physical inputs under -O0 -fno-fast-math -ffp-contract=off,
so every observed difference comes from the SDFG and not from the C++ compiler. Two arms:
the Maps forced sequential (canonicalize's own reassociation only) and the Maps left as
canonicalize produced them (adds OpenMP reduction order) -- the second is what proves the
emitted parallelism is sound rather than merely compilable.

The structural arm pins the end state: 234 Maps, 27 loops still sequential, and 234
'#pragma omp parallel for' in the generated host code. The last number is the one that
matters -- a Map count alone does not prove parallelism reached the backend.

Lives with the CloudSC corpus rather than under tests/canonicalize: that directory is
auto-marked canonicalization by path, which would pull a multi-minute parse and a
multi-hour canonicalize into the canonicalization workflow's 600s per-test timeout.

The integration workflow gains --dist loadfile so each file's module-scoped CloudSC build
is paid once instead of once per xdist worker, and its timeout goes to 7200s: measured on
a loaded 16-core box the parse is 1245s and the canonicalize 4433s.
CollapseNoOpCast leads the canonicalize `clean` block. A Fortran kind coercion
lands as `__out = dace.float64(__inp)` where `__out` already has that dtype;
every downstream matcher reads tasklet bodies textually, and
TrivialTaskletElimination's predicate is the exact string `out = in`, so it can
never collapse the cast itself. Rewriting to the plain assignment first is what
lets the Simplify five entries later fold the copy away.

LoopToTranspose joins the `loop_to_x` lifts, gated like the other semantic
lifts. Nothing else covers a hand-written permutation nest: LiftEinsum refuses a
single tensor operand, and AssignmentAndCopyKernelToMemsetAndMemcpy explicitly
REJECTS permutations because `_out = _in` reads the same for a copy, a broadcast
and a transpose. Left unlifted it stays a strided element-wise copy.

PowerOperatorExpansion joins the vectorizer's tasklet prep, before SplitTasklets.
`**`/`pow`/`ipow` carry no ISA character, so a TileBinop holding one falls back
to the per-lane pure loop; the unrolled product lowers to a native SIMD multiply.
The tile emitter already carried the consumer for the `_out = _a * _a` shape this
produces -- the producer was simply never constructed. Comments in
vectorize_multi_dim, tile_binop and convert_tasklets_to_tile_ops claimed the
expansion ran; corrected to what it actually does (literal integer exponent > 1
only).

Also drop two things another pass demonstrably provides:
BranchNormalizationPipeline -- BranchNormalization.apply_pass runs its own
`while progress` fixed point over all control-flow regions recursively, which is
what the outer MAX_ITERS loop existed to provide; and utils/iteration.py, whose
`assert_no_lane_memlet_reads` gates on an `_iter_mask` array the multi-dim
vectorizer never creates (it names them `_tile_iter_mask`), so it cannot fire.
fastgraph (rustworkx) and polyhedral (islpy) are both non-default, so their
tests skip wherever the extra is absent. One job installs both and asserts they
imported, so a missing extra fails loudly instead of turning the run into silent
skips. py3.14, simplify on.

canonicalization keeps polyhedral -- its 11 wavefront tests carry the
canonicalization mark and would skip there otherwise. vectorization needs
neither.
Last remaining loop over the singular specialize_symbol. Same N=6 set as the
canonicalize pipeline, so the same 2.00x on that stage.
The node_connected_component defect was found by hand; this catches its whole
class in one pass. dace.graphlib stands in for networkx (`from dace import
graphlib as nx`), so a call site can name a symbol networkx has but graphlib
never wrapped and nothing complains until that line executes. The new scan walks
every module in dace/ that binds dace.graphlib and checks each attribute read
against graphlib's exports. It fails on the parent commit naming exactly
cuda.py:989, and passes here across 139 accesses -- that one call was the only
instance in the tree.

general-ci gains the fastgraph extra so tests/graphlib's rustworkx half actually
runs instead of silently skipping on find_spec. No new workflow. graph.backend
stays defaulted to networkx: this tests the opt-in backend, it does not ship it.
Measured marginal cost 1.1 s per matrix cell (7.4 s -> 8.4 s wall), 6 cells.
…pAndReduce

"Absorb" implied nesting into the second loop, which the pass explicitly does not
do -- it only reorders a sibling state past it so FuseLoops sees an adjacent pair.
The name also hid the pass: it was reported as the missing
reorder-state-for-loop-fusion capability while sitting in the pipeline.

AccumulatorToMapAndReduce lifted a GPU accumulator to buffer+Reduce so the Reduce
libnode could tree-reduce. cuda.py and the experimental scope strategies now fold
a device map-exit WCR with cub::BlockReduce directly under emit_tree_reductions,
so the buffer was bought for nothing.
There is now ONE pass that forms perfect nests, and it does so in both
directions. Fission distributes `for i: {S1; S2}` when the statements are
independent; it is a no-op on `for i: {pre; for j: body; post}` whenever `pre`
feeds `body` -- the usual case, since that is why they share a nest -- because
the group analysis merges on any shared written container. Even when they ARE
independent, distributing yields three loops rather than the single 2-D nest a
GPU grid collapse needs. So `target='gpu'` additionally sinks the surviving
pre/post blocks into the inner loop under boundary guards, last in each fixpoint
round, giving fission first refusal.

sift_statements_into_perfect_nest keeps the matcher and the rewrite -- with the
S1 non-empty and S7 outer-axis-independence gates unchanged -- and loses its Pass
class; it is now driven through `sift_imperfect_nests`. The target policy moves
with it: the matcher is target-agnostic, `PerfectLoopNesting` decides.

The pipeline threads `target=` into the fission stage, so the GPU recipe gets the
sink and the CPU recipe is unchanged -- burying pre/post work under a
`j == <boundary>` guard would cost the outer level its sequential-fusion locality.
A WCR edge lowers to dst = dst OP src, so the destination's prior value is an
input, but AccessSets classified read/write purely by edge degree and a
WCR-written node with no outgoing edge came out write-only. DeadDataflowElimination
does no analysis of its own -- AccessSets is its only liveness source -- so this is
the one category where a read is invisible to it.
# Conflicts:
#	dace/transformation/passes/canonicalize/pipeline.py
# Conflicts:
#	.github/workflows/general-ci.yml
A numerical divergence in a 200-stage pipeline is only findable by binary search over the
intermediate SDFGs, and re-running canonicalize per candidate costs 74 minutes on CloudSC.
Dumps after every stage including no-ops -- a bisect needs a dense index, and 5 units rewrite
the graph while reporting no change.
4 runners -> 1. Drop the {cmake, native} builder axis: native is not a CI
build path any more, and the split test parametrizes both builders itself
while native_{build,corpus}_test.py run unmarked in General Tests.

Drop the gpu-codegen job: it selected the whole readable suite's gpu-marked
tests under the experimental CUDA backend, which is exactly what the GPU
workflows' whole-tree -m gpu run already collects -- on a self-hosted gpu
runner that the GPU workflows themselves document as not serving CI.

Pin the caches the remaining path depends on (command_cache, PCH,
configure_cache) and install ninja-build, since the compdb replay only
engages when ninja is on PATH.
ThrudPrimrose and others added 26 commits August 8, 2026 00:56
…imports

The backslash-in-expression f-string needs Python 3.12; CI runs 3.11.
The cloudsc perf files kept their pre-move import path.
Salvaged from the interrupted CloudSC bisection agent; its loop_to_map suite
(46 tests incl. two new regression files) and a corpus spot-check are green.
The CloudSC stage-088 bisection itself is unfinished.
…tial report)

Single-entry job; report regenerable. Sweep was interrupted at 206/277 --
tables cover the completed subset only.
…axis

Each compiler now gets an OpenBLAS built by itself: the clang arms resolve `openblas
threads=openmp %clang` (links libomp) and the gcc arms `%gcc` (links libgomp), so no arm's BLAS
drags the OTHER OpenMP runtime into its process -- the co-residency that measured ~34x per region.
toolchain_env switches OPENBLAS_DIR per arm from CANON_PERF_OPENBLAS_{GCC,LLVM}, both fed by
`spack location -i`; blas_note applies the same override so the evidence line names the library
that actually produced the numbers (it was printing the process-wide fallback for every arm).

The mechanism relies on the loader, so the submit script now deliberately keeps BOTH openblas dirs
OFF LD_LIBRARY_PATH: dace links the full .so path, cmake bakes each build's dir into the kernel's
RUNPATH, and LD_LIBRARY_PATH would override RUNPATH and force one variant onto every arm.
Verified: a fresh clang-arm kernel carries the %clang prefix in its RUNPATH, and a 9-arm debug
sweep (job 4381744) records j5wfyon (libgomp) on the gcc arms and t2t72xo (libomp) on the llvm
arms, 16/16 kernels, zero errors.

llvm pin moves 22.1.7 -> 22.1.5 (the fuller +mlir build; both installed 22.1.5 have +polly), and
both doubly-installed packages get variant pins -- gcc@16.1.0 +graphite because its twin is
~graphite, which would kill the autopar arms; llvm@22.1.5 +mlir against its ~mlir twin.

emit_tree_reductions now rides the codegen axis by request: auto_optimize is measured as it ships
(legacy codegen, no tree reductions), canonicalize with tree reductions on the experimental
generator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both full sweeps completed (4381745 loops, 4381746 array). canon-gcc reaches
7.24x over sequential C++ and 12.18x over numpy, ~2x auto_optimize, and the gain
is parallelism coverage rather than tuning.

Every GCC-arm loss traces to one of three classes, each confirmed against the
generated C++ and the responsible source site:

  - lost parallelism: a codegen-inserted copy scheduled CPU_Multicore inside a
    349k-trip loop (stockham_fft, 44x), a whole-array copy lowered to a
    single-threaded memcpy because the size heuristic rejects symbolic counts
    (va, 22x), a provably-false LoopToScan guard leaving dead parallel code, and
    reduce_atomic on a thread-private scalar
  - one parallel region per outer row in six 2-D nests, because the in_loop
    guard in sequentialize_nested_parallel_scopes covers library nodes but not
    maps (s115, 116x slower than sequential, ~100% team startup)
  - the anti-dependence snapshot is a single-threaded memcpy costing 1.5x the
    whole sequential kernel; ChunkAntiDependence would fix it but never matches,
    because BreakAntiDependence puts the copy in a preceding state and state
    fusion runs after the cpu_specialize band

A guard that merely never loses to sequential is worth +24% geomean on loops and
+16% on array. One 72-thread parallel region costs ~10us on this machine, which
is the constant behind every fixed-cost cluster in the data.

Also records three harness caveats: channel_flow times one converged iteration,
seq-cpp keeps blas=pure by design, and arm geomeans span different kernel sets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Installing openblas for gcc@16.1.0 alongside the existing gcc@14.2.0 build made
the bare `%gcc` spec ambiguous, so `spack location -i` resolved to nothing. The
submit script swallowed that with `2>/dev/null || true`, which would have left
CANON_PERF_OPENBLAS_GCC empty and run the gcc arms with no OPENBLAS_DIR at all --
a wrong measurement that still produces plausible numbers.

Both the preflight and the submit path now pin %gcc@16.1.0 and %llvm@22.1.5, the
versions the arms actually compile with, and an unresolvable spec is fatal rather
than empty. Verified by debug job 4383341: gcc arms resolve to syyeqfx, llvm arms
to t2t72xo, 16/16 kernels, 0 errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
'ik,ik->i' and 'xyzk,xyzk->xyz' share every index between the two operands:
the batch indices reach the output, the rest is contracted, and neither
operand has a private index. That class is C[batch] = sum_k A[batch,k] *
B[batch,k] -- no BLAS-2/3 form expresses it. is_bmm() called it a batched
matmul anyway, so the contraction path minted a degenerate M=N=1 MatMul whose
operand views collapse under simplify into two equal 2-D shapes the MatMul
dispatch rejects (NotImplementedError). Classify it as not-a-BMM, next to the
existing 'ij,i->i' carve-out; the unbatched dot 'i,i->' keeps its BLAS path.

Three pure-path defects the reroute walks into, fixed here as well:
- a repeated operand (a, a) added a read node the dict comprehension then
  dropped, leaving an isolated node that fails validation;
- the index letters named the map parameters, so a letter that matches an SDFG
  symbol ('k' contracted over a [..., k] array) shadowed it and emitted the
  self-referential bound k = 0:k -- zero iterations, silently wrong results;
- the accumulator init state was prepended to the SDFG rather than to the
  state's own control-flow region, so an einsum inside a loop body raised
  KeyError.

Fixes issues/einsum_rowdot_matmul_dispatch.md.
… gate floor

Complex-typed chains>1 failed to compile: the UDRs in dace::scan::detail are
invisible to unqualified lookup from the spliced multi-chain pragmas. Emit the
declare-reduction at the tasklet's own block scope instead (GCC and clang both
accept it with inscan).

The corpus gate's constant fp64 atol=1e-11 is smaller than the scalar oracle's
own rounding error at the paper preset (2.7e-11 vs float128), so it rejected a
candidate 10x MORE accurate than the reference (scan_multi_5carry, canon arms
only -- the arms that reproduce the oracle bit-for-bit passed). The absolute
term becomes max(constant, 1e-12 * max|ref|), still elementwise; explicit atol
overrides are never floored; integer gates stay exact; a non-finite reference
keeps the constant (overflowing wavefront kernels would make the floor vacuous).
Exact-int64 sweep over K x n x threads x seeded proves the lowering itself
correct.
…ed benchmarks

Two recovery gaps: a rung tiling a parent window emitted T3*int_ceil(T2,T3) as
its bound, which the next sweep no longer recognized as T2, stalling 3-level
symbolic cascades half-collapsed (fix records Mod(span,K)==0 under the same
contract as the cascade-stride assumption); and dace.map-tiled nests were never
seen at all (pipeline runs map_roundtrip=False, matcher reads LoopRegions only)
-- now detected in-pass and round-tripped behind a probe-on-deepcopy gate that
declines if any map would come back as a loop (fires on 0/76 corpus kernels).

New corpus kernels jacobi2d_triple_tiled_{const,sym} and
heat3d_double_tiled_{const,sym}; untile/unroll tests now assert static
structure (collapse counts, one perfect chain, no leftover tile index or
remainder guard, generated-C++ loop counts, exact unroll body-copy counts),
not just values.
…join cost model moves to the specialization bands

The canonical representation is the maximally parallel one; making a scope
sequential again is a target decision. Copy/memset expansions now emit a
parallel element map unconditionally -- a symbolic count is assumed big (only
a provably-small constant keeps the libc call), and the expansion no longer
reasons about its enclosing scope at all.

The sequentialization that lived in canonicalize/finalize moves to
cpu_specialization/SequentializeParallelScopes, the single home of the CPU
fork/join cost model: it pins maps whose work per region cannot pay for a
parallel region and scopes re-entered by a parallel map or a long loop
(s115/s119 forked 768 teams per call; stockham_fft 349,525). The re-entry and
short-loop rules live once, in libraries/standard/helper, consumed by the band.
SpecializeCpuTransfers then hands sequentialized contiguous transfers their
single memcpy/memset back, and runs again in codegen for the late-born explicit
copies. The GPU counterpart moves to
gpu_specialization/SequentializeNestedDeviceScopes, so canonical output keeps
nested parallelism intact for both targets. Placed at the end of the pipeline:
the verdict must read the final map shapes, and early Sequential pins would
block map fusion.
…oMap guards

NestedCall.add_state chained new states onto the visitor's cfg_target, which
can disagree with the region owning last_state inside a loop or branch -- the
interstate edge then joined a node the graph does not own. Use
last_state.parent_graph. Einsum lifting and LoopToMap carry the matching
guards for the shapes the corpus gate exposed.
Frontend minted loop iterators with explicit-None assumption kwargs,
which sympy treats as distinct from omitted kwargs: two same-named
symbols coexist and free_symbols/match silently give wrong answers.
Omit None-valued kwargs at minting. Add opt-in validation walk and
add_symbol check that raise on same-name different-assumption symbols
(flag experimental.check_symbol_assumption_collisions; stays opt-in
here until canonicalize assumption stamping moves to a registry).
Fusion removed the entry block and pinned start_block even when the
region could derive it from its single source; BlockFusion pinned
before remove_node, which clears the cached value. New helper pins
only when underivable, and after removal.
LICM Python-parsed C++ tasklet code, got an empty symbol set, and
hoisted tasklets out of the map defining their symbols; the resulting
degenerate staging shape was then corrupted by whole-tree renames in
ScalarFission and replicate_scope. Identifier-scan fallback for
non-Python code, value-carrying write counts, and rename trees that
stop at a different access node of the same container.
Clean-pattern passes counted scalar reuse only in other states;
LoopToScan accepted carrier reads at foreign offsets; wavefront skew
window widened with ISL containment check.
Bare a/b/o connectors in the sequential reduce expansion shadowed
same-named parent arrays after codegen inlining; dotted structure
member names reached copy/memset labels and produced invalid C++
function names.
C++ tasklet bodies lose their free symbols when rebuilt as Python
AST, so such maps stay scalar; symbol definition map refuses names
with WCR or multi-writer producers instead of resolving them to a
stale seed.
Canonicalization stamps nonnegative on integer symbols while the
rebuilt loop symbol is unassumed; match then binds the wildcard and
reads per-iteration writes as invariant. Normalize same-named symbols
to one instance before matching; only ever recognizes more a*i+b
subsets.
Scope generation allocates arrays outside the tasklet/nested-SDFG
calls that set calling_codegen, so idx helpers flushed there under
the host key were re-emitted in the .cu as C++ redefinitions. Test
regex updated to the 3-dim BlockReduce form with structural asserts
on the register-partial protocol.
Entry canonicalize no longer short-unrolls constant-trip loops the
tiler was called to widen; producer-only remainder bodies get
ordering edges so topological emission drains; copy writes no longer
count as supersedes in stage-global.
Assert parallel reduce entry points and reduction clauses instead of
absence markers, pin the cost-model sequential default with the
zero-threshold arm, wire the serialize path through explicit copies,
and drop stale xfails.
@ThrudPrimrose ThrudPrimrose added the no-ci Do not run any CI or actions for this PR label Aug 10, 2026
Bare 'out' connector collides with caller array named 'out' after inlining.
insert_scatter_guard hard-required 1-D index arrays, crashing on ICON
2-D connectivity (edge_blk[jb,jc]). Classify a single contiguous
varying dim per loop and guard that 1-D window instead; anything else
stays un-lifted.
Guard states landed inside the loop's own owning sdfg (a
LoopToMap-produced single-state wrapper), turning it multi-state and
blocking InlineMultistateSDFG + MapCollapse from fusing the
surrounding map nest, so MarkTileDims saw single-param maps below K.
Place the guard in the outermost LoopRegion that natively defines the
slice's fixed-dim symbols instead, leaving the inner wrapper trivial.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-ci Do not run any CI or actions for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant