vporpo e2b0d5df84
[SandboxVec][Scheduler] Enforce scheduling SchedBundle instrs back-to-back (#128092)
This patch fixes the behavior of the scheduler by making sure the instrs
that are part of a SchedBundle are scheduled back-to-back.
2025-02-27 10:23:50 -08:00

361 lines
12 KiB
C++

//===- Scheduler.cpp ------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "llvm/Transforms/Vectorize/SandboxVectorizer/Scheduler.h"
#include "llvm/Transforms/Vectorize/SandboxVectorizer/VecUtils.h"
namespace llvm::sandboxir {
// TODO: Check if we can cache top/bottom to reduce compile-time.
DGNode *SchedBundle::getTop() const {
DGNode *TopN = Nodes.front();
for (auto *N : drop_begin(Nodes)) {
if (N->getInstruction()->comesBefore(TopN->getInstruction()))
TopN = N;
}
return TopN;
}
DGNode *SchedBundle::getBot() const {
DGNode *BotN = Nodes.front();
for (auto *N : drop_begin(Nodes)) {
if (BotN->getInstruction()->comesBefore(N->getInstruction()))
BotN = N;
}
return BotN;
}
void SchedBundle::cluster(BasicBlock::iterator Where) {
for (auto *N : Nodes) {
auto *I = N->getInstruction();
if (I->getIterator() == Where)
++Where; // Try to maintain bundle order.
I->moveBefore(*Where.getNodeParent(), Where);
}
}
#ifndef NDEBUG
void SchedBundle::dump(raw_ostream &OS) const {
for (auto *N : Nodes)
OS << *N;
}
void SchedBundle::dump() const {
dump(dbgs());
dbgs() << "\n";
}
#endif // NDEBUG
#ifndef NDEBUG
void ReadyListContainer::dump(raw_ostream &OS) const {
auto ListCopy = List;
while (!ListCopy.empty()) {
OS << *ListCopy.top() << "\n";
ListCopy.pop();
}
}
void ReadyListContainer::dump() const {
dump(dbgs());
dbgs() << "\n";
}
#endif // NDEBUG
void Scheduler::scheduleAndUpdateReadyList(SchedBundle &Bndl) {
// Find where we should schedule the instructions.
assert(ScheduleTopItOpt && "Should have been set by now!");
auto Where = *ScheduleTopItOpt;
// Move all instructions in `Bndl` to `Where`.
Bndl.cluster(Where);
// Update the last scheduled bundle.
ScheduleTopItOpt = Bndl.getTop()->getInstruction()->getIterator();
// Set nodes as "scheduled" and decrement the UnsceduledSuccs counter of all
// dependency predecessors.
for (DGNode *N : Bndl) {
for (auto *DepN : N->preds(DAG)) {
DepN->decrUnscheduledSuccs();
if (DepN->ready() && !DepN->scheduled())
ReadyList.insert(DepN);
}
N->setScheduled(true);
}
}
void Scheduler::notifyCreateInstr(Instruction *I) {
// The DAG notifier should have run by now.
auto *N = DAG.getNode(I);
// If there is no DAG node for `I` it means that this is out of scope for the
// DAG and as such out of scope for the scheduler too, so nothing to do.
if (N == nullptr)
return;
// If the instruction is inserted below the top-of-schedule then we mark it as
// "scheduled".
bool IsScheduled = ScheduleTopItOpt &&
*ScheduleTopItOpt != I->getParent()->end() &&
(*ScheduleTopItOpt.value()).comesBefore(I);
if (IsScheduled)
N->setScheduled(true);
// If the new instruction is above the top of schedule we need to remove its
// dependency predecessors from the ready list and increment their
// `UnscheduledSuccs` counters.
if (!IsScheduled) {
for (auto *PredN : N->preds(DAG)) {
ReadyList.remove(PredN);
PredN->incrUnscheduledSuccs();
}
}
}
SchedBundle *Scheduler::createBundle(ArrayRef<Instruction *> Instrs) {
SchedBundle::ContainerTy Nodes;
Nodes.reserve(Instrs.size());
for (auto *I : Instrs)
Nodes.push_back(DAG.getNode(I));
auto BndlPtr = std::make_unique<SchedBundle>(std::move(Nodes));
auto *Bndl = BndlPtr.get();
Bndls[Bndl] = std::move(BndlPtr);
return Bndl;
}
void Scheduler::eraseBundle(SchedBundle *SB) { Bndls.erase(SB); }
bool Scheduler::tryScheduleUntil(ArrayRef<Instruction *> Instrs) {
// Create a bundle for Instrs. If it turns out the schedule is infeasible we
// will dismantle it.
auto *InstrsSB = createBundle(Instrs);
// Keep scheduling ready nodes until we either run out of ready nodes (i.e.,
// ReadyList is empty), or all nodes that correspond to `Instrs` (the nodes of
// which are collected in DeferredNodes) are all ready to schedule.
SmallVector<DGNode *> Retry;
bool KeepScheduling = true;
while (KeepScheduling) {
enum class TryScheduleRes {
Success, ///> We successfully scheduled the bundle.
Failure, ///> We failed to schedule the bundle.
Finished, ///> We successfully scheduled the bundle and it is the last
/// bundle to be scheduled.
};
/// TryScheduleNode() attempts to schedule all DAG nodes in the bundle that
/// ReadyN is in. If it's not in a bundle it will create a singleton bundle
/// and will try to schedule it.
auto TryScheduleBndl = [this, InstrsSB](DGNode *ReadyN) -> TryScheduleRes {
auto *SB = ReadyN->getSchedBundle();
if (SB == nullptr) {
// If ReadyN does not belong to a bundle, create a singleton bundle
// and schedule it.
auto *SingletonSB = createBundle({ReadyN->getInstruction()});
scheduleAndUpdateReadyList(*SingletonSB);
return TryScheduleRes::Success;
}
if (SB->ready()) {
// Remove the rest of the bundle from the ready list.
// TODO: Perhaps change the Scheduler + ReadyList to operate on
// SchedBundles instead of DGNodes.
for (auto *N : *SB) {
if (N != ReadyN)
ReadyList.remove(N);
}
// If all nodes in the bundle are ready.
scheduleAndUpdateReadyList(*SB);
if (SB == InstrsSB)
// We just scheduled InstrsSB bundle, so we are done scheduling.
return TryScheduleRes::Finished;
return TryScheduleRes::Success;
}
return TryScheduleRes::Failure;
};
while (!ReadyList.empty()) {
auto *ReadyN = ReadyList.pop();
auto Res = TryScheduleBndl(ReadyN);
switch (Res) {
case TryScheduleRes::Success:
// We successfully scheduled ReadyN, keep scheduling.
continue;
case TryScheduleRes::Failure:
// We failed to schedule ReadyN, defer it to later and keep scheduling
// other ready instructions.
Retry.push_back(ReadyN);
continue;
case TryScheduleRes::Finished:
// We successfully scheduled the instruction bundle, so we are done.
return true;
}
llvm_unreachable("Unhandled TrySchedule() result");
}
// Try to schedule nodes from the Retry list.
KeepScheduling = false;
for (auto *N : make_early_inc_range(Retry)) {
auto Res = TryScheduleBndl(N);
if (Res == TryScheduleRes::Success) {
Retry.erase(find(Retry, N));
KeepScheduling = true;
}
}
}
eraseBundle(InstrsSB);
return false;
}
Scheduler::BndlSchedState
Scheduler::getBndlSchedState(ArrayRef<Instruction *> Instrs) const {
assert(!Instrs.empty() && "Expected non-empty bundle");
auto *N0 = DAG.getNode(Instrs[0]);
auto *SB0 = N0 != nullptr ? N0->getSchedBundle() : nullptr;
bool AllUnscheduled = SB0 == nullptr;
bool FullyScheduled = SB0 != nullptr && !SB0->isSingleton();
for (auto *I : drop_begin(Instrs)) {
auto *N = DAG.getNode(I);
auto *SB = N != nullptr ? N->getSchedBundle() : nullptr;
if (SB != nullptr) {
// We found a scheduled instr, so there is now way all are unscheduled.
AllUnscheduled = false;
if (SB->isSingleton()) {
// We found an instruction in a temporarily scheduled singleton. There
// is no way that all instructions are scheduled in the same bundle.
FullyScheduled = false;
}
}
if (SB != SB0) {
// Either one of SB, SB0 is null, or they are in different bundles, so
// Instrs are definitely not in the same vector bundle.
FullyScheduled = false;
// One of SB, SB0 are in a vector bundle and they differ.
if ((SB != nullptr && !SB->isSingleton()) ||
(SB0 != nullptr && !SB0->isSingleton()))
return BndlSchedState::AlreadyScheduled;
}
}
return AllUnscheduled ? BndlSchedState::NoneScheduled
: FullyScheduled ? BndlSchedState::FullyScheduled
: BndlSchedState::TemporarilyScheduled;
}
void Scheduler::trimSchedule(ArrayRef<Instruction *> Instrs) {
// | Legend: N: DGNode
// N <- DAGInterval.top() | B: SchedBundle
// N | *: Contains instruction in Instrs
// B <- TopI (Top of schedule) +-------------------------------------------
// B
// B *
// B
// B * <- LowestI (Lowest in Instrs)
// B
// N
// N
// N <- DAGInterval.bottom()
//
Instruction *TopI = &*ScheduleTopItOpt.value();
Instruction *LowestI = VecUtils::getLowest(Instrs);
// Destroy the singleton schedule bundles from LowestI all the way to the top.
for (auto *I = LowestI, *E = TopI->getPrevNode(); I != E;
I = I->getPrevNode()) {
auto *N = DAG.getNode(I);
if (N == nullptr)
continue;
auto *SB = N->getSchedBundle();
if (SB->isSingleton())
eraseBundle(SB);
}
// The DAG Nodes contain state like the number of UnscheduledSuccs and the
// Scheduled flag. We need to reset their state. We need to do this for all
// nodes from LowestI to the top of the schedule. DAG Nodes that are above the
// top of schedule that depend on nodes that got reset need to have their
// UnscheduledSuccs adjusted.
Interval<Instruction> ResetIntvl(TopI, LowestI);
for (Instruction &I : ResetIntvl) {
auto *N = DAG.getNode(&I);
N->resetScheduleState();
// Recompute UnscheduledSuccs for nodes not only in ResetIntvl but even for
// nodes above the top of schedule.
for (auto *PredN : N->preds(DAG))
PredN->incrUnscheduledSuccs();
}
// Refill the ready list by visiting all nodes from the top of DAG to LowestI.
ReadyList.clear();
Interval<Instruction> RefillIntvl(DAG.getInterval().top(), LowestI);
for (Instruction &I : RefillIntvl) {
auto *N = DAG.getNode(&I);
if (N->ready())
ReadyList.insert(N);
}
}
bool Scheduler::trySchedule(ArrayRef<Instruction *> Instrs) {
assert(all_of(drop_begin(Instrs),
[Instrs](Instruction *I) {
return I->getParent() == (*Instrs.begin())->getParent();
}) &&
"Instrs not in the same BB, should have been rejected by Legality!");
// TODO: For now don't cross BBs.
if (!DAG.getInterval().empty()) {
auto *BB = DAG.getInterval().top()->getParent();
if (any_of(Instrs, [BB](auto *I) { return I->getParent() != BB; }))
return false;
}
if (ScheduledBB == nullptr)
ScheduledBB = Instrs[0]->getParent();
// We don't support crossing BBs for now.
if (any_of(Instrs,
[this](Instruction *I) { return I->getParent() != ScheduledBB; }))
return false;
auto SchedState = getBndlSchedState(Instrs);
switch (SchedState) {
case BndlSchedState::FullyScheduled:
// Nothing to do.
return true;
case BndlSchedState::AlreadyScheduled:
// Instructions are part of a different vector schedule, so we can't
// schedule \p Instrs in the same bundle (without destroying the existing
// schedule).
return false;
case BndlSchedState::TemporarilyScheduled:
// If one or more instrs are already scheduled we need to destroy the
// top-most part of the schedule that includes the instrs in the bundle and
// re-schedule.
DAG.extend(Instrs);
trimSchedule(Instrs);
ScheduleTopItOpt = std::next(VecUtils::getLowest(Instrs)->getIterator());
return tryScheduleUntil(Instrs);
case BndlSchedState::NoneScheduled: {
// TODO: Set the window of the DAG that we are interested in.
if (!ScheduleTopItOpt)
// We start scheduling at the bottom instr of Instrs.
ScheduleTopItOpt = std::next(VecUtils::getLowest(Instrs)->getIterator());
// Extend the DAG to include Instrs.
Interval<Instruction> Extension = DAG.extend(Instrs);
// Add nodes to ready list.
for (auto &I : Extension) {
auto *N = DAG.getNode(&I);
if (N->ready())
ReadyList.insert(N);
}
// Try schedule all nodes until we can schedule Instrs back-to-back.
return tryScheduleUntil(Instrs);
}
}
llvm_unreachable("Unhandled BndlSchedState enum");
}
#ifndef NDEBUG
void Scheduler::dump(raw_ostream &OS) const {
OS << "ReadyList:\n";
ReadyList.dump(OS);
OS << "Top of schedule: ";
if (ScheduleTopItOpt)
OS << **ScheduleTopItOpt;
else
OS << "Empty";
OS << "\n";
}
void Scheduler::dump() const { dump(dbgs()); }
#endif // NDEBUG
} // namespace llvm::sandboxir