模型压缩框架概述
=======================================

.. contents::

下图展示了模型压缩框架的组件概览。


.. image:: ../../img/compressor_framework.jpg
   :target: ../../img/compressor_framework.jpg
   :alt: 


NNI 模型压缩框架中主要有三个组件/类： ``Compressor``\ , ``Pruner`` 和 ``Quantizer``。 下面会逐个详细介绍：

Compressor
----------

Compressor 是 Pruner 和 Quantizer 的基类，提供了统一的接口，可用同样的方式使用它们。 例如，使用 Pruner：

.. code-block:: python

   from nni.algorithms.compression.pytorch.pruning import LevelPruner

   # 读取预训练的模型，或在使用 Pruner 前进行训练。

   configure_list = [{
       'sparsity': 0.7,
       'op_types': ['Conv2d', 'Linear'],
   }]

   optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4)
   pruner = LevelPruner(model, configure_list, optimizer)
   model = pruner.compress()

   # 剪枝已准备好，开始调优模型，
   # 模型会在训练过程中自动剪枝

使用 Quantizer：

.. code-block:: python

   from nni.algorithms.compression.pytorch.pruning import DoReFaQuantizer

   configure_list = [{
       'quant_types': ['weight'],
       'quant_bits': {
           'weight': 8,
       },
       'op_types':['Conv2d', 'Linear']
   }]
   optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4)
   quantizer = DoReFaQuantizer(model, configure_list, optimizer)
   quantizer.compress()

查看 :githublink:`示例代码 <examples/model_compress>` 了解更多信息。

``Compressor`` 类提供了一些工具函数：

设置包装的属性
^^^^^^^^^^^^^^^^^^^^^

有时，``calc_mask`` 需要保存一些状态数据，可以像 PyTorch 的 module 一样，使用 ``set_wrappers_attribute`` API 来注册属性。 这些缓存会注册到 ``module 包装`` 中。 用户可以通过 ``module 包装``来直接访问这些缓存。
在上述示例中，使用了 ``set_wrappers_attribute`` 类设置缓冲 ``if_calculated``，它用来标识某层的掩码是否已经计算过了。

在 forward 时收集数据
^^^^^^^^^^^^^^^^^^^^^^^^^^^

有时，需要在 forward 方法中收集数据，例如，需要激活的平均值。 可通过向 module 中添加定制的 Collector 来做到。

.. code-block:: python

   class MyMasker(WeightMasker):
       def __init__(self, model, pruner):
           super().__init__(model, pruner)
           # 为所有包装类设置 `collected_activation` 属性
           # 保存所有层的激活值
           self.pruner.set_wrappers_attribute("collected_activation", [])
           self.activation = torch.nn.functional.relu

           def collector(wrapper, input_, output):
               # 通过每个包装的 collected_activation 属性，
               # 来评估收到的激活值
               wrapper.collected_activation.append(self.activation(output.detach().cpu()))

           self.pruner.hook_id = self.pruner.add_activation_collector(collector)

收集函数会在每次 forward 方法运行时调用。

还可这样来移除收集方法：

.. code-block:: python

   # 保存 Collector 的标识
   collector_id = self.pruner.add_activation_collector(collector)

   # 当 Collector 不再需要后，
   # 可以通过保存的 Collector 标识来删除
   self.pruner.remove_activation_collector(collector_id)

----

Pruner
------

Pruner 接收 ``模型``，``配置`` 和 ``优化器`` 作为参数。 通过往 ``optimizer.step()`` 上增加回调，在训练过程中根据 ``config_list`` 来对模型剪枝。

Pruner 类是 Compressor 的子类，因此它包含了 Compressor 的所有功能，并添加了剪枝所需要的组件，包括：

权重掩码
^^^^^^^^^^^^^

``权重掩码`` 是剪枝算法的实现，可将由 ``module 包装`` 所包装起来的一层根据稀疏度进行修建。

剪枝模块包装
^^^^^^^^^^^^^^^^^^^^^^

``剪枝 module 的包装`` 包含：


#. 原始的 module
#. ``calc_mask`` 使用的一些缓存
#. 新的 forward 方法，用于在运行原始的 forward 方法前应用掩码。

使用 ``module 包装`` 的原因：


#. 计算掩码所需要的 ``calc_mask`` 方法需要一些缓存，这些缓存需要注册在 ``module 包装`` 里，这样就不需要修改原始的 module。
#. 新的 ``forward`` 方法用来在原始 ``forward`` 调用前，将掩码应用到权重上。

剪枝回调
^^^^^^^^^^^^

当 Pruner 构造时会添加剪枝的回调，用来在 ``optimizer.step()`` 被调用时，调用 Pruner 的 calc_mask。

----

Quantizer
---------

Quantizer 也是 ``Compressor`` 的子类，用来通过减少权重或激活值的位宽来压缩模型，这样可以减少模型推理时的计算时间。 它包含：

量化 module 包装
^^^^^^^^^^^^^^^^^^^^^^^^^^^

模型中每个要量化的模块和层，都需要量化包装，它通过提供 ``forward`` 方法来量化原始模型的权重、输入和输出。

量化回调
^^^^^^^^^^^^^^^^^

量化回调会在调用 ``optimizer.step()`` 时设置。

量化相关函数
^^^^^^^^^^^^^^^^^^^^

``Quantizer`` 类为子类提供一下方法来实现量化算法：

.. code-block:: python

   class Quantizer(Compressor):
       """
       PyTorch 的量化基类
       """
       def quantize_weight(self, weight, wrapper, **kwargs):
           """
           重载此方法实现权重的量化。
           此方法挂载于模型的 :meth:`forward`。
           参数量
           ----------
           weight : Tensor
               要被量化的权重
           wrapper : QuantizerModuleWrapper
               原始 module 的包装
           """
           raise NotImplementedError('Quantizer must overload quantize_weight()')

       def quantize_output(self, output, wrapper, **kwargs):
           """
           重载此方法实现输出的量化。
           此方法挂载于模型的 :meth:`forward`。
           参数量
           ----------
           output : Tensor
               需要被量化的输出
           wrapper : QuantizerModuleWrapper
               原始 module 的包装
           """
           raise NotImplementedError('Quantizer must overload quantize_output()')

       def quantize_input(self, *inputs, wrapper, **kwargs):
           """
           重载此方法量化输入
           此方法挂载于模型的 :meth:`forward`。
           参数量
           ----------
           inputs : Tensor
               需要被量化的张量
           wrapper : QuantizerModuleWrapper
               原始 module 的包装
           """
           raise NotImplementedError('Quantizer must overload quantize_input()')

----

多 GPU 支持
-----------------

在多 GPU 训练中，缓存和参数会在每次 ``forward`` 方法被调用时，复制到多个 GPU 上。 如果缓存和参数要在 ``forward`` 更新，就需要通过 ``原地`` 更新来提高效率。
因为 ``calc_mask`` 会在 ``optimizer.step`` 方法中的调用，会在 ``forward`` 方法后才被调用，且只会发生在单 GPU 上，因此它天然的就支持多 GPU 的情况。
