Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e620b5c
Fix symbol-promotion and squeezing behavior in Frontend
ThrudPrimrose Jul 18, 2026
f8f53d8
Update docstring for promote_size_scalars_in_shape
ThrudPrimrose Jul 18, 2026
a2a7809
Merge branch 'main' into fix-frontend-symbol-promotion-squeezing
ThrudPrimrose Jul 20, 2026
638022d
Squeeze only rank-reducing indices in MatMul operands
ThrudPrimrose Jul 20, 2026
c0b05c4
Track integer-index dimensions in Range, revert MatMul operand change
ThrudPrimrose Jul 20, 2026
be83a8c
Validate and generate GEMM against the operand view it was dispatched on
ThrudPrimrose Jul 20, 2026
c62c428
Read a size scalar into a symbol without deleting the descriptor
ThrudPrimrose Jul 20, 2026
bc66639
Maintain index-dim flags through Range mutation and unify the GEMM ma…
ThrudPrimrose Jul 20, 2026
4c7e766
Revert integer-index dimension tracking from Range
ThrudPrimrose Jul 20, 2026
a6e26d4
Capture each array's size in its own symbol; read PBLAS as a matrix
ThrudPrimrose Jul 20, 2026
d67d7f0
Trim PR comments to Sphinx style and reuse dace utilities
ThrudPrimrose Jul 20, 2026
b8c664f
fix(frontend): keep subscript promotions out of the visitor globals
ThrudPrimrose Jul 21, 2026
6481b35
fix(codegen): allocate data after the symbol sizing it is assigned
ThrudPrimrose Jul 21, 2026
d479143
fix: keep nested-scope outputs in the GPU kernel signature
ThrudPrimrose Jul 21, 2026
7660850
Merge branch 'main' into fix-frontend-symbol-promotion-squeezing
ThrudPrimrose Jul 21, 2026
0e0d793
fix: read a size-1 array through a subscript and share the matrix view
ThrudPrimrose Jul 21, 2026
34f319a
Merge remote-tracking branch 'origin/fix-frontend-symbol-promotion-sq…
ThrudPrimrose Jul 21, 2026
7ec090c
fix: make the fill constructors accept a size computed in the program
ThrudPrimrose Jul 21, 2026
4702912
Bracket only the regions whose size symbols need it
ThrudPrimrose Jul 21, 2026
84043b1
Merge branch 'main' into fix-frontend-symbol-promotion-squeezing
ThrudPrimrose Jul 30, 2026
6984030
Merge branch 'main' into fix-frontend-symbol-promotion-squeezing
ThrudPrimrose Jul 31, 2026
940f535
Locate the allocation without depending on how it is spelled
ThrudPrimrose Jul 31, 2026
1ec363e
Merge remote-tracking branch 'origin/main' into fix-frontend-symbol-p…
ThrudPrimrose Jul 31, 2026
8269a24
Merge remote-tracking branch 'origin/main' into upd_fix-frontend-symb…
Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dace/codegen/codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from dace.codegen.instrumentation import InstrumentationProvider
from dace.sdfg.state import SDFGState
from dace.transformation.pass_pipeline import FixedPointPipeline
from dace.transformation.passes.region_boundary_states import RegionBoundaryStates
from dace.transformation.passes.simplification.control_flow_raising import ControlFlowRaising


Expand Down Expand Up @@ -198,6 +199,9 @@ def generate_code(sdfg: SDFG, validate=True) -> List[CodeObject]:
# where explicit control flow was not used, continue to work as expected.
FixedPointPipeline([ControlFlowRaising()]).apply_pass(sdfg, {})

# Data sized by a symbol that a region's incoming edge assigns can only be allocated once the region is entered.
RegionBoundaryStates().apply_pass(sdfg, {})

# Before generating the code, run type inference on the SDFG connectors
infer_types.infer_connector_types(sdfg)

Expand Down
62 changes: 43 additions & 19 deletions dace/frontend/python/newast.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from dace.sdfg.replace import replace_datadesc_names
from dace.sdfg.type_inference import infer_expr_type
from dace.symbolic import pystr_to_symbolic, inequal_symbols
from dace.utils import until
from dace.utils import until, find_new_name

import numpy
import sympy
Expand Down Expand Up @@ -5370,6 +5370,46 @@ def range_is_index(range: subsets.Range) -> bool:
wcr=expr.wcr))
return tmp

def promote_scalar_to_symbol(self, scalar: str, key: Optional[str] = None, fresh: bool = False) -> symbolic.symbol:
"""
Reads a scalar into a symbol on an interstate edge, leaving its descriptor in place.

:param scalar: Name of the scalar data descriptor to read.
:param key: Cache key; repeated promotions of the same expression reuse the symbol.
:param fresh: Mint a new suffixed symbol instead of reusing a cached one, so two shapes
sized from the same reassigned scalar keep their own values.
:return: The symbol carrying the scalar's value.
"""
key = key if key is not None else scalar
desc = self.sdfg.arrays[scalar]
sym = None if fresh else self.indirections.get(key)
if sym is None:
base = f'__sym_{scalar}'
if fresh:
# Reserve ``base`` so a fresh promotion never lands on the bare name a cached one
# reuses; a later index on the same scalar would otherwise re-bind the extent.
reserved = self.sdfg.symbols.keys() | self.sdfg.arrays.keys() | {base}
name = self.sdfg.add_symbol(find_new_name(base, reserved), desc.dtype)
else:
name = base
try:
self.sdfg.add_symbol(name, desc.dtype)
except FileExistsError:
pass # A cached promotion may re-add an existing symbol.
sym = dace.symbol(name, dtype=desc.dtype)
if not fresh:
self.indirections[key] = sym
else:
# Shape symbols must resolve inside nested scopes, which look up free symbols in
# ``globals``. Subscript promotions must stay out: they shadow names there.
self.globals[str(sym)] = sym
state = self._add_state(f'promote_{scalar}_to_{str(sym)}')
edge = state.parent_graph.in_edges(state)[0]
# A Scalar reads by name; a size-1 array needs the subscript, or the assignment takes its pointer.
rhs = scalar if isinstance(desc, data.Scalar) else f'{scalar}[{", ".join(["0"] * len(desc.shape))}]'
edge.data.assignments = {str(sym): rhs}
return sym

def _parse_subscript_slice(self,
s: ast.AST,
multidim: bool = False) -> Union[Any, Tuple[Union[Any, str, symbolic.symbol]]]:
Expand All @@ -5379,29 +5419,13 @@ def _parse_subscript_slice(self,

def _promote(node: ast.AST) -> Union[Any, str, symbolic.symbol]:
node_str = astutils.unparse(node)
sym = None
if node_str in self.indirections:
sym = self.indirections[node_str]
if isinstance(node, str):
scalar = node_str
else:
scalar = self.visit(node)
if isinstance(scalar, str) and scalar in self.sdfg.arrays:
desc = self.sdfg.arrays[scalar]
if isinstance(desc, data.Scalar):
if not sym:
sym = dace.symbol(f'__sym_{scalar}', dtype=desc.dtype)
self.indirections[node_str] = sym
try:
self.sdfg.add_symbol(f'__sym_{scalar}', desc.dtype)
except FileExistsError:
# NOTE: By design, it is possible to try here to add an already existing symbol even if
# `not sym` returns True. This exception is benign.
pass
state = self._add_state(f'promote_{scalar}_to_{str(sym)}')
edge = state.parent_graph.in_edges(state)[0]
edge.data.assignments = {str(sym): scalar}
return sym
if isinstance(self.sdfg.arrays[scalar], data.Scalar):
return self.promote_scalar_to_symbol(scalar, key=node_str)
return scalar

if isinstance(s, (Number, bool, numpy.bool_, sympy.Basic)):
Expand Down
5 changes: 5 additions & 0 deletions dace/frontend/python/replacements/array_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from dace.frontend.common import op_repository as oprepo
from dace.frontend.python.common import DaceSyntaxError
from dace.frontend.python.replacements.utils import ProgramVisitor, Shape, sym_type, broadcast_together
from dace.frontend.python.replacements.array_creation_dace import promote_size_scalars_in_shape
from dace.frontend.python.replacements.operators import result_type
from dace import data, dtypes, symbolic, Memlet, SDFG, SDFGState

Expand Down Expand Up @@ -65,6 +66,10 @@ def _numpy_full(pv: ProgramVisitor,
if isinstance(shape, (Number, str)) or symbolic.issymbolic(shape):
shape = [shape]

shape, promoted = promote_size_scalars_in_shape(pv, sdfg, shape)
if promoted:
# Promotion opens a state to carry the symbol assignment; the fill has to follow it.
state = pv.last_block
if any(isinstance(s, str) for s in shape):
raise DaceSyntaxError(
pv, None, f'Data-dependent shape {shape} is currently not allowed. Only constants '
Expand Down
32 changes: 31 additions & 1 deletion dace/frontend/python/replacements/array_creation_dace.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,39 @@

from copy import deepcopy as dcpy
from numbers import Integral
from typing import Any, Optional
from typing import Any, Optional, Tuple

import sympy
import numpy as np

from dace import symbolic


def promote_size_scalars_in_shape(pv: ProgramVisitor, sdfg: SDFG, shape: Shape) -> Tuple[Shape, bool]:
"""
Rewrites a shape so that a size scalar used as an extent is read through a symbol.

A size computed in the program (``nt = Nt + 1; np.empty(nt)``) is a size-1 descriptor, but an
extent must be a symbol. One fresh symbol per shape keeps two arrays sized from the same
reassigned scalar from collapsing onto one value.

:param pv: The program visitor.
:param sdfg: The SDFG being built.
:param shape: The requested shape.
:return: The shape with scalar extents replaced by symbols, and whether anything was promoted.
"""
resolved = [symbolic.pystr_to_symbolic(e) if isinstance(e, str) else e for e in shape]
names = [
n for n in symbolic.symlist(resolved)
if n in sdfg.arrays and n not in sdfg.symbols and sdfg.arrays[n].total_size == 1
]
if not names:
return shape, False

# One symbol per distinct name; sorted() keeps the promotion states deterministic.
replacements = {symbolic.pystr_to_symbolic(n): pv.promote_scalar_to_symbol(n, fresh=True) for n in sorted(names)}
return [e.subs(replacements) if isinstance(e, sympy.Basic) else e for e in resolved], True


@oprepo.replaces('dace.define_local')
@oprepo.replaces('dace.ndarray')
Expand All @@ -32,6 +61,7 @@ def _define_local_ex(pv: ProgramVisitor,
if not isinstance(strides, (list, tuple)):
strides = [strides]
strides = [int(s) if isinstance(s, Integral) else s for s in strides]
shape, _ = promote_size_scalars_in_shape(pv, sdfg, shape)
name = pv.get_target_name()
name, _ = sdfg.add_transient(name,
shape,
Expand Down
22 changes: 21 additions & 1 deletion dace/libraries/blas/blas_helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
# Copyright 2019-2021 ETH Zurich and the DaCe authors. All rights reserved.
import numpy as np
from copy import deepcopy as dc
from dace import dtypes, data
from typing import Any, Dict, Tuple
from typing import Any, Dict, List, Tuple


def matrix_view(subset) -> Tuple[List[Any], List[int]]:
"""
Returns an operand's matrix view: the raw subset if it is already 2D, otherwise the squeezed one.

Squeezing unconditionally rejects a genuine ``(N, 1)`` column as "not a matrix"; not squeezing at
all rejects an ``(NQ, 1, NP)`` reshape. Callers that read a size, a stride or a dimension index
must all use this view, or they disagree about which dimensions the operand has.

:param subset: The subset of the memlet accessing the operand.
:return: The size in the matrix view, and the subset dimensions it kept.
"""
size = subset.size()
if len(size) == 2:
return size, list(range(len(size)))
squeezed = dc(subset)
dims = squeezed.squeeze()
return squeezed.size(), dims


def to_blastype(dtype):
Expand Down
25 changes: 16 additions & 9 deletions dace/libraries/blas/nodes/gemm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import dace.sdfg.nodes
from dace.transformation.transformation import ExpandTransformation
from dace.libraries.blas.blas_helpers import to_blastype, check_access, dtype_to_cudadatatype, to_cublas_computetype
from dace.libraries.blas.nodes.matmul import (_get_matmul_operands, _get_codegen_gemm_opts)
from dace.libraries.blas.nodes.matmul import (_get_matmul_operands, _get_codegen_gemm_opts, _matrix_operand,
_matrix_subset_size)
from .. import environments
import numpy as np
import warnings
Expand Down Expand Up @@ -46,8 +47,10 @@ class ExpandGemmPure(ExpandTransformation):
def make_sdfg(node, parent_state, parent_sdfg):
sdfg = dace.SDFG(node.label + "_sdfg")

((edge_a, outer_array_a, shape_a, strides_a, _, _), (edge_b, outer_array_b, shape_b, strides_b, _, _),
cdata) = _get_matmul_operands(node, parent_state, parent_sdfg)
adata, bdata, cdata = _get_matmul_operands(node, parent_state, parent_sdfg)
edge_a, outer_array_a, shape_a, strides_a = _matrix_operand(adata)
edge_b, outer_array_b, shape_b, strides_b = _matrix_operand(bdata)
_, outer_array_c, _, strides_c = _matrix_operand(cdata)

dtype_a = outer_array_a.dtype.type
dtype_b = outer_array_b.dtype.type
Expand Down Expand Up @@ -79,7 +82,7 @@ def make_sdfg(node, parent_state, parent_sdfg):

_, array_a = sdfg.add_array("_a", shape_a, dtype_a, strides=strides_a, storage=outer_array_a.storage)
_, array_b = sdfg.add_array("_b", shape_b, dtype_b, strides=strides_b, storage=outer_array_b.storage)
_, array_c = sdfg.add_array("_c", shape_c, dtype_c, strides=cdata[-3], storage=cdata[1].storage)
_, array_c = sdfg.add_array("_c", shape_c, dtype_c, strides=strides_c, storage=outer_array_c.storage)

if equal_valued(1, node.alpha):
mul_program = "__out = __a * __b"
Expand Down Expand Up @@ -464,7 +467,11 @@ class ExpandGemmPBLAS(ExpandTransformation):
@staticmethod
def expansion(node, state, sdfg):
node.validate(sdfg, state)
(_, adesc, ashape, _, _, _), (_, bdesc, bshape, _, _, _), _ = _get_matmul_operands(node, state, sdfg)
# Read the same matrix view validate accepted; the raw subset would mis-size an operand
# such as an (NQ, 1, NP) reshape.
adata, bdata, _ = _get_matmul_operands(node, state, sdfg)
_, adesc, ashape, _ = _matrix_operand(adata)
_, bdesc, bshape, _ = _matrix_operand(bdata)
dtype = adesc.dtype.base_type

if not equal_valued(0, node.beta):
Expand Down Expand Up @@ -551,11 +558,11 @@ def validate(self, sdfg, state):
size2 = None
for _, _, _, dst_conn, memlet in state.in_edges(self):
if dst_conn == '_a':
size0 = memlet.subset.size()
size0 = _matrix_subset_size(memlet.subset)
if dst_conn == '_b':
size1 = memlet.subset.size()
size1 = _matrix_subset_size(memlet.subset)
if dst_conn == '_c':
size2 = memlet.subset.size()
size2 = _matrix_subset_size(memlet.subset)

if self.transA:
size0 = list(reversed(size0))
Expand All @@ -575,7 +582,7 @@ def validate(self, sdfg, state):
UserWarning)
elif not res:
raise ValueError("Inputs to matrix-matrix product must agree in the k-dimension")
size3 = out_memlet.subset.size()
size3 = _matrix_subset_size(out_memlet.subset)
if size2 is not None:
res = [equal(s0, s1) for s0, s1 in zip(size2, size3)]
fail = any([r is False for r in res])
Expand Down
33 changes: 31 additions & 2 deletions dace/libraries/blas/nodes/matmul.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
# Copyright 2019-2025 ETH Zurich and the DaCe authors. All rights reserved.
import dace
from dace import properties, symbolic
from dace.libraries.blas.blas_helpers import matrix_view
from copy import deepcopy as dc
from typing import Any, Dict
import warnings
from math import prod


def _matrix_subset_size(subset):
"""
Returns an operand's size in the matrix view ``SpecializeMatMul`` matched on.

:param subset: The subset of the memlet accessing the operand.
:return: The size in the matrix view.
"""
return matrix_view(subset)[0]


def _matrix_operand(operand):
"""
Returns a GEMM operand as ``(edge, descriptor, shape, strides)`` in its matrix view, applying
the rule of :func:`_matrix_subset_size` so shape and strides come from the same view.

:param operand: One of the three tuples returned by :func:`_get_matmul_operands`.
:return: The edge, outer descriptor, and 2D shape and strides.
"""
edge, desc, size, strides, squeezed_size, squeezed_strides = operand
if len(size) == 2:
return edge, desc, size, strides
return edge, desc, squeezed_size, squeezed_strides


def _get_matmul_operands(node, state, sdfg, name_lhs="_a", name_rhs="_b", name_out="_c"):
"""Returns the matrix multiplication input edges, arrays, and shape."""
res_lhs = None
res_rhs = None
res_out = None
for edge in state.all_edges(node):
if edge.dst_conn in [name_lhs, name_rhs]:
size = edge.data.subset.size()
Expand Down Expand Up @@ -137,8 +163,11 @@ def _get_codegen_gemm_opts(node, state, sdfg, adesc, bdesc, cdesc, alpha, beta,
from dace.codegen.common import sym2cpp
from dace.libraries.blas.blas_helpers import get_gemm_opts

(_, _, ashape, astride, _, _), (_, _, bshape, bstride, _, _), (_, _, cshape, cstride, _,
_) = _get_matmul_operands(node, state, sdfg)
# M/N/K and the leading dimensions come from the matrix view the dispatcher matched.
adata, bdata, cdata = _get_matmul_operands(node, state, sdfg)
_, _, ashape, astride = _matrix_operand(adata)
_, _, bshape, bstride = _matrix_operand(bdata)
_, _, cshape, cstride = _matrix_operand(cdata)

if node.transA:
ashape = list(reversed(ashape))
Expand Down
17 changes: 4 additions & 13 deletions dace/libraries/linalg/nodes/transpose.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Copyright 2019-2026 ETH Zurich and the DaCe authors. All rights reserved.
import functools
from copy import deepcopy as dc
import dace.library
import dace.properties
import dace.sdfg.nodes
Expand All @@ -14,9 +13,7 @@ def _get_transpose_input(node, state, sdfg):
"""Returns the transpose input edge, array, and shape."""
for edge in state.in_edges(node):
if edge.dst_conn == "_inp":
subset = dc(edge.data.subset)
idx = subset.squeeze()
size = subset.size()
size, idx = blas_helpers.matrix_view(edge.data.subset)
outer_array = sdfg.data(dace.sdfg.find_input_arraynode(state, edge).data)
return edge, outer_array, (size[0], size[1]), (outer_array.strides[idx[0]], outer_array.strides[idx[1]])
raise ValueError("Transpose input connector \"_inp\" not found.")
Expand All @@ -26,9 +23,7 @@ def _get_transpose_output(node, state, sdfg):
"""Returns the transpose output edge, array, and shape."""
for edge in state.out_edges(node):
if edge.src_conn == "_out":
subset = dc(edge.data.subset)
idx = subset.squeeze()
size = subset.size()
size, idx = blas_helpers.matrix_view(edge.data.subset)
outer_array = sdfg.data(dace.sdfg.find_output_arraynode(state, edge).data)
return edge, outer_array, (size[0], size[1]), (outer_array.strides[idx[0]], outer_array.strides[idx[1]])
raise ValueError("Transpose output connector \"_out\" not found.")
Expand Down Expand Up @@ -248,18 +243,14 @@ def validate(self, sdfg, state):
raise ValueError("Expected exactly one input to transpose operation")
for _, _, _, dst_conn, memlet in state.in_edges(self):
if dst_conn == '_inp':
subset = dc(memlet.subset)
subset.squeeze()
in_size = subset.size()
in_size, _ = blas_helpers.matrix_view(memlet.subset)
out_edges = state.out_edges(self)
if len(out_edges) != 1:
raise ValueError("Expected exactly one output from transpose operation")
out_memlet = out_edges[0].data
if len(in_size) != 2:
raise ValueError("Transpose operation only supported on matrices")
out_subset = dc(out_memlet.subset)
out_subset.squeeze()
out_size = out_subset.size()
out_size, _ = blas_helpers.matrix_view(out_memlet.subset)
if len(out_size) != 2:
raise ValueError("Transpose operation only supported on matrices")
if list(out_size) != [in_size[1], in_size[0]]:
Expand Down
Loading