Skip to content

[Only for CI] Extensions - #2475

Draft
ThrudPrimrose wants to merge 7832 commits into
mainfrom
extended
Draft

ThrudPrimrose wants to merge 7832 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
Yakup Koray Budanaz and others added 19 commits October 16, 2026 18:34
A strided operand of np.outer is flattened by a copy map into a transient,
and the product map read that transient from a second, unconnected access
node in the same state. Nothing ordered the read after the copy: the
frontend's emission order happened to, and RemoveViews reordered it, so the
npbench LU spelling A[k+1:, k+1:] -= np.outer(A[k+1:, k], A[k, k+1:])
read the previous column for every intermediate graph from the first
cleanup stage on. The product now goes into a step state, the pattern the
other multi-map lowerings use; simplification fuses the states once the
access nodes are linked. The augmented LU spelling now lifts to
GetrfNoPivot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
at_most_one_trip read only the count's numeric bounds, which a registry symbol never gives (no
SymPy sign), so a map whose extent the SDFG's facts bound by one stayed a parallel region. It now
falls back to asking count <= 1 under the facts at the state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the config, and prove a relation whose extremum sat in a division

ChunkAntiDependence matched a constant read-ahead only. A symbolic offset that is
nonnegative under the canonical symbol assumption now gets a seam row of K cells per
chunk, the trailing row copied by a map so K = 0 runs it zero times. Its snapshot copy
shares a state with BreakAntiDependence's K >= 0 trap, so the matcher also finds the
copy in the one state before the map. Bit-exact for K = 0..300 including N <= K.

A D >= 2 chunk count is int_ceil over a Max; prove() generalized the division away and
then indexed the Max it had just removed (IndexError in the fork/join threshold test).
It now asks the generalized relation directly.

canonicalize's CPU and GPU presets pinned peel_limit to 4 while the config default is 8;
the presets no longer carry it, so every entry point reads
optimizer.canonicalization.peel_limit, like the unroll limits.
Fold the Python spelling of each tile op into the existing op tables as a
template field, rename an in-out read connector in place instead of in a
postprocessing hook, split Emitter.scope below the complexity limit, and
route the three tile-kernel expansion filters through one helper. Add the
design document of the Python, tile DSL and CUDA Tile C++ backends.
The binary-operator result type classed every float outside float16/32/64
as an unsigned integer, so a bfloat16 array divided by an integer size was
a "true division of integers" and came out float64, and ml_dtypes' floats
(numpy kind V, not inexact) kept an integer symbol a strong int64. HB's bf16
kernels then fed a double block into a bf16 tensordot
(conv_transpose3d_avg_pool_clamp_softmax_multiply). dtypes.FLOAT_TYPES is
the float set now, for both rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
``b:b:s`` held ceiling(1/s) elements unless s was 1 and b a plain sum of
symbols, so a strided window's per-iteration element (``x[..., i*p + t ::
p]``, ``max(0, lo) * s + k``) read as a possibly-larger range and its
tasklet connector became a pointer the body used as a value (HB
conv_transpose3d_avg_pool_clamp_softmax_multiply through CPF hip, at any
precision). ``b + c - b`` cancels to ``c`` for any finite expression without
floats; that is the condition now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loop expansion passed the operands' numpy scalar types to add_array,
which maps only numpy's own: a bf16 GEMM raised ``Invalid type "type" for
property dtype`` (HB gemm_add_relu at bf16).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HB grades its ML kernels in bf16, and CPF refused the type outright.

Device dialects spell dace::bfloat16 as the toolkit's __hip_bfloat16 /
__nv_bfloat16 -- what dace::bfloat16 IS in a device build -- and include
hip_bf16.h / cuda_bf16.h. An integer reaches it through cpf_narrow<T>
(``__hip_bfloat16(n)`` is ambiguous between its converting constructors),
and cpf_gpu_atomic swaps the 32-bit word holding a 2-byte accumulator.

The C++ dialect spells bfloat16 / float16 as the compiler's __bf16 /
_Float16 (GCC 13+, Clang 18+: arithmetic, conversions and OpenMP
reductions). Its math runs in float through cpf_lowp_* wrappers, emitted
only for a unit that holds a 16-bit float, because std::exp(__bf16) is
ambiguous (Clang in every standard, GCC before C++23). The C dialect,
whose helpers are typed by name, refuses them up front with the way out.

All four types are two bytes in the bit layout of HB's __npb_bf16
(uint16_t) storage, so an entry taking them takes the caller's storage
pointers as they are. A vendor library is not offered a 16-bit float
operand: the BLAS / FFT expansions take single and double precision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	dace/subsets.py
Define py_mod, c_mod and CMod in the Python prelude, so an interstate
condition that calls them runs. Refuse a C++ tasklet inside a CUDA Tile C++
kernel with a CodegenError instead of a Python SyntaxError. Type a
one-element GPU operand of a CuPy expansion as a device pointer, as the cuBLAS
expansions do, so a GPU scalar result validates. Add CuPy expansions of
TensorTranspose and ScatterConflictCheck.
…uch one element of an argument array

AnnotateLoopKinds called a loop sequential only on a constant-distance access pair or a
carried symbol, so pivot rows and triangular solves (lu, cholesky, trisolv, ludcmp,
gramschmidt, durbin) stayed unsure. It now also asks Z3 for two iterations of the loop,
each side with its own copy of every inner iterator and its bounds, at which a write and
another access of the same argument array touch one element. The witness may choose only
array sizes: any other symbol is a fixed unknown (a[i] = a[i - k] stays unsure, since
k = 0 carries nothing). Only accesses every iteration makes count: no branch bodies, no
loops with an early exit or without a constant-stride counter, no transients. An array
copy contributes its whole ranges, since it moves every element; tasklet and library
ranges do not. The hint names the access and the witness values.

test_writes_the_test_cannot_separate_are_unsure_not_a_proof asserted a[5i] / a[3i] stays
unsure; i = 3 and i = 5 write the same element once N >= 6, so it is now
test_writes_the_test_cannot_separate_are_proven_by_a_witness and checks the witness.
…4-bit strides

CopyLibraryNode gets aligned_words: rows contiguous on both sides move through
dace::BlockCollective3DVec in aligned 16-byte words, peeling each row's unaligned
head and tail. A region of rank above 3 is a loop nest of rank-3 collectives with
one barrier after it (was a ValueError). dace::BlockCollective3D/3DVec take 64-bit
strides: a tile of a 144-level x 16M-column field spans more than 2**31 elements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code generation reorganizes states and host nodes, so their events timed nothing
reliable. The provider now times GPU_Device maps only, recording on the stream the
kernel is launched on; validation rejects GPU_Events on SDFGs, states and other
nodes; dace.instrument annotates kernels only; the tuners instrument a state's
kernels and measure() sums them. AddThreadBlockMap keeps the timing on the kernel map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StageThreadBlockData stages the bounded regions a kernel's thread block reads (and,
on request, writes) through shared memory with block-collective copies and one
barrier per direction. CoarsenKernelThreads gives every thread a serial tile of one
or two map parameters. DefaultSharedMemorySync adds no barrier where one already
follows; block_tile_kernels does not stride SMALL-only axes across the lanes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `tile` marker on the cuTile/TileLang/Triton and CUDA Tile C++ test modules, deselected in the GPU CI;
the CPU jobs already deselect them as `gpu`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yakup Koray Budanaz and others added 30 commits October 20, 2026 02:48
… passes device buffers itself

The stage applied CloudSC's hand-written mirror, which leaves an array written by both a host loop the
pipeline cannot parallelize (nssopt left symbolic) and a kernel unmirrored, so kernels read host memory
(illegal access in the parallelize leg). The pipeline's own offload places copies both ways.
run_candidate reads the device-resident arguments off the descriptors (moved from the target test).
…() lookups

LoopToMap declares lift_context, symbol_resolver and contract_arrays (unset on a plain match) instead of
reading them with vars(self).get; ParallelizeLoopsHoldingSiblingChains copies them directly.
BreakAntiDependence declares its per-loop memos in __init__. LoopStridePermutation's revert restores the
loop metadata tuple its snapshot now holds.
…r-element scalars instead of refusing

A fused map's per-iteration buffer (CloudSC's __map_fusion_zpfplsx[2, 1, 1]: tasklets write and read
single elements, one copy moves a range out) is lane-dependent with more than one element, and the GPU
vectorizer aborted the whole pipeline on it. When step 3 meets such a buffer it is split into one scalar
per element, its range copies into one element copy each, and the propagation reruns; each element is
then a per-lane temporary. Only a buffer the widening would refuse is split. The pipeline offload stage
test reads the device-resident arguments the real offload leaves.
# Conflicts:
#	dace/dtypes.py
#	tests/codegen/gpu_schedule_groups_test.py
…e SDFG declares

A symbol is its name, so the parse and SymPy caches can hand back an object of the same name another
program declared at another dtype: after a program that (correctly) refused a float scalar promoted to
__sym_n_0 as an extent, an integer program's __sym_n_0 extent was refused too (an xdist-order failure of
size_scalar_shape_promotion_test). The shape is now validated inside the SDFG's symbol-dtype authority,
as symbol_dtype is designed to be read.
… candidate array

LoopToMap records the container a dependence refusal names (last_refusal_container); a refusal on any other
container, or for structure, bounds or symbols, survives the privatization, so the pass no longer promotes,
re-probes and undoes those loops.
… once per map split

MapExpansion propagates its own scope; the whole-SDFG propagation apply_to ran after every split now runs
once at the end. Same generated code on the corpus; pb_correlation's inner-scope volume becomes the exact j
instead of a stale N.
…d of on copies

The split search isolated the loop into a mini SDFG (JSON round trip), deep-copied it per
candidate, split the copy, ran the LiftTrivialIf/EmptyStateElimination/modulo cleanup and
PrivatizeScalars/SymbolPropagation/ConstantPropagation on it, and only then probed LoopToMap.
On the CloudSC monolith that cost 18.7 s of thread time and split nothing.

The search now analyzes first and transforms only the winner. Each candidate's segments come
from _split_plan, the header computation _split_loop_at now builds from, so the scored segments
are the ones the chosen split emits. Each non-singleton segment is probed on the real loop:
its init statement and condition are swapped to the segment's, the guards the range-aware
LiftTrivialIf would resolve over that range are resolved in the branch lists, the modulo
subsets the range makes affine are folded, LoopToMap.can_be_applied_to decides, and every
edit is put back in a finally block (original CodeBlocks, branch entries, subset ranges).
Selection is unchanged: the unsplit loop maps nothing, the first candidate to reach a count
keeps it, and the bare-bound (guarded) form wins only by mapping strictly more.

LiftTrivialIf exposes its per-conditional decision as dead_branch/taken_branch, used by the
pass itself and by the virtual resolution, so the decision exists once.

_mappable_loop_count, _loop_to_map_prober and mappable_count_upper_bound had no other user and
are removed. The modulo peel/split stages keep their copy-based probes (_isolate_loop).

Corpus (tsvc, tsvc25, poly; --peel 4): all 257 kernels identical. canonicalize hashes on the
27 canon-speed2 inputs identical. Replay of the peel unit: mono 18.7 s -> 1.7 s, cloudsc
8.5 s -> 1.2 s, same output.
…uceExpansion does not validate, codegen resolves scope symbols and nested-SDFG arguments once
…tself

The modulo stages isolated the loop into a mini SDFG and, per (count, direction) peel or per
split point, deep-copied it, peeled or split the copy, ran the cleanup and asked whether a
wrapping modulo survived. They now decide by range analysis on the real loop and transform only
the winner with the existing _peel_one_loop / _specialize_modulo_split.

A peel's segments come from LoopPeeling.peel_plan, the header and iterate values LoopPeeling.apply
now builds from: the remaining loop's header and, when the body holds a loop, a single-iteration
header per peeled iteration (only loops can keep a wrapping modulo, and a peeled iteration is no
loop). A split's segments come from _split_plan. Each segment is probed under _as_segment, which
swaps the loop header, resolves the guards and folds the modulo subsets the range decides (the
cleanup the real rewrite runs), and restores everything when the block exits; the candidate is
accepted when no loop of any segment keeps a wrapping modulo, and the folds' offset < modulus
relations are the split's branch condition as before. Selection order is unchanged: smallest
count first, front/back/both, then the first split point that folds every wrap.

_peel_one_loop's gates move to _peel_sides, shared with the search. _isolate_loop has no user
left and is removed with its test.
…ument symbols through the frame's free_symbols
…r handled wherever sympy expressions are inspected
# Conflicts:
#	dace/transformation/helpers.py
#	tests/nest_subgraph_test.py
#	tests/sdfg/state_test.py
…o-extended

# Conflicts:
#	dace/codegen/targets/cpu.py
#	dace/codegen/targets/framecode.py
#	dace/sdfg/state.py
…stop repeating the nest per region, the descriptor per access node and the parse per assignment; nested-SDFG argument symbols through the frame's free_symbols
# Conflicts:
#	dace/codegen/targets/framecode.py
#	dace/sdfg/sdfg.py
#	dace/sdfg/state.py
#	dace/transformation/passes/prune_symbols.py
…read before assignment

Extended already carries #2706 as a port (41640a3, f1747c3) and its own free_symbols walk, so each
conflict keeps extended's side; from main it takes the program-callable modulo spellings (PyMod,
FtnMod, FtnModulo through operators.MODULO_CALLS and binop_tasklet_code) and main's tests. Two of
main's modulo tests are adapted to extended's documented semantics: a CMod of operands not proven
integer prints as c_mod, and sign facts of a dace.symbol live in the symbol registry, so the
CMod->Mod fold is checked on SymPy symbols with assumptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed dace

uv counts the pin's equal version as satisfied, so --no-deps -e alone left the pinned commit installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…v sources

From the bench directory uv resolved the reinstall through the bench's dace pin again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bench's [tool.uv] override-dependencies re-pin dace for any install run from the bench's directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elper warnings

A one-element GPU-resident container crosses an ExternCall as its device pointer
instead of the address of a host copy, and a GPU-scheduled call waits for the
SDFG's stream before it and the device after it, so no device-wide fence (and
warning) is added. Standalone CPF CUDA/HIP units mark their optional helpers
[[maybe_unused]] / inline constexpr, so nvcc reports no unreferenced function.
…d SymPy's Mod prints as PyMod

float(i * 7 % N) handed the cast the call's one-element result list, so its tasklet kept a dangling __inp
(tsvc s315 failed validation). A call argument now unwraps a single symbolic or numeric result; a list literal
stays a list. astutils.unparse printed SymPy's floored Mod as %, which an SDFG reads as C's truncating modulo;
it prints PyMod. The Python backend knows every modulo spelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…GEMM shares out its batch

examinimd's np.sum(w[:, :, None] * d, axis=1) lifts to an Einsum whose BatchedMatMul ran 10976
3x1xK cblas_dgemm calls in a row: 452 ms, 82 ms now.

- ExpandTransformation: the top-level maps and library nodes of an expansion, through top-level
  nested SDFGs, take the expanded node's schedule; inferred as nested, the Einsum's
  BatchedMatMul was Sequential.
- BatchedMatMul OpenBLAS: a top-level node over a batch that is not small runs the batch loop
  under omp parallel for, one single-core GEMM per element.
- libnode_is_sequential follows the map rules: sequential under a parallel parent map, over a
  small domain, or over a data segment under a sequential loop; a sequential loop alone no
  longer serializes it.
- symbolic.is_small_extent: a SMALL symbol or a literal below 64, for schedules only (the
  canonical form keeps literals parallel). SequentializeSmallAxes and expansions isolate
  literal small axes without a declared SMALL symbol.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant