Skip to content

Commit 9d30275

Browse files
committed
accept non-net-zero outcomes
1 parent 24869fe commit 9d30275

4 files changed

Lines changed: 291 additions & 41 deletions

File tree

lib/Dialect/AIE/Transforms/AIEObjectFifoStatefulTransform.cpp

Lines changed: 120 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,26 +1483,28 @@ struct AIEObjectFifoStatefulTransformPass
14831483
return nullptr;
14841484
}
14851485

1486-
// True if op is nested inside an scf.if anywhere strictly below loopOp.
1486+
// True if op is nested inside an scf.if / scf.index_switch anywhere
1487+
// strictly below loopOp.
14871488
bool isInsideIfInsideLoop(Operation *op, Operation *loopOp) {
14881489
Operation *cur = op->getParentOp();
14891490
while (cur && cur != loopOp) {
1490-
if (isa<scf::IfOp>(cur))
1491+
if (isa<scf::IfOp, scf::IndexSwitchOp>(cur))
14911492
return true;
14921493
cur = cur->getParentOp();
14931494
}
14941495
return false;
14951496
}
14961497

1497-
// Returns the child region of the innermost scf.if that encloses `op`
1498-
// strictly below `loopOp` (i.e. the specific then/else branch `op` lives
1499-
// in), or null if `op` is not inside any conditional below loopOp. Used to
1500-
// group conditional acq/rel by branch so per-branch balance can be checked.
1498+
// Returns the child region of the innermost scf.if / scf.index_switch that
1499+
// encloses `op` strictly below `loopOp` (i.e. the specific branch/case `op`
1500+
// lives in), or null if `op` is not inside any conditional below loopOp.
1501+
// Used to group conditional acq/rel by branch so the per-branch net can be
1502+
// compared across all branches of the conditional.
15011503
Region *immediateCondRegion(Operation *op, Operation *loopOp) {
15021504
Operation *cur = op->getParentOp();
15031505
Region *childRegion = op->getParentRegion();
15041506
while (cur && cur != loopOp) {
1505-
if (isa<scf::IfOp>(cur))
1507+
if (isa<scf::IfOp, scf::IndexSwitchOp>(cur))
15061508
return childRegion;
15071509
childRegion = cur->getParentRegion();
15081510
cur = cur->getParentOp();
@@ -1608,26 +1610,31 @@ struct AIEObjectFifoStatefulTransformPass
16081610
}
16091611

16101612
// Decide whether loopOp's body has positive cyclostatic carry on at least
1611-
// one (fifo, port). Diagnostics:
1612-
// - an in-body acq/rel inside an scf.if whose branch is *unbalanced* for
1613-
// a (fifo, port) that also has unconditional acq/rel -> *condDiag is set
1614-
// and we return false (caller emits the diagnostic).
1613+
// one (fifo, port). Conditionals (scf.if / scf.index_switch) whose branches
1614+
// all agree on the same per-fifo net are folded into the static carry (the
1615+
// delta is branch-independent, hence deterministic); only conditionals
1616+
// whose branches disagree are treated as unanalyzable. Diagnostics:
1617+
// - an in-body acq/rel inside a conditional whose branches are *unbalanced*
1618+
// (disagree on the net) for a (fifo, port) that also has an analyzable
1619+
// contribution (unconditional acq/rel, or a foldable conditional net)
1620+
// -> *condDiag is set and we return false (caller emits the diagnostic).
16151621
// Sets `taintedDiag` if peel must be skipped because a fifo's pre-loop
1616-
// held count cannot be analyzed (sibling loop / scf.if touched the same
1622+
// held count cannot be analyzed (sibling loop / conditional touched the same
16171623
// fifo) — this is silent, not an error, since the lowering still produces
16181624
// correct (just unoptimized) code.
16191625
bool bodyHasCyclostaticCarry(Region *bodyRegion, Operation *loopOp,
16201626
Operation *&condDiag) {
16211627
llvm::MapVector<FifoPort, int> maxAcq;
16221628
llvm::MapVector<FifoPort, int> sumRel;
1623-
// Net (acquire - release) per (fifo, port) within each conditional
1624-
// branch region. A branch that nets zero for a fifo (it acquires and
1625-
// releases the same amount, e.g. an if that both acquires and releases)
1626-
// does not perturb the loop's steady-state carry, so it is harmless even
1627-
// when the same fifo is also used unconditionally.
1628-
llvm::DenseMap<Region *, llvm::MapVector<FifoPort, int>> condNet;
1629-
// Conditional ops in walk order, for pointing the diagnostic at the
1630-
// first offending op.
1629+
// For every conditional op (scf.if / scf.index_switch) directly below
1630+
// loopOp, accumulate the net (acquire - release) per (fifo, port) within
1631+
// each of its branch regions:
1632+
// condNet[condOp][branchRegion][fp] = net for that fifo on that path.
1633+
llvm::MapVector<Operation *,
1634+
llvm::MapVector<Region *, llvm::MapVector<FifoPort, int>>>
1635+
condNet;
1636+
// Conditional acq/rel ops in walk order, for pointing the diagnostic at
1637+
// the first offending op of a fifo.
16311638
SmallVector<std::pair<Operation *, FifoPort>> condOps;
16321639
condDiag = nullptr;
16331640

@@ -1637,7 +1644,7 @@ struct AIEObjectFifoStatefulTransformPass
16371644
return;
16381645
FifoPort fp{a.getObjectFifo(), a.getPort()};
16391646
if (Region *r = immediateCondRegion(a, loopOp)) {
1640-
condNet[r][fp] += a.acqNumber();
1647+
condNet[r->getParentOp()][r][fp] += a.acqNumber();
16411648
condOps.push_back({a, fp});
16421649
} else {
16431650
maxAcq[fp] = std::max(maxAcq.lookup(fp), a.acqNumber());
@@ -1647,35 +1654,96 @@ struct AIEObjectFifoStatefulTransformPass
16471654
return;
16481655
FifoPort fp{r.getObjectFifo(), r.getPort()};
16491656
if (Region *reg = immediateCondRegion(r, loopOp)) {
1650-
condNet[reg][fp] -= r.relNumber();
1657+
condNet[reg->getParentOp()][reg][fp] -= r.relNumber();
16511658
condOps.push_back({r, fp});
16521659
} else {
16531660
sumRel[fp] = sumRel.lookup(fp) + r.relNumber();
16541661
}
16551662
}
16561663
});
16571664

1658-
// A (fifo, port) is "unbalanced-conditional" if some conditional branch
1659-
// has a nonzero net for it. Only such fifos make the straight-line carry
1660-
// computation unsound, and only when they *also* have unconditional
1661-
// acq/rel — then we cannot tell whether the conditional acquire/release
1662-
// belongs to the steady state. Balanced conditional branches and
1663-
// pure-conditional fifos (no unconditional ops) are well-formed: the
1664-
// lock-lowering tracks each conditional path on its own without peeling.
1665-
llvm::SetVector<FifoPort> unbalancedCond;
1666-
for (auto &regionEntry : condNet)
1667-
for (auto &kv : regionEntry.second)
1668-
if (kv.second != 0)
1669-
unbalancedCond.insert(kv.first);
1665+
// Classify each conditional op's contribution per (fifo, port).
1666+
//
1667+
// A conditional whose branches all agree on the same net C for a fifo
1668+
// contributes a *deterministic* per-iteration delta of C, regardless of
1669+
// which branch runs. That is indistinguishable from an unconditional
1670+
// acquire/release of net C, so it can be folded into the static carry.
1671+
// C == 0 (a branch that acquires and releases the same amount) is just
1672+
// the special case that adds nothing.
1673+
//
1674+
// When the branches *disagree* the per-iteration delta is data
1675+
// dependent, so the fifo is "unbalanced-conditional": the straight-line
1676+
// carry computation cannot account for it. Every branch region of the
1677+
// conditional participates — a branch that never touches the fifo (or an
1678+
// empty/absent else branch) counts as net 0, so an asymmetric
1679+
// "acquire on one path only" is correctly flagged.
1680+
llvm::MapVector<FifoPort, int> condCarry; // folded deterministic net
1681+
llvm::SetVector<FifoPort> unbalancedCond; // branches disagree
1682+
for (auto &opEntry : condNet) {
1683+
Operation *condOp = opEntry.first;
1684+
auto &branchMap = opEntry.second;
1685+
llvm::SetVector<FifoPort> touched;
1686+
for (auto &regEntry : branchMap)
1687+
for (auto &kv : regEntry.second)
1688+
touched.insert(kv.first);
1689+
for (const FifoPort &fp : touched) {
1690+
bool first = true, allEqual = true;
1691+
int common = 0;
1692+
for (Region &reg : condOp->getRegions()) {
1693+
int net = 0;
1694+
auto it = branchMap.find(&reg);
1695+
if (it != branchMap.end())
1696+
net = it->second.lookup(fp);
1697+
if (first) {
1698+
common = net;
1699+
first = false;
1700+
} else if (net != common) {
1701+
allEqual = false;
1702+
break;
1703+
}
1704+
}
1705+
if (allEqual)
1706+
condCarry[fp] += common;
1707+
else
1708+
unbalancedCond.insert(fp);
1709+
}
1710+
}
16701711

1712+
// An unbalanced-conditional fifo is unanalyzable. We only *error* when it
1713+
// is mixed with an analyzable contribution for the same fifo — an
1714+
// unconditional acq/rel or a foldable conditional net — because then the
1715+
// peel would be based on an incomplete carry. A fifo that is *purely*
1716+
// unbalanced-conditional (no other use) is left alone: the lock-lowering
1717+
// tracks each path on its own and the loop still lowers correctly,
1718+
// just without the peel optimization.
16711719
for (const auto &fp : unbalancedCond) {
1672-
if (maxAcq.count(fp) || sumRel.count(fp)) {
1720+
if (maxAcq.count(fp) || sumRel.count(fp) || condCarry.count(fp)) {
16731721
for (const auto &co : condOps) {
16741722
if (co.second != fp)
16751723
continue;
16761724
Region *reg = immediateCondRegion(co.first, loopOp);
1677-
auto it = reg ? condNet.find(reg) : condNet.end();
1678-
if (it != condNet.end() && it->second.lookup(fp) != 0) {
1725+
Operation *condOp = reg ? reg->getParentOp() : nullptr;
1726+
// Point at an op living in a branch that disagrees with the rest.
1727+
auto opIt = condOp ? condNet.find(condOp) : condNet.end();
1728+
if (opIt == condNet.end())
1729+
continue;
1730+
bool opUnbalanced = false;
1731+
int common = 0;
1732+
bool first = true;
1733+
for (Region &r : condOp->getRegions()) {
1734+
int net = 0;
1735+
auto bit = opIt->second.find(&r);
1736+
if (bit != opIt->second.end())
1737+
net = bit->second.lookup(fp);
1738+
if (first) {
1739+
common = net;
1740+
first = false;
1741+
} else if (net != common) {
1742+
opUnbalanced = true;
1743+
break;
1744+
}
1745+
}
1746+
if (opUnbalanced) {
16791747
condDiag = co.first;
16801748
break;
16811749
}
@@ -1684,13 +1752,24 @@ struct AIEObjectFifoStatefulTransformPass
16841752
}
16851753
}
16861754

1755+
// Fold the deterministic conditional net into the static carry. For a
1756+
// pure-conditional fifo whose branches all agree, this is the whole
1757+
// carry (maxAcq/sumRel are zero); the peel still clones the entire
1758+
// iteration 0 including the conditional, so AcquireGreaterEqual clamping
1759+
// keeps it correct.
1760+
llvm::SetVector<FifoPort> fifos;
1761+
for (const auto &kv : maxAcq)
1762+
fifos.insert(kv.first);
1763+
for (const auto &kv : condCarry)
1764+
fifos.insert(kv.first);
1765+
16871766
llvm::SetVector<FifoPort> tainted;
16881767
auto held = heldBeforeLoop(loopOp, tainted);
1689-
for (const auto &kv : maxAcq) {
1690-
if (tainted.contains(kv.first))
1768+
for (const FifoPort &fp : fifos) {
1769+
if (tainted.contains(fp))
16911770
continue;
1692-
int carry = kv.second - sumRel.lookup(kv.first);
1693-
int peelCarry = carry - held.lookup(kv.first);
1771+
int carry = maxAcq.lookup(fp) - sumRel.lookup(fp) + condCarry.lookup(fp);
1772+
int peelCarry = carry - held.lookup(fp);
16941773
if (peelCarry > 0)
16951774
return true;
16961775
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//===- dynamic_cyclostatic_switch_balanced.mlir --------------*- MLIR -*-===//
2+
//
3+
// Copyright (C) 2026 Advanced Micro Devices, Inc.
4+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5+
//
6+
//===----------------------------------------------------------------------===//
7+
8+
// The net-equal cyclostatic analysis treats scf.index_switch the same way as
9+
// scf.if: every case region *and* the default region is a branch, and they
10+
// are compared against each other. Here a (fifo, port) is used unconditionally
11+
// and inside an scf.index_switch whose every branch (case 0, case 1, default)
12+
// is balanced (acquire 1 / release 1, net 0). All branches agree, so the
13+
// conditional contributes zero deterministic carry and the pass must NOT emit
14+
// "cannot statically analyze cyclostatic acquire pattern"; the loop lowers
15+
// normally with the switch preserved.
16+
17+
// RUN: aie-opt --aie-objectFifo-stateful-transform="dynamic-objFifos=true" %s | FileCheck %s
18+
19+
// CHECK: aie.core
20+
// CHECK: scf.for
21+
// CHECK: aie.use_lock(%{{.*}}_cons_cons_lock_0, AcquireGreaterEqual, 1)
22+
// CHECK: aie.use_lock(%{{.*}}_cons_prod_lock_0, Release, 1)
23+
// CHECK: scf.index_switch
24+
25+
module {
26+
aie.device(npu2) {
27+
%tile_0_1 = aie.tile(0, 1)
28+
%tile_0_2 = aie.tile(0, 2)
29+
30+
aie.objectfifo @fifo(%tile_0_1, {%tile_0_2}, 4 : i32) : !aie.objectfifo<memref<8xi8>>
31+
32+
%core_0_2 = aie.core(%tile_0_2) {
33+
%c0 = arith.constant 0 : index
34+
%c1 = arith.constant 1 : index
35+
%c14 = arith.constant 14 : index
36+
scf.for %arg0 = %c0 to %c14 step %c1 {
37+
// Unconditional, balanced: net 0.
38+
%u = aie.objectfifo.acquire @fifo(Consume, 1) : !aie.objectfifosubview<memref<8xi8>>
39+
aie.objectfifo.release @fifo(Consume, 1)
40+
// Every branch (including default) has the same net 0.
41+
scf.index_switch %arg0
42+
case 0 {
43+
%a = aie.objectfifo.acquire @fifo(Consume, 1) : !aie.objectfifosubview<memref<8xi8>>
44+
aie.objectfifo.release @fifo(Consume, 1)
45+
scf.yield
46+
}
47+
case 1 {
48+
%b = aie.objectfifo.acquire @fifo(Consume, 1) : !aie.objectfifosubview<memref<8xi8>>
49+
aie.objectfifo.release @fifo(Consume, 1)
50+
scf.yield
51+
}
52+
default {
53+
%d = aie.objectfifo.acquire @fifo(Consume, 1) : !aie.objectfifosubview<memref<8xi8>>
54+
aie.objectfifo.release @fifo(Consume, 1)
55+
scf.yield
56+
}
57+
}
58+
aie.end
59+
}
60+
}
61+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//===- dynamic_cyclostatic_switch_unbalanced.mlir ------------*- MLIR -*-===//
2+
//
3+
// Copyright (C) 2026 Advanced Micro Devices, Inc.
4+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5+
//
6+
//===----------------------------------------------------------------------===//
7+
8+
// Negative test for scf.index_switch: the branches *disagree* on their per-fifo
9+
// net (case 0 nets +1: acquire 2 / release 1; the default nets 0: acquire 1 /
10+
// release 1). Because the delta is data dependent (it varies with the runtime
11+
// switch value) the carry cannot be computed statically. Since the same fifo
12+
// is also used unconditionally, the pass must emit the diagnostic rather than
13+
// peel on an incomplete carry.
14+
15+
// RUN: aie-opt --verify-diagnostics --aie-objectFifo-stateful-transform="dynamic-objFifos=true" %s
16+
17+
module {
18+
aie.device(npu2) {
19+
%tile_0_1 = aie.tile(0, 1)
20+
%tile_0_2 = aie.tile(0, 2)
21+
22+
aie.objectfifo @fifo(%tile_0_1, {%tile_0_2}, 4 : i32) : !aie.objectfifo<memref<8xi8>>
23+
24+
%core_0_2 = aie.core(%tile_0_2) {
25+
%c0 = arith.constant 0 : index
26+
%c1 = arith.constant 1 : index
27+
%c14 = arith.constant 14 : index
28+
scf.for %arg0 = %c0 to %c14 step %c1 {
29+
%u = aie.objectfifo.acquire @fifo(Consume, 1) : !aie.objectfifosubview<memref<8xi8>>
30+
aie.objectfifo.release @fifo(Consume, 1)
31+
scf.index_switch %arg0
32+
case 0 {
33+
// net +1 on this path
34+
%a = aie.objectfifo.acquire @fifo(Consume, 2) : !aie.objectfifosubview<memref<8xi8>>
35+
aie.objectfifo.release @fifo(Consume, 1)
36+
scf.yield
37+
}
38+
default {
39+
// net 0 on this path -> branches disagree -> unanalyzable
40+
// expected-error@+1 {{cannot statically analyze cyclostatic acquire pattern: acquire/release is inside a conditional}}
41+
%d = aie.objectfifo.acquire @fifo(Consume, 1) : !aie.objectfifosubview<memref<8xi8>>
42+
aie.objectfifo.release @fifo(Consume, 1)
43+
scf.yield
44+
}
45+
}
46+
aie.end
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)