MLIR-AIE 1.4.5.dev
AIEObjectFifoUnroll.cpp
Go to the documentation of this file.
1//===- AIEObjectFifoUnroll.cpp ----------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
10
11#include "mlir/Dialect/Arith/IR/Arith.h"
12#include "mlir/Dialect/MemRef/IR/MemRef.h"
13#include "mlir/Dialect/SCF/IR/SCF.h"
14#include "mlir/Dialect/SCF/Transforms/Transforms.h"
15#include "mlir/Dialect/SCF/Utils/Utils.h"
16#include "mlir/Dialect/Utils/StaticValueUtils.h"
17#include "mlir/IR/Dominance.h"
18#include "mlir/IR/IRMapping.h"
19#include "mlir/IR/Matchers.h"
20#include "mlir/IR/PatternMatch.h"
21#include "mlir/Interfaces/DataLayoutInterfaces.h"
22#include "mlir/Interfaces/MemorySlotInterfaces.h"
23#include "mlir/Interfaces/SideEffectInterfaces.h"
24#include "mlir/Pass/Pass.h"
25#include "mlir/Pass/PassManager.h"
26#include "mlir/Transforms/Mem2Reg.h"
27#include "mlir/Transforms/Passes.h"
28
29#include "llvm/ADT/DenseSet.h"
30
31namespace xilinx::AIE {
32#define GEN_PASS_DEF_AIEOBJECTFIFOUNROLL
33#include "aie/Dialect/AIE/Transforms/AIEPasses.h.inc"
34} // namespace xilinx::AIE
35
36#define DEBUG_TYPE "aie-objectFifo-unroll"
37
38using namespace mlir;
39using namespace xilinx;
40using namespace xilinx::AIE;
41
42bool xilinx::AIE::isForeverLoop(scf::WhileOp whileOp) {
43 scf::ConditionOp condition = whileOp.getConditionOp();
44 return matchPattern(condition.getCondition(), m_One()) &&
45 llvm::equal(condition.getArgs(), whileOp.getBeforeArguments()) &&
46 llvm::all_of(whileOp.getBeforeBody()->without_terminator(),
47 [](Operation &op) { return isPure(&op); });
48}
49
50bool xilinx::AIE::isObjectFifoLoop(Operation *op) {
51 if (auto whileOp = dyn_cast<scf::WhileOp>(op)) {
52 return isForeverLoop(whileOp);
53 }
54 return isa<scf::ForOp>(op);
55}
56
57Operation *xilinx::AIE::getObjectFifoLoopParent(Operation *op) {
58 for (Operation *parent = op->getParentOp(); parent;
59 parent = parent->getParentOp()) {
60 if (isObjectFifoLoop(parent)) {
61 return parent;
62 }
63 }
64 return nullptr;
65}
66
67namespace {
68
69// Statically known trip count of a loop, or nullopt if it cannot be computed.
70static std::optional<int64_t> getStaticTripCount(scf::ForOp forOp) {
71 if (forOp.getSingleLowerBound() && forOp.getSingleUpperBound() &&
72 forOp.getSingleStep()) {
73 if (std::optional<llvm::APInt> tc = forOp.getStaticTripCount()) {
74 return tc->getSExtValue();
75 }
76 }
77 return std::nullopt;
78}
79
80// Unroll a forever loop by `factor`: its body runs `factor` times per trip
81// around the loop, each copy taking the values the previous one yields. The
82// loop never exits, so there is no remainder to handle.
83static void unrollForeverLoop(scf::WhileOp whileOp, int64_t factor) {
84 Block *body = whileOp.getAfterBody();
85 auto yield = cast<scf::YieldOp>(body->getTerminator());
86 SmallVector<Operation *> bodyOps;
87 for (Operation &op : body->without_terminator()) {
88 bodyOps.push_back(&op);
89 }
90 OpBuilder builder(yield);
91 SmallVector<Value> yielded(yield.getOperands());
92 SmallVector<Value> carried = yielded;
93 for (int64_t i = 1; i < factor; ++i) {
94 IRMapping mapping;
95 mapping.map(body->getArguments(), carried);
96 for (Operation *op : bodyOps) {
97 builder.clone(*op, mapping);
98 }
99 for (auto [next, value] : llvm::zip_equal(carried, yielded)) {
100 next = mapping.lookupOrDefault(value);
101 }
102 }
103 yield->setOperands(carried);
104}
105
106// Remove redundant AIE1 binary-lock acquires.
107//
108// The dynamic lowering acquires the whole sliding window ([counter, counter +
109// acqNumber)) on every loop iteration; after unrolling and constant folding
110// that expands to an explicit `Acquire` of each element's lock.
111// This drops the needless re-acquire of locks already held.
112//
113// The analysis is block-local and conservatively assumes nothing is held on
114// block entry. That keeps the very first acquire of each unrolled loop body
115// (which legitimately re-establishes the window across the back-edge) while
116// dropping the intra-body re-acquires of locks whose element is still held.
117static bool removeRedundantBinaryAcquires(DeviceOp device) {
118 bool erased = false;
119 device.walk([&](Block *block) {
120 llvm::DenseSet<Value> held;
121 SmallVector<UseLockOp> toErase;
122 for (Operation &op : *block) {
123 auto useLock = dyn_cast<UseLockOp>(&op);
124 if (!useLock) {
125 continue;
126 }
127 Value lock = useLock.getLock();
128 if (useLock.acquire()) {
129 // A binary `Acquire` of a lock already in the held set is redundant.
130 if (!held.insert(lock).second) {
131 toErase.push_back(useLock);
132 }
133 } else if (useLock.release()) {
134 held.erase(lock);
135 }
136 }
137 for (UseLockOp op : toErase) {
138 op.erase();
139 }
140 erased |= !toErase.empty();
141 });
142 return erased;
143}
144
145// Marks a loop already offered to the peeling trial below, so that neither the
146// peeled loop nor the reverted copy is considered again.
147static constexpr llvm::StringLiteral kPeelTriedAttrName = "aie.peel_tried";
148
149// Number of `use_lock` ops in `op` whose lock value is not a compile-time
150// constant. UseLockOp::getConstantValue() reports an error when it fails, so
151// this probes the value directly.
152static int64_t countRuntimeLockValues(Operation *op) {
153 int64_t count = 0;
154 op->walk([&](UseLockOp useLockOp) {
155 if (!getConstantIntValue(useLockOp.getValue())) {
156 count++;
157 }
158 });
159 return count;
160}
161
162// Number of memrefs in `op` that are picked at run time: block arguments and
163// the results of region ops and selects.
164static int64_t countRuntimeMemrefs(Operation *op) {
165 int64_t count = 0;
166 auto isMemref = llvm::IsaPred<BaseMemRefType>;
167 op->walk([&](Block *block) {
168 count += llvm::count_if(block->getArgumentTypes(), isMemref);
169 });
170 op->walk([&](Operation *nested) {
171 if (nested != op &&
172 (nested->getNumRegions() > 0 || isa<arith::SelectOp>(nested))) {
173 count += llvm::count_if(nested->getResultTypes(), isMemref);
174 }
175 });
176 return count;
177}
178
179// Canonicalize, SCCP, canonicalize. The fold needs the folding patterns only:
180// region simplification walks every region of the device, runtime sequences
181// included, and the canonicalizes after this pass still run it.
182static OpPassManager buildFoldPipeline() {
183 GreedyRewriteConfig config;
184 config.setRegionSimplificationLevel(GreedySimplifyRegionLevel::Disabled);
185 OpPassManager pipeline(DeviceOp::getOperationName());
186 pipeline.addPass(mlir::createCanonicalizerPass(config));
187 pipeline.addPass(mlir::createSCCPPass());
188 pipeline.addPass(mlir::createCanonicalizerPass(config));
189 return pipeline;
190}
191
192struct AIEObjectFifoUnrollPass
193 : xilinx::AIE::impl::AIEObjectFifoUnrollBase<AIEObjectFifoUnrollPass> {
194 using Base::Base;
195
196 void getDependentDialects(DialectRegistry &registry) const override {
197 registry.insert<AIEDialect>();
198 registry.insert<scf::SCFDialect>();
199 }
200
201 /// Peel the first iteration off each loop whose lock values are still
202 /// computed at run time, keeping a peel only where it makes them constant.
203 ///
204 /// Whether a peel pays off depends on foldings that cannot be predicted from
205 /// the objectFifo access pattern alone (a conditional acquire may or may not
206 /// collapse, for instance), so each candidate is peeled for real and the
207 /// result measured. While the fold pipeline runs, the unpeeled core is held
208 /// aside as a detached clone, so exactly one version of the loop is ever
209 /// live; that clone is swapped back in when the peel bought nothing, and
210 /// destroyed otherwise.
211 ///
212 /// The measurement is a per-core count, so attributing a change to a peel
213 /// needs one peel per core per fold; the loop below drains the candidates one
214 /// round at a time, peeling every core in parallel so a round costs a single
215 /// run of the fold pipeline. Candidates are marked before they are tried and
216 /// never unmarked while draining, so the number of rounds is bounded by the
217 /// most objectFifo loops in any one core.
218 LogicalResult peelObjectFifoLoops(DeviceOp device) {
219 OpPassManager foldPipeline = buildFoldPipeline();
220
221 IRRewriter rewriter(device.getContext());
222 struct Trial {
223 CoreOp core;
224 Operation *unpeeled;
225 int64_t runtimeLocksBefore;
226 };
227
228 while (true) {
229 SmallVector<Trial> trials;
230 for (CoreOp core : SmallVector<CoreOp>(device.getOps<CoreOp>())) {
231 scf::ForOp target;
232 // Post-order, so the innermost untried loop is taken first.
233 core.walk([&](scf::ForOp forOp) {
234 if (forOp->hasAttr(kPeelTriedAttrName) ||
235 countRuntimeLockValues(forOp) == 0) {
236 return WalkResult::advance();
237 }
238 // A single-iteration loop has nothing to peel off.
239 std::optional<int64_t> trip = getStaticTripCount(forOp);
240 if (trip && *trip <= 1) {
241 return WalkResult::advance();
242 }
243 target = forOp;
244 return WalkResult::interrupt();
245 });
246 if (!target) {
247 continue;
248 }
249 // Marked before the core is cloned, so neither the peeled loop nor the
250 // fallback copy is offered as a candidate again.
251 target->setAttr(kPeelTriedAttrName, rewriter.getUnitAttr());
252
253 Trial trial{core, core->clone(), countRuntimeLockValues(core)};
254 scf::ForOp firstIteration;
255 if (failed(scf::peelForLoopFirstIteration(rewriter, target,
256 firstIteration))) {
257 trial.unpeeled->erase();
258 continue;
259 }
260 trials.push_back(trial);
261 }
262 if (trials.empty()) {
263 break;
264 }
265
266 if (failed(runPipeline(foldPipeline, device))) {
267 for (Trial &trial : trials) {
268 trial.unpeeled->erase();
269 }
270 return failure();
271 }
272
273 for (Trial &trial : trials) {
274 if (countRuntimeLockValues(trial.core) < trial.runtimeLocksBefore) {
275 trial.unpeeled->erase();
276 continue;
277 }
278 rewriter.setInsertionPoint(trial.core);
279 Operation *restored = rewriter.insert(trial.unpeeled);
280 rewriter.replaceOp(trial.core, restored->getResults());
281 }
282 }
283
284 device.walk(
285 [&](scf::ForOp forOp) { forOp->removeAttr(kPeelTriedAttrName); });
286 return success();
287 }
288
289 /// Run `foldPipeline`, first promoting the object slots collected above to
290 /// SSA on every core where that folds each object to its buffer; elsewhere
291 /// they are left in memory.
292 ///
293 /// A promoted object still picked at run time is a memref descriptor carried
294 /// through control flow, which LLVM's alias analysis cannot tell apart from
295 /// the others; left in memory, SROA reduces it to a pointer. As with peeling,
296 /// each core is promoted for real against a detached clone. Unless the
297 /// promoted core picks no memref at run time, the clone is swapped in and
298 /// folded too, and the promoted core returns only if it picks no more: with
299 /// both sides folded, unrelated folding cannot tip the comparison.
300 LogicalResult foldPromotingObjectSlots(DeviceOp device,
301 ArrayRef<memref::AllocaOp> slots,
302 OpPassManager &foldPipeline) {
303 if (slots.empty()) {
304 return runPipeline(foldPipeline, device);
305 }
306 struct Trial {
307 CoreOp core;
308 Operation *unpromoted;
309 };
310 SmallVector<Trial> trials;
311 llvm::SmallPtrSet<Operation *, 8> seen;
312 SmallVector<PromotableAllocationOpInterface> allocators;
313 for (memref::AllocaOp slot : slots) {
314 auto core = slot->getParentOfType<CoreOp>();
315 if (seen.insert(core).second) {
316 trials.push_back({core, core->clone()});
317 }
318 allocators.push_back(
319 cast<PromotableAllocationOpInterface>(slot.getOperation()));
320 }
321 DataLayout dataLayout = DataLayout::closest(device);
322 DominanceInfo dominance(device);
323 OpBuilder builder(device.getContext());
324 (void)tryToPromoteMemorySlots(allocators, builder, dataLayout, dominance);
325
326 if (failed(runPipeline(foldPipeline, device))) {
327 for (Trial &trial : trials) {
328 trial.unpromoted->erase();
329 }
330 return failure();
331 }
332
333 IRRewriter rewriter(device.getContext());
334 // Puts `in` in place of `out` and detaches `out`.
335 auto swap = [&](Operation *out, Operation *in) {
336 rewriter.setInsertionPoint(out);
337 rewriter.insert(in);
338 rewriter.replaceAllOpUsesWith(out, in->getResults());
339 out->remove();
340 };
341 struct Contest {
342 Operation *promoted;
343 Operation *unpromoted;
344 int64_t promotedRuntimeMemrefs;
345 };
346 SmallVector<Contest> contests;
347 bool restoredAny = false;
348 for (Trial &trial : trials) {
349 // mem2reg may also have left a slot in place.
350 WalkResult leftover = trial.core.walk([&](memref::AllocaOp allocaOp) {
351 return allocaOp->hasAttr(kObjectFifoObjectSlotAttrName)
352 ? WalkResult::interrupt()
353 : WalkResult::advance();
354 });
355 int64_t runtimeMemrefs = countRuntimeMemrefs(trial.core);
356 if (!leftover.wasInterrupted() && runtimeMemrefs == 0) {
357 trial.unpromoted->erase();
358 continue;
359 }
360 swap(trial.core, trial.unpromoted);
361 trial.unpromoted->walk([&](memref::AllocaOp allocaOp) {
362 allocaOp->removeAttr(kObjectFifoObjectSlotAttrName);
363 });
364 if (leftover.wasInterrupted()) {
365 trial.core->erase();
366 } else {
367 contests.push_back({trial.core, trial.unpromoted, runtimeMemrefs});
368 }
369 restoredAny = true;
370 }
371 if (!restoredAny) {
372 return success();
373 }
374
375 // The restored cores have not been folded yet.
376 if (failed(runPipeline(foldPipeline, device))) {
377 for (Contest &contest : contests) {
378 contest.promoted->erase();
379 }
380 return failure();
381 }
382 for (Contest &contest : contests) {
383 if (contest.promotedRuntimeMemrefs <=
384 countRuntimeMemrefs(contest.unpromoted)) {
385 swap(contest.unpromoted, contest.promoted);
386 contest.unpromoted->erase();
387 } else {
388 contest.promoted->erase();
389 }
390 }
391 return success();
392 }
393
394 void runOnOperation() override {
395 DeviceOp device = getOperation();
396
397 bool peelFirstIteration = false;
398 if (clPeelFirstIteration == "auto") {
399 peelFirstIteration = !clDefaultDynamic;
400 } else if (clPeelFirstIteration == "true") {
401 peelFirstIteration = true;
402 } else if (clPeelFirstIteration != "false") {
403 device.emitOpError("invalid peel-first-iteration value '")
404 << clPeelFirstIteration << R"('; expected "true", "false" or "auto")";
405 return signalPassFailure();
406 }
407
408 SmallVector<memref::AllocaOp> objectSlots;
409 for (auto coreOp : device.getOps<CoreOp>()) {
410 // `default-dynamic` picks the lowering for cores that do not pin their
411 // own choice; a core's `dynamic_objfifo_lowering` attribute overrides it
412 // in either direction. Dynamic cores keep their loops rolled (drop the
413 // hints, skip unrolling) and keep in memory the objects they reach from
414 // anywhere but straight-line code; their runtime bookkeeping is tidied by
415 // the shared fold/cleanup pipeline below.
416 if (coreOp.getDynamicObjfifoLowering().value_or(clDefaultDynamic)) {
417 coreOp.walk([&](Operation *op) {
418 op->removeAttr(kObjectFifoUnrollHintAttrName);
419 });
420 bool straightLine = coreOp.getBody().hasOneBlock();
421 coreOp.walk([&](memref::AllocaOp allocaOp) {
422 if (!allocaOp->hasAttr(kObjectFifoObjectSlotAttrName)) {
423 return;
424 }
425 if (straightLine &&
426 llvm::all_of(allocaOp->getUsers(), [&](Operation *user) {
427 return user->getParentOp() == coreOp.getOperation();
428 })) {
429 objectSlots.push_back(allocaOp);
430 } else {
431 allocaOp->removeAttr(kObjectFifoObjectSlotAttrName);
432 }
433 });
434 continue;
435 }
436 coreOp.walk([&](memref::AllocaOp allocaOp) {
437 if (allocaOp->hasAttr(kObjectFifoObjectSlotAttrName)) {
438 objectSlots.push_back(allocaOp);
439 }
440 });
441 SmallVector<Operation *> loops;
442 coreOp.walk([&](Operation *op) {
443 if (isObjectFifoLoop(op)) {
444 loops.push_back(op);
445 }
446 });
447
448 // Operation::walk uses post-order traversal by default, so a nested loop
449 // is visited before its enclosing loop; iterating the list in order thus
450 // processes the innermost loops first. Unrolling innermost loops first
451 // avoids invalidating references to inner loops when an outer loop (which
452 // duplicates its nested loops) is unrolled.
453 for (Operation *loop : loops) {
454 // Loops without the objectFifo unroll hint (the LCM of the accessed
455 // fifo depths) carry no objectFifo access; factor 1 leaves them rolled.
456 auto hint =
457 loop->getAttrOfType<IntegerAttr>(kObjectFifoUnrollHintAttrName);
458 int64_t unrollFactor = hint ? hint.getInt() : 1;
459 if (unrollFactor <= 1) {
460 continue;
461 }
462
463 // A forever loop has no trip count to cover, so repeating its body
464 // once per rotation is enough.
465 auto forOp = dyn_cast<scf::ForOp>(loop);
466 if (!forOp) {
467 unrollForeverLoop(cast<scf::WhileOp>(loop), unrollFactor);
468 continue;
469 }
470
471 std::optional<int64_t> trip = getStaticTripCount(forOp);
472 // When the loop performs fewer iterations than a full rotation of the
473 // objectFifos, unroll it completely: every iteration must map to an
474 // explicit buffer/lock slot.
475 if (trip && *trip <= unrollFactor) {
476 if (failed(mlir::loopUnrollFull(forOp))) {
477 forOp.emitOpError()
478 << "failed to fully unroll objectFifo loop (trip count "
479 << *trip << ")";
480 return signalPassFailure();
481 }
482 continue;
483 }
484
485 // Otherwise unroll by the rotation period. loopUnrollByFactor peels a
486 // cleanup/epilogue loop for the remaining iterations when the trip
487 // count is not an exact multiple of the factor.
488 FailureOr<mlir::UnrolledLoopInfo> info = mlir::loopUnrollByFactor(
489 forOp, static_cast<uint64_t>(unrollFactor));
490 if (failed(info)) {
491 forOp.emitOpError()
492 << "failed to unroll objectFifo loop by factor " << unrollFactor;
493 return signalPassFailure();
494 }
495
496 // The epilogue runs the remaining (< factor) iterations. Fully unroll
497 // it as well so that each of those iterations maps to an explicit
498 // buffer/lock rotation slot. This is best-effort: an epilogue with a
499 // non-constant trip count cannot be fully unrolled and is left rolled.
500 std::optional<scf::ForOp> epilogue =
501 info->epilogueLoopOp; // NOLINT(bugprone-unchecked-optional-access)
502 if (epilogue) {
503 (void)mlir::loopUnrollFull(*epilogue);
504 }
505 }
506
507 // Drop any lingering unroll hints so they do not leak into the output.
508 coreOp.walk([&](Operation *op) {
509 op->removeAttr(kObjectFifoUnrollHintAttrName);
510 });
511 }
512
513 // --aie-objectfifo-lower-cores promotes the lock bookkeeping counters to
514 // loop-carried SSA values, and the objects are promoted here where they
515 // fold. Once the loops have been unrolled by their rotation period, the
516 // objects and counters become loop-invariant, so every object and lock
517 // value collapses to a constant. Run that fold here as a scoped
518 // sub-pipeline: canonicalize exposes the constants, SCCP propagates them
519 // across any remainder loop that survives a partial unroll, and a final
520 // canonicalize deletes the now-dead counter arithmetic and iter_args.
521 OpPassManager foldPipeline = buildFoldPipeline();
522 if (failed(foldPromotingObjectSlots(device, objectSlots, foldPipeline))) {
523 return signalPassFailure();
524 }
525
526 if (peelFirstIteration && failed(peelObjectFifoLoops(device))) {
527 return signalPassFailure();
528 }
529
530 // With the window acquires now folded to concrete per-lock `Acquire` ops,
531 // drop the ones that re-acquire an already-held AIE1 binary lock. Only a
532 // drop leaves dead constants for a final canonicalize; the fold left the
533 // device canonical otherwise.
534 if (!removeRedundantBinaryAcquires(device)) {
535 return;
536 }
537 OpPassManager cleanupPipeline(DeviceOp::getOperationName());
538 cleanupPipeline.addPass(mlir::createCanonicalizerPass());
539 if (failed(runPipeline(cleanupPipeline, device))) {
540 return signalPassFailure();
541 }
542 }
543};
544
545} // namespace
546
547std::unique_ptr<mlir::OperationPass<xilinx::AIE::DeviceOp>>
549 return std::make_unique<AIEObjectFifoUnrollPass>();
550}
551
552std::unique_ptr<OperationPass<DeviceOp>>
554 const AIEObjectFifoUnrollOptions &options) {
555 return std::make_unique<AIEObjectFifoUnrollPass>(options);
556}
constexpr llvm::StringLiteral kObjectFifoObjectSlotAttrName
Discardable attribute set by --aie-objectfifo-lower-cores on the rank-0 memref.allocas that hold a co...
Definition AIEPasses.h:34
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIEObjectFifoUnrollPass()
constexpr llvm::StringLiteral kObjectFifoUnrollHintAttrName
Discardable attribute set by --aie-objectfifo-lower-cores on scf.for and forever scf....
Definition AIEPasses.h:25
bool isForeverLoop(mlir::scf::WhileOp whileOp)
Whether whileOp repeats forever: its condition is a constant true, and its condition region does noth...
mlir::Operation * getObjectFifoLoopParent(mlir::Operation *op)
The innermost loop around op that isObjectFifoLoop accepts, or null.
bool isObjectFifoLoop(mlir::Operation *op)
Whether op is a loop that objectFifo lowering unrolls by the rotation period of the objectFifos acces...