From cda5fde6e7cff4655c6341cf9b756a35f477c2f7 Mon Sep 17 00:00:00 2001 From: Kai Chen <chenkaidev@gmail.com> Date: Mon, 12 Aug 2019 10:50:48 +0800 Subject: [PATCH] Add flops counter (#1127) * add flops counter * minor fix * add forward_dummy() for most detectors * add documentation for some tools --- GETTING_STARTED.md | 97 +++++ demo/loss_curve.png | Bin 0 -> 37484 bytes mmdet/models/detectors/cascade_rcnn.py | 31 ++ mmdet/models/detectors/double_head_rcnn.py | 24 ++ mmdet/models/detectors/grid_rcnn.py | 25 ++ mmdet/models/detectors/htc.py | 40 ++ mmdet/models/detectors/mask_scoring_rcnn.py | 3 + mmdet/models/detectors/rpn.py | 5 + mmdet/models/detectors/single_stage.py | 5 + mmdet/models/detectors/two_stage.py | 29 ++ mmdet/utils/__init__.py | 3 +- mmdet/utils/flops_counter.py | 421 ++++++++++++++++++++ tools/get_flops.py | 52 +++ 13 files changed, 734 insertions(+), 1 deletion(-) create mode 100644 demo/loss_curve.png create mode 100644 mmdet/utils/flops_counter.py create mode 100644 tools/get_flops.py diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 0138a3f..4dc4a37 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -162,6 +162,103 @@ pytorch [launch utility](https://pytorch.org/docs/stable/distributed_deprecated. Usually it is slow if you do not have high speed networking like infiniband. +## Useful tools + +### Analyze logs + +You can plot loss/mAP curves given a training log file. Run `pip install seaborn` first to install the dependency. + + + +```shell +python tools/analyze_logs.py plot_curve [--keys ${KEYS}] [--title ${TITLE}] [--legend ${LEGEND}] [--backend ${BACKEND}] [--style ${STYLE}] [--out ${OUT_FILE}] +``` + +Examples: + +- Plot the classification loss of some run. + +```shell +python tools/analyze_logs.py plot_curve log.json --keys loss_cls --legend loss_cls +``` + +- Plot the classification and regression loss of some run, and save the figure to a pdf. + +```shell +python tools/analyze_logs.py plot_curve log.json --keys loss_cls loss_reg --out losses.pdf +``` + +- Compare the bbox mAP of two runs in the same figure. + +```shell +python tools/analyze_logs.py plot_curve log1.json log2.json --keys bbox_mAP --legend run1 run2 +``` + +You can also compute the average training speed. + +```shell +python tools/analyze_logs.py cal_train_time ${CONFIG_FILE} [--include-outliers] +``` + +The output is expected to be like the following. + +``` +-----Analyze train time of work_dirs/some_exp/20190611_192040.log.json----- +slowest epoch 11, average time is 1.2024 +fastest epoch 1, average time is 1.1909 +time std over epochs is 0.0028 +average iter time: 1.1959 s/iter + +``` + +### Get the FLOPs and params (experimental) + +We provide a script adapted from [flops-counter.pytorch](https://github.com/sovrasov/flops-counter.pytorch) to compute the FLOPs and params of a given model. + +```shell +python tools/get_flops.py ${CONFIG_FILE} [--shape ${INPUT_SHAPE}] +``` + +You will get the result like this. + +``` +============================== +Input shape: (3, 1280, 800) +Flops: 239.32 GMac +Params: 37.74 M +============================== +``` + +**Note**: This tool is still experimental and we do not guarantee that the number is correct. You may well use the result for simple comparisons, but double check it before you adopt it in technical reports or papers. + +(1) FLOPs are related to the input shape while parameters are not. The default input shape is (1, 3, 1280, 800). +(2) Some operators are not counted into FLOPs like GN and custom operators. +You can add support for new operators by modifying [`mmdet/utils/flops_counter.py`](mmdet/utils/flops_counter.py). +(3) The FLOPs of two-stage detectors is dependent on the number of proposals. + +### Publish a model + +Before you upload a model to AWS, you may want to +(1) convert model weights to CPU tensors, (2) delete the optimizer states and +(3) compute the hash of the checkpoint file and append the hash id to the filename. + +```shell +python tools/publish_model.py ${INPUT_FILENAME} ${OUTPUT_FILENAME} +``` + +E.g., + +```shell +python tools/publish_model.py work_dirs/faster_rcnn/latest.pth faster_rcnn_r50_fpn_1x_20190801.pth +``` + +The final output filename will be `faster_rcnn_r50_fpn_1x_20190801-{hash id}.pth`. + +### Test the robustness of detectors + +Please refer to [ROBUSTNESS_BENCHMARKING.md](ROBUSTNESS_BENCHMARKING.md). + + ## How-to ### Use my own datasets diff --git a/demo/loss_curve.png b/demo/loss_curve.png new file mode 100644 index 0000000000000000000000000000000000000000..02425551174d57ae6fecd51be7960acad84c934c GIT binary patch literal 37484 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,Nlm1C_D{d+uYU`BbHroLXXfhTcQ6-d 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 R_&ow%ZZ4k3E1d&V{|j|B--iGI literal 0 HcmV?d00001 diff --git a/mmdet/models/detectors/cascade_rcnn.py b/mmdet/models/detectors/cascade_rcnn.py index 7ecbdff..bd878eb 100644 --- a/mmdet/models/detectors/cascade_rcnn.py +++ b/mmdet/models/detectors/cascade_rcnn.py @@ -117,6 +117,37 @@ class CascadeRCNN(BaseDetector, RPNTestMixin): x = self.neck(x) return x + def forward_dummy(self, img): + outs = () + # backbone + x = self.extract_feat(img) + # rpn + if self.with_rpn: + rpn_outs = self.rpn_head(x) + outs = outs + (rpn_outs, ) + proposals = torch.randn(1000, 4).cuda() + # bbox heads + rois = bbox2roi([proposals]) + if self.with_bbox: + for i in range(self.num_stages): + bbox_feats = self.bbox_roi_extractor[i]( + x[:self.bbox_roi_extractor[i].num_inputs], rois) + if self.with_shared_head: + bbox_feats = self.shared_head(bbox_feats) + cls_score, bbox_pred = self.bbox_head[i](bbox_feats) + outs = outs + (cls_score, bbox_pred) + # mask heads + if self.with_mask: + mask_rois = rois[:100] + for i in range(self.num_stages): + mask_feats = self.mask_roi_extractor[i]( + x[:self.mask_roi_extractor[i].num_inputs], mask_rois) + if self.with_shared_head: + mask_feats = self.shared_head(mask_feats) + mask_pred = self.mask_head[i](mask_feats) + outs = outs + (mask_pred, ) + return outs + def forward_train(self, img, img_meta, diff --git a/mmdet/models/detectors/double_head_rcnn.py b/mmdet/models/detectors/double_head_rcnn.py index 08f998b..7a78335 100644 --- a/mmdet/models/detectors/double_head_rcnn.py +++ b/mmdet/models/detectors/double_head_rcnn.py @@ -12,6 +12,30 @@ class DoubleHeadRCNN(TwoStageDetector): super().__init__(**kwargs) self.reg_roi_scale_factor = reg_roi_scale_factor + def forward_dummy(self, img): + outs = () + # backbone + x = self.extract_feat(img) + # rpn + if self.with_rpn: + rpn_outs = self.rpn_head(x) + outs = outs + (rpn_outs, ) + proposals = torch.randn(1000, 4).cuda() + # bbox head + rois = bbox2roi([proposals]) + bbox_cls_feats = self.bbox_roi_extractor( + x[:self.bbox_roi_extractor.num_inputs], rois) + bbox_reg_feats = self.bbox_roi_extractor( + x[:self.bbox_roi_extractor.num_inputs], + rois, + roi_scale_factor=self.reg_roi_scale_factor) + if self.with_shared_head: + bbox_cls_feats = self.shared_head(bbox_cls_feats) + bbox_reg_feats = self.shared_head(bbox_reg_feats) + cls_score, bbox_pred = self.bbox_head(bbox_cls_feats, bbox_reg_feats) + outs += (cls_score, bbox_pred) + return outs + def forward_train(self, img, img_meta, diff --git a/mmdet/models/detectors/grid_rcnn.py b/mmdet/models/detectors/grid_rcnn.py index 2c32164..853242c 100644 --- a/mmdet/models/detectors/grid_rcnn.py +++ b/mmdet/models/detectors/grid_rcnn.py @@ -80,6 +80,31 @@ class GridRCNN(TwoStageDetector): sampling_result.pos_bboxes = new_bboxes return sampling_results + def forward_dummy(self, img): + outs = () + # backbone + x = self.extract_feat(img) + # rpn + if self.with_rpn: + rpn_outs = self.rpn_head(x) + outs = outs + (rpn_outs, ) + proposals = torch.randn(1000, 4).cuda() + # bbox head + rois = bbox2roi([proposals]) + bbox_feats = self.bbox_roi_extractor( + x[:self.bbox_roi_extractor.num_inputs], rois) + if self.with_shared_head: + bbox_feats = self.shared_head(bbox_feats) + cls_score, bbox_pred = self.bbox_head(bbox_feats) + # grid head + grid_rois = rois[:100] + grid_feats = self.grid_roi_extractor( + x[:self.grid_roi_extractor.num_inputs], grid_rois) + if self.with_shared_head: + grid_feats = self.shared_head(grid_feats) + grid_pred = self.grid_head(grid_feats) + return rpn_outs, cls_score, bbox_pred, grid_pred + def forward_train(self, img, img_meta, diff --git a/mmdet/models/detectors/htc.py b/mmdet/models/detectors/htc.py index 7135fe1..d0a7024 100644 --- a/mmdet/models/detectors/htc.py +++ b/mmdet/models/detectors/htc.py @@ -153,6 +153,46 @@ class HybridTaskCascade(CascadeRCNN): mask_pred = mask_head(mask_feats) return mask_pred + def forward_dummy(self, img): + outs = () + # backbone + x = self.extract_feat(img) + # rpn + if self.with_rpn: + rpn_outs = self.rpn_head(x) + outs = outs + (rpn_outs, ) + proposals = torch.randn(1000, 4).cuda() + # semantic head + if self.with_semantic: + _, semantic_feat = self.semantic_head(x) + else: + semantic_feat = None + # bbox heads + rois = bbox2roi([proposals]) + for i in range(self.num_stages): + cls_score, bbox_pred = self._bbox_forward_test( + i, x, rois, semantic_feat=semantic_feat) + outs = outs + (cls_score, bbox_pred) + # mask heads + if self.with_mask: + mask_rois = rois[:100] + mask_roi_extractor = self.mask_roi_extractor[-1] + mask_feats = mask_roi_extractor( + x[:len(mask_roi_extractor.featmap_strides)], mask_rois) + if self.with_semantic and 'mask' in self.semantic_fusion: + mask_semantic_feat = self.semantic_roi_extractor( + [semantic_feat], mask_rois) + mask_feats += mask_semantic_feat + last_feat = None + for i in range(self.num_stages): + mask_head = self.mask_head[i] + if self.mask_info_flow: + mask_pred, last_feat = mask_head(mask_feats, last_feat) + else: + mask_pred = mask_head(mask_feats) + outs = outs + (mask_pred, ) + return outs + def forward_train(self, img, img_meta, diff --git a/mmdet/models/detectors/mask_scoring_rcnn.py b/mmdet/models/detectors/mask_scoring_rcnn.py index b035f53..9c16ab1 100644 --- a/mmdet/models/detectors/mask_scoring_rcnn.py +++ b/mmdet/models/detectors/mask_scoring_rcnn.py @@ -42,6 +42,9 @@ class MaskScoringRCNN(TwoStageDetector): self.mask_iou_head = builder.build_head(mask_iou_head) self.mask_iou_head.init_weights() + def forward_dummy(self, img): + raise NotImplementedError + # TODO: refactor forward_train in two stage to reduce code redundancy def forward_train(self, img, diff --git a/mmdet/models/detectors/rpn.py b/mmdet/models/detectors/rpn.py index 2f947fa..c9de290 100644 --- a/mmdet/models/detectors/rpn.py +++ b/mmdet/models/detectors/rpn.py @@ -38,6 +38,11 @@ class RPN(BaseDetector, RPNTestMixin): x = self.neck(x) return x + def forward_dummy(self, img): + x = self.extract_feat(img) + rpn_outs = self.rpn_head(x) + return rpn_outs + def forward_train(self, img, img_meta, diff --git a/mmdet/models/detectors/single_stage.py b/mmdet/models/detectors/single_stage.py index f7e0fa6..9587392 100644 --- a/mmdet/models/detectors/single_stage.py +++ b/mmdet/models/detectors/single_stage.py @@ -42,6 +42,11 @@ class SingleStageDetector(BaseDetector): x = self.neck(x) return x + def forward_dummy(self, img): + x = self.extract_feat(img) + outs = self.bbox_head(x) + return outs + def forward_train(self, img, img_metas, diff --git a/mmdet/models/detectors/two_stage.py b/mmdet/models/detectors/two_stage.py index e1536b5..6ec1541 100644 --- a/mmdet/models/detectors/two_stage.py +++ b/mmdet/models/detectors/two_stage.py @@ -87,6 +87,35 @@ class TwoStageDetector(BaseDetector, RPNTestMixin, BBoxTestMixin, x = self.neck(x) return x + def forward_dummy(self, img): + outs = () + # backbone + x = self.extract_feat(img) + # rpn + if self.with_rpn: + rpn_outs = self.rpn_head(x) + outs = outs + (rpn_outs, ) + proposals = torch.randn(1000, 4).cuda() + # bbox head + rois = bbox2roi([proposals]) + if self.with_bbox: + bbox_feats = self.bbox_roi_extractor( + x[:self.bbox_roi_extractor.num_inputs], rois) + if self.with_shared_head: + bbox_feats = self.shared_head(bbox_feats) + cls_score, bbox_pred = self.bbox_head(bbox_feats) + outs = outs + (cls_score, bbox_pred) + # mask head + if self.with_mask: + mask_rois = rois[:100] + mask_feats = self.mask_roi_extractor( + x[:self.mask_roi_extractor.num_inputs], mask_rois) + if self.with_shared_head: + mask_feats = self.shared_head(mask_feats) + mask_pred = self.mask_head(mask_feats) + outs = outs + (mask_pred, ) + return outs + def forward_train(self, img, img_meta, diff --git a/mmdet/utils/__init__.py b/mmdet/utils/__init__.py index c0a1244..f65e3b2 100644 --- a/mmdet/utils/__init__.py +++ b/mmdet/utils/__init__.py @@ -1,3 +1,4 @@ +from .flops_counter import get_model_complexity_info from .registry import Registry, build_from_cfg -__all__ = ['Registry', 'build_from_cfg'] +__all__ = ['Registry', 'build_from_cfg', 'get_model_complexity_info'] diff --git a/mmdet/utils/flops_counter.py b/mmdet/utils/flops_counter.py new file mode 100644 index 0000000..3005a14 --- /dev/null +++ b/mmdet/utils/flops_counter.py @@ -0,0 +1,421 @@ +# Modified from flops-counter.pytorch by Vladislav Sovrasov +# original repo: https://github.com/sovrasov/flops-counter.pytorch + +# MIT License + +# Copyright (c) 2018 Vladislav Sovrasov + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys + +import numpy as np +import torch +import torch.nn as nn +from torch.nn.modules.batchnorm import _BatchNorm +from torch.nn.modules.conv import _ConvNd, _ConvTransposeMixin +from torch.nn.modules.pooling import (_AdaptiveAvgPoolNd, _AdaptiveMaxPoolNd, + _AvgPoolNd, _MaxPoolNd) + +CONV_TYPES = (_ConvNd, ) +DECONV_TYPES = (_ConvTransposeMixin, ) +LINEAR_TYPES = (nn.Linear, ) +POOLING_TYPES = (_AvgPoolNd, _MaxPoolNd, _AdaptiveAvgPoolNd, + _AdaptiveMaxPoolNd) +RELU_TYPES = (nn.ReLU, nn.PReLU, nn.ELU, nn.LeakyReLU, nn.ReLU6) +BN_TYPES = (_BatchNorm, ) +UPSAMPLE_TYPES = (nn.Upsample, ) + +SUPPORTED_TYPES = ( + CONV_TYPES + DECONV_TYPES + LINEAR_TYPES + POOLING_TYPES + RELU_TYPES + + BN_TYPES + UPSAMPLE_TYPES) + + +def get_model_complexity_info(model, + input_res, + print_per_layer_stat=True, + as_strings=True, + input_constructor=None, + ost=sys.stdout): + assert type(input_res) is tuple + assert len(input_res) >= 2 + flops_model = add_flops_counting_methods(model) + flops_model.eval().start_flops_count() + if input_constructor: + input = input_constructor(input_res) + _ = flops_model(**input) + else: + batch = torch.ones(()).new_empty( + (1, *input_res), + dtype=next(flops_model.parameters()).dtype, + device=next(flops_model.parameters()).device) + flops_model(batch) + + if print_per_layer_stat: + print_model_with_flops(flops_model, ost=ost) + flops_count = flops_model.compute_average_flops_cost() + params_count = get_model_parameters_number(flops_model) + flops_model.stop_flops_count() + + if as_strings: + return flops_to_string(flops_count), params_to_string(params_count) + + return flops_count, params_count + + +def flops_to_string(flops, units='GMac', precision=2): + if units is None: + if flops // 10**9 > 0: + return str(round(flops / 10.**9, precision)) + ' GMac' + elif flops // 10**6 > 0: + return str(round(flops / 10.**6, precision)) + ' MMac' + elif flops // 10**3 > 0: + return str(round(flops / 10.**3, precision)) + ' KMac' + else: + return str(flops) + ' Mac' + else: + if units == 'GMac': + return str(round(flops / 10.**9, precision)) + ' ' + units + elif units == 'MMac': + return str(round(flops / 10.**6, precision)) + ' ' + units + elif units == 'KMac': + return str(round(flops / 10.**3, precision)) + ' ' + units + else: + return str(flops) + ' Mac' + + +def params_to_string(params_num): + if params_num // 10**6 > 0: + return str(round(params_num / 10**6, 2)) + ' M' + elif params_num // 10**3: + return str(round(params_num / 10**3, 2)) + ' k' + else: + return str(params_num) + + +def print_model_with_flops(model, units='GMac', precision=3, ost=sys.stdout): + total_flops = model.compute_average_flops_cost() + + def accumulate_flops(self): + if is_supported_instance(self): + return self.__flops__ / model.__batch_counter__ + else: + sum = 0 + for m in self.children(): + sum += m.accumulate_flops() + return sum + + def flops_repr(self): + accumulated_flops_cost = self.accumulate_flops() + return ', '.join([ + flops_to_string( + accumulated_flops_cost, units=units, precision=precision), + '{:.3%} MACs'.format(accumulated_flops_cost / total_flops), + self.original_extra_repr() + ]) + + def add_extra_repr(m): + m.accumulate_flops = accumulate_flops.__get__(m) + flops_extra_repr = flops_repr.__get__(m) + if m.extra_repr != flops_extra_repr: + m.original_extra_repr = m.extra_repr + m.extra_repr = flops_extra_repr + assert m.extra_repr != m.original_extra_repr + + def del_extra_repr(m): + if hasattr(m, 'original_extra_repr'): + m.extra_repr = m.original_extra_repr + del m.original_extra_repr + if hasattr(m, 'accumulate_flops'): + del m.accumulate_flops + + model.apply(add_extra_repr) + print(model, file=ost) + model.apply(del_extra_repr) + + +def get_model_parameters_number(model): + params_num = sum(p.numel() for p in model.parameters() if p.requires_grad) + return params_num + + +def add_flops_counting_methods(net_main_module): + # adding additional methods to the existing module object, + # this is done this way so that each function has access to self object + net_main_module.start_flops_count = start_flops_count.__get__( + net_main_module) + net_main_module.stop_flops_count = stop_flops_count.__get__( + net_main_module) + net_main_module.reset_flops_count = reset_flops_count.__get__( + net_main_module) + net_main_module.compute_average_flops_cost = \ + compute_average_flops_cost.__get__(net_main_module) + + net_main_module.reset_flops_count() + + # Adding variables necessary for masked flops computation + net_main_module.apply(add_flops_mask_variable_or_reset) + + return net_main_module + + +def compute_average_flops_cost(self): + """ + A method that will be available after add_flops_counting_methods() is + called on a desired net object. + Returns current mean flops consumption per image. + """ + + batches_count = self.__batch_counter__ + flops_sum = 0 + for module in self.modules(): + if is_supported_instance(module): + flops_sum += module.__flops__ + + return flops_sum / batches_count + + +def start_flops_count(self): + """ + A method that will be available after add_flops_counting_methods() is + called on a desired net object. + Activates the computation of mean flops consumption per image. + Call it before you run the network. + """ + add_batch_counter_hook_function(self) + self.apply(add_flops_counter_hook_function) + + +def stop_flops_count(self): + """ + A method that will be available after add_flops_counting_methods() is + called on a desired net object. + Stops computing the mean flops consumption per image. + Call whenever you want to pause the computation. + """ + remove_batch_counter_hook_function(self) + self.apply(remove_flops_counter_hook_function) + + +def reset_flops_count(self): + """ + A method that will be available after add_flops_counting_methods() is + called on a desired net object. + Resets statistics computed so far. + """ + add_batch_counter_variables_or_reset(self) + self.apply(add_flops_counter_variable_or_reset) + + +def add_flops_mask(module, mask): + + def add_flops_mask_func(module): + if isinstance(module, torch.nn.Conv2d): + module.__mask__ = mask + + module.apply(add_flops_mask_func) + + +def remove_flops_mask(module): + module.apply(add_flops_mask_variable_or_reset) + + +def is_supported_instance(module): + if isinstance(module, SUPPORTED_TYPES): + return True + else: + return False + + +def empty_flops_counter_hook(module, input, output): + module.__flops__ += 0 + + +def upsample_flops_counter_hook(module, input, output): + output_size = output[0] + batch_size = output_size.shape[0] + output_elements_count = batch_size + for val in output_size.shape[1:]: + output_elements_count *= val + module.__flops__ += int(output_elements_count) + + +def relu_flops_counter_hook(module, input, output): + active_elements_count = output.numel() + module.__flops__ += int(active_elements_count) + + +def linear_flops_counter_hook(module, input, output): + input = input[0] + batch_size = input.shape[0] + module.__flops__ += int(batch_size * input.shape[1] * output.shape[1]) + + +def pool_flops_counter_hook(module, input, output): + input = input[0] + module.__flops__ += int(np.prod(input.shape)) + + +def bn_flops_counter_hook(module, input, output): + module.affine + input = input[0] + + batch_flops = np.prod(input.shape) + if module.affine: + batch_flops *= 2 + module.__flops__ += int(batch_flops) + + +def deconv_flops_counter_hook(conv_module, input, output): + # Can have multiple inputs, getting the first one + input = input[0] + + batch_size = input.shape[0] + input_height, input_width = input.shape[2:] + + kernel_height, kernel_width = conv_module.kernel_size + in_channels = conv_module.in_channels + out_channels = conv_module.out_channels + groups = conv_module.groups + + filters_per_channel = out_channels // groups + conv_per_position_flops = ( + kernel_height * kernel_width * in_channels * filters_per_channel) + + active_elements_count = batch_size * input_height * input_width + overall_conv_flops = conv_per_position_flops * active_elements_count + bias_flops = 0 + if conv_module.bias is not None: + output_height, output_width = output.shape[2:] + bias_flops = out_channels * batch_size * output_height * output_height + overall_flops = overall_conv_flops + bias_flops + + conv_module.__flops__ += int(overall_flops) + + +def conv_flops_counter_hook(conv_module, input, output): + # Can have multiple inputs, getting the first one + input = input[0] + + batch_size = input.shape[0] + output_dims = list(output.shape[2:]) + + kernel_dims = list(conv_module.kernel_size) + in_channels = conv_module.in_channels + out_channels = conv_module.out_channels + groups = conv_module.groups + + filters_per_channel = out_channels // groups + conv_per_position_flops = np.prod( + kernel_dims) * in_channels * filters_per_channel + + active_elements_count = batch_size * np.prod(output_dims) + + if conv_module.__mask__ is not None: + # (b, 1, h, w) + output_height, output_width = output.shape[2:] + flops_mask = conv_module.__mask__.expand(batch_size, 1, output_height, + output_width) + active_elements_count = flops_mask.sum() + + overall_conv_flops = conv_per_position_flops * active_elements_count + + bias_flops = 0 + + if conv_module.bias is not None: + + bias_flops = out_channels * active_elements_count + + overall_flops = overall_conv_flops + bias_flops + + conv_module.__flops__ += int(overall_flops) + + +def batch_counter_hook(module, input, output): + batch_size = 1 + if len(input) > 0: + # Can have multiple inputs, getting the first one + input = input[0] + batch_size = len(input) + else: + print('Warning! No positional inputs found for a module, ' + 'assuming batch size is 1.') + module.__batch_counter__ += batch_size + + +def add_batch_counter_variables_or_reset(module): + + module.__batch_counter__ = 0 + + +def add_batch_counter_hook_function(module): + if hasattr(module, '__batch_counter_handle__'): + return + + handle = module.register_forward_hook(batch_counter_hook) + module.__batch_counter_handle__ = handle + + +def remove_batch_counter_hook_function(module): + if hasattr(module, '__batch_counter_handle__'): + module.__batch_counter_handle__.remove() + del module.__batch_counter_handle__ + + +def add_flops_counter_variable_or_reset(module): + if is_supported_instance(module): + module.__flops__ = 0 + + +def add_flops_counter_hook_function(module): + if is_supported_instance(module): + if hasattr(module, '__flops_handle__'): + return + + if isinstance(module, CONV_TYPES): + handle = module.register_forward_hook(conv_flops_counter_hook) + elif isinstance(module, RELU_TYPES): + handle = module.register_forward_hook(relu_flops_counter_hook) + elif isinstance(module, LINEAR_TYPES): + handle = module.register_forward_hook(linear_flops_counter_hook) + elif isinstance(module, POOLING_TYPES): + handle = module.register_forward_hook(pool_flops_counter_hook) + elif isinstance(module, BN_TYPES): + handle = module.register_forward_hook(bn_flops_counter_hook) + elif isinstance(module, UPSAMPLE_TYPES): + handle = module.register_forward_hook(upsample_flops_counter_hook) + elif isinstance(module, DECONV_TYPES): + handle = module.register_forward_hook(deconv_flops_counter_hook) + else: + handle = module.register_forward_hook(empty_flops_counter_hook) + module.__flops_handle__ = handle + + +def remove_flops_counter_hook_function(module): + if is_supported_instance(module): + if hasattr(module, '__flops_handle__'): + module.__flops_handle__.remove() + del module.__flops_handle__ + + +# --- Masked flops counting +# Also being run in the initialization +def add_flops_mask_variable_or_reset(module): + if is_supported_instance(module): + module.__mask__ = None diff --git a/tools/get_flops.py b/tools/get_flops.py new file mode 100644 index 0000000..e64bac6 --- /dev/null +++ b/tools/get_flops.py @@ -0,0 +1,52 @@ +import argparse + +from mmcv import Config + +from mmdet.models import build_detector +from mmdet.utils import get_model_complexity_info + + +def parse_args(): + parser = argparse.ArgumentParser(description='Train a detector') + parser.add_argument('config', help='train config file path') + parser.add_argument( + '--shape', + type=int, + nargs='+', + default=[1280, 800], + help='input image size') + args = parser.parse_args() + return args + + +def main(): + + args = parse_args() + + if len(args.shape) == 1: + input_shape = (3, args.shape[0], args.shape[0]) + elif len(args.shape) == 2: + input_shape = (3, ) + tuple(args.shape) + else: + raise ValueError('invalid input shape') + + cfg = Config.fromfile(args.config) + model = build_detector( + cfg.model, train_cfg=cfg.train_cfg, test_cfg=cfg.test_cfg).cuda() + model.eval() + + if hasattr(model, 'forward_dummy'): + model.forward = model.forward_dummy + else: + raise NotImplementedError( + 'FLOPs counter is currently not currently supported with {}'. + format(model.__class__.__name__)) + + flops, params = get_model_complexity_info(model, input_shape) + split_line = '=' * 30 + print('{0}\nInput shape: {1}\nFlops: {2}\nParams: {3}\n{0}'.format( + split_line, input_shape, flops, params)) + + +if __name__ == '__main__': + main() -- GitLab