| //===- 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 |