From 77fe790448bca5e964893fc01ada7ebbca161944 Mon Sep 17 00:00:00 2001 From: Oguzhan Buyuksolak Date: Mon, 2 Sep 2024 16:01:38 +0300 Subject: [PATCH 01/12] QATv2 updates, minor bug fixes --- .github/linters/.python-lint | 4 +- README.md | 21 +- ai8x.py | 489 ++++++++++++++++++++---- ai8x_blocks.py | 17 +- datasets/imagenet.py | 4 +- docs/unload_example.png | Bin 0 -> 66400 bytes models/ai85net-actiontcn.py | 23 +- models/ai85net-autoencoder.py | 5 +- models/ai85net-faceid_112.py | 5 +- models/ai87net-imagenet-effnetv2.py | 15 + models/ai87net-mobilefacenet_112.py | 5 +- parse_qat_yaml.py | 7 +- policies/qat_policy_cifar100.yaml | 1 + policies/qat_policy_imagenet.yaml | 2 +- policies/qat_policy_late_cifar.yaml | 1 + policies/qat_policy_mnist.yaml | 1 + policies/qat_policy_pascalvoc.yaml | 1 + policies/schedule-imagenet-effnet2.yaml | 8 +- scripts/train_imagenet_effnet2.sh | 2 +- scripts/train_mnist_qat.sh | 2 +- train.py | 77 +++- 21 files changed, 571 insertions(+), 119 deletions(-) create mode 100644 docs/unload_example.png diff --git a/.github/linters/.python-lint b/.github/linters/.python-lint index c4f4a6f6c..c194cc279 100644 --- a/.github/linters/.python-lint +++ b/.github/linters/.python-lint @@ -7,11 +7,11 @@ ignored-classes = ModelProto max-line-length = 99 [DESIGN] max-locals=100 -max-statements=350 +max-statements=360 min-public-methods=1 max-branches=130 max-module-lines=5000 max-args=20 max-returns=10 -max-attributes=25 +max-attributes=30 max-nested-blocks=10 diff --git a/README.md b/README.md index 4df6c395d..7b1f6a494 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ADI MAX78000/MAX78002 Model Training and Synthesis -July 22, 2024 +August 27, 2024 **Note: This branch requires PyTorch 2. Please see the archive-1.8 branch for PyTorch 1.8 support. [KNOWN_ISSUES](KNOWN_ISSUES.txt) contains a list of known issues.** @@ -1620,13 +1620,15 @@ When using the `-8` command line switch, all module outputs are quantized to 8-b The last layer can optionally use 32-bit output for increased precision. This is simulated by adding the parameter `wide=True` to the module function call. -##### Weights: Quantization-Aware Training (QAT) +##### Weights and Activations: Quantization-Aware Training (QAT) Quantization-aware training (QAT) is enabled by default. QAT is controlled by a policy file, specified by `--qat-policy`. -* After `start_epoch` epochs, training will learn an additional parameter that corresponds to a shift of the final sum of products. +* After `start_epoch` epochs, an intermediate epoch with no backpropagation will be realized to collect activation statistics. Each layer's activation ranges will be determined based on the range & resolution trade-off from the collected activations. Then, QAT will start and an additional parameter (`output_shift`) will be learned to shift activations for compensating weights & biases scaling down. * `weight_bits` describes the number of bits available for weights. * `overrides` allows specifying the `weight_bits` on a per-layer basis. +* `outlier_removal_z_score` defines the z-score threshold for outlier removal during activation range calculation. (default: 8.0) +* `shift_quantile` defines the quantile of the parameters distribution to be used for the `output_shift` parameter. (default: 1.0) By default, weights are quantized to 8-bits after 30 epochs as specified in `policies/qat_policy.yaml`. A more refined example that specifies weight sizes for individual layers can be seen in `policies/qat_policy_cifar100.yaml`. @@ -1745,7 +1747,7 @@ For both approaches, the `quantize.py` software quantizes an existing PyTorch ch #### Quantization-Aware Training (QAT) -Quantization-aware training is the better performing approach. It is enabled by default. QAT learns additional parameters during training that help with quantization (see [Weights: Quantization-Aware Training (QAT)](#weights-quantization-aware-training-qat). No additional arguments (other than input, output, and device) are needed for `quantize.py`. +Quantization-aware training is the better performing approach. It is enabled by default. QAT learns additional parameters during training that help with quantization (see [Weights and Activations: Quantization-Aware Training (QAT)](#weights-and-activations-quantization-aware-training-qat). No additional arguments (other than input, output, and device) are needed for `quantize.py`. The input checkpoint to `quantize.py` is either `qat_best.pth.tar`, the best QAT epoch’s checkpoint, or `qat_checkpoint.pth.tar`, the final QAT epoch’s checkpoint. @@ -2004,7 +2006,7 @@ The behavior of a training session might change when Quantization Aware Training While there can be multiple reasons for this, check two important settings that can influence the training behavior: * The initial learning rate may be set too high. Reduce LR by a factor of 10 or 100 by specifying a smaller initial `--lr` on the command line, and possibly by reducing the epoch `milestones` for further reduction of the learning rate in the scheduler file specified by `--compress`. Note that the the selected optimizer and the batch size both affect the learning rate. -* The epoch when QAT is engaged may be set too low. Increase `start_epoch` in the QAT scheduler file specified by `--qat-policy`, and increase the total number of training epochs by increasing the value specified by the `--epochs` command line argument and by editing the `ending_epoch` in the scheduler file specified by `--compress`. *See also the rule of thumb discussed in the section [Weights: Quantization-Aware Training (QAT)](#weights:-auantization-aware-training \(qat\)).* +* The epoch when QAT is engaged may be set too low. Increase `start_epoch` in the QAT scheduler file specified by `--qat-policy`, and increase the total number of training epochs by increasing the value specified by the `--epochs` command line argument and by editing the `ending_epoch` in the scheduler file specified by `--compress`. *See also the rule of thumb discussed in the section [Weights and Activations: Quantization-Aware Training (QAT)](#weights-and-activations-quantization-aware-training-qat).* @@ -2209,6 +2211,7 @@ The following table describes the most important command line arguments for `ai8 | `--no-unload` | Do not create the `cnn_unload()` function | | | `--no-kat` | Do not generate the `check_output()` function (disable known-answer test) | | | `--no-deduplicate-weights` | Do not deduplicate weights and and bias values | | +| `--scale-output` | Use scales from the checkpoint to recover output range while generating `cnn_unload()` function | | ### YAML Network Description @@ -2330,6 +2333,12 @@ The following keywords are required for each `unload` list item: `width`: Data width (optional, defaults to 8) — either 8 or 32 `write_gap`: Gap between data words (optional, defaults to 0) +When `--scale-output` is specified, scales from the checkpoint file are used to recover the output range. If there is a non-zero scale for the 8 bits output, the output will be scaled and kept in 16 bits. If the scale is zero, the output will be 8 bits. For 32 bits output, the output will be kept in 32 bits always. + +Example: + +![Unload Array](docs/unload_example.png) + ##### `layers` (Mandatory) `layers` is a list that defines the per-layer description, as shown below: @@ -2654,7 +2663,7 @@ Example: By default, the final layer is used as the output layer. Output layers are checked using the known-answer test, and they are copied from hardware memory when `cnn_unload()` is called. The tool also checks that output layer data isn’t overwritten by any later layers. When specifying `output: true`, any layer (or a combination of layers) can be used as an output layer. -*Note:* When `unload:` is used, output layers are not used for generating `cnn_unload()`. +*Note:* When `--no-unload` is used, output layers are not used for generating `cnn_unload()`. Example: `output: true` diff --git a/ai8x.py b/ai8x.py index 277cd6ff3..b74e15abe 100644 --- a/ai8x.py +++ b/ai8x.py @@ -1,6 +1,6 @@ ################################################################################################### # -# Copyright (C) 2020-2023 Maxim Integrated Products, Inc. All Rights Reserved. +# Copyright (C) 2020-2024 Maxim Integrated Products, Inc. All Rights Reserved. # # Maxim Integrated Products, Inc. Default Copyright Notice: # https://www.maximintegrated.com/en/aboutus/legal/copyrights.html @@ -13,9 +13,11 @@ the limits into account. """ +import numpy as np import torch from torch import nn from torch.autograd import Function +from torch.fx import symbolic_trace import devices @@ -327,7 +329,7 @@ def forward(self, x): # pylint: disable=arguments-differ return x.mul(factor).floor().div(factor) -def quantize_clamp(wide, quantize_activation=False, weight_bits=8): +def quantize_clamp(wide, quantize_activation=False, clamp_activation=False, weight_bits=8): """ Return new Quantization and Clamp objects. """ @@ -352,21 +354,25 @@ def quantize_clamp(wide, quantize_activation=False, weight_bits=8): quantize = Quantize(num_bits=dev.WIDE_LAYER_RESOLUTION_BITS) else: quantize = Empty() - if not wide: - clamp = Clamp( # Do not combine with ReLU - min_val=-1., - max_val=(2.**(dev.ACTIVATION_BITS-1)-1)/(2.**(dev.ACTIVATION_BITS-1)), - ) + + if clamp_activation: + if not wide: + clamp = Clamp( # Do not combine with ReLU + min_val=-1., + max_val=(2.**(dev.ACTIVATION_BITS-1)-1)/(2.**(dev.ACTIVATION_BITS-1)), + ) + else: + clamp = Clamp( + min_val=-(2.**((dev.FULL_ACC_BITS-2*(dev.DATA_BITS-1))-1)), + max_val=2.**((dev.FULL_ACC_BITS-2*(dev.DATA_BITS-1))-1), + ) else: - clamp = Clamp( - min_val=-(2.**((dev.FULL_ACC_BITS-2*(dev.DATA_BITS-1))-1)), - max_val=2.**((dev.FULL_ACC_BITS-2*(dev.DATA_BITS-1))-1), - ) + clamp = Empty() return quantize, clamp -def quantize_clamp_pool(pooling, quantize_activation=False): +def quantize_clamp_pool(pooling, quantize_activation=False, clamp_activation=False): """ Return new Quantization and Clamp objects for pooling. """ @@ -385,7 +391,10 @@ def quantize_clamp_pool(pooling, quantize_activation=False): if pooling == 'Avg': if quantize_activation: quantize = RoundQat() if dev.round_avg else FloorQat() - clamp = Clamp(min_val=-1., max_val=127./128.) + if clamp_activation: + clamp = Clamp(min_val=-1., max_val=127./128.) + else: + clamp = Empty() else: # Max, None clamp = Empty() @@ -494,7 +503,7 @@ class One(nn.Module): """ def forward(self, x): # pylint: disable=arguments-differ """Forward prop""" - return torch.ones(1, device=x.device) + return torch.ones(1).to(x.device) class WeightScale(nn.Module): @@ -563,6 +572,148 @@ def get_activation(activation=None): return Empty() +def histogram(inp, bins): + """ + CUDA compatible histogram calculation + """ + minimum, maximum = inp.min(), inp.max() + counts = torch.histc(inp, bins, min=minimum, max=maximum) + boundaries = torch.linspace(minimum, maximum, bins + 1) + return counts, boundaries + + +def calc_q_error(module, threshold, bits, eps=1e-9): + """ + Activation quantization error calculation + """ + quantized_hist = module.hist[1].clone() + quantized_hist = torch.round((quantized_hist / (threshold + eps)) * 2**(bits-1)) + quantized_hist = torch.clamp(quantized_hist, -2**(bits-1), 2**(bits-1)-1) + quantized_hist = (quantized_hist * (threshold + eps) / 2**(bits-1)) + err = torch.sum(((quantized_hist - module.hist[1])**2)*module.hist[0]) \ + / torch.sum(module.hist[0]) + + return err + + +def _merge_hist(module): + """ + Merge histograms of activations + """ + bins_to_stack = [] + for hist in module.hist: + bins_to_stack.append(hist[1]) + stacked_bins = torch.stack(bins_to_stack) + min_edge = stacked_bins.min() + max_edge = stacked_bins.max() + # 2048 is the number of bins + width = (max_edge - min_edge) / 2048 + merged_bins = torch.arange(min_edge.item(), (max_edge+width).item(), width.item()) + merged_counts = None + + for hist in module.hist: + if merged_counts is None: + merged_counts = _interpolate_hist(hist[0], hist[1], merged_bins) + else: + merged_counts += _interpolate_hist(hist[0], hist[1], merged_bins) + + module.hist = (merged_counts, merged_bins) + + +def _interpolate_hist(counts, bins, new_bins): + """ + Helper function for interpolating histograms to new bins + """ + cumulative_hist = torch.cumsum(counts, dim=0).to(device=bins.device) + cumulative_hist = torch.cat((torch.tensor([0]), cumulative_hist)) + cumulative_interp_hist = torch.from_numpy(np.interp(new_bins.numpy(), bins.numpy(), + cumulative_hist.numpy())) + interp_counts = torch.diff(cumulative_interp_hist, prepend=torch.tensor([0])) + + return interp_counts + + +# pylint: disable=unused-argument +def _hist_hook(module, inp, output): + """ + Hook to collect histogram of activations + """ + if not hasattr(module, 'hist'): + module.hist = [] + # dynamic histogram collection + hist = histogram(output.clone().detach().flatten(), bins=2048) + module.hist.append(hist) + + +def register_hist_hooks(module): + """ + Register hooks for histogram collection + """ + module.handle = module.register_forward_hook(_hist_hook, always_call=True) + + +def release_hist_hooks(module): + """ + Release hooks after histogram collection + """ + module.handle.remove() + + +def _remove_outliers(module, outlier_removal_z_score=8.0): + """ + Remove outliers from histogram + """ + # Get mean and std of histogram + hist_count = module.hist[0] + hist_bins = module.hist[1] + mean = torch.sum(hist_count * hist_bins) / torch.sum(hist_count) + std = torch.sqrt(torch.sum(hist_count * (hist_bins - mean)**2) / torch.sum(hist_count)) + + # When activations are very small, std ends up being 0 due to rounding. + # In this case, we set std to a very small value to prevent zero element histogram. + if std == 0: + std = 1e-9 + # Calculate bounds according to z-score + upper_bound = mean + outlier_removal_z_score * std + lower_bound = mean - outlier_removal_z_score * std + + # Remove outliers according to bounds + hist_count[hist_bins > upper_bound] = 0 + hist_count[hist_bins < lower_bound] = 0 + non_zero_bins = hist_count != 0 + hist_count = hist_count[non_zero_bins] + hist_bins = hist_bins[non_zero_bins] + module.hist = (hist_count, hist_bins) + + +def init_threshold_module(module, outlier_removal_z_score): + """ + Initialize activation threshold + """ + _merge_hist(module) + _remove_outliers(module, outlier_removal_z_score) + module.threshold = nn.Parameter(module.hist[1].abs().max().log2().ceil().exp2(), + requires_grad=False) + + +def calc_threshold(module, iterations=5, bits=8): + """ + Iteratively calculate threshold for activation quantization + """ + e_min = torch.inf + t_nc = module.threshold + t = None + + for i in range(iterations): + t_i = t_nc / (2**i) + e_i = calc_q_error(module, t_i, bits) + if e_i < e_min: + e_min = e_i + t = t_i + + module.threshold = nn.Parameter(torch.log2(t), requires_grad=False) + + class QuantizationAwareModule(nn.Module): """ Common code for Quantization-Aware Training @@ -579,6 +730,7 @@ def __init__( op=None, bn=None, shift_quantile=1.0, + clamp_activation=False, ): super().__init__() @@ -609,13 +761,18 @@ def __init__( self.pooling = pooling self.output_shift = nn.Parameter(torch.tensor([0.]), requires_grad=False) - self.init_module(weight_bits, bias_bits, quantize_activation, shift_quantile) + self.threshold = nn.Parameter(torch.tensor(0.), requires_grad=False) + self.final_scale = nn.Parameter(torch.tensor(0.), requires_grad=False) + + self.init_module(weight_bits, bias_bits, quantize_activation, + clamp_activation, shift_quantile) def init_module( self, weight_bits, bias_bits, quantize_activation, + clamp_activation, shift_quantile, export=False, ): @@ -625,12 +782,15 @@ def init_module( self.weight_bits = nn.Parameter(torch.tensor([0]), requires_grad=False) self.bias_bits = nn.Parameter(torch.tensor([0]), requires_grad=False) self.quantize_activation = nn.Parameter(torch.tensor([False]), requires_grad=False) + self.clamp_activation = nn.Parameter(torch.tensor([clamp_activation]), + requires_grad=False) self.adjust_output_shift = nn.Parameter(torch.tensor([False]), requires_grad=False) elif weight_bits in [1, 2, 4, 8] and bias_bits in [1, 2, 4, 8] and quantize_activation: self.weight_bits = nn.Parameter(torch.tensor([weight_bits]), requires_grad=False) if not export: self.bias_bits = nn.Parameter(torch.tensor([bias_bits]), requires_grad=False) self.quantize_activation = nn.Parameter(torch.tensor([True]), requires_grad=False) + self.clamp_activation = nn.Parameter(torch.tensor([True]), requires_grad=False) self.adjust_output_shift = nn.Parameter(torch.tensor([not dev.simulate]), requires_grad=False) else: @@ -659,9 +819,11 @@ def set_functions(self): self.bias_bits.detach().item()) self.quantize, self.clamp = \ quantize_clamp(self.wide, bool(self.quantize_activation.detach().item()), + bool(self.clamp_activation.detach().item()), int(self.weight_bits.detach().item())) self.quantize_pool, self.clamp_pool = \ - quantize_clamp_pool(self.pooling, bool(self.quantize_activation.detach().item())) + quantize_clamp_pool(self.pooling, bool(self.quantize_activation.detach().item()), + bool(self.clamp_activation.detach().item())) def forward(self, x): # pylint: disable=arguments-differ """Forward prop""" @@ -676,8 +838,13 @@ def forward(self, x): # pylint: disable=arguments-differ params_r = torch.flatten(self.op.weight.detach()) out_shift = self.calc_out_shift(params_r, self.output_shift.detach()) weight_scale = self.calc_weight_scale(out_shift) - out_scale = self.calc_out_scale(out_shift) + # Quantized checkpoint will have subtracted threshold from output shift + # Therefore, it shouldn't be done again in simulate mode + if not dev.simulate: + out_shift = (out_shift - self.threshold).clamp(min=-15., max=15.) + + out_scale = self.calc_out_scale(out_shift) x = self._conv_forward( # pylint: disable=protected-access x, self.clamp_weight(self.quantize_weight(self.op.weight.mul(weight_scale))), @@ -686,11 +853,14 @@ def forward(self, x): # pylint: disable=arguments-differ ) if self.bn is not None: - x = self.bn(x).div(4.) + x = self.bn(x) if not self.wide: # The device does not apply output shift in wide mode x = self.scale(x, out_scale) x = self.clamp(self.quantize(self.activate(x))) + + # This is the final scale for the output, in the device it will be realized in SW + x = x.mul(2.**(self.final_scale)) return x @@ -1607,14 +1777,24 @@ class Eltwise(nn.Module): """ Base Class for Elementwise Operation """ - def __init__(self, f): + def __init__(self, f, clamp_activation=False): super().__init__() self.f = f + self.threshold = None + self.set_clamp(clamp_activation) + + def set_clamp(self, clamp_activation): + """ + Set Clamping Function + """ if dev.simulate: bits = dev.ACTIVATION_BITS self.clamp = Clamp(min_val=-(2**(bits-1)), max_val=2**(bits-1)-1) else: - self.clamp = Clamp(min_val=-1., max_val=127./128.) + if clamp_activation: + self.clamp = Clamp(min_val=-1., max_val=127./128.) + else: + self.clamp = Empty() def forward(self, *x): """Forward prop""" @@ -1822,19 +2002,28 @@ def initiate_qat(m, qat_policy, export=False): if 'shift_quantile' in qat_policy: module.init_module(qat_policy['weight_bits'], qat_policy['weight_bits'], - True, qat_policy['shift_quantile'], export) + True, True, qat_policy['shift_quantile'], export) else: module.init_module(qat_policy['weight_bits'], - qat_policy['weight_bits'], True, 1.0, export) + qat_policy['weight_bits'], True, True, 1.0, export) if 'overrides' in qat_policy: if name in qat_policy['overrides']: - weight_field = qat_policy['overrides'][name]['weight_bits'] - if 'shift_quantile' in qat_policy: - module.init_module(weight_field, weight_field, + if 'weight_bits' in qat_policy['overrides'][name]: + weight_field = qat_policy['overrides'][name]['weight_bits'] + else: + weight_field = qat_policy['weight_bits'] + if 'shift_quantile' in qat_policy['overrides'][name]: + module.init_module(weight_field, weight_field, True, + True, qat_policy['overrides'][name]['shift_quantile'], + export) + elif 'shift_quantile' in qat_policy: + module.init_module(weight_field, weight_field, True, True, qat_policy['shift_quantile'], export) else: module.init_module(weight_field, - weight_field, True, 1.0, export) + weight_field, True, True, 1.0, export) + elif isinstance(module, Eltwise): + module.set_clamp(True) def update_model(m): @@ -1842,14 +2031,9 @@ def update_model(m): Update model `m` with the current parameters. It is used to update model functions after loading a checkpoint file. """ - def _update_model(m): - for attr_str in dir(m): - target_attr = getattr(m, attr_str) - if isinstance(target_attr, QuantizationAwareModule): - target_attr.set_functions() - setattr(m, attr_str, target_attr) - - m.apply(_update_model) + for _, module in m.named_modules(): + if isinstance(module, QuantizationAwareModule): + module.set_functions() def update_optimizer(m, optimizer): @@ -1898,38 +2082,211 @@ def fuse_bn_layers(m): """ Fuse the bn layers before the quantization aware training starts. """ - def _fuse_bn_layers(m): - for attr_str in dir(m): - target_attr = getattr(m, attr_str) - if isinstance(target_attr, QuantizationAwareModule) \ - and target_attr.bn is not None: - w = target_attr.op.weight.data - b = target_attr.op.bias.data - device = w.device - - r_mean = target_attr.bn.running_mean - r_var = target_attr.bn.running_var - r_inv_std = torch.rsqrt(r_var + target_attr.bn.eps) - beta = target_attr.bn.weight - gamma = target_attr.bn.bias - - if beta is None: - beta = torch.ones(w.shape[0], device=device) - if gamma is None: - gamma = torch.zeros(w.shape[0], device=device) - - beta = 0.25 * beta - gamma = 0.25 * gamma - - w_new = w * (beta * r_inv_std).reshape((w.shape[0],) + (1,) * (len(w.shape) - 1)) - b_new = (b - r_mean) * r_inv_std * beta + gamma - - target_attr.op.weight.data = w_new - target_attr.op.bias.data = b_new - target_attr.bn = None - setattr(m, attr_str, target_attr) - - m.apply(_fuse_bn_layers) + for _, module in m.named_modules(): + if isinstance(module, QuantizationAwareModule) and module.bn is not None: + w = module.op.weight.data + b = module.op.bias.data + device = w.device + + r_mean = module.bn.running_mean + r_var = module.bn.running_var + r_inv_std = torch.rsqrt(r_var + module.bn.eps) + beta = module.bn.weight + gamma = module.bn.bias + + if beta is None: + beta = torch.ones(w.shape[0], device=device) + if gamma is None: + gamma = torch.zeros(w.shape[0], device=device) + + w_new = w * (beta * r_inv_std).reshape((w.shape[0],) + (1,) * (len(w.shape) - 1)) + b_new = (b - r_mean) * r_inv_std * beta + gamma + + module.op.weight.data = w_new + module.op.bias.data = b_new + module.bn = None + + +def apply_scales(model): + """ + Readjust the scales and apply according to the model graph. + """ + net_graph = symbolic_trace(model) + adds = {} + concats = {} + prevs = {} + op_names = ["torch.conv2d", "torch.conv1d", "torch.linear", + "torch._C._nn.linear", "torch.conv_transpose2d"] + nodes_to_search = [] + name_prev = None + + # Model graph traversal for finding the adds, concats and previous layers + for node in net_graph.graph.nodes: + name = node.format_node() + if ("torch.add" in name) or ("torch.cat" in name): + nodes_to_search.clear() + if "target=view" in name: + if len(node.all_input_nodes) > 0: + input_node = (node.all_input_nodes)[0] + nodes_to_search.append(input_node) + else: + nodes_to_search.extend(node.all_input_nodes) + for node_prev in reversed(net_graph.graph.nodes): + name_prev = node_prev.format_node() + if any(op_name in name_prev for op_name in op_names): + if node_prev in nodes_to_search: + node_prev_name = next(reversed(node_prev.__dict__['meta'] + ['nn_module_stack'])) + if "torch.add" in name: + node_name = next(reversed(node.__dict__['meta']['nn_module_stack'])) + adds[node_prev_name] = node_name + elif "torch.cat" in name: + concats[node_prev_name] = str(node) + nodes_to_search.pop(nodes_to_search.index(node_prev)) + else: + if node_prev in nodes_to_search: + nodes_to_search.pop(nodes_to_search.index(node_prev)) + if "target=view" in name_prev: + if len(node_prev.all_input_nodes) > 0: + input_node = (node_prev.all_input_nodes)[0] + nodes_to_search.append(input_node) + else: + nodes_to_search.extend(node_prev.all_input_nodes) + + elif any(op_name in name for op_name in op_names): + nodes_to_search.clear() + if len(node.all_input_nodes) > 0: + input_node = (node.all_input_nodes)[0] + nodes_to_search.append(input_node) + for node_prev in reversed(net_graph.graph.nodes): + name_prev = node_prev.format_node() + if any(op_name in name_prev for op_name in op_names): + if node_prev in nodes_to_search: + node_prev_name = next(reversed(node_prev.__dict__['meta'] + ['nn_module_stack'])) + node_name = next(reversed(node.__dict__['meta']['nn_module_stack'])) + if prevs.get(str(node_name)) is None: + node_prevs = [] + node_prevs.append(str(node_prev_name)) + prevs[str(node_name)] = node_prevs + else: + prevs[str(node_name)].append(str(node_prev_name)) + nodes_to_search.pop(nodes_to_search.index(node_prev)) + + else: + for name_node in nodes_to_search: + if node_prev == name_node: + nodes_to_search.extend(node_prev.all_input_nodes) + nodes_to_search.pop(nodes_to_search.index(name_node)) + + # Override the thresholds of layers that are connected to adds + for name, module in model.named_modules(): + if isinstance(module, QuantizationAwareModule): + if name in adds: + for name1, module1 in model.named_modules(): + if isinstance(module1, Eltwise): + if adds[name] == name1: + module.threshold = module1.threshold + break + + # Find the maximum threshold from the layers that are concatenated together + concat_thresholds = {} + for name, module in model.named_modules(): + if isinstance(module, QuantizationAwareModule): + if name in concats: + if concat_thresholds.get(concats[name]) is None: + concat_thresholds[concats[name]] = module.threshold + elif module.threshold > concat_thresholds[concats[name]]: + concat_thresholds[concats[name]] = module.threshold + + # Apply the maximum threshold to the layers that are concatenated together + for name, module in model.named_modules(): + if isinstance(module, QuantizationAwareModule): + if name in concats: + module.threshold = nn.Parameter(concat_thresholds[concats[name]], + requires_grad=False) + + # Find weight sharing layers and apply the maximum threshold from the multiple passes + shared_threshold = {} + for name, module in model.named_modules(): + if isinstance(module, QuantizationAwareModule): + if prevs.get(name) is not None: + for prev in prevs[name]: + for name1, module1 in model.named_modules(): + if isinstance(module1, QuantizationAwareModule): + if prev == name1: + if shared_threshold.get(name) is None: + shared_threshold[name] = module1.threshold + elif module1.threshold > shared_threshold[name]: + shared_threshold[name] = module1.threshold + for prev in prevs[name]: + for name1, module1 in model.named_modules(): + if isinstance(module1, QuantizationAwareModule): + if prev == name1: + module1.threshold = shared_threshold[name] + + # Get the thresholds after overrides + thresholds = {} + for name, module in model.named_modules(): + if isinstance(module, QuantizationAwareModule): + thresholds[name] = module.threshold + + # Adjust bias and threshold values according to the previous layers, + # and set the final scale value for output layers + for name, module in model.named_modules(): + if isinstance(module, QuantizationAwareModule): + if name in prevs: + prev_threshold_set = False + for name1, module1 in model.named_modules(): + if isinstance(module1, QuantizationAwareModule): + if name1 in prevs[name]: + if not prev_threshold_set: + if module.op is not None and module.op.bias is not None: + module.op.bias = nn.Parameter(module.op.bias / + torch.exp2(thresholds[name1])) + module.threshold = nn.Parameter((module.threshold - + thresholds[name1]), + requires_grad=False) + if module.wide: + module.final_scale = nn.Parameter(thresholds[name] - + module.threshold, + requires_grad=False) + else: + module.final_scale = nn.Parameter(thresholds[name], + requires_grad=False) + prev_threshold_set = True + module1.final_scale = nn.Parameter(torch.tensor(0.), + requires_grad=False) + + +def init_hist(model): + """ + Place forward hooks to collect histograms of activations + """ + for _, module in model.named_modules(): + if isinstance(module, (Eltwise, QuantizationAwareModule)): + register_hist_hooks(module) + + +def release_hist(model): + """ + Remove forward hooks after histogram collection + """ + for _, module in model.named_modules(): + if isinstance(module, (Eltwise, QuantizationAwareModule)): + release_hist_hooks(module) + + +def init_threshold(model, outlier_removal_z_score=8.0): + """ + Calculate thresholds based on the collected histograms + """ + for _, module in model.named_modules(): + if isinstance(module, (Eltwise, QuantizationAwareModule)): + # If module defined but not called on forward, it won't have hist + if hasattr(module, 'hist'): + init_threshold_module(module, outlier_removal_z_score) + calc_threshold(module) def onnx_export_prep(m, simplify=False, remove_clamp=False): diff --git a/ai8x_blocks.py b/ai8x_blocks.py index fc2b314c7..42fffc693 100644 --- a/ai8x_blocks.py +++ b/ai8x_blocks.py @@ -1,6 +1,6 @@ ################################################################################################### # -# Copyright (C) 2020-2023 Maxim Integrated Products, Inc. All Rights Reserved. +# Copyright (C) 2020-2024 Maxim Integrated Products, Inc. All Rights Reserved. # # Maxim Integrated Products, Inc. Default Copyright Notice: # https://www.maximintegrated.com/en/aboutus/legal/copyrights.html @@ -240,12 +240,11 @@ def __init__(self, eps=1e-03, momentum=0.01, **kwargs) # Depthwise Convolution phase if fused is not True: - self.depthwise_conv = ai8x.FusedConv2dBNReLU(in_channels=out, out_channels=out, - groups=out, # groups makes it depthwise - padding=1, kernel_size=kernel_size, - stride=stride, batchnorm='Affine', - bias=bias, eps=1e-03, momentum=0.01, - **kwargs) + self.depthwise_conv = ai8x.FusedDepthwiseConv2dBNReLU(out, out, kernel_size, + padding=1, stride=stride, + batchnorm='Affine', bias=bias, + eps=1e-03, momentum=0.01, + **kwargs) # Squeeze and Excitation phase if self.has_se: num_squeezed_channels = max(1, int(in_channels * se_ratio)) @@ -260,7 +259,9 @@ def __init__(self, kernel_size=1, batchnorm='Affine', bias=bias, eps=1e-03, momentum=0.01, **kwargs) # Skip connection - self.resid = ai8x.Add() + input_filters, output_filters = self.in_channels, self.out_channels + if self.stride == 1 and input_filters == output_filters: + self.resid = ai8x.Add() def forward(self, inputs): """MBConvBlock's forward function. diff --git a/datasets/imagenet.py b/datasets/imagenet.py index 4e77633e5..59e0972ba 100644 --- a/datasets/imagenet.py +++ b/datasets/imagenet.py @@ -1,6 +1,6 @@ # # Copyright (c) 2018 Intel Corporation -# Portions Copyright (C) 2019-2023 Maxim Integrated Products, Inc. +# Portions Copyright (C) 2019-2024 Maxim Integrated Products, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -48,7 +48,6 @@ def imagenet_get_datasets(data, load_train=True, load_test=True, transforms.RandomResizedCrop(input_size, antialias=True), transforms.RandomHorizontalFlip(), transforms.ToTensor(), - transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)), ai8x.normalize(args=args), ]) @@ -71,7 +70,6 @@ def imagenet_get_datasets(data, load_train=True, load_test=True, transforms.Resize(int(input_size / 0.875), antialias=True), # type: ignore transforms.CenterCrop(input_size), transforms.ToTensor(), - transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)), ai8x.normalize(args=args), ]) diff --git a/docs/unload_example.png b/docs/unload_example.png new file mode 100644 index 0000000000000000000000000000000000000000..7ecacf2bc10bdb5e5f225dbba4ae0e7fd41c33dc GIT binary patch literal 66400 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