Skip to content

[AIE] Add inner-loop versioning to pipeline small-trip-count loops#1078

Open
mludevid wants to merge 4 commits into
aie-publicfrom
mludevid.loop.versioning.public
Open

[AIE] Add inner-loop versioning to pipeline small-trip-count loops#1078
mludevid wants to merge 4 commits into
aie-publicfrom
mludevid.loop.versioning.public

Conversation

@mludevid

Copy link
Copy Markdown
Member

This PR adds inner-loop versioning for AIE targets so the post-pipeliner can software-pipeline inner loops even when their statically known minimum trip count is too small. A loop marked with the aie-loop-versioning hint is split into two runtime-guarded copies: a pipelined fast copy and a verbatim, un-pipelined slow copy. A trip-count check chooses between them at runtime, so small trip counts stay correct while large ones get the pipelined schedule.

@andcarminati

Copy link
Copy Markdown
Collaborator

Could we merge the first commit in advance?

Comment thread llvm/lib/Target/AIE/Utils/AIELoopUtils.h
Comment thread llvm/lib/Target/AIE/aie2ps/AIE2PSInstrInfo.cpp
Comment thread llvm/lib/Target/AIE/AIEInnerLoopVersioning.cpp Outdated
Comment thread llvm/lib/Target/AIE/AIEInnerLoopVersioning.cpp Outdated
Comment thread llvm/lib/Target/AIE/AIEInnerLoopVersioning.cpp Outdated
Comment thread llvm/lib/Target/AIE/AIEBaseInstrInfo.cpp
Comment thread llvm/test/CodeGen/AIE/aie2ps/loop-version-threshold.ll
Comment thread llvm/lib/Target/AIE/AIEPostPipeliner.cpp
Comment thread llvm/lib/Target/AIE/AIEPostPipeliner.cpp
Comment thread llvm/lib/Target/AIE/AIEPostPipeliner.cpp Outdated
Comment thread llvm/lib/Target/AIE/AIEPostPipeliner.cpp Outdated
Comment thread llvm/lib/Target/AIE/AIEPostPipeliner.cpp Outdated
// Reaching definition of Reg seen from Use, scanning backwards into
// predecessors -- the threshold pseudo may sit in a dominating block.
static MachineInstr *
findReachingDef(MachineBasicBlock &BB, MachineBasicBlock::reverse_iterator Use,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to reimplement reachingDef? I have a suspicion we may not need to reimplement it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what we could reuse for this. If you have a specific suggestion please let me know. I'm not aware of any.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say that this is a simplified version for when you know that you have a single definition that dominates uses. General reaching defs for non-ssa code require a bit more expensive fixed-point dataflow analysis that could return you a set of definitions. Maybe findUniqueReachingDef?

@andcarminati andcarminati Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that we cannot hoist the intrinsic here (properties of the instruction), your search will stop in the same block of the instruction that uses the register discovered by findGuardConditionReg. Basically, def and use will be in the same block, could we aggressively simplify? I feel we have a complex code to deal with a simple problem.

Comment thread llvm/lib/Target/AIE/AIEInnerLoopVersioning.cpp Outdated
Comment thread llvm/lib/Target/AIE/AIEInnerLoopVersioning.cpp Outdated
Comment thread llvm/lib/Target/AIE/AIEInnerLoopVersioning.cpp Outdated
bool runOnFunction(Function &F) override;

void getAnalysisUsage(AnalysisUsage &AU) const override {
// We deliberately do not force LoopSimplify/LCSSA: that canonicalization

@andcarminati andcarminati Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could be moved to an earlier position, as this is part of the contract of the pass.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where to? The file header? Personally I think it makes sense to keep it here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments only, to help to describe the pass pre and post conditions. Please no header files.

Comment thread llvm/lib/Target/AIE/AIEInnerLoopVersioning.cpp Outdated
@mludevid
mludevid force-pushed the mludevid.loop.versioning.public branch 3 times, most recently from bc06e36 to 3b02253 Compare July 20, 2026 15:21
return Intrinsic::not_intrinsic;
}

void dropLoopMetadata(Loop &L, ArrayRef<StringRef> Keys) {

@F-Stuckmann F-Stuckmann Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHECK: we are dropping the Key metadata, correct? You may want to rename the argument to clarify the intention.

}

bool AIEBaseInstrInfo::isLoopVersionThresholdDef(const MachineInstr &MI) const {
auto Opcode = getLoopVersionThresholdOpcode();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can cost the opcode

if (!isInt<10>(MinTripCount))
report_fatal_error(
"AIE loop versioning: threshold does not fit the scalar-move immediate");
report_fatal_error("AIE loop versioning: threshold does not fit the "

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a matter of taste ;-). This fatal is in TII, but the message resembles something emitted by the loop versioning pass.

// A fall-through preheader may be empty, so pick a DebugLoc defensively.
const DebugLoc GuardLoc =
Preheader->empty() ? DebugLoc() : Preheader->front().getDebugLoc();
auto bailWithMsg = [&](StringRef Msg) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit BailWithMsg

// Reaching definition of Reg seen from Use, scanning backwards into
// predecessors -- the threshold pseudo may sit in a dominating block.
static MachineInstr *
findReachingDef(MachineBasicBlock &BB, MachineBasicBlock::reverse_iterator Use,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could live in a utility file.

for (const MachineOperand &MO : MI.operands()) {
if (MO.isMBB())
TargetsBlock = true;
else if (MO.isReg() && MO.isUse() && MO.getReg().isPhysical())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why checking if it is physical?

else if (MO.isReg() && MO.isUse() && MO.getReg().isPhysical())
RegUse = MO.getReg();
}
if (TargetsBlock && RegUse.isValid())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would restructure this function in such a way we stop the search if we hit this if. Or simply a return here. Alternatively, we are looking to the second last instruction (before the barrier), why do we need a search? Also, If we keep the search for simplicity, why not a reverse traversal of the MBB?

return nullptr;
}

MachineBasicBlock *PostPipeliner::findVersionGuard() const {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: AIELoopUtils with a nice comment?

;
; (c) Copyright 2026 Advanced Micro Devices, Inc. or its affiliates
;
; End-to-end: a versioned loop that the post-pipeliner can schedule gets its

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may use two sets of folders: loop versioning exclusive folder (IR tests - we need to create) and end-to-end (this folder exists).

addExitPHIs(HighLoop, ExitBlock, VMap, DefsUsedOutside);
formDedicatedExitBlocks(HighLoop, &DT, &LI, nullptr, /*PreserveLCSSA=*/true);
formDedicatedExitBlocks(&L, &DT, &LI, nullptr, /*PreserveLCSSA=*/true);

@andcarminati andcarminati Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we extract all the code below as a member function related to metadata handling, like updateLoopsMetadata? You can use more descriptive methods for the previous steps also. This is a suggestion to improve readability - if you agree, of course.

// not re-version either. Mark the high-trip-count copy as versioned (carrying
// the request's value) so the postpipeliner recognizes it as the pipelined
// copy; the low copy stays the verbatim un-pipelined fallback.
int64_t HintValue = *AIELoopUtils::getLoopHintInt(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const?

}

bool AIELoopVersioner::isProfitable() const {
// The runtime guard compares the trip count in i32, and the pipelined

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have a test rejected by this function?

Value *Threshold =
Builder.CreateIntrinsic(ThresholdIID, {}, {Builder.getInt32(-1)},
/*FMFSource=*/nullptr, "lver.threshold");
if (TripCount->getType() != I32Ty)

@andcarminati andcarminati Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We guarantee to not be greater than 32, so we may have 16bit or 8bit, right? Could we have a test?

Value *TakeLow = Builder.CreateICmpULT(TripCount, Threshold, "lver.low");
GuardBB->setName(Header->getName() + ".lver.guard");

SmallVector<Instruction *, 8> DefsUsedOutside = findDefsUsedOutsideOfLoop(&L);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to move this a bit more close to the user?

break;
if (PN)
continue;
PN = PHINode::Create(Inst->getType(), 2, Inst->getName() + ".lver");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are enforcing LCSSA everywhere. Here we catch a possible LCSSA violation, how is this possible? If we don't have a phi node and we need to create a new one we are definitely out of LCSSA. I think we could assert the property and simplify the code.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this works as well:

void AIELoopVersioner::addExitPHIs(
    Loop *ClonedLoop, BasicBlock *ExitBlock, ValueToValueMapTy &VMap,
    ArrayRef<Instruction *> DefsUsedOutside) const {
  for (PHINode &PN : ExitBlock->phis()) {
    assert(PN.getNumIncomingValues() == 1 &&
           PN.getIncomingBlock(0) == L.getExitingBlock() &&
           "expected single-edge LCSSA phi");
    Value *OrigValue = PN.getIncomingValue(0);
    Value *ClonedValue = VMap.lookup(OrigValue);
    PN.addIncoming(ClonedValue ? ClonedValue : OrigValue,
                   ClonedLoop->getExitingBlock());
  }
}

I can't find a counter example.


DT.changeImmediateDominator(ExitBlock, GuardBB);
addExitPHIs(HighLoop, ExitBlock, VMap, DefsUsedOutside);
formDedicatedExitBlocks(HighLoop, &DT, &LI, nullptr, /*PreserveLCSSA=*/true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I guess we don't need to call formDedicatedExitBlocks here, simplifyLoop called previously guarantees this. If you see a potential need, we could create a test.

@mludevid
mludevid force-pushed the mludevid.loop.versioning.public branch from 3b02253 to 8cca6fe Compare July 23, 2026 12:57
}

void dropLoopMetadata(Loop &L, ArrayRef<StringRef> Keys) {
MDNode *LoopID = L.getLoopID();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: const

void dropLoopMetadata(Loop &L, ArrayRef<StringRef> Keys) {
MDNode *LoopID = L.getLoopID();
if (!LoopID)
return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we could also early exit if Keys is empty

// Collect enabled loops before mutating, so cloning does not disturb
// iteration.
SmallVector<Loop *, 4> Candidates;
for (Loop *L : LI.getLoopsInPreorder())

@F-Stuckmann F-Stuckmann Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the order relevant?

// Read-only profitability gate first, so an unprofitable loop is rejected
// without any IR mutation.
if (!isProfitable()) {
LLVM_DEBUG(dbgs() << " Not profitable to version\n");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please mention the loop that is not profitable.
We may also want to emit optimiazation remarks, about loopVersion success/skipping.

ArrayRef<Instruction *> DefsUsedOutside) const;
/// Remove the versioning request hint so the postpipeliner leaves \p Loop
/// alone.
void stripVersioningHint(Loop &Loop) const;

@F-Stuckmann F-Stuckmann Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why do we strip loop version hints? The postpipeliner specifically only checks if we have versioned metadata and not the versioning request. I don't think removing the loop version request is necessary if we didn't loopversion.


BasicBlock *ExitBlock = canonicalizeAndCheckStructure();
if (!ExitBlock) {
// Versioning bailed but the request hint is still on the loop. Consume it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Instead of a comment, could you just print the debug message?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants