概述
作为调度pass实现类,MachineScheduler 和 PostMachineScheduler 的优点之一是允许开发者在LLVM 已有调度算法和策略基础上做不同的定制化
PostMachineScheduler 类的定制相对简单,主要是MachineSchedStrategy 接口的定制。 MachineScheduler 类的定制化更为复杂,主要包括三个方面: 调度策略定制、MachineSchedStrategy 接口定制和ScheduleDAGMILive 类定制
调度策略定制
MachineScheduler 类仅负责选择要调度的区域。如果目标后端不需要要通过定制MachineSchedStrategy 接口对区域调度做过多定制,而只是希望调整调度策略类GenericScheduler 的某些策略,如定制 MachineScheduler 调度方向、寄存器压力跟踪策略
等,可以通过指定调度器的某些高层配置,实现定制调度策略
这些高层配置一般在 overrideSchedPolicy() 函数中指定,以此在子目标层级设定指定调度的配置选项。例如,AMDGPU后端GCN子目标的重写 overrideSchedPolicy() 函数实现代码如下
void GCNSubtarget::overrideSchedPolicy(MachineSchedPolicy &Policy,
unsigned NumRegionInstrs) const {
// Track register pressure so the scheduler can try to decrease
// pressure once register usage is above the threshold defined by
// SIRegisterInfo::getRegPressureSetLimit()
Policy.ShouldTrackPressure = true;
// Enabling both top down and bottom up scheduling seems to give us less
// register spills than just using one of these approaches on its own.
Policy.OnlyTopDown = false;
Policy.OnlyBottomUp = false;
// Enabling ShouldTrackLaneMasks crashes the SI Machine Scheduler.
if (!enableSIScheduler())
Policy.ShouldTrackLaneMasks = true;
}
其中,结构体 MachineSchedPolicy 定义了 MachineSchedStrategy 接口中没有提供的调度策略
- 例如,MachineSchedPolicy 中的 ShouldTrackPressure 字段表示调度器开启寄存器压力跟踪
- 一旦寄存器使用量超过SIRegisterInfo::getRegPressureSetLimit() 定义的阀值,调度器可以尝试降低寄存器压力
- 寄存器分配的指令调度不需要跟踪寄存器压力
MachineSchedPolicy 中 的 OnlyTopDonw 和 OnlyBottomUp 字段分别表示调度强制自顶向下或自底向上调度指令。如果二者都为false,则表示双向调度
GCN 子目标使用了双向调度策略,因为开发者认为这样可以减少寄存器溢出。用户可以通过llc命令行选项-misched-topdown/bottomup 改变默认策略。 MachineSchedPolicy 中的字段ShouldTrackLaneMasks 表示调度器是否跟踪LaneMask
LaneMask 是寄存器的位掩码,通常用于显示组成寄存器的子寄存器的活跃性。跟踪LaneMask有助于对子寄存器的写入操作进行重新排序
MachineSchedStrategy 接口定制
LLVM 中的大多数后端不需要重写DAG构建器和列表调度,但如果某些后端的子目标需要定制调度启发式策略,则可以通过定制 MachineSchedStrategy 接口,并在其中实现自定义调度算法
MachineSchedStrategy 接口需要实现用于跟踪、保存就绪节点的优先级队列,以及操作队列的函数,包括队列中添加节点、从队列中调度节点的操作等
- 例如:AMDGPU 后端的R600子目标定义了 MachineSchedStrategy 接口类R600SchedStrategy,其实现代码如下:
class R600SchedStrategy final : public MachineSchedStrategy {
void initialize(ScheduleDAGMI *dag) override;
SUnit *pickNode(bool &IsTopNode) override;
void schedNode(SUnit *SU, bool IsTopNode) override;
void releaseTopNode(SUnit *SU) override;
void releaseBottomNode(SUnit *SU) override;
}
MachineSchedStrategy 类中定义了保存就绪节点的队列,以及操作队列的releaseTopNode(),releaseBottomNode() 等函数
上述代码中的pickNode() 函数是指令调度框架的入口函数,其中根据 “AMD Accelerated Paralled Processing OpenCL Programming Guide” 文档的要求实现了R600 的调度策略
- 如果R600SchedStrategy 类的pickNode() 函数没有选中符合条件的节点,仍可调用GenericScheduler 类的 pickNode() 函数完成调度
上述代码中的 schedNode() 函数在节点被调度后被调用,可用于更新调度器的内部状态,如已发射指令计数等
R600 子目标可以在调用 createMachineScheduler() 函数为标准 MachineScheduler 调度 pass 生成 ScheduleDAGInstrs 实例时,在 ScheduleDAGILive 实例中 插入自定义 MachineSchedStrategy 接口 R600SchedStrategy, 以此决定调度节点方式
class R600PassConfig final : public AMDGPUPassConfig {
public:
R600PassConfig(LLVMTargetMachine &TM, PassManagerBase &PM)
: AMDGPUPassConfig(TM, PM) {}
ScheduleDAGInstrs *
createMachineScheduler(MachineSchedContext *C) const override {
return createR600MachineScheduler(C);
}
bool addPreISel() override;
bool addInstSelector() override;
void addPreRegAlloc() override;
void addPreSched2() override;
void addPreEmitPass() override;
};
上述通过重写 MachineSchedStrategy 接口的方法实现自定义调度算法的工作量较大。如果希望仅对 GenericScheduler 类中已有的启发式策略做调整,但复用大部分调度框架基础设施,则可以由GenericScheduler 类派生自定义子类,在其中添加定制调度策略,实现某些GenericScheduler 类中未定义的架构行为
- 例如,AMDGPU 后端的GCN 子目标定义了自己的 MachineSchedStrateg 派生类GCNMaxOccupancySchedStrategy。
- CGNMAxOccpancySchedStrategy 类与GenericScheduler 不同之处在于,GCNMaxOccupancySchedStrategy 类使用不同的启发式策略确定寄存器超额(excess) 和 临界(critical) 压力,其目标是最大化内核占用率,即最大化每个SIMD的最大wavefront 数
与R600 子目标实现方式类似,GCN子目标的 createMachineScheduler() 函数通过调用 createGCNMaxOccupancyMachineScheduler() 函数,在生成GCNScheduleDAGMILive实例时插入自定义 GCNMaxOccupancySchedStrategy接口
/// Instantiate a ScheduleDAGInstrs that will be owned by the caller.
ScheduleDAGInstrs *MachineScheduler::createMachineScheduler() {
// Select the scheduler, or set the default.
MachineSchedRegistry::ScheduleDAGCtor Ctor = MachineSchedOpt;
if (Ctor != useDefaultMachineSched)
return Ctor(this);
// Get the default scheduler set by the target for this function.
ScheduleDAGInstrs *Scheduler = PassConfig->createMachineScheduler(this);
if (Scheduler)
return Scheduler;
// Default to GenericScheduler.
return createGenericSchedLive(this);
}
/// Create the standard converging machine scheduler. This will be used as the
/// default scheduler if the target does not set a default.
ScheduleDAGMILive *llvm::createGenericSchedLive(MachineSchedContext *C) {
ScheduleDAGMILive *DAG =
new ScheduleDAGMILive(C, std::make_unique<GenericScheduler>(C));
// Register DAG post-processors.
//
// FIXME: extend the mutation API to allow earlier mutations to instantiate
// data and pass it to later mutations. Have a single mutation that gathers
// the interesting nodes in one pass.
DAG->addMutation(createCopyConstrainDAGMutation(DAG->TII, DAG->TRI));
return DAG;
}
createGenericSchedLive() 函数中调用了函数 addMutation(),其目的是在 DAG 构造器中增加一些后处理步骤,这些后处理步骤以ScheduleDAGMuation 对象的作用是在调度前根据硬件目标相关知识,向数据依赖图中添加TableGen语言不能表达的调度约束,即通过在数据依赖图中增加边调整图中的依赖关系,其代价是降低了调度的灵活性
上述代码中添加了三个ScheduleDAGMuation对象,这三个对象按照其添加的顺序,依次在正常DAG构造后作用于DAG
ScheduleDAGMILive 类定制
通常,定制目标后端的MachineSchedStrategy 接口就应该足以实现新的调度算法。不过,目标后端调度程序可以通过进一步派生自己的ScheduleDAGMILive 子类试想调度器,并重载其schedule() 虚函数,实现任何后端特定的调度处理
. 例如,AMDGPU 后端的GCN子目标定义了自己的 ScheduleDAGMILive派生类 GCNScheduleDAGMILive,定义如下:
class GCNScheduleDAGMILive final : public ScheduleDAGMILive {
void schedule() override;
void finalizeSchedule() override;
}
finalizeSchedule() 函数允许目标后端在MachineFunction 级别执行最终的调度操作,即遍历所有的区域,通过寄存器压力监视器RPTracker计算区域的寄存器压力,然后调用 schedule()
GCNScheduleDAGMILive::schedule() 函数实现了 ScheduleDAGInstrs 调度指令序列接口,并通过上述GCNMaxOccupancySchedStrategy 调度策略接口,完成GCN子目标特定的调度处理。
ScheduleDAGMILive::schedule() 函数通过调度策略实现类对象 SchedImpl,调用GCNMaxOccpuancySchedStrategy类的 pickNode() 函数选择调度节点
此外,还可以通过GCNMaxOccupancySchedStrategy 调度策略接口判断寄存器压力是否超过限制