blob: 6ce5aaeda8c82ed15d7ec50d59daa25785fe5bf8 [file] [edit]
//===- AMDGPUCoExecSchedStrategy.h - CoExec Scheduling Strategy -*- C++ -*-===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
/// \file
/// Coexecution-focused scheduling strategy for AMDGPU.
//
//===----------------------------------------------------------------------===//
#ifndef LLVM_LIB_TARGET_AMDGPU_AMDGPUCOEXECSCHEDSTRATEGY_H
#define LLVM_LIB_TARGET_AMDGPU_AMDGPUCOEXECSCHEDSTRATEGY_H
#include "AMDGPUCoExecInfo.h"
#include "GCNSchedStrategy.h"
#include "llvm/CodeGen/MachineScheduler.h"
namespace llvm {
namespace AMDGPU {
namespace DefaultBufferSizes {
constexpr unsigned DS = 16;
} // namespace DefaultBufferSizes
/// AMDGPU-specific scheduling decision reasons. These provide more granularity
/// than the generic CandReason enum for debugging purposes.
enum class AMDGPUSchedReason : uint8_t {
None,
Stall,
MemoryPipeline,
CritResourceBalance, // tryCriticalResource chose based on resource pressure
CritResourceDep, // tryCriticalResourceDependency chose based on enabling
NUM_REASONS
};
inline StringRef getReasonName(AMDGPUSchedReason R) {
switch (R) {
case AMDGPUSchedReason::None:
return "None";
case AMDGPUSchedReason::Stall:
return "Stall";
case AMDGPUSchedReason::MemoryPipeline:
return "MemoryPipeline";
case AMDGPUSchedReason::CritResourceBalance:
return "CritResource";
case AMDGPUSchedReason::CritResourceDep:
return "CritResourceDep";
case AMDGPUSchedReason::NUM_REASONS:
llvm_unreachable("Unknown AMDGPUSchedReason");
}
llvm_unreachable("Unknown AMDGPUSchedReason");
}
} // End namespace AMDGPU
//===----------------------------------------------------------------------===//
// Hardware Unit Information
//===----------------------------------------------------------------------===//
/// HardwareUnitInfo is a wrapper class which maps to some real hardware
/// resource. This is used to model hardware resource pressure per region, and
/// guide scheduling heuristics.
class HardwareUnitInfo {
private:
/// PrioritySUs maintains a list of the SUs we want to prioritize scheduling
/// for this HardwareUnit. This is used for agreement between
/// tryCriticalResourceDependency and tryCriticalResource: we schedule the
/// dependencies for a SU on critical resource, then schedule that same SU on
/// the critical resource. This agreement results in shorter live ranges and
/// more regular HardwareUnit access patterns. SUs are prioritized based on
/// depth for top-down scheduling.
SmallSetVector<SUnit *, 16> PrioritySUs;
/// All the SUs in the region that consume this resource.
SmallSetVector<SUnit *, 16> AllSUs;
/// All the SUs for this HardwareUnit that have already been scheduled.
SmallVector<SUnit *, 16> ScheduledSUs;
/// The total number of busy cycles for this HardwareUnit for a given region.
unsigned TotalCycles = 0;
/// InstructionFlavor mapping.
AMDGPU::InstructionFlavor Type;
/// Whether or not instructions on this HardwareUnit may produce a window in
/// which instructions in other HardwareUnits can coexecute. For example, WMMA
/// / MFMA instructions may take multiple cycles, which may be overlapped with
/// instructions on other HardwareUnits.
bool ProducesCoexecWindow = false;
/// How many instructions can be held simultaneously for this HardwareUnit.
/// A value of 0 means there is no limit. A value of 1 models an unbuffered
/// resource with a single in-flight instruction.
///
/// This may approximate the hardware. For example, for LDS instructions
/// it is a well-known phenomena that oversubscribing the LDS unit results in
/// longer latency for the LDS instructions. While it is true that there is a
/// hard limit to the amount of simulatenous in-flight LDS instructions, good
/// scheduling would also cool off the LDS to avoid other forms of hardware
/// contention and increasing LDS latency. Thus, we limit the amount of LDS
/// instructions we are willing to schedule close together, though this does
/// not correspond 1:1 with a hardware mechanism.
unsigned BufferSize = 0;
/// How many cycles it takes for an instruction to clear the buffer.
///
/// Again, this may be an apprxoimation. For example, for memory FIFOs, the
/// actual amount of cycles it will take to clear it is dependent on how
/// quickly prior instructions evacuate the FIFO, which is based on runtime
/// behavior which is not modelled in the compiler.
unsigned BufferCycles = 0;
public:
HardwareUnitInfo() {}
unsigned size() { return AllSUs.size(); }
unsigned getTotalCycles() { return TotalCycles; }
void setType(unsigned TheType) {
assert(TheType < (unsigned)AMDGPU::InstructionFlavor::NUM_FLAVORS);
Type = (AMDGPU::InstructionFlavor)(TheType);
}
AMDGPU::InstructionFlavor getType() const { return Type; }
bool producesCoexecWindow() const { return ProducesCoexecWindow; }
void setProducesCoexecWindow(bool Val) { ProducesCoexecWindow = Val; }
bool contains(SUnit *SU) const { return AllSUs.contains(SU); }
void setBufferSize(unsigned Size) { BufferSize = Size; }
unsigned getBufferSize() { return BufferSize; }
/// \returns the next cycle where there is space in the buffer.
unsigned getBufferAvailableCycle(unsigned CurrCycle) {
// An unlimited buffer is always available.
if (BufferSize == 0)
return CurrCycle;
// Buffer is available now.
if (ScheduledSUs.size() < BufferSize)
return CurrCycle;
return BufferCycles +
ScheduledSUs[ScheduledSUs.size() - BufferSize]->TopReadyCycle;
}
/// \returns the most recently scheduled SU for this HardwareUnit.
SUnit *getLastScheduledSU() {
unsigned ScheduledCount = ScheduledSUs.size();
if (!ScheduledCount)
return nullptr;
return ScheduledSUs[ScheduledCount - 1];
}
/// \returns the SUnit with higher priority or nullptr if they are the same.
/// This method looks through the PrioritySUs to determine if one SU is more
/// prioritized than the other. If neither are in the PrioritySUs list, then
/// neither have priority over each other.
SUnit *getHigherPriority(SUnit *SU, SUnit *Other) const {
for (SUnit *SUOrder : PrioritySUs) {
if (SUOrder == SU)
return SU;
if (SUOrder == Other)
return Other;
}
return nullptr;
}
void reset() {
AllSUs.clear();
PrioritySUs.clear();
ScheduledSUs.clear();
TotalCycles = 0;
Type = AMDGPU::InstructionFlavor::Other;
ProducesCoexecWindow = false;
BufferSize = 0;
BufferCycles = 0;
}
/// \returns the next SU in PrioritySUs that is not ready. If \p LookDeep is
/// set, we will look beyond the PrioritySUs (if all the PrioritySUs are
/// ready) to AllSUs to attempt to find a target SU. When looking through
/// AllSUs we sort pick the target SU by minimal depth for top-down
/// scheduling. getNextTargetSU is useful for determining which SU on this
/// HardwareUnit we are trying to schedule - this info helps us determine
/// which dependencies to schedule. LookDeep is useful if the dependencies are
/// long latency (e.g. memory instructions). If we have many long latency
/// dependencies, it is beneficial to enable SUs multiple levels ahead.
SUnit *getNextTargetSU(bool LookDeep = false) const;
/// Insert the \p SU into AllSUs and account its \p BlockingCycles into
/// the TotalCycles. This maintains the list of PrioritySUs.
void insert(SUnit *SU, unsigned BlockingCycles);
/// Update the state for \p SU being scheduled by removing it from the AllSUs
/// and reducing its \p BlockingCycles from the TotalCycles. This maintains
/// the list of PrioritySUs.
void markScheduled(SUnit *SU, unsigned BlockingCycles);
/// After we've collected all the region pressure for this HWUI, correct for
/// any specifics of the behavior of this resource. For example, if the
/// HardwareUnit can hold N instructions simultaneously, then there is no
/// penalty for scheduling N instructions back to back.
void finalizeCycles();
};
//===----------------------------------------------------------------------===//
// Candidate Heuristics
//===----------------------------------------------------------------------===//
/// CandidateHeuristics contains state and implementations to facilitate making
/// per instruction scheduling decisions; it contains methods used in
/// tryCandidate to decide which instruction to schedule next.
class CandidateHeuristics {
protected:
struct StallCosts {
unsigned Ready = 0;
unsigned Structural = 0;
unsigned Latency = 0;
unsigned Carried = 0;
unsigned Buffer = 0;
unsigned Fence = 0;
unsigned Effective = 0;
};
ScheduleDAGMI *DAG;
const SIInstrInfo *SII;
const SIRegisterInfo *SRI;
const TargetSchedModel *SchedModel;
SmallVector<HardwareUnitInfo, 8> HWUInfo;
DenseMap<MachineInstr *, unsigned> CarriedLatencies;
/// Walk over the region and collect characteristics for the various
/// heuristics.
void collectRegionSummary();
/// \returns the maximum blocking cycles according to the SchedModel for a
/// given MCSchedClassDesc \p SC.
unsigned getMaxBlockingCycles(const MCSchedClassDesc *SC,
const MachineInstr *MI);
/// Compute the blocking cycles for the appropriate HardwareUnit given an \p
/// SU.
unsigned getHWUICyclesForSU(SUnit *SU);
/// Compute the blocking cycles for the appropriate HardwareUnit given an \p
/// MI.
unsigned getHWUICyclesForMI(MachineInstr *MI);
/// Estimate the block carried latency from loads for a given \p SU. This is
/// essentially global scheduling info that our local scheduling
/// infrastructure lacks the necessary infrastructure to accurately measure.
/// Thus, this method just attempts to find a reasonable upper bound for
/// carried load latency to avoid long stalls.
unsigned getCarriedLatency(SUnit *SU);
StallCosts getStallCosts(SUnit *SU, SchedBoundary &Zone);
public:
CandidateHeuristics() = default;
void initialize(ScheduleDAGMI *DAG, const TargetSchedModel *SchedModel,
const TargetRegisterInfo *TRI);
/// Update the state to reflect that \p SU is going to be scheduled.
void updateForScheduling(SUnit *SU);
/// Given a \p Flavor , find the corresponding HardwareUnit. \returns the
/// mapped HardwareUnit.
HardwareUnitInfo *getHWUIFromFlavor(AMDGPU::InstructionFlavor Flavor);
/// Sort the HardwarUnitInfo vector. After sorting, the HWUI that are highest
/// priority are first. Priority is determined by maximizing coexecution and
/// keeping the critical HardwareUnit busy.
void sortHWUIResources();
unsigned getStructuralStallCycles(SchedBoundary &Zone, SUnit *SU);
bool tryEffectiveStall(GenericSchedulerBase::SchedCandidate &TryCand,
GenericSchedulerBase::SchedCandidate &Cand,
SchedBoundary &Zone);
/// Prioritize instructions involved the memory pipeline. Currently we don't
/// have any modelling of pipelined loads, so we control the layout of the
/// pipeline per iteration by giving the user some control over the stalls
/// (e.g. between s_barrier_signal and s_barrier_wait) and scheduling the
/// pipeline instructions as soon as they are ready.
///
/// TODO -- add better modelling and heuristics for pipelining based
/// scheduling.
bool tryMemoryPipeline(GenericSchedulerBase::SchedCandidate &TryCand,
GenericSchedulerBase::SchedCandidate &Cand,
SchedBoundary &Zone);
/// Check for critical resource consumption. Prefer the candidate that uses
/// the most prioritized HardwareUnit. If both candidates use the same
/// HarwareUnit, prefer the candidate with higher priority on that
/// HardwareUnit.
bool tryCriticalResource(GenericSchedulerBase::SchedCandidate &TryCand,
GenericSchedulerBase::SchedCandidate &Cand,
SchedBoundary *Zone) const;
/// Check for dependencies of instructions that use prioritized HardwareUnits.
/// Prefer the candidate that is a dependency of an instruction that uses the
/// most prioritized HardwareUnit. If both candidates enable the same
/// HardwareUnit, prefer the candidate that enables the higher priority
/// instruction on that HardwareUnit.
bool
tryCriticalResourceDependency(GenericSchedulerBase::SchedCandidate &TryCand,
GenericSchedulerBase::SchedCandidate &Cand,
SchedBoundary *Zone) const;
void dumpRegionSummary();
};
class AMDGPUCoExecSchedStrategy final : public GCNSchedStrategy {
protected:
AMDGPU::AMDGPUSchedReason LastAMDGPUReason = AMDGPU::AMDGPUSchedReason::None;
CandidateHeuristics Heurs;
#ifndef NDEBUG
void dumpPickSummary(SUnit *SU, bool IsTopNode, SchedCandidate &Cand);
#endif
bool tryCandidateCoexec(SchedCandidate &Cand, SchedCandidate &TryCand,
SchedBoundary *Zone);
void pickNodeFromQueue(SchedBoundary &Zone, const CandPolicy &ZonePolicy,
const RegPressureTracker &RPTracker,
SchedCandidate &Cand, bool &PickedPending,
bool IsBottomUp);
public:
AMDGPUCoExecSchedStrategy(const MachineSchedContext *C);
void initPolicy(MachineBasicBlock::iterator Begin,
MachineBasicBlock::iterator End,
unsigned NumRegionInstrs) override;
void initialize(ScheduleDAGMI *DAG) override;
SUnit *pickNode(bool &IsTopNode) override;
void schedNode(SUnit *SU, bool IsTopNode) override;
};
ScheduleDAGInstrs *createGCNCoExecMachineScheduler(MachineSchedContext *C);
ScheduleDAGInstrs *createGCNNoopPostMachineScheduler(MachineSchedContext *C);
} // End namespace llvm
#endif // LLVM_LIB_TARGET_AMDGPU_AMDGPUCOEXECSCHEDSTRATEGY_H