"test/training_service/nnitest/run_tests.py" did not exist on "fb26187de05dcf60db3ac07d8a6d45eab6f6323b"
test_compressor.py 11.9 KB
Newer Older
liuzhe-lz's avatar
liuzhe-lz committed
1
2
3
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

4
from unittest import TestCase, main
5
import numpy as np
6
import tensorflow as tf
7
8
import torch
import torch.nn.functional as F
9
import nni.compression.torch as torch_compressor
10
import math
11

12
13
14
if tf.__version__ >= '2.0':
    import nni.compression.tensorflow as tf_compressor

Tang Lang's avatar
Tang Lang committed
15

16
def get_tf_model():
17
    model = tf.keras.models.Sequential([
18
        tf.keras.layers.Conv2D(filters=5, kernel_size=7, input_shape=[28, 28, 1], activation='relu', padding="SAME"),
19
        tf.keras.layers.MaxPooling2D(pool_size=2),
20
        tf.keras.layers.Conv2D(filters=10, kernel_size=3, activation='relu', padding="SAME"),
21
22
23
24
25
26
27
        tf.keras.layers.MaxPooling2D(pool_size=2),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(units=128, activation='relu'),
        tf.keras.layers.Dropout(0.5),
        tf.keras.layers.Dense(units=10, activation='softmax'),
    ])
    model.compile(loss="sparse_categorical_crossentropy",
Tang Lang's avatar
Tang Lang committed
28
29
                  optimizer=tf.keras.optimizers.SGD(lr=1e-3),
                  metrics=["accuracy"])
30
    return model
31

Tang Lang's avatar
Tang Lang committed
32

33
class TorchModel(torch.nn.Module):
34
35
    def __init__(self):
        super().__init__()
36
        self.conv1 = torch.nn.Conv2d(1, 5, 5, 1)
Tang Lang's avatar
Tang Lang committed
37
        self.bn1 = torch.nn.BatchNorm2d(5)
38
        self.conv2 = torch.nn.Conv2d(5, 10, 5, 1)
Tang Lang's avatar
Tang Lang committed
39
        self.bn2 = torch.nn.BatchNorm2d(10)
40
41
        self.fc1 = torch.nn.Linear(4 * 4 * 10, 100)
        self.fc2 = torch.nn.Linear(100, 10)
42
43

    def forward(self, x):
Tang Lang's avatar
Tang Lang committed
44
        x = F.relu(self.bn1(self.conv1(x)))
45
        x = F.max_pool2d(x, 2, 2)
Tang Lang's avatar
Tang Lang committed
46
        x = F.relu(self.bn2(self.conv2(x)))
47
        x = F.max_pool2d(x, 2, 2)
48
        x = x.view(-1, 4 * 4 * 10)
49
50
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
51
52
        return F.log_softmax(x, dim=1)

Tang Lang's avatar
Tang Lang committed
53

54
def tf2(func):
55
    def test_tf2_func(*args):
56
        if tf.__version__ >= '2.0':
57
            func(*args)
Tang Lang's avatar
Tang Lang committed
58

59
    return test_tf2_func
60

Tang Lang's avatar
Tang Lang committed
61

chicm-ms's avatar
chicm-ms committed
62
# for fpgm filter pruner test
Tang Lang's avatar
Tang Lang committed
63
w = np.array([[[[i + 1] * 3] * 3] * 5 for i in range(10)])
64

Tang Lang's avatar
Tang Lang committed
65

66
class CompressorTestCase(TestCase):
67
68
69
70
71
72
    def test_torch_quantizer_modules_detection(self):
        # test if modules can be detected
        model = TorchModel()
        config_list = [{
            'quant_types': ['weight'],
            'quant_bits': 8,
Tang Lang's avatar
Tang Lang committed
73
            'op_types': ['Conv2d', 'Linear']
74
75
76
77
        }, {
            'quant_types': ['output'],
            'quant_bits': 8,
            'quant_start_step': 0,
Tang Lang's avatar
Tang Lang committed
78
            'op_types': ['ReLU']
79
80
81
82
83
84
        }]

        model.relu = torch.nn.ReLU()
        quantizer = torch_compressor.QAT_Quantizer(model, config_list)
        quantizer.compress()
        modules_to_compress = quantizer.get_modules_to_compress()
Tang Lang's avatar
Tang Lang committed
85
        modules_to_compress_name = [t[0].name for t in modules_to_compress]
86
87
88
89
90
91
92
        assert "conv1" in modules_to_compress_name
        assert "conv2" in modules_to_compress_name
        assert "fc1" in modules_to_compress_name
        assert "fc2" in modules_to_compress_name
        assert "relu" in modules_to_compress_name
        assert len(modules_to_compress_name) == 5

93
94
    def test_torch_level_pruner(self):
        model = TorchModel()
Cjkkkk's avatar
Cjkkkk committed
95
        optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
chicm-ms's avatar
chicm-ms committed
96
        configure_list = [{'sparsity': 0.8, 'op_types': ['default']}]
Cjkkkk's avatar
Cjkkkk committed
97
        torch_compressor.LevelPruner(model, configure_list, optimizer).compress()
98

99
100
101
102
    @tf2
    def test_tf_level_pruner(self):
        configure_list = [{'sparsity': 0.8, 'op_types': ['default']}]
        tf_compressor.LevelPruner(get_tf_model(), configure_list).compress()
103

104
105
    def test_torch_naive_quantizer(self):
        model = TorchModel()
Cjkkkk's avatar
Cjkkkk committed
106
107
108
109
110
        configure_list = [{
            'quant_types': ['weight'],
            'quant_bits': {
                'weight': 8,
            },
Tang Lang's avatar
Tang Lang committed
111
            'op_types': ['Conv2d', 'Linear']
Cjkkkk's avatar
Cjkkkk committed
112
113
        }]
        torch_compressor.NaiveQuantizer(model, configure_list).compress()
114

115
    @tf2
116
117
    def test_tf_naive_quantizer(self):
        tf_compressor.NaiveQuantizer(get_tf_model(), [{'op_types': ['default']}]).compress()
118

119
120
    def test_torch_fpgm_pruner(self):
        """
chicm-ms's avatar
chicm-ms committed
121
        With filters(kernels) weights defined as above (w), it is obvious that w[4] and w[5] is the Geometric Median
122
123
124
125
        which minimize the total geometric distance by defination of Geometric Median in this paper:
        Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration,
        https://arxiv.org/pdf/1811.00250.pdf

chicm-ms's avatar
chicm-ms committed
126
127
        So if sparsity is 0.2, the expected masks should mask out w[4] and w[5], this can be verified through:
        `all(torch.sum(masks, (1, 2, 3)).numpy() == np.array([45., 45., 45., 45., 0., 0., 45., 45., 45., 45.]))`
128

chicm-ms's avatar
chicm-ms committed
129
130
        If sparsity is 0.6, the expected masks should mask out w[2] - w[7], this can be verified through:
        `all(torch.sum(masks, (1, 2, 3)).numpy() == np.array([45., 45., 0., 0., 0., 0., 0., 0., 45., 45.]))`
131
132
133
        """

        model = TorchModel()
Cjkkkk's avatar
Cjkkkk committed
134
135
136
        optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
        config_list = [{'sparsity': 0.6, 'op_types': ['Conv2d']}, {'sparsity': 0.2, 'op_types': ['Conv2d']}]
        pruner = torch_compressor.FPGMPruner(model, config_list, optimizer)
137

Cjkkkk's avatar
Cjkkkk committed
138
139
140
        model.conv2.module.weight.data = torch.tensor(w).float()
        masks = pruner.calc_mask(model.conv2)
        assert all(torch.sum(masks['weight_mask'], (1, 2, 3)).numpy() == np.array([45., 45., 45., 45., 0., 0., 45., 45., 45., 45.]))
141

Cjkkkk's avatar
Cjkkkk committed
142
143
144
145
146
        model.conv2.module.weight.data = torch.tensor(w).float()
        model.conv2.if_calculated = False
        model.conv2.config = config_list[0]
        masks = pruner.calc_mask(model.conv2)
        assert all(torch.sum(masks['weight_mask'], (1, 2, 3)).numpy() == np.array([45., 45., 0., 0., 0., 0., 0., 0., 45., 45.]))
147
148
149

    @tf2
    def test_tf_fpgm_pruner(self):
150
        model = get_tf_model()
Cjkkkk's avatar
Cjkkkk committed
151
        config_list = [{'sparsity': 0.2, 'op_types': ['Conv2D']}]
152
153
154
155
156
157
158
159

        pruner = tf_compressor.FPGMPruner(model, config_list)
        weights = model.layers[2].weights
        weights[0] = np.array(w).astype(np.float32).transpose([2, 3, 0, 1]).transpose([0, 1, 3, 2])
        model.layers[2].set_weights([weights[0], weights[1].numpy()])

        layer = tf_compressor.compressor.LayerInfo(model.layers[2])
        masks = pruner.calc_mask(layer, config_list[0]).numpy()
chicm-ms's avatar
chicm-ms committed
160
        masks = masks.reshape((-1, masks.shape[-1])).transpose([1, 0])
161

chicm-ms's avatar
chicm-ms committed
162
        assert all(masks.sum((1)) == np.array([45., 45., 45., 45., 0., 0., 45., 45., 45., 45.]))
Cjkkkk's avatar
Cjkkkk committed
163
        
Tang Lang's avatar
Tang Lang committed
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
    def test_torch_l1filter_pruner(self):
        """
        Filters with the minimum sum of the weights' L1 norm are pruned in this paper:
        PRUNING FILTERS FOR EFFICIENT CONVNETS,
        https://arxiv.org/abs/1608.08710

        So if sparsity is 0.2, the expected masks should mask out filter 0, this can be verified through:
        `all(torch.sum(mask1, (1, 2, 3)).numpy() == np.array([0., 27., 27., 27., 27.]))`

        If sparsity is 0.6, the expected masks should mask out filter 0,1,2, this can be verified through:
        `all(torch.sum(mask2, (1, 2, 3)).numpy() == np.array([0., 0., 0., 27., 27.]))`
        """
        w = np.array([np.zeros((3, 3, 3)), np.ones((3, 3, 3)), np.ones((3, 3, 3)) * 2,
                      np.ones((3, 3, 3)) * 3, np.ones((3, 3, 3)) * 4])
        model = TorchModel()
Cjkkkk's avatar
Cjkkkk committed
179
        optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
Tang Lang's avatar
Tang Lang committed
180
181
        config_list = [{'sparsity': 0.2, 'op_types': ['Conv2d'], 'op_names': ['conv1']},
                       {'sparsity': 0.6, 'op_types': ['Conv2d'], 'op_names': ['conv2']}]
Cjkkkk's avatar
Cjkkkk committed
182
        pruner = torch_compressor.L1FilterPruner(model, config_list, optimizer)
Tang Lang's avatar
Tang Lang committed
183

Cjkkkk's avatar
Cjkkkk committed
184
185
186
187
188
189
        model.conv1.module.weight.data = torch.tensor(w).float()
        model.conv2.module.weight.data = torch.tensor(w).float()
        mask1 = pruner.calc_mask(model.conv1)
        mask2 = pruner.calc_mask(model.conv2)
        assert all(torch.sum(mask1['weight_mask'], (1, 2, 3)).numpy() == np.array([0., 27., 27., 27., 27.]))
        assert all(torch.sum(mask2['weight_mask'], (1, 2, 3)).numpy() == np.array([0., 0., 0., 27., 27.]))
Tang Lang's avatar
Tang Lang committed
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206

    def test_torch_slim_pruner(self):
        """
        Scale factors with minimum l1 norm in the BN layers are pruned in this paper:
        Learning Efficient Convolutional Networks through Network Slimming,
        https://arxiv.org/pdf/1708.06519.pdf

        So if sparsity is 0.2, the expected masks should mask out channel 0, this can be verified through:
        `all(mask1.numpy() == np.array([0., 1., 1., 1., 1.]))`
        `all(mask2.numpy() == np.array([0., 1., 1., 1., 1.]))`

        If sparsity is 0.6, the expected masks should mask out channel 0,1,2, this can be verified through:
        `all(mask1.numpy() == np.array([0., 0., 0., 1., 1.]))`
        `all(mask2.numpy() == np.array([0., 0., 0., 1., 1.]))`
        """
        w = np.array([0, 1, 2, 3, 4])
        model = TorchModel()
Cjkkkk's avatar
Cjkkkk committed
207
        optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
Tang Lang's avatar
Tang Lang committed
208
209
210
        config_list = [{'sparsity': 0.2, 'op_types': ['BatchNorm2d']}]
        model.bn1.weight.data = torch.tensor(w).float()
        model.bn2.weight.data = torch.tensor(-w).float()
Cjkkkk's avatar
Cjkkkk committed
211
        pruner = torch_compressor.SlimPruner(model, config_list, optimizer)
Tang Lang's avatar
Tang Lang committed
212

Cjkkkk's avatar
Cjkkkk committed
213
214
215
216
217
218
        mask1 = pruner.calc_mask(model.bn1)
        mask2 = pruner.calc_mask(model.bn2)
        assert all(mask1['weight_mask'].numpy() == np.array([0., 1., 1., 1., 1.]))
        assert all(mask2['weight_mask'].numpy() == np.array([0., 1., 1., 1., 1.]))
        assert all(mask1['bias_mask'].numpy() == np.array([0., 1., 1., 1., 1.]))
        assert all(mask2['bias_mask'].numpy() == np.array([0., 1., 1., 1., 1.]))
Tang Lang's avatar
Tang Lang committed
219

Cjkkkk's avatar
Cjkkkk committed
220
221
        model = TorchModel()
        optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
Tang Lang's avatar
Tang Lang committed
222
223
224
        config_list = [{'sparsity': 0.6, 'op_types': ['BatchNorm2d']}]
        model.bn1.weight.data = torch.tensor(w).float()
        model.bn2.weight.data = torch.tensor(w).float()
Cjkkkk's avatar
Cjkkkk committed
225
        pruner = torch_compressor.SlimPruner(model, config_list, optimizer)
Tang Lang's avatar
Tang Lang committed
226

Cjkkkk's avatar
Cjkkkk committed
227
228
229
230
231
232
        mask1 = pruner.calc_mask(model.bn1)
        mask2 = pruner.calc_mask(model.bn2)
        assert all(mask1['weight_mask'].numpy() == np.array([0., 0., 0., 1., 1.]))
        assert all(mask2['weight_mask'].numpy() == np.array([0., 0., 0., 1., 1.]))
        assert all(mask1['bias_mask'].numpy() == np.array([0., 0., 0., 1., 1.]))
        assert all(mask2['bias_mask'].numpy() == np.array([0., 0., 0., 1., 1.]))
Tang Lang's avatar
Tang Lang committed
233

234
235
236
237
238
    def test_torch_QAT_quantizer(self):
        model = TorchModel()
        config_list = [{
            'quant_types': ['weight'],
            'quant_bits': 8,
Tang Lang's avatar
Tang Lang committed
239
            'op_types': ['Conv2d', 'Linear']
240
241
242
243
        }, {
            'quant_types': ['output'],
            'quant_bits': 8,
            'quant_start_step': 0,
Tang Lang's avatar
Tang Lang committed
244
            'op_types': ['ReLU']
245
246
247
248
249
250
251
252
        }]
        model.relu = torch.nn.ReLU()
        quantizer = torch_compressor.QAT_Quantizer(model, config_list)
        quantizer.compress()
        # test quantize
        # range not including 0
        eps = 1e-7
        weight = torch.tensor([[1, 2], [3, 5]]).float()
Cjkkkk's avatar
Cjkkkk committed
253
254
255
        quantize_weight = quantizer.quantize_weight(weight, model.conv2)
        assert math.isclose(model.conv2.module.scale, 5 / 255, abs_tol=eps)
        assert model.conv2.module.zero_point == 0
Tang Lang's avatar
Tang Lang committed
256
        # range including 0
257
        weight = torch.tensor([[-1, 2], [3, 5]]).float()
Cjkkkk's avatar
Cjkkkk committed
258
259
260
        quantize_weight = quantizer.quantize_weight(weight, model.conv2)
        assert math.isclose(model.conv2.module.scale, 6 / 255, abs_tol=eps)
        assert model.conv2.module.zero_point in (42, 43)
261
262
263
264

        # test ema
        x = torch.tensor([[-0.2, 0], [0.1, 0.2]])
        out = model.relu(x)
chicm-ms's avatar
chicm-ms committed
265
266
        assert math.isclose(model.relu.module.tracked_min_biased, 0, abs_tol=eps)
        assert math.isclose(model.relu.module.tracked_max_biased, 0.002, abs_tol=eps)
267

Cjkkkk's avatar
Cjkkkk committed
268
        quantizer.step_with_optimizer()
269
270
        x = torch.tensor([[0.2, 0.4], [0.6, 0.8]])
        out = model.relu(x)
chicm-ms's avatar
chicm-ms committed
271
272
        assert math.isclose(model.relu.module.tracked_min_biased, 0.002, abs_tol=eps)
        assert math.isclose(model.relu.module.tracked_max_biased, 0.00998, abs_tol=eps)
273

Tang Lang's avatar
Tang Lang committed
274

275
276
if __name__ == '__main__':
    main()