PPQ ~ OpenPPL 之多平台量化部署工具来啦!
Posted OpenPPL
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PPQ ~ OpenPPL 之多平台量化部署工具来啦!相关的知识,希望对你有一定的参考价值。
ProgramEntrance.py
量化接口文件中,可选择是否进行量化误差分析、存储模拟量化中间结果,以及图融合、优化策略。
PPQ 完成量化后,输出量化后的模型和量化参数文件。目前 OpenPPL CUDA 已经支持 INT8 推理,小伙伴们只需在 PPQ 中选定 TargetPlatform.PPL_CUDA_INT8 部署平台,即可生成所需的量化文件,完成 CUDA INT8 的量化推理啦!
详情可参考:《OpenPPL CUDA 支持 INT8》
更多自定义量化平台的部署实践,我们将在后续教程中为大家详细介绍。
OpenPPL PPQ量化:执行引擎 源码剖析
目录
PPQ Backend Functions(PPQ 算子库)
前面四篇博客其实就讲了下面两行代码:
ppq_ir = load_onnx_graph(onnx_import_file=onnx_import_file)
ppq_ir = dispatch_graph(graph=ppq_ir, platform=platform,
dispatcher=setting.dispatcher,
dispatching_table=setting.dispatching_table)
量化部分的精髓才刚刚开始,但是别急,咱先来点轻松的哈。这一讲看看执行引擎executor是如何实现的。虽然这部分和量化原理关系不大,但是学一学总没有坏处呀!
感动哭了,PPQ的中文注释也写得太详细了吧,这就看看官方注释,让博主水一篇博客吧哈哈哈
PPQ Graph Executor(PPQ 执行引擎)
为了量化并优化神经网络模型,PPQ 实现了基于 Pytorch 的执行引擎,该执行引擎能够执行 Onnx 与 Caffe 的模型文件,目前支持 90 余种常见 Onnx 算子,涵盖 1d, 2d, 3d 视觉、语音、文本模型。
PPQ 的执行引擎位于 ppq.executor 目录下,由两个主要部分组成: ppq.executor.torch.py 文件中包含了执行引擎自身; ppq.executor.op 文件夹中则包含了不同后端的算子库。
在开始阅理解执行引擎之前,我们先介绍算子库的相关内容
PPQ Backend Functions(PPQ 算子库)
核心算子库位于 ppq.executor.op.torch.default 文件中,该文件中包含了所有算子默认的执行逻辑。
我们知道,对于一个量化算子而言,由于硬件的不同其执行逻辑也可能发生变化。例如 LeakyRelu 算子的负数部分在 GPU 上会采用 x * alpha 的方式完成计算,而在 FPGA 则会采用 x = x >> 3 完成计算。正因为这种差异的存在, PPQ 允许相同的算子在不同平台(TargetPlatform)上拥有不同的执行逻辑。
这也意味着针对每一个平台,我们都将实现一个平台独特的算子库文件,这些算子库都继承于 ppq.executor.op.torch.default。
def Mul_forward(op: Operation, values: List[torch.Tensor], ctx: TorchBackendContext = None, **kwargs) -> torch.Tensor:
ASSERT_NUM_OF_INPUT(op=op, values=values, min_num_of_input=2, max_num_of_input=2)
values = VALUE_TO_EXECUTING_DEVICE(op=op, ctx=ctx, values=values)
multiplicand, multiplier = values
return multiplicand * multiplier
上文中的内容即 ppq.executor.op.torch.default 中 Mul 算子的执行逻辑,在 PPQ 中,所有算子在执行时都将接受一系列 torch.Tensor 作为输入,而后我们调用 pytorch 完成算子的计算逻辑。
你可以打开 PPQ 的算子库文件查看其他算子的执行逻辑,并且 PPQ 也提供了 register_operation_handler 函数,借助该函数你可以注册自定义算子的执行逻辑;或是覆盖现有算子的执行逻辑。
def register_operation_handler(handler: Callable, operation_type: str, platform: TargetPlatform):
if platform not in GLOBAL_DISPATCHING_TABLE:
raise ValueError('Unknown Platform detected, Please check your platform setting.')
GLOBAL_DISPATCHING_TABLE[platform][operation_type] = handler
该函数位于 ppq.api, 你可以使用语句 from ppq.api import register_operation_handler 来引入它。
PPQ Executor(PPQ 执行引擎)
接下来我们向你介绍 PPQ 执行引擎 TorchExecutor,你可以使用语句 from ppq import TorchExecutor 导入执行引擎。初始化执行引擎则需要传入一个 PPQ 计算图实例对象,
在这里我们假设已经获取到了一个量化后的计算图对象 ppq_quant_ir,并使用下面的语句初始化计算引擎
executor = TorchExecutor(graph=ppq_quant_ir)
executor.forward(inputs=..., output_names=..., hooks=...)
我们使用 executor.forward 接口获取图的执行结果,它将可以传入三个参数:
- inputs: inputs (Union[dict, list, torch.Tensor]): [input tensors or somewhat]
- output_names (List[str], optional): output variable names. default is None.
- hooks (Dict[str, RuntimeHook], optional): A hook table for customizing operation behaviour and collate data during executing.
当执行引擎获取到推理请求时,它将按拓扑顺序依次执行图中的算子,下图展示了一个简单的示例:
在这里,我们的图中包含三个算子 Conv, Relu, Softmax,他们将按照拓扑次序被依次执行。PPQ 的执行引擎会在执行完 Conv 算子后,将 Conv 算子的结果暂存于 Var 1 中,供 Relu 算子取用。
而在执行完 Relu 算子后,PPQ 执行引擎则会及时地释放 Var 1 中暂存的数据,因为他们不会被其他算子取用,而且也不是网络的输出 Variable。在每一次推理过后,PPQ 还会清空网络中所有的暂存变量以释放显存。
下面的代码段展示了一个非量化算子的执行逻辑:
for operation in executing_order:
outputs = operation_forward_func(operation, inputs, self._executing_context)
outputs = outputs if isinstance(outputs, (list, tuple)) else [outputs]
fp_outputs = outputs
for output_idx, output_var in enumerate(operation.outputs):
output_var = operation.outputs[output_idx]
output_var.value = outputs[output_idx]
for var in self._graph.variables.values():
if not var.is_parameter:
var.value = None
PPQ 的执行引擎是专为量化计算图的执行而设计的————接下来让我们深入到量化算子的执行过程中去。
对于一个量化算子而言,其每一个输入和输出变量都会有一个 Tensor Quantization Config (TQC) 控制结构体对量化过程进行描述。
对于一个量化 Conv 算子而言,PPQ 将为它创建 2-3 个 Input TQC,以及一个 Output TQC。分别对其输入变量以及输出变量的量化行为进行描述。
下面的代码展示了如何为量化算子创建特定的 TQC 描述量化逻辑。
if operation.type == 'Conv':
config = self.create_default_quant_config(
op = operation,
num_of_bits = 8,
quant_max = 127,
quant_min = -128,
observer_algorithm = 'percentile',
policy = QuantizationPolicy(
QuantizationProperty.PER_TENSOR +
QuantizationProperty.LINEAR +
QuantizationProperty.SYMMETRICAL),
rounding = RoundingPolicy.ROUND_HALF_EVEN)
for tensor_quant_config in config.input_quantization_config:
tensor_quant_config.state = QuantizationStates.FP32
operation.config = config
在 图2 中,我们展示了 PPQ 执行引擎对于量化算子的执行逻辑:
在 PPQ 中,算子的执行被分为四个过程:
- 首先 PPQ 将根据算子上的 TQC 信息量化算子的输入。量化过程并非是原地的,量化后的数据将会是一个新的 torch.Tensor。
- 随后 PPQ 在算子库中寻找算子的执行逻辑,我们已经提到对于每一个平台,他们都可以拥有自己的一套独立的算子库。PPQ 将按照算子的平台找到特定的计算逻辑,并调用他们完成计算得到结果。
- PPQ 将根据算子上的 TQC 信息量化算子的输出。同样地,输出的量化也不是原地的。
- 最后我们将量化好的结果写入到计算图的 Variable 上,从而供后续的算子取用。
对于一个非量化算子而言,上述步骤中的 1,3 是可以省略的。
下 图3 展示了一个量化卷积算子 与 TQC 之间的关系:
Quantize Delegate (量化代理函数)
PPQ 允许你为网络中特定的 TQC 注册量化代理函数。这样你就可以注册自定义的量化处理逻辑,而非使用 PPQLinearQuantFunction 完成量化。
def register_quantize_delegate(
self, config: TensorQuantizationConfig,
delegator: TorchQuantizeDelegator):
使用 executor.register_quantize_delegate(config, function) 完成函数注册,被注册的函数必须满足 TorchQuantizeDelegator 所定义的接口。
下面我们给出一个简单的量化代理函数例子:
class MyQuantDelegator(TorchQuantizeDelegator):
def __call__(self, tensor: torch.Tensor, config: TensorQuantizationConfig) -> torch.Tensor:
if config.policy.has_property(QuantizationProperty.ASYMMETRICAL):
raise ValueError('Sorry, this delegator handles only Symmetrical Quantizations.')
print('You are invoking cusitmized quant function now.')
return torch.round(tensor / config.scale) * config.scale
在执行器遇到 TQC 时,将会调用 executor.quantize_function 执行量化,其逻辑为:
def quantize_function(self, tensor: torch.Tensor, config: TensorQuantizationConfig = None) -> torch.Tensor:
if config is None or not QuantizationStates.is_activated(config.state): return tensor
elif config in self._delegates: return self._delegates[config](tensor, config)
else:
if config.policy.has_property(QuantizationProperty.DYNAMIC):
return self._dynamic_quant_fn(tensor, config)
else:
return self._default_quant_fn(tensor, config)
Usage (用法示例)
PPQ 的执行器初始化需要一个计算图实例作为参数:
executor = TorchExecutor(graph=ppq_quant_ir)
executor.forward(inputs=..., output_names=..., hooks=...)
这一计算图可以是量化过后的,也可以是没有量化的。但 PPQ 希望传入的计算图经过正确调度,传入没有调度的计算图将会触发警报:
if not graph.extension_attrib.get(IS_DISPATCHED_GRAPH, False):
ppq_warning('Can not create executor with your graph, graph is not correctly dispatched, '
'use dispatch_graph(graph=ir, platform=platfrom, setting=setting) first.')
executor.forward 需要三个参数,下面举例对其进行说明:
# 传入三个变量 a, b, c 作为输入
executor.forward(inputs=[a, b, c], output_names=..., hooks=...)
# 分别对图中 input, var 1 两个变量传入 a, b 作为输入
executor.forward(inputs='input': a, 'var 1': b, output_names=..., hooks=...)
# 传入一个完整的 tensor 作为输入
executor.forward(inputs=torch.zeros(shape=[1,3,224,224]), output_names=..., hooks=...)
# 要求网络输出 output, Var 1 的值
executor.forward(inputs=..., output_names=['output 1', 'Var 1'], hooks=...)
executor.forward 函数默认不需要梯度,如果希望执行带有梯度的网络,需要使用 executor.forward_with_gradient 函数。 forward 函数的返回值永远是一个 torch.Tensor 数组,其中元素的顺序由 output_names 参数决定。
Hook (执行钩子函数)
在调用 executor.forward 函数时可以传入 hooks 参数。钩子函数是注册在 op 上的,你可以传入一个字典用来说明需要调用的钩子函数:
字典 'Conv 1': myhook 说明了希望在算子 Conv 1 的执行器件调用钩子函数 myhook。
钩子函数必须继承于 RuntimeHook 类,必须实现成员函数 pre_forward_hook, post_forward_hook。在这两个函数中,你可以制定特定的逻辑修改算子输入输出的值。
class RuntimeHook(metaclass=ABCMeta):
def __init__(self, operation: Operation) -> None:
self._hook_to = operation
def pre_forward_hook(self, inputs: list, **kwargs) -> list:
return inputs
def post_forward_hook(self, outputs: list, **kwargs) -> list:
return outputs
TorchExecutor - executor object which use torch as its backend.
torch backend is used to graph simulating & training(QAT)
all operation forward functions are written with pytorch,so that they will have gradient recorded by torch engine. which means you can directly access to tensor.grad after using output.backward()
Args:
graph (BaseGraph):
executing graph object,
TorchExecutor will automatically send all graph parameters towards executing device.
fp16_mode (bool, optional): [whether the simulator is running in fp16 mode(unimplemented).]. Defaults to True.
device (str, optional): [
executing device, as same as torch.device,
you can not select gpu to executing yet,
graph will always be send to the very first visible cuda device.
]. Defaults to 'cuda'.
以上是关于PPQ ~ OpenPPL 之多平台量化部署工具来啦!的主要内容,如果未能解决你的问题,请参考以下文章
OpenPPL PPQ量化:量化计算图的加载和预处理 源码剖析