子图切分与调度
Graph Cutting & Dispatching.
为什么需要子图切分
PPQ 被设计为一个静态图的量化工具,这与 pytorch,tensorflow 中你的操作习惯不同,在PPQ的视角下,你的所有程序逻辑都是由图的某些算子完成的,这可能包括了一些对于shape, index的python原生操作,例如:
shape = data.shape; data.reshape(shape[0] * 2, shape[1] / 2);
在PPQ眼中,上述操作可能会形成一个这样的子图结构:

是的,python中你的任何操作,都将被 onnx 与 ppq 转换为大量算子去完成你的逻辑,并且这些与shape有关的操作还将带来两个严重问题:
- 对于这个子图中的Mul, Div算子而言,它们不可以被量化,因为一旦它们参与了量化,导致其结果发生变化后,在Reshape算子中将导致shape不一致而发生错误。
- 对于这个子图中所有黄底色算子而言,将他们的运算调度到Host会更快,这些算子的计算量太小了,将它们发往终端设备的代价比执行它们更高。
为了解决上述问题,PPQ 需要划分你得网络,确定那些算子需要量化,确定那些算子需要部署在终端。PPQ 引入了子图调度器来解决上述问题。
PPQ 子图切分
子图切分是 PPQ 的核心逻辑之一,ppq 使用 graph dispatcher 将图中所有算子划分为三个种类:
- 不可量化区:这区域的算子与shape或者index有关,一旦量化将导致图的计算发生错误,因此不可量化,同时默认被调度到Host端以浮点精度执行。
- 可量化区:这区域的算子被认为是可以量化的,它们是input, conv, gemm的延伸算子,PPQ使用数值追踪技术标记这些算子,这些算子处理的运算一定是input, conv, gemm的计算结果。它们被调度到设备端以int8精度执行。
- 争议区:这区域的算子同时接收来自不可量化区以及可量化区的输入,所有争议区的算子延伸也是争议算子,量化这些算子是有风险的,PPQ不能保证量化产生的影响。该区算子被调度到设备端以浮点精度执行。
首先值得你注意的是:在计算图中,可以出现多个不连续的不可量化区/可量化区/争议区,这是可能发生的。
为了找出这些区域,PPQ 使用图搜索引擎进行区域划分,其基本思想是通过枚举所有算子的计算情况,确定输入的来源是否与shape或index相关。我不想继续展开赘述这部分的逻辑,但你可以通过 ppq.scheduler 中的代码看到它们的具体实现。
在PPQ中,我们实现了三种不同的调度逻辑,不同的调度逻辑将产生不同的区域划分:
- 激进式调度:该调度方法将所有争议区算子视作可量化的。
- 保守式调度:该调度方法将所有争议区算子视作不可量化的(它们依然将被调度到设备端)。
- pplnn:该调度方法只量化卷积层与其相关算子。
对于 mask_rcnn 这样的复杂网络结构来说,我们推荐你尝试使用 保守式调度 或 pplnn 调度方式,对于这样的复杂检测网络而言,量化其后处理算子是风险的,可能导致网络精度大幅下降。
下面我们给出一个具体例子来说明三种调度方式的差异性:

对于激进式调度而言,所有的蓝色(可量化区算子)与紫色(争议区算子)都将被视作可以量化的,而被发往目标部署平台。量化器会接受调度器的子图划分结果,并只对量化区的算子展开量化操作。
对于保守式调度而言,只有蓝色算子会被发往目标平台完成量化,争议区算子将被送往 Host 以浮点精度完成计算。如果部署平台是 GPU 而言,它们将被发往 GPU 平台以浮点精度完成计算。
对于pplnn调度而言,最后一个卷积层之后的所有算子都将被视作争议区算子,被发往 Host 以浮点精度完成计算。如果部署平台是 GPU 而言,它们将被发往 GPU 平台以浮点精度完成计算。
通常而言,你都可以使用保守式调度完成量化,量化后的网络将具备较好的稳定性与性能。但在一些检测网络当中,保守式量化会牵扯太多后处理相关的算子,从而导致网络性能严重下降,此时你将只能使用 pplnn 调度器完成量化。量化的算子越多,显然你的网络执行性能将会越高。
最后你需要注意,并不是所有量化区算子都一定会被量化,一个算子位于量化区只说明这个算子可以被量化且不会影响网络执行,是否真正量化这个算子还需要量化器根据目标平台特性来决定。