Skip to content

[Draft, for CI] FaCe - #2448

Draft
ThrudPrimrose wants to merge 522 commits into
mainfrom
FaCe
Draft

[Draft, for CI] FaCe#2448
ThrudPrimrose wants to merge 522 commits into
mainfrom
FaCe

Conversation

@ThrudPrimrose

Copy link
Copy Markdown
Collaborator

Draft PR to run CI on the FaCe branch. Base is ci-fix for now; retarget to main later.

Not for review yet.

ThrudPrimrose and others added 30 commits May 15, 2026 18:58
Exercises generated kernel_flat_dace binding on the ICON
z_w_concorr_me stencil (flat explicit-shape arrays + dim/loop
scalars), linked against the SDFG .so vs plain-gfortran reference.
ICON-shaped counterpart to cloudsc_flux_bindings_e2e_test.
Pin the self-accumulate / loop-carried prefix-scan family the
CLOUDSC Section-8 flux loop exercises: (1) flux(jk+1)=flux(jk)+
(qn-q0)*g with q0 saved EARLY and qn produced LATE across a big
intervening LoopRegion, per inlined-callee block; (2) a long
8-operand + self accumulate mixing early-saved and late-produced
operands.  Both vs f2py/gfortran, both PASS -- documents the
bridge's data-dependency ordering is correct for these shapes in
isolation, so cloudsc_full flux divergence is full-kernel-context
specific (carry+accumulate two-write fusion ruled out: unchanged).
…init

The transient zero-init pass (gfortran -finit-local-zero parity)
only descended one nested-SDFG level; `_all_sdfgs` now recurses to
any depth so every transient Array/Scalar AccessNode in deeply
nested SDFGs is stamped `setzero`.  Robustness improvement (does
NOT affect the cloudsc flux divergence -- proven independent of
zero-init: zqxn2d/zqx0 are already memset and strengthening this
pass leaves the 130-cell PFSQLF mismatch unchanged).
…e-global auto-capture; py13 package imports

cloudsc_full and the 5 bisection carve-outs now pass at strict
rtol=atol=1e-15 against gfortran on physically-plausible, NaN-free
inputs.  The old uniform-random get_inputs drove microphysics
intermediates (ZINEW) to NaN; Fortran MIN/MAX with a NaN operand is
processor-dependent by the standard, so that comparison tested
unspecified behaviour, not a bridge defect.  get_inputs_physical
(verified 0 NaN/Inf over 12 seeds incl. the ice-deposition chain,
SDFG and gfortran) replaces it; the dead generator is deleted and the
obsolete xfails removed.

Bridge / bindings:
- IntegerizePowerExponents post-cleanup pass: retype integer-valued
  float ** exponents to int so codegen uses repeated-multiply ipow
  (bit-matching gfortran) instead of libm pow.
- F90-binding e2e coverage: SDFG-direct AND generated .f90 binding
  both vs gfortran -- LOGICAL kinds 1/2/4/8 (incl. KIND=2 copy-out),
  scalar/array logical in/out/inout, integer kinds, mixed-rank
  struct, full velocity_tendencies (hand iface + auto module-source).
- Module-global provenance auto-detection (_QM<mod>E<entity>):
  extract_vars decode threaded through FrozenSignature ->
  block_builders; FlattenStructs peelMemberWrappers + per-leaf
  FlattenEntry for heterogeneous nested structs; scalar
  LOGICAL intent(out/inout) rank-0->length-1-array bridge fix.
- FP flags reduced to the LLVM-flang-portable core
  -O0 -fno-fast-math -ffp-contract=off (gfortran-only
  -ffree-line-length-none isolated for the long-line source); bridge
  transient setzero stamping dropped (full sweep 0 regressions).

Hygiene:
- py13: bare build_bridge/hlfir_to_sdfg imports -> package-absolute,
  sys.path hacks removed across bridge + tests; no `import X as _X`.
- Audit: cloudsc strict-FP fixture + flag string consolidated into a
  conftest + CLOUDSC_F90FLAGS constant; dead FrozenArg.module_origin
  dual-state removed; change-narration comments trimmed.
- .gitignore: .dacecache_gw* (pytest-xdist caches) + stray root cmake
  build output.

Full sweep: 784 passed / 1 xfailed / 1 xpassed / 0 failed.
…ackage (mirrors cloudsc_full/)

The full-ICON (velocity_tendencies) e2e set is moved into a dedicated
tests/hlfir/icon_full/ package, structured like tests/hlfir/cloudsc_full/:
colocated Fortran sources + test_*.py modules + __init__.py xdist marker.

  velocity_full.f90, velocity_full_caller.f90        -> icon_full/
  velocity_full_test.py            -> icon_full/test_velocity_full.py
  bindings/velocity_full_bindings_e2e_test.py
                       -> icon_full/test_velocity_full_bindings_e2e.py
  bindings/velocity_full_auto_module_e2e_test.py
                   -> icon_full/test_velocity_full_auto_module_e2e.py

Path/import fixes after the move: the bindings e2e test now resolves
the driver/caller from its own dir (_HERE) instead of _HERE.parent;
the auto-module test points spec_from_file_location at the renamed
sibling.  Same e2e workflow as before (build SDFG + emit_bindings +
gfortran reference, ctypes compare).  icon_full: 3 passed.

Also: .gitignore now excludes the remaining Fortran/flang/object
build artifacts (*.smod, *.hlfir, *.o alongside the existing *.mod)
so test-run output cannot be accidentally staged; fix a stale test
docstring referencing the old auto-module module name.
…nto _util

_compile_so and the flang-portable _FFLAGS list were copy-pasted
(logically byte-identical) across the three new bindings e2e tests
(logical_io, numeric_io, weird_struct).  Consolidate into the shared
tests/hlfir/_util.py those files already import:

  FLANG_PORTABLE_FFLAGS  -- the -O0 -fno-fast-math -ffp-contract=off trio
  gfortran_compile_so(out_so, *sources, mod_dir=, link_so=)

_build_binding_lib is intentionally left per-file: it genuinely
differs between logical_io and numeric_io, so a shared abstraction
would paper over a real divergence.

Full sweep: 783 passed / 1 xfailed / 1 xpassed / 0 failed.
…ss + icon_full/_harness

cloudsc_full: lower_keys / f2py_argnames / sdfg_call_args were
logically identical across all six test_cloudsc_*.py (only local
variable-name drift), and the build-SDFG + run-f2py-ref + scalar-ABI
+ call block was a ~20-line near-duplicate.  Consolidate the three
helpers + a run_cloudsc(src, name, f2py_ref, sdfg_dir) driver into
cloudsc_full/_harness.py.  The five standard tests use run_cloudsc;
test_cloudsc_top_half keeps its bespoke body (it captures ZSOLQA /
ZSOLQB through extra INTENT(OUT) dummies, which run_cloudsc cannot
model) but still shares the three helpers -- forcing the driver onto
that genuine outlier was reverted after the sweep caught it.

icon_full: the flat-arg ABI order (_INIT_ARRAY_ORDER, correctness-
critical), _OUTPUT_NAMES and the _allocate input builder were
byte-identical in test_velocity_full and test_velocity_full_bindings_e2e.
Move them to icon_full/_harness.py so the ABI order has one source
of truth (verified byte-identical to the originals before the move);
the auto-module test still reaches them via the bindings module's
re-exported attributes.  Trivial path 1-liners deliberately left
per-file.

Full sweep: 783 passed / 1 xfailed / 1 xpassed / 0 failed.
Port f2dace-windmill's single-translation-unit model: inline every
externally-USE-d module's real source so flang sees one self-contained
file.  Reimplemented minimally as a text/comment-aware USE-graph walk
(no fparser dependency) -- pass-through for self-contained input, so
wiring it default-on in compile_to_hlfir is a verified no-op across the
whole inline-source suite; only a real multi-file project under the
scratch dir activates it.

Also mkdir(out_dir) in compile_to_hlfir for symmetry with f2py_compile.

module_merge_test: transitive physmod <- drivermod <- drv project,
modelled on recursive_ast_improver_test.  Asserts all-files-together
f2py == merged-single-file f2py (bit-exact) and SDFG == reference,
plus pass-through / idempotence / intrinsic-USE / dependency-order
guards.  Sweep 786P/1xF/1xP/0F (783 baseline + 3 new, 0 regressions).
Audit-driven, behaviour-preserving (full sweep 786P/1xF/1xP,
baseline-identical):

* delete dead freeze_signature (zero callers; built an incomplete
  object that contradicted the real freeze path)
* delete dead _view_aliases/_view_shape_strs writes; correct the
  build() docstring that described copy-in/out staging which does
  not exist (view linking is lazy in acc())
* centralise the 3-site section-alias view_dim_map splice decode
  into iter_view_dim_map (intentional 0/1-based output divergence
  documented, not merged)
* lift duplicated _fortran_strides to one module-level helper
* lift _raw_hazard to a documented module-level _sibling_rw_hazard;
  cross-reference it from the realised-graph WAR/WAW guard
* replace eval(reduce_identity) with _parse_reduce_identity
  (named-constant -> int -> float -> loud NotImplementedError);
  eval-free, covers every identity both producers emit plus any
  numeric literal, preserves the integer iinfo max/min path
* drop redundant deferred acc import and double SimpleNamespace
  import; refresh stale docstrings
…ording, fix xdist collection race

* derived_type_test: rename test_batched_csr_allocatable_xfail ->
  _jagged (it had no xfail marker and no assertion); add the f2py
  oracle compare every sibling test uses.  Bridge is confirmed
  correct on the allocatable-array-of-records path (passes
  rtol=1e-12); it was untested, not buggy.  Stale cross-ref fixed.
* correct 13 comment sites that claimed 'bit-exact'/'bit-identical'
  where the check is assert_allclose / np.isclose (tolerance-based,
  never exact).  Genuinely-exact checks (assert_array_equal, bool,
  algebraic/representational identity) left as-is.
* remove the two redundant module-level sys.path.insert calls in
  bindings/multi_file_test.py and flatten_plan_roundtrip_test.py
  (conftest.py already puts tests/hlfir on the path); they ran at
  parallel-collection time and caused the xdist 'Different tests
  collected between gw0 and gwN' error under -n 8.

Full sweep 786P/1xF/1xP/0F, baseline-identical.
… FaCe

# Conflicts:
#	dace/libraries/standard/environments/cpu.py
#	dace/libraries/standard/helper.py
#	dace/libraries/standard/nodes/__init__.py
#	dace/libraries/standard/nodes/copy_node.py
#	dace/libraries/standard/nodes/memset_node.py
#	dace/sdfg/construction_utils.py
#	dace/transformation/passes/insert_explicit_copies.py
#	tests/library/copy_node_test.py
#	tests/library/memset_node_test.py
#	tests/passes/insert_explicit_copies_test.py
…onstruction_utils

Use SDFG.parent (O(1)) instead of the recursive _get_parent_state scan
to find the state containing a nested-SDFG node; verified equivalent.
copy_node.py imports the helper from dace.transformation.helpers
(top-level, no import cycle).  dace/sdfg/construction_utils.py removed
(it only held these two helpers on this branch).
… FaCe

# Conflicts:
#	dace/transformation/helpers.py
…nvertScalarsToLengthOneArrays; add unit tests
# Conflicts:
#	dace/libraries/standard/environments/__init__.py
ThrudPrimrose and others added 30 commits August 10, 2026 17:26
The exact-leaf assertion assumed cache mode 'name'. A workflow whose
DACE_cache resolves to anything else (env or a persisted config value
the unlaunched fixture does not clear) flipped the leaf to a hash
suffix and broke the path match. Pin it explicitly like the sibling
tests pin their env, same env-wins-over-config precedence used to
root-cause the distaware default flip.
Under srun/mpirun every build subprocess otherwise inherits (a) a blocked
SIGCHLD mask, hanging cmake configure in select() forever, and (b) the
launcher's PMI/PMIx rank identity, hanging any PMI-linked child in init.
Both fixes ported from the extended branch at the single fork point so a
new caller cannot reintroduce the hang. Required for warming and
measuring dace-fortran kernels inside srun steps on daint.alps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All workers/ranks see every GPU and pile CUDA contexts onto device 0,
which flakes as invalid device ordinal (101) under -n 32 on cscs CI.
…t host copy-outs

Steps 4 (map/library-node outputs) and 6 (top-level transients) flip
containers to GPU_Global without recording them in data_already_on_gpu, so
Step 8's interstate-edge machinery never saw them: a host-code interstate
edge reading such a container (e.g. a loop condition indexing a
GPU-written transient bool array) read device memory directly and failed
validation with InvalidSDFGInterstateEdgeError during simplify.

Both flip sites now route through a _move_to_gpu helper that flips storage
once and registers the name, letting Step 8 clone a host copy and insert
the copy-out state before each reading edge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… SDFG

Nesting the map body replicates the LoopRegion inside a new nested SDFG,
making its loop-bound symbols free there. The missing-symbol loop added
them to symbol_mapping but never to the nested SDFG's symbols dict;
validation only checks mapping coverage so this stayed green, and codegen
first failed in arglist() with a KeyError when assembling GPU kernel
arguments. Reuse the parent's symbol dtype when registering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gistration

Two ways apply() broke the validator invariant (nested free symbols minus
connectors must be covered by symbol_mapping): loop-assigned and shape
symbols were registered into the nested SDFG's symbols after
add_nested_sdfg froze the mapping, and a later application inside an
existing nested SDFG rerouted the loop exit edge onto the new map state
while tail bookkeeping only removed the symbol - leaving uses whose parent
node mapping never gained the symbol. Both surfaced as "Missing symbols on
nested SDFG" under specific transformation application orders only.

Reconcile in both places: after symbol-type propagation, self-map any
nested free symbol missing from the node mapping (mirroring dtype
registration, sorted for determinism); after tail symbol removal,
identity-map newly-free symbols into the parent node's mapping when one
exists. Both blocks are no-ops on healthy graphs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rotation added here never ran in the GPU CI. That pipeline launches a single
pytest process under slurm, which sets SLURM_PROCID=0, so the xdist CONTROLLER
resolved a launcher rank and pinned itself to one device. Workers are spawned with
the controller's environment, so all 32 inherited a one-device CUDA_VISIBLE_DEVICES,
took the `len(device_pool) <= 1` early return, and piled onto GPU 0 -- precisely the
storm this was written to remove. Measured on a 4-GPU GH200 node: 33 of 33 processes
(controller plus 32 workers) on device 0.

The controller runs no tests of its own, so it must never narrow the pool, and that
has to win over any rank variable it happens to see. It now republishes the whole
pool instead, which leaves the workers something to rotate over and saves each of
them an nvidia-smi. The same node now reports 8 workers on each of the 4 GPUs. Paths
without an xdist controller are unchanged: a plain session and the `mpirun -n 2
pytest --with-mpi` GPU workflow still pin by LAUNCHER_RANK_VARS rank.

Also name the CUDA architecture for the GH200 runners. Left on `native`, cmake
reports "No local CUDA-capable GPUs found" whenever the GPUs are busy and falls back
to compute_60, which newer CUDA toolkits reject outright; that fallback, not the
tests, produced most of the failures observed while the workers were stacked on one
device. Plain 90 rather than 90a, whose SASS has no JIT fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…match)

The loop compared sympy symbols against the str-keyed sdfg.symbols dict,
so it never executed; nested SDFGs relied on the add_nested_sdfg backfill,
which registers enclosing-map parameters with a default int32 dtype
instead of their defined type (e.g. int64), truncating in generated code.
Restores the string-keyed logic lost in merge 9ac67d6 (original fix
03c71c3) and additionally includes the top scope entry's own parameters
via new_symbols. WIP: regression test passes solo but fails under
pytest -n 16 alongside other test files (suspected cross-test symbol
dtype leakage), not yet resolved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ExpandPure declared the three input connectors of the inner SDFG with
default packed-C strides while the output connector carried the parent
descriptor's own strides. A connector is a view onto the caller's
buffer, so an operand that is not packed C -- a Fortran-layout array, a
sub-range of a larger array -- was read at the wrong addresses: the
select returned other elements' values, and positions past the operand's
real extent came back as zeros or uninitialized bytes.

Give an array input the operand's strides, exactly as the output already
does. A single-element (broadcast) input stays a length-1 view, where
strides do not apply.

Covered by two tests that fail before the fix (46/48 elements wrong):
Fortran-strided operands, and the same with a broadcast scalar. Both
assert the connector strides structurally, not just the numerics.

Ported from dace extended e8d3f1a.
Frontends need somewhere to park JSON-serializable state that survives a
save/load round-trip without dace interpreting it. dace-fortran uses it to
carry a frozen argument signature through .sdfgz, so a later pipeline stage
can check that the SDFG's arglist still matches what the Fortran binding
was generated against.

dace never reads the contents, so both directions are a plain deep copy --
enough to keep the stored dict and the emitted JSON from aliasing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The read-set walk recursed only into LoopRegion / ConditionalBlock, so it
stopped at a ConditionalBlock's branch -- a branch is a plain
ControlFlowRegion, not either of those. Any data container read by a header
below that point never entered read_set, so it never became an input
connector on the loop-body NestedSDFG and stayed a free symbol of the body
instead. The parent's interstate edge was then left referencing a name that
exists only inside the nest.

Surfaced by enabling preserve_abi length-1 scalarization in the dace-fortran
post-cleanup: staging rewrites `lextra_diffu[0]` to the transient scalar
`scal_lextra_diffu`, which lands inside the nested SDFG, and the velocity
CPU build died in apply_transformations_repeated(LoopToMap) with
`Undefined symbols in edge: {'scal_lextra_diffu'}` at `block -> if_15`.

Walk all_control_flow_blocks() instead. Ported from the `extended` branch,
which carries this fix and consequently deletes two symbol self-mapping
workarounds in apply(); those depend on extended-only APIs
(symbolic.equalize_symbols, LoopRegion.pinned_sequential) and are left alone
here.

Verified: 34/34 loop_to_map tests; both velocity variants (loopexch,
noloopexch) complete phase A at 56 maps, having previously crashed ~2 min in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Staging KEEPS the signature array by design -- that is what preserves the
ABI -- so the array stays eligible on every subsequent run. Each
re-application therefore created another transient scalar and another
copy-in/copy-out pair, chaining a redundant hop onto the previous one.
Measured on a velocity baseline: scalars 19 -> 41 -> 63 -> 85 and states
102 -> 104 -> 106 -> 108 over three runs. Every intermediate stayed valid
with a byte-identical arglist, so this was silent growth rather than a
failure.

Skip a non-transient whose only references are a previous run's staging
states, and skip one that is neither read nor written (staging it produced
a dead scalar). Same baseline now holds flat at 20 scalars / 104 states
across three runs.

This inverts the contract of test_repeated_forward_finds_new_name_and_stays_correct,
which asserted a second application MUST produce a fresh scalar. That assert
tested the find_new_name mechanism; the test's stated intent is "must not
collide", which idempotency satisfies more strongly. Renamed to
test_repeated_forward_is_idempotent_and_stays_correct and now asserts the
descriptor set and state count are unchanged, keeping the numerical check.

Verified: 26/26 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The header already aliased the types (gpuStream_t, gpuEvent_t, gpuError_t)
and gpuGetLastError / gpuGetErrorString for both the HIP and CUDA arms, but
shipped no function aliases -- DaCe's own codegen emits
'%sStreamSynchronize' % self.backend and so never needed them. Backends that
emit the backend-agnostic spelling directly (the f2dace fork's velocity
passes) failed to compile with `identifier "gpuStreamSynchronize" is
undefined`.

Add gpuStreamSynchronize / gpuDeviceSynchronize / gpuEventSynchronize to
both arms. Deliberately NOT adding gpuMalloc / gpuFree / gpuMemcpyAsync /
gpuMemcpyD2H: consumers such as the velocity reductions kernel self-define
those, and duplicating them here would collide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t access

The 'i:i+stride' special case in AffineSMemlet.propagate returns the map
range verbatim. It is meant for a range access whose length equals the
map stride, but a single-element access has length one, which equals the
stride of any unit-stride map range, and a*i at a zero map begin starts
where the map range starts. Both of its tests therefore hold for an
access it was never meant to cover, and returning the map range drops
the multiplier: C[2*i] over i=0:N propagated to C[0:N] instead of
C[0:2*N-2:2]. That under-approximates the write set, so it is unsound
rather than merely imprecise. A non-zero offset takes the general path
and was already correct, which is why this went unnoticed.

Require a unit multiplier for the special case.
Innermost Sequential map gets #pragma omp simd on its innermost loop;
innermost CPU_Multicore map folds simd into its parallel-for pragma.
"Innermost" means the map body holds no inner Map and no NestedSDFG,
checked non-recursively. Both gated by new default-on config keys
(compiler.cpu.simd_sequential_maps / simd_innermost_multicore_maps)
that serve only as an off-switch. min/max WCR and (Sequential-only)
scatter WCR targets are excluded as unsafe.

Adds tests/codegen/simd_pragma_test.py covering both rules, the
inner-map/NestedSDFG disqualifiers, and the config off-switches.
The simd config keys were re-applied from a backup taken before the
merge that added cache_distaware, so committing that file removed the
key. Config.get_bool('cache_distaware') then raised KeyError on every
build_folder_root call.
…e fusion

apply() re-verified can_be_applied in strict mode regardless of how the
match was made, turning permissive-only matches into silent no-ops that
apply_transformations_repeated kept re-matching forever. Record the mode
can_be_applied ran in and replay it. Also route start-block updates
through keep_start_block, which skips a redundant pin when the target is
already the sole source, matching state_fusion.py's helper.
The innermost CPU_Multicore rule stamps simd onto the parallel-for
directive, so the exact-string goldens now carry it. The invariant the
test checks -- how schedule and num_threads compose on the directive --
is unchanged.
Port keep_start_block from extended: skip re-pinning start_block when
the target is already the sole implicit source, instead of always
writing an explicit (redundant) pin. Behavior-neutral for the getter
(a unique source always wins over the explicit pin) and de-duplicates
the three was_start call sites in apply().

Kept on FaCe's side: the networkx import (no dace.graphlib here, and
none is planned) and the current is_start_block, which already carries
a bounds check on a stale _start_block index that extended's simplified
rewrite of the same fix dropped.
sympy_numeric_fix and two other call sites use math.isfinite, but the module never
imported math. Any SDFG whose interstate edge assignments reach symstr() ->
sympy_numeric_fix therefore failed to deserialize with

    NameError: name 'math' is not defined

which serialize.from_json swallows into a SerializableObject, so the real failure
surfaced far away as

    TypeError: Expected InterstateEdge, got <class 'dace.serialize.SerializableObject'>

Reproduced by loading the stored velocity_tendencies stage 3/4/5 SDFGs; they load
and validate again with this import, under DACE_testing_deserialize_exception=1.

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