目录-
1 前言-
2 关键概念-
2.1 模型-
2.2 task-
2.3 function-call / FC-
2.4 FC最大连续执行时间-
2.5 优先级-
3 调度策略总览-
4 板端预测库调度策略-
4.1 基础规则-
4.2 多进程调度-
4.3 模型首尾有CPU算子-
4.4 模型中间有CPU算子-
5 系统软件抢占策略-
5.1 high与normal队列-
5.2 抢占功能环境变量-
6 配置方法总结-
6.1 模型编译-
6.2 代码编写-
6.3 板端部署
1 前言
在板端部署的应用程序开发中,经常会遇到多个模型同时运行的场景,此时每个模型都需要使用有限的计算资源完成推理,因而不可避免地会出现计算资源的争夺情况。地平线的板端预测库(libDNN)具有高效的调度策略,通常情况下,无需开发者手动干预即可对多模型推理的场景做出合理的调度,以充分利用硬件资源。但有时,我们会希望某些模型的推理任务具有更高的执行优先级,因此为了更加准确地控制多模型的执行情况,地平线提供了优先级调度策略供开发者使用。-
需要强调的是,优先级调度的概念针对地平线计算平台的BPU资源,模型的CPU部分会由Linux系统本身处理,本教程的重点在于,同一时刻有多个模型时,如何对其进行调度,并使用BPU资源计算。同时,优先级调度主要针对多个模型争夺同一个BPU资源的场景,如果两个模型各自独占不同的BPU核,则不需要考虑优先级调度。-
本教程首先会梳理优先级调度涉及的关键概念,再介绍板端预测库层面的调度策略,以及更底层的系统软件的抢占功能,最后总结实现优先级调度的具体配置方法。
2 关键概念
2.1 模型
在本教程中特指runtime模型。runtime指代嵌入式应用开发,runtime模型为用于板端部署的深度学习模型,后缀名为bin或hbm。
2.2 task
task指代推理任务,模型在推理前会和task绑定。执行板端推理必须先提交task,每1帧推理任务都对应1个task。-
提交推理任务的接口有两个,分别是hbDNNInfer()和hbDNNRoiInfer(),二者根据输入参数执行推理任务,其中后者专用于执行ROI推理任务。hbDNNWaitTaskDone()接口用于等待任务完成或超时。hbDNNReleaseTask()接口用于释放任务。1个task的生命周期从hbDNNInfer()或hbDNNRoiInfer()开始,到hbDNNReleaseTask()结束。-
1个task可以绑定1个或多个model,地平线的XJ3和J5计算平台均支持多模型批量推理场景,在1个推理任务中调用多个小模型预测多份数据,能有效提升推理性能,相关介绍可见社区文章《多模型批量推理》。
2.3 function-call / FC
function-call是BPU的执行粒度,缩写为FC。runtime模型(bin/hbm)在BPU上执行推理计算时,表现为1个或多个FC的调用,当一个模型所有的FC都执行完成, 那么这个模型也就推理完成了。-
需要强调的是,FC是模型层面的属性,而非task层面的属性。
2.4 FC最大连续执行时间
这是一个编译参数。在PTQ中,参数名为max_time_per_fc,QAT中,参数名为max-time-per-fc。-
该参数用于指定模型每个FC的最大可连续执行时间,取值范围是0和1000-4294967295(2^32-1),单位微秒(μs)。默认值为0,代表无限制。-
无论使用PTQ还是QAT流程编译runtime模型,都可以配置该参数。-
对于PTQ,max_time_per_fc在yaml文件中的编译参数组(compiler_parameters)中配置。-
对于QAT,在使用horizon_plugin_pytorch.quantization.compile_model接口编译模型时,可在额外参数(extra_args)中配置max-time-per-fc,例如extra_args=[‘–max-time-per-fc’,str(1000)]。-
如果不配置该参数或者配置为0,则模型的BPU子图只有1段FC,若配置max_time_per_fc=1000,则BPU子图会被分割为多段FC,每段FC的执行时间为1000微秒,最后一个除外,如下图所示。
拆分多段的FC的总执行时间,会比1段完整FC的执行时间略长。拆分的FC段数越多,增加的额外耗时就越多,但通常,运行多段FC的额外耗时不会超过运行单段完整FC耗时的2%.-
需要注意的是,和FC一样,FC最大执行时间是模型层面的属性。
2.5 优先级
在提交推理任务前,需要先配置推理任务的控制参数,相关参数在结构体hbDNNInferCtrlParam中定义。
typedef struct {
int32_t bpuCoreId;
int32_t dspCoreId;
int32_t priority;
int32_t more;
int64_t customId;
int32_t reserved1;
int32_t reserved2;
} hbDNNInferCtrlParam;
其中,priority和customId均能表示task的优先级,priority的优先级高于customId。-
priority参数的配置范围为0-255,priority数值越大,优先级越高。当priority=255时,task会具有比较特殊的抢占优先级,板端预测库和系统软件会提供更加激进的调度策略,关于这方面的内容会在后文详述。-
当两个task的priority相同时,customId参数发挥作用。customId数值越小,优先级越高,通常用时间戳或者frame id赋值。-
此外,bpuCoreId为0代表BPU任意核,1代表BPU核0,2代表BPU核1。考虑优先级调度策略时,需要配置成1或2,如果配置为0则无法控制模型运行在哪个BPU核上。
3 调度策略总览
优先级调度策略存在于板端预测库和系统软件两个模块,如上图所示。板端预测库是地平线针对嵌入式应用开发提供的推理库,用于将runtime模型部署到开发板运行。系统软件比板端预测库更靠近系统底层,会提供BPU驱动,并解析模型指令,模型的BPU部分最终以FC的形式执行。-
总体来说,当task被提交后,会进入板端预测库的队列并排序,模型的BPU部分提交到系统软件FC队列前会再做排序,最终以FC的形式执行,完成整个优先级调度过程。
4 板端预测库调度策略
4.1 基础规则
由于在实际的部署场景中,我们希望模型有极致的推理速度,所以通常会将首尾的CPU算子从模型中去除,并将相关计算转移到前后处理中进行,同时会将模型优化成一个完整的BPU子图,因此本小节会针对这种情况进行说明。
入队
当部署代码执行到hbDNNInfer或hbDNNRoiInfer时,task会被提交到板端预测库的队列中,队列会对其中的task做排序。位于队列中的task,均为尚未处理,正在等待被执行的task。每当有新的task进入队列时,都会按照各个task的优先级进行一次排序。排序按照推理控制参数(hbDNNInferCtrlParam)的priority和customId进行。首先比较priority,数值越大优先级越高,在priority相同时比较customId,数值越小优先级越高。
出队
优先级高的task,会优先出队并执行。-
假设我们有priority较高的taskA和priority较低的taskB,各绑定了modelA和modelB。原本在队列中只有taskB等待处理,此时taskA被提交入队,由于taskA的优先级高于taskB,因此taskA会优先于taskB出队,且modelA会优先于modelB被处理。-
在taskA出队后,modelA的BPU部分会转变为FC,下发到系统软件的FC队列。之后taskB出队,modelB的BPU部分转变为FC,下发到系统软件的FC队列。在系统软件层面,会对这两组FC做进一步的调度。
4.2 多进程调度
4.1节只介绍了单进程内部的调度方式,此处补充介绍多进程调度。J5计算平台支持以Service Mode方式运行程序,优化了多进程场景下的调度策略。通常情况下,也就是Service Mode关闭时,板端预测库只能对单进程内的任务做调度,无法实现跨进程的任务调度。在当前进程正在执行任务的情况下,如果别的进程出现了高优先级的任务,由于这个信息无法跨进程传递,那么高优先级的任务可能无法及时运行。在开启Service Mode后,不同进程的信息会维护在共享内存中,从而实现多进程之间任务的统一调度,能让多个进程中的高优任务优先执行。-
注:XJ3计算平台不支持Service Mode。
4.3 模型首尾有CPU算子
如果模型的输入端有CPU算子,则CPU算子会在task出队的时候,由linux系统做调度和计算,CPU计算不会下发到系统软件,当输入端的CPU算子执行结束后,BPU部分才会以FC的形式下发到系统软件。-
需要注意一种情况:如果taskA先出队,taskB后出队,但是taskA绑定的modelA在输入端有CPU算子,taskB绑定的modelB是一个纯BPU模型,那么在modelA的CPU算子计算期间,modelB会先转变为FC并率先进入系统软件队列。-
对于模型输出端的CPU算子,只有模型BPU部分的全部FC在系统软件中执行完毕后,才会得到执行,同样由linux系统做调度和计算。
4.4 模型中间有CPU算子
假设taskA绑定的modelA结构为BPU-CPU-BPU,taskB绑定的modelB为纯BPU模型,taskA先出队,taskB后出队,首先执行modelA,modelB排队等待。-
在执行到modelA中间的CPU算子时,BPU资源会空出来,此时可以选择让modelB插入运行,以充分利用BPU资源。该功能和三个环境变量有关,满足任一环境变量设定的条件即可插入运行。
HB_DNN_BPU_SCHEDULE_THRESHOLD
该环境变量默认值10,单位为百分比(%),表示modelB插入运行后,对modelA的下一个BPU段运行的影响程度,影响程度小于设定值则允许插入运行。该值通过除法计算得出,公式为:
modelA第二段BPU被插队导致延后运行的那部分时间 / modelA第二段BPU运行的总时间 * 100%
此公式也可以改写成:
(modelB的BPU运行时间 - modelA的CPU运行时间)/ modelA第二段BPU运行的总时间 * 100%
举例来说,假设modelA第二段BPU运行时间为100ms,如果modelB中途插入,导致modelA第二段BPU相比原本延后了5ms才开始运行,那么此时受影响程度便为5%,如果该环境变量配置为10,则表示允许的影响程度为10%,5%小于10%,所以在modalA运行到CPU段时,会允许modelB插入运行。
HB_DNN_SCHEDULE_INSERT_FC_MAX_TIME
该环境变量默认值1000,单位毫秒,如果modelB的运行时间小于该值,则允许插入运行。
HB_DNN_SCHEDULE_WAIT_DISPATCH_TIME
该环境变量默认值20,单位毫秒,如果modelA的CPU段运行时间超过该值,则会允许modelB插入运行。
注:模型的BPU段和CPU段运行时间会被板端预测库记录,因此随着推理次数的增加,对耗时的预估会更加准确。
5 系统软件抢占策略
在系统软件层面,不存在模型的概念,模型的BPU部分已经转变为FC,系统软件只会对FC做调度。系统软件接收到的FC均由板端预测库下发。
5.1 high与normal队列
系统软件有两个FC队列,一个是抢占优先级的high队列,另一个是非抢占优先级的normal队列。只有priority=255的FC才能进入high队列,priority在0-254的FC都会进入normal队列。模型一个BPU子图拆分出的多段FC会一次性完整下发给high或者normal队列。-
如果没有接收到priority=255的FC,则high队列保持为空,所有priority<255的FC都会进入normal队列,按照先进先出的顺序,先入队的FC先执行(与priority无关)。此时,如果系统软件接收到了priority=255的抢占优先级FC,则这些FC会进入high队列,并在特定的时机抢占执行,high队列中的FC全部执行结束后才会继续执行normal队列中的FC。-
对于具有抢占优先级的FC,抢占执行的时机与系统软件的环境变量BPLAT_CORELIMIT有关。
5.2 抢占功能环境变量
BPLAT_CORELIMIT是系统软件层面的环境变量,用于设置FC的抢占,取值范围为0和正整数。该参数默认值为0,不发生FC抢占。如果设置为n(n>0),则抢占优先级FC的最长抢占等待时间为:非抢占优先级队列中前n个FC的执行时间。
上图中normal队列蓝色的三段非抢占优先级FC来自同一个BPU子图,normal队列左侧的FC先出队执行,如果BPLAT_CORELIMIT=1,则抢占优先级FC会在第一个非抢占优先级FC执行结束后立刻抢占执行,如果BPLAT_CORELIMIT=2,则会在第一个或者第二个非抢占优先级FC执行结束后再抢占执行。-
此外,如果BPLAT_CORELIMIT=0,则不发生FC抢占,FC执行顺序为系统软件先接收到的先执行。-
通常来说,启用系统软件层面的FC抢占功能,配置export BPLAT_CORELIMIT=1即可。
此外,还需要注意以下两点:
- priority=255的两个task,模型的FC之间无法实现抢占,即使customId不同也不行。
- 如果模型拆分出了大于两段的FC,那么这个模型从开始推理到结束,可以被多次抢占。
6 配置方法总结
6.1 模型编译
在编译阶段,低优模型需要配置FC最大连续执行时间,这样才能在系统软件队列中被高优模型的FC抢占。-
对于PTQ流程,可在yaml中按如下方式配置:
compiler_parameters:
max_time_per_fc: 1000
对于QAT流程,可在调用compile_model接口时按如下方式配置:
compile_model(
...
extra_args=['--max-time-per-fc',str(1000)]
)
6.2 代码编写
在编写部署代码时,需要配置模型推理控制参数的bpuCoreId、priority和customId(可选)。
hbDNNInferCtrlParam infer_ctrl_param;
infer_ctrl_param.bpuCoreId = 1;
infer_ctrl_param.priority = 255;
infer_ctrl_param.customId = 0;
bpuCoreId为0代表BPU任意核,1代表BPU核0,2代表BPU核1。需要再次强调,在考虑优先级调度策略时,bpuCoreId需要配置为1或2,如果配置成0则无法控制模型运行在哪个BPU核上。-
priority必须配置,customId选配,customId只在priority相同时起作用。
6.3 板端部署
执行推理程序前,请在当前进程中运行以下命令启动系统软件抢占功能的环境变量:
export BPLAT_CORELIMIT=1
若您对优先级调度有困惑,或者希望在J5计算平台开启Service Mode,请联系地平线技术支持。