Skip to content

[Only for CI] Extensions - #2475

Draft
ThrudPrimrose wants to merge 4743 commits into
mainfrom
extended
Draft

[Only for CI] Extensions#2475
ThrudPrimrose wants to merge 4743 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 ThrudPrimrose added no-ci Do not run any CI or actions for this PR and removed no-ci Do not run any CI or actions for this PR labels Aug 7, 2026
ThrudPrimrose added a commit that referenced this pull request Aug 20, 2026
…skip_gpu_outputs; restore OMP thread default; fix extras

- codegen.py: restore scalar_emission conditional form; pass skip_gpu_outputs=True
  so GPU map outputs are not scalarized (the unconditional preserve_abi run in PR #2475
  introduced broad CPU regressions).
- conftest.py: do not force OMP_NUM_THREADS=1; keep setdefault so callers control it.
- pyproject.toml: re-add fastgraph/polyhedral extras and testing z3-solver/matplotlib
  that were dropped when moving from setup.py.
ThrudPrimrose and others added 21 commits August 29, 2026 02:26
pad (constant mode), fill_diagonal, diagflat, diff, ediff1d and meshgrid were the last of the
shape-and-stencil numpy calls still reaching the pyobject callback path.

meshgrid pins the 'xy' against 'ij' axis order: the two spellings differ only in the first two
axes, which a stencil gets wrong silently. The edge modes of pad read a mirrored or clamped index,
which is a different kernel, so they are refused by name rather than lowered wrong.
hipSPARSE and hipTensor mirror their CUDA counterparts call for call, so each shares one expansion
body through a dialect table; the tile-op headers named no NVIDIA intrinsic and only assumed a
32-lane warp.
Four conflicts, all in main's scalar-fission work. Resolved per hunk, not per
file: extended carries 502/263/623 changed lines in these files against main's
9/17/12, so taking either whole side would have discarded real work.

scalar_fission.py and its test keep extended's shape. Extended factors the
memlet rename into rename_node_memlets, which renames the whole memlet PATH
rather than the single edge main renames, and the test refactors main's two new
functions through assert_memlets_name_an_endpoint -- both strict supersets of
what main added.

analysis.py and redundant_array.py take main's text. The code is byte-identical
on both sides; only a comment and a docstring differ, so upstream's wording
wins and the two files stop conflicting on the next merge.
A compile-time-unrolled Python loop replays its body, so every copy minted the
same for_<lineno> label and validation rejected the graph with "Found multiple
blocks with the same name". _add_loop_region added the region without asking
for a unique name, unlike every other block the frontend creates.

Both loop paths now pass the label the region actually received down to
_recursive_visit, so the inner state names follow it too.
ExpandReduceGPUAuto built its tasklets with connectors named a, b and o, so
reducing an array that is itself called `a` produced "Connector name 'a' is
already used as a symbol, constant, or array name" and the graph never
validated. Namespaced all three to __a_in / __b_in / __o_out.

Renaming only the dst_conn/src_conn keyword forms is not enough: three
add_edge(node, 'o', ...) positional calls kept the old spelling and turned the
failure into a dangling connector instead. The patch covers both spellings.
Replace the name-only merge in _independent_groups with an SMT-backed
live-range check. Two chains that touch the same written container are
kept together only when prove_disjoint_access_boxes cannot show their
subsets are independent across loop iterations. This avoids the CloudSC
miscompile where disjoint live ranges on the same transient were forced
into one group.

Also fixes _container_per_iter_only to inspect the subset on the node's
side of an edge (it previously looked at the source subset on a copy
edge) and switches grouping sets to OrderedSet for PYTHONHASHSEED
stability.
A scalar promoted to __sym_<name> lands directly in sdfg.symbols without
touching the visitor's scope map, so a range over the promoted slice's own
shape was reported as an undefined variable. Accept a symbol declared on the
SDFG as defined.

numpy.where with a symbolic branch (2 * N) reached dtype_to_typeclass with a
sympy expression, whose type() is sympy.Mul and is in no dtype map. Infer the
branch dtype with symbolic.symtype and spell the branch with symstr, since
sympy prints ** and / that C++ does not accept.

The closure qualname sanitizer only replaced attribute and subscript
punctuation, but a hoisted qualname can be a whole call expression whose
parentheses, spaces and operators then failed name validation. Replace every
non-identifier character, and materialize the hoisted value when it evaluates
to a numpy view, which the calling convention refuses.
…a ROCm host

The GPU runtime environment asked CMake for find_package(CUDA) and put cuda_runtime.h in the host
frame no matter the backend, which aborted the configure of every CUB-backed libnode on a ROCm-only
machine; the canonicalize perf tail named the CUDA vendor row a second time, so every rocBLAS
lowering fell back to pure there; and the K=1 tile-op GPU header reached cuda_fp16.h and the two
FP16x2 min/max intrinsics ROCm does not ship.
Insertion order is why DaCe uses this container everywhere: it stands in for a
canonical order so codegen does not move with PYTHONHASHSEED. That order is a
property of how a set was built, not of what it holds, but the upstream class is
a Sequence, so == between two of them compared element order. Invariant checks
were therefore asserting on the order the two sides happened to be accumulated
in, and MapFusionVertical's partition self-check failed for a nest whose
intermediates were classified in a different order than the exit edges reached
them, refusing a legal fusion through its safety net.

dace.ordered.OrderedSet overrides __eq__ to compare membership when the other
side is a set, and keeps the order-sensitive meaning when it is a real sequence
such as a list. Iteration order is untouched.
tiled_producer_subset() sizes the reduced intermediate with size(), which only a
Range offers, so a SubsetUnion producer raised AttributeError from inside the
matcher instead of being refused by a guard. The equivalent check already ran a
few lines later; this moves it to the first use of the subset.
The trapezoid rewrite makes a triangular pair interchangeable, but the new outer
loop is left a min/int_floor bound that LoopToMap will not take. On a nest whose
levels were both DOALL that traded two Maps for one Map plus a sequential loop,
which unit stride does not pay for. Count the parallel levels before and after
the bubble and revert when the move loses one; TSVC s1232 still permutes, since
its outer level was never parallel.

The adversarial test that pinned the old "a triangular bound must refuse the
swap" contract predates the trapezoid rewrite. It now asserts the stronger
property the rewrite is supposed to have: the bounds change direction with the
loops, and the interchanged nest enumerates the same iteration set at sizes the
single N=8 case cannot distinguish. The s1232 test asserts the rewritten bounds
the same way instead of resting on order and a stopwatch alone.
Scalar-to-symbol promotion wrote the promoted value into an AccessNode with a host tasklet without
checking storage, so an argmax on GPU produced a host write to a GPU_Global container that
validation rejected; the CUB workspace test asserted a .cu extension the HIP backend never emits.
The producer-reads/consumer-writes hazard guard is already in place
(5cbfd5b); add a direct seidel_2d-shaped test so the anti-dependence
refusal stays pinned.
The assertion that the two halves recombine into `first_nodes` had to convert both sides to
plain `set`s to dodge the order-sensitive `==` that `OrderedSet` used to inherit; with set
equality in place it compares the containers as they are.

`new_first_nodes` was a plain `set`, so the order it fed back into `nodes_to_scan` and
`first_nodes` moved with `PYTHONHASHSEED`.
# Conflicts:
#	dace/libraries/blas/nodes/axpy.py
#	dace/libraries/blas/nodes/ger.py
The closure evaluator copied any numpy view it produced, so an array reached through an
attribute chain came back as a fresh object on every call. `combine_nested_closures` dedups
by `id(evaluator())` against `array_mapping`, which is keyed on the object the closure was
registered with, so the dedup never matched and each nested convertible holding the same
array minted another `__g_<name>_N` argument. pyFV3's D_SW then asked for a
`__g_self__del6_u_1` that no caller supplies.

Copying it was also wrong on its own terms: closure arrays are resolved before parsing, so
nothing yet knows whether the kernel writes the array, and a write to the copy is discarded
in silence. Only a value hoisted out of an expression is a temporary nobody else holds --
that one is materialized once, which is also what keeps its identity stable.
`multidim_gbar.cuh` moved to the gpu* aliases, but `types.h` includes it before `DACE_DFI`
and `dace::vec` exist, so `cudacommon.cuh` cannot be pulled in alongside it and every CUDA
build failed on an undefined `gpuError_t`. It belongs in `dace.h`, after both.
`-DCUDA_HOST_COMPILER` was emitted but nothing in `codegen/CMakeLists.txt` ever read it, so
`compiler.cpu.executable` could not steer nvcc at all and host and device objects were free
to disagree on the ABI. CMake now consumes it before `enable_language(CUDA)`, and the CUDA
targets forward the host compiler unconditionally, the way the CPU target already pins
`CMAKE_CXX_COMPILER`.
The refusal covered every mix of an index array and an integer index, which took a working
case with it: the indirection numbers dimensions against the full-rank array, so only a
dropped axis BEFORE the index array shifts that numbering. A trailing one leaves it alone,
and A[2:4, ind[1], 3] += B computes the same values as numpy.
…LoopLocalMemoryReduction, helpers, and writeset_underapproximation

D2 determinism sites:
- transient_reuse.py:50: bucket order decides which array survives a reuse merge.
- loop_local_memory_reduction.py:229,231,234: [0][0] index selection feeds the modulo buffer size.
- helpers.py:1019,1038: forward/reverse scan sets in isolate_nested_sdfg.

D3 writeset_underapproximation.py:600-613: loop-nest root order drives per-loop-nest processing order into approximation_dict. Unfrozen and converted despite earlier freeze note; the bug is real and the change is mechanical.
ThrudPrimrose and others added 30 commits September 4, 2026 12:29
…e fully writes

`OffloadToAccelerator` staged every signature array down to the device before the
kernels that use it, including arrays nothing ever reads. For a write-only
output that is a full host-to-device transfer on every call whose result is
immediately overwritten and discarded.

The copy is only dead when BOTH hold. Nothing may read the container -- no
access node with an out-edge and no interstate edge naming it, since an
interstate read has no node for an access-node walk to find -- and the device
must write all of it, because the copy-out sends the whole device buffer back
and it is the copy-in that currently makes untouched elements round-trip
unchanged.

Proving the second is where this gets subtle, and a covering subset does not do
it. An indirect write carries the whole array as its subset because that is
where it MIGHT land, while its volume says how much it actually writes:
`write_subset_dynamic`'s `A[x[i], y[j]]` spans `0:20, 0:20` and writes 256 of the
400 elements. Trusting the subset alone drops its copy-in and returns
uninitialised memory for the 144 the scatter misses -- which is how that existing
test caught this. `written_in_full` therefore requires the subset to cover AND
the volume to equal the descriptor's total size, and refuses a dynamic or
write-conflict-resolution memlet outright.

Three tests: the elision, a partial-write control, and a structural one asserting
the indirect write over-approximates its subset and keeps its copy-in. Only the
first fails when the elision is removed, so the other two are controls rather
than passengers.

This restores the property `GPUTransformSDFG` had and the pass did not, which
went untested when that transformation was removed.
The wrong product only appears on the self-hosted runner, and a bare np.allclose assert throws
away everything needed to tell a layout error from a corrupted broadcast.
…ing it

nvcc does not fail on a host without a GPU: -arch=native warns once and substitutes a default
architecture older than sm_53, where <cuda_fp16.h> declares no __half operators and no half2
intrinsics, so a generated fp16 kernel dies on undefined __hadd2 and ambiguous __half conversions.
The architecture also stopped being a CMake CACHE entry, which had pinned the first configure's
choice and silently ignored every later compiler.cuda.cuda_arch in the same build folder.
A toolkit that cannot build sm_80 still drops to the oldest architecture it has that can compile
the runtime's fp16.
An expansion bakes its tasklet text once, and prepare() ran before the dialect scope was
entered, so a node that spells its own element transform had no way to ask which dialect it
was rendering for. ArgReduce always chose std::abs, which is C++ only, and every argmax over
a transformed element then failed the C rendering at the self-containment check rather than
at anything the caller could act on.

Measured on the llr-focus40 roster: the C forms go from 37 of 40 to 38, with tsvc_2_s318
rendering in both dialects instead of C++ alone.
…nd kernel-local transients

Swapping GPUTransformSDFG for OffloadToAccelerator turned five General-CI tests
red, all for the same reason: the new pass judged three things by the wrong
question.

A Stream descriptor has no subset to collect, so asking which arrays an edge
uses raised instead of answering "none", and single-sided placement tried to
give it a device twin. Both now treat a Stream as data that stays where it is,
and apply_gpu_transformations no longer refuses an SDFG that holds one.

A library node was called device-wide on the strength of the node type alone,
but only the chosen expansion knows whether it emits device code. Expansions
now declare runs_inside_kernel, the CUDA block-reduction ones set it, and the
taskloop classifier asks the expansion instead of the node.

A transient live only inside a kernel scope kept host storage, which registered
IllegalCopy as a target that then emitted nothing -- tripping the codegen
assertion that every registered target is used. register_kernel_local_transients
moves those to Register once, after placement, for transients that never escape
their kernel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dace::float16 converts implicitly both to and from the built-in floats, so
1.0 / x with x an fp16 gives nvcc two equally good operators -- the built-in
one converting x up, and the class's own converting the literal down -- and it
rejects the expression as ambiguous. A nested fp64 program called from an fp16
one hits this on every literal it divides by.

The C++ unparser now prints an untyped numeric literal as a cast to the dtype
of the operand beside it, but only when that operand is one of the class-typed
floats; every other typeclass is a built-in whose usual arithmetic conversions
are unambiguous, and its literals are printed unchanged. The cast is what
Python and NumPy compute here too, where a weak scalar takes the array's dtype.

The readable CPU generator renders some statements itself rather than leaving
them to unparse_tasklet, so it now carries the dtypes of the operands it
splices in -- a surviving connector under its own name, an inlined one under
its access text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A dynamic map range spells its bounds with the map's connector names, and
used_symbols treats a connector as locally defined, so such a bound never
reached the scope arglist. On the host that is harmless -- the connector is a
local variable in the same function -- but a kernel is a separate function in
a separate translation unit, so the device code named an identifier nothing
declared ("kidia" is undefined, on CloudSC). The kernel arglist now includes
the dynamic-range connectors as by-value scalars, excluding the stream, which
the launch wrapper appends itself.

The host side had the mirror bug: when a connector reuses its container's own
name, defining it again emitted "int kfdia = kfdia;", shadowing the variable
that held the value with a self-initialised copy. That definition is now
skipped, since the value is already in scope under that name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A boundary memlet must name the container on the parent side of its edge, and
after versioning it does not: ScalarFission renames the inner end of a staging
copy and the memlet follows the inner name, so nesting that subgraph left the
parent referencing data it no longer had, with a connector minted around the
moved descriptor. Both nest_state_subgraph and the map-boundary tasklet
insertion now re-anchor such an edge on the AccessNode outside the scope, and
only where the access is a single element -- a wider memlet does not carry
which outer element it moves, so it is left alone rather than guessed at.

Two neighbouring cases in nest_state_subgraph fall out of the same question.
An AccessNode a boundary edge already carries needs no second, referenced-in-
full interface: the extra one reads an in-scope allocation nothing writes, and
pruning the unused boundary connector took the ordering edge with it, emitting
the map before its input was assigned. And an input edge already ending on the
whole container needs no access node inside, where the name -> name copy is a
no-op whose write turns an input connector into a written descriptor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two tests pulled OpenMP into the interpreter at collection time -- one loading
libgomp.so.1 through ctypes at module level, one importing a module that
imports torch -- which is what made a later dace build in the same process
find the wrong runtime. The ctypes load is gone and the torch-bearing import
moved into the test body.

Casting ITE arms early, in resolve_mixed_dtype_binops, duplicated what
CastScalarIteLiteralArms already does after the types are settled, where the
information to do it correctly exists. The early pass is back to leaving ITEs
alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight tests guarded themselves with a skipif on nvcc but carried no gpu
marker, so the GPU CI selection never picked them up and they only ever ran on
CPU runners, where they skipped. The marker makes them run where the hardware
is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Twenty kernel tests carried a marker that skipped them unless --run-simple was
passed, on the grounds that a harder sibling covered the same ground. Nothing
in CI passed that flag, so the sweep they were meant to join never included
them and they had been skipping for their whole existence. All twenty pass.

The marker, its option and the skipping hook are gone. Where the note said
which test is the harder canonical one, it stays as a comment, since that is
the part worth keeping: it tells the next reader what already covers the case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The nested-boundary test covers the CUDA side and needs a device, so the half
of the fix that lives in the readable CPU generator -- carrying the dtypes of
the operands it splices in, since it renders those statements itself -- had no
test that CI runs on a CPU runner.

Three tests on one map that computes 1.0 / (a + 1.0) in fp16: the literals are
emitted as dace::float16 casts, an fp64 version of the same program keeps its
literals bare, and the compiled result matches the numpy fp16 oracle exactly.
The assertions are on the generated text, because a host compile does not
reproduce the bug -- dace::float16 is a struct there and g++ still ranks its
conversions, so the ambiguity is nvcc's __half specifically. The run only
shows the cast did not move the answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The perf tests asserted that canonicalize is never slower than auto_optimize,
which twelve of the corpus kernels have never satisfied, so every selection in
CI filtered them out with "not perf" and nothing ran them at all. An assertion
no job evaluates does not defend anything.

They now assert a ratchet: each kernel that is currently slower carries its
measured ratio, and the test fails when a kernel exceeds its own number by
more than the run-to-run slack, or when a kernel that was not slower becomes
so. Nothing is skipped, and a regression on any kernel is still a red.

The job is advisory for now. The baselines were measured on a loaded machine
and several are wrong by a factor of two; the flag comes off once they are
re-measured on an idle one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The anchoring fix shipped with no test: the whole surrounding suite passed
identically with it reverted, so nothing would have caught it coming back.

Five tests now build the shape that produces it -- a loop-invariant scalar
staged in through the map entry under the same name on both sides, run through
ScalarFission, which versions the two ends apart and leaves the boundary
memlet on the inner name. Reverted, they fail as the three distinct artifacts:
a memlet naming an array the parent SDFG no longer has, a second interface
that turns a read-only container into one the map writes every iteration, and
a no-op copy inside the nest. The fifth covers the guard that declines to
split a locally named memlet whose outer container is wider than one element.

Failure mode (b) is pinned structurally rather than by a wrong answer. It
produces the wrong graph reliably, but the miscomputation the commit describes
needs the connector to be pruned as well, and pruning does not fire on a shape
this pass produces on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…form

MPR -> CPF throughout: dace/codegen/mpr.py becomes codegen/cpf.py,
dace/mpr_lowering.py becomes cpf_lowering.py, dace.mpr() becomes dace.cpf(),
and tests/codegen/mpr moves with them. The rendering is the canonical parallel
form of an SDFG, and "maximal" claimed something the pass never promised.

Names only -- no lowering, dialect or emitted text changes.
Both backends declare ``FutureValue<T, Iter = T*>`` and take the iterator by
``const Iter``, so leaving it defaulted binds ``T* const`` -- which the
``const T*`` the scan wrapper is handed cannot convert to. rocPRIM rejects it
("would lose const qualifier") and the whole translation unit fails to
compile on gfx942; CUB happens not to reach the same instantiation, so the
break has only ever shown on HIP.

Spell ``const T*`` instead, from one helper rather than the two call sites
that each built the type string. That accepts a seed that is const and one
that is not, on both backends.
The perf marker covered two different things: the vectorization ratchet, which
is a gate, and the canonicalization corpus sweep, which is a measurement
harness -- a few hundred kernels times eight arms, each built and timed over
two dataset presets with warm-ups and repetitions, resumable from a result
directory because a full sweep is hours.

The new CI perf job selected both and would have run the sweep on every push:
336 cases where it means 55. The sweep now carries long as well as perf, which
every CI selection already excludes, and the job asks for "perf and not long".
Nothing that was running stops running -- the marker kept both out of CI until
this job existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each of the four recipes this branch ships for CloudSC gets a leg that
re-drives the dwarf's own physical inputs and compares every output array
against the same oracle: the un-transformed simplify=False SDFG run
sequentially under the IEEE build. Canonicalize for the host; vectorize on top
of that; canonicalize for the device followed by CloudSC's own offload recipe;
and the generic device move over the host-canonical graph. The two device
recipes are deliberately different, so a red device leg separates the offload
from the target preset that fed it.

Every leg also asserts structurally, because a numeric comparison alone passes
for the wrong reason: the host legs check that Maps exist and that "#pragma
omp parallel for" reached the generated code, the vectorize leg that tile ops
were emitted, the device legs that something is scheduled on the device and
that __global__ kernels came out. Tolerances are the parallel-arm bound the
sibling canonicalize test already uses, and the device legs build with the
strict FP flags, so the residual is reassociation and device libm, nothing
else.

No CI job selects it yet. It is slow enough that the runner it goes on has to
be sized off a measured canonicalize, and that measurement is not in yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four-way test had no job selecting it. It gets two, split by where the
hardware is: cloudsc-host-pipelines on an ubuntu runner takes the host legs --
parse and simplify the dwarf, canonicalize for the CPU and check the numbers,
then vectorize that same graph and check them again -- and
test_cscs_gh200_cloudsc_pipelines takes the device legs on the CSCS GH200,
which is the only runner in this repo with a GPU.

The broad GPU legs deselect the file by PATH rather than by the integration
marker. The marker would also have dropped cloudsc_offload_numeric_test, which
those legs run today inside their per-test timeout; only this file needs a
whole-dwarf canonicalize per case and a budget to match.

Budgets are deliberately loose -- four hours per test, four hours of SLURM --
because a canonicalize of the dwarf is measured in tens of minutes, and a gate
that dies on the clock reports nothing about the numbers it was meant to
check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hipCUB's DeviceReduce static_asserts a std random_access_iterator_tag that
thrust's transform_iterator does not report, so the strided argmax did not
compile on gfx942; a single-element WCR stage-out now lifts to an explicit
copy instead of dace::CopyND::Accumulate.
rocPRIM's arg_index_iterator, which DeviceReduce::ArgMax wraps its input in,
static_asserts that iterator_category IS std::random_access_iterator_tag.
Both libraries' transform iterators report a thrust tag instead once thrust is
in the unit, and DaCe puts it there unconditionally via thrust::complex, so
every strided or transformed ArgReduce failed to compile on gfx942 (s318).
Declaring the tag ourselves is independent of what else the unit includes.
OffloadToAccelerator returned None whatever it did, so apply_gpu_transformations
did too, and a Pipeline read every offload as "nothing changed". It now returns
the containers left in device storage. Two tests asserted the old
GPUTransformSDFG application count, which no longer exists; they assert the
postcondition instead -- something is placed, and a map or library node carries
a GPU schedule.

The predicate is GPU_RESIDENT_STORAGES, not dtypes.GPU_STORAGES, which holds
only GPU_Shared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eleven jobs of four to eight minutes each paid a checkout and an environment
sync apiece. CPF's three legs become one job of three steps; HPCAgent-Bench's
two tags become one job that still writes and summarizes a CSV per tag; and
General Tests grows an "extensions" job holding cpu-codegen and the rustworkx
graphlib suite.

layout and canon-perf are fused separately, into "advisory", because both are
non-blocking and continue-on-error is a job-level setting -- putting either in
with a real gate would swallow that gate's failures. LibNodes stays on its own
runner: it builds MKL and TBLIS first, and folding it in would push the shared
job past half an hour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Left implicit it is the one copy the readable generator has no explicit form
for, so cpu.py fell back to dace::CopyND::Accumulate and CPF refused the
render (segment_reduce_ragged). Multi-element and GPU-resident WCR edges stay
unlifted -- a memcpy expansion would turn the accumulate into an overwrite.
The test put its parallelism in a map nested inside another, and asserted a
tasklet specialized to block 1. Offloading assigns GPU_Device at a host level
and Sequential below one, so the inner map became a serial loop and the kernel
ran in a single block: the guard was emitted, never taken, and three elements
kept the unspecialized value.

One flat map of two blocks' worth instead, so blockIdx 1 and threadIdx 2, 5, 8
all exist. Same assertions, and the expected array is built from the block size
the config actually carries rather than from a literal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The selection criterion is what a standalone unit can compile, not the name
"pure": Auto gives a contiguous copy one memcpy where a mapped tasklet spent an
element loop. It stays an allowlist because environments is empty for Reduce's
OpenMP, FindFirst's OpenMP and Scan's CUDA, all of which name a dace:: symbol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plain `inline` there is ill-formed C: an inline function with external linkage
may not reference an identifier with internal linkage (C11 6.7.4p3), and every
body calls the `static` <array>_idx helpers. 14 of 38 rendered llr forms were
rejected by gcc -Werror for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code generation writes an `assert((step) > 0 && ...)` guard on every non-unit
step map straight into the stream, so no call-site table discovers it and the
declaration was never pulled in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A symbolic size is assumed large and must stay a parallel map; only a small
compile-time-constant copy takes the single libc call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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