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