Skip to content

Symbol-dependent transient allocated before its shape symbol is defined (shape symbol assigned inside a ConditionalBlock) #2377

Description

@ThrudPrimrose

Summary

A transient whose shape depends on a symbol that is assigned inside a ConditionalBlock is allocated before that symbol is defined, when the transient is accessed in two or more blocks after the conditional. The generated new T[s] runs with s still uninitialized, producing a wrong-size allocation and out-of-bounds writes (observed downstream as glibc free(): invalid next size / corrupted size vs. prev_size).

Reproducer (pure DaCe, no frontend)

import dace
from dace.sdfg.state import LoopRegion, ConditionalBlock, ControlFlowRegion
from dace.properties import CodeBlock

sdfg = dace.SDFG('alloc_before_symbol')
for sym in ('cond', 'n', 'm'):
    sdfg.add_symbol(sym, dace.int64)
sdfg.add_symbol('s', dace.int64)
sdfg.add_array('out', [10], dace.float64)
sdfg.add_transient('a', ['s'], dace.float64)   # shape symbol `s`

pre = sdfg.add_state('pre', is_start_block=True)

# Conditional that defines `s` on its branch interstate edges.
cb = ConditionalBlock('cb')
sdfg.add_node(cb)
sdfg.add_edge(pre, cb, dace.InterstateEdge())
then_reg = ControlFlowRegion('then_reg', sdfg=sdfg)
t0 = then_reg.add_state('t0', is_start_block=True); t1 = then_reg.add_state('t1')
then_reg.add_edge(t0, t1, dace.InterstateEdge(assignments={'s': 'n'}))
cb.add_branch(CodeBlock('cond > 0'), then_reg)
else_reg = ControlFlowRegion('else_reg', sdfg=sdfg)
e0 = else_reg.add_state('e0', is_start_block=True); e1 = else_reg.add_state('e1')
else_reg.add_edge(e0, e1, dace.InterstateEdge(assignments={'s': 'm'}))
cb.add_branch(CodeBlock('not (cond > 0)'), else_reg)

# Loop after the conditional -> first access of `a`.
loop = LoopRegion('loop', loop_var='i', initialize_expr='i = 0',
                  condition_expr='i < s', update_expr='i = i + 1')
sdfg.add_node(loop)
sdfg.add_edge(cb, loop, dace.InterstateEdge())
body = loop.add_state('body', is_start_block=True)
w = body.add_tasklet('w', {}, {'o'}, 'o = 1.0')
body.add_edge(w, 'o', body.add_access('a'), None, dace.Memlet('a[i]'))

# Post-loop -> second access of `a`.
post = sdfg.add_state('post')
sdfg.add_edge(loop, post, dace.InterstateEdge())
r = post.add_tasklet('r', {'x'}, {'o'}, 'o = x')
post.add_edge(post.add_access('a'), None, r, 'x', dace.Memlet('a[0]'))
post.add_edge(r, 'o', post.add_access('out'), None, dace.Memlet('out[0]'))

sdfg.validate()
print('\n'.join(c.clean_code for c in sdfg.generate_code()))

Generated code (wrong order)

a = new double DACE_ALIGN(64)[s];   // allocation
...
s = n;                              // shape symbol defined here (then-branch)
...
s = m;                              // ... or here (else-branch)

a is sized by s, but s is assigned only after the allocation, so the buffer is allocated with an uninitialized s.

Root cause

In dace/codegen/targets/framecode.py, determine_allocation_lifetime correctly flags a as symbol-dependent (is_nonfree_sym_dependent returns True, since s is not a free SDFG symbol). Because a is accessed in two blocks (body, post), it takes the multi-state path and calls _get_dominator_and_postdominator, which returns the pre-conditional state (pre) as the closest common dominator. The allocation is then emitted at that dominator — upstream of where s is assigned (inside the ConditionalBlock branches).

_get_dominator_and_postdominator already anticipates this:

# TODO(later): If any of the symbols were not yet defined, or have changed afterwards, fail
# raise NotImplementedError

i.e. the case where the chosen dominator does not have the descriptor's shape symbols defined is known to be unhandled.

Expected behaviour / proposed fix

If a symbol-dependent transient's allocation lifetime is S1 -> S2, the allocation must be emitted before S1 starts but after all interstate-edge assignments that precede S1 — in particular, the chosen S1 must post-dominate every interstate-edge assignment to the descriptor's shape symbols. For the reproducer, S1 must be a block reached after the ConditionalBlock (where s is merged-defined), not the pre-conditional state.

Concretely, the symbol-dependent branch in determine_allocation_lifetime (resp. _get_dominator_and_postdominator) should advance the allocation state forward until all of the descriptor's shape symbols are defined on entry (e.g. to the post-dominator of their definitions that still dominates the accesses), instead of allocating at a dominator where they are not yet defined.

(Validated locally: inserting a single guard state on the edge out of the symbol-defining ConditionalBlock moves the allocation after s = n/m and produces correct code — confirming the placement is the issue.)

Environment

  • DaCe: commit 8dc9e92a4 (branch FaCe)
  • Python: 3.13.7

Attached SDFG

The reproducer SDFG is attached as dace_alloc_bug_repro.sdfg.txt (GitHub
rejects the .sdfg extension; rename to .sdfg to load via
dace.SDFG.from_file). The inline Python script above regenerates it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions